/* Portal del Alumno */

/** Avatar del alumno: foto de registro (gallery/students/…) o iniciales. */
const StudentAvatar = ({ student, size = 48, theme, color }) => {
  const s = student || {};
  return (
    <Avatar
      name={s.shortName || s.name || '?'}
      size={size}
      color={color || theme?.primary}
      src={s.photoUrl || null}
      theme={theme}
    />
  );
};

const ALUMNO_MENU = [
  { id: 'a-dashboard', l: 'Inicio', icon: 'home' },
  { id: 'a-pagos', l: 'Calendario de pagos', icon: 'money' },
  { id: 'a-papeleria', l: 'Mi papelería', icon: 'doc' },
  { id: 'a-expediente', l: 'Expediente de pagos', icon: 'doc' },
  { id: 'a-compras', l: 'Mis compras', icon: 'cart' },
  { id: 'a-eventos', l: 'Eventos', icon: 'cal' },
  { id: 'a-avisos', l: 'Avisos', icon: 'megaphone' },
  { id: 'a-notificaciones', l: 'Notificaciones', icon: 'bell' },
  { id: 'a-perfil', l: 'Mi perfil', icon: 'user' },
];

const AlumnoSidebar = ({ theme, current, onNav, onLogout, student, mobileOpen = false, onMobileClose }) => {
  const s = student || { shortName: 'Alumno', code: '—', cat: '—' };
  const isMobile = useIsMobile();
  const navTo = React.useCallback((id) => {
    onNav(id);
    if (isMobile) onMobileClose?.();
  }, [onNav, isMobile, onMobileClose]);

  React.useEffect(() => {
    if (!isMobile || !mobileOpen) return undefined;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, [isMobile, mobileOpen]);

  const asideStyle = {
    width: isMobile ? 'min(100vw, 320px)' : 240,
    background: theme.bgElev,
    borderRight: `1px solid ${theme.border}`,
    display: 'flex', flexDirection: 'column',
    height: isMobile ? '100dvh' : '100vh',
    maxHeight: isMobile ? '100dvh' : '100vh',
    overflow: 'hidden',
    ...(isMobile ? {
      position: 'fixed',
      top: 0,
      left: 0,
      zIndex: 200,
      transform: mobileOpen ? 'translateX(0)' : 'translateX(-110%)',
      transition: 'transform 0.25s ease',
      boxShadow: mobileOpen ? '4px 0 24px rgba(0,0,0,0.35)' : 'none',
    } : {
      position: 'sticky',
      top: 0,
      overflowY: 'auto',
    }),
  };

  return (
  <>
    {isMobile && <DrawerBackdrop open={mobileOpen} onClose={onMobileClose} />}
  <aside className={isMobile ? 'tecos-sidebar-drawer tecos-admin-sidebar-drawer' : 'tecos-sidebar'} style={asideStyle}>
    <div className="tecos-sidebar-drawer__head" style={{ padding: '16px 18px', borderBottom: `1px solid ${theme.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexShrink: 0 }}>
      <Logo size={48} theme={theme} />
      {isMobile && <MobileMenuButton theme={theme} open onClick={onMobileClose} />}
    </div>
    <div style={{ padding: 16, borderBottom: `1px solid ${theme.border}`, display: 'flex', gap: 12, alignItems: 'center', flexShrink: 0 }}>
      <StudentAvatar student={s} size={48} theme={theme} color={theme.primary} />
      <div style={{ minWidth: 0 }}>
        <div style={{ color: theme.text, fontWeight: 700, fontFamily: 'Inter, sans-serif', fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.shortName || s.name}</div>
        <div style={{ color: theme.primary, fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform: 'uppercase', fontFamily: 'Inter, sans-serif' }}>{s.code} · {s.cat}</div>
      </div>
    </div>
    <nav className="tecos-sidebar-drawer__body" style={{ padding: '12px 14px', display: 'grid', gap: 4, flex: 1, minHeight: 0, overflowY: 'auto', WebkitOverflowScrolling: 'touch' }}>
      {ALUMNO_MENU.map(item => (
        <button key={item.id} type="button" className="tecos-admin-nav-btn" onClick={() => navTo(item.id)} style={{
          display: 'flex', alignItems: 'center', gap: 12,
          padding: '10px 12px', borderRadius: 8,
          background: current === item.id ? `${theme.primary}22` : 'transparent',
          color: current === item.id ? theme.primary : theme.textDim,
          border: 'none', cursor: 'pointer', width: '100%',
          fontSize: 13, fontWeight: 600, fontFamily: 'Inter, sans-serif',
          letterSpacing: 0.3, position: 'relative',
          textAlign: 'left',
        }}
          onMouseEnter={e => current !== item.id && (e.currentTarget.style.background = theme.bgInput)}
          onMouseLeave={e => current !== item.id && (e.currentTarget.style.background = 'transparent')}
        >
          {current === item.id && <div style={{ position: 'absolute', left: -14, top: '50%', transform: 'translateY(-50%)', width: 3, height: 20, borderRadius: 2, background: theme.primary }}></div>}
          <Icon name={item.icon} size={18} /><span>{item.l}</span>
        </button>
      ))}
    </nav>
    <div className="tecos-sidebar-drawer__foot" style={{ padding: 14, borderTop: `1px solid ${theme.border}`, flexShrink: 0 }}>
      <Btn theme={theme} kind="ghost" icon="arrowLeft" onClick={onLogout} style={{ width: '100%' }}>Salir</Btn>
    </div>
  </aside>
  </>
  );
};

/** Frase motivacional del día (1 frase por día del mes, 31 en total). */
const ALUMNO_DAILY_MOTIVATION = [
  'Cada saque es una oportunidad: entrena con intención y confía en tu proceso.',
  'La cancha premia a quien llega temprano y se queda un poco más.',
  'Hoy entrena tu mente: visualiza el pase perfecto antes de ejecutarlo.',
  'Un buen bloqueo empieza con los pies bien plantados y la mirada alta.',
  'No compares tu capítulo 1 con el capítulo 20 de otro; compárate con el de ayer.',
  'La recepción baja te enseña paciencia; la alta te enseña valentía.',
  'Entrena como si el quinto set dependiera de ti, porque a veces así es.',
  'La constancia en el gimnasio se nota en la cancha cuando nadie te ve.',
  'Cada error corregido en práctica es un punto que no regalas en el juego.',
  'Tu equipo gana cuando tú comunicas: grita, señala y apoya en cada rally.',
  'El voleibol es ritmo: respira, lee la jugada y responde, no reacciones a ciegas.',
  'Hoy trabaja un solo detalle hasta que salga limpio diez veces seguidas.',
  'La fuerza sin técnica cansa; la técnica sin fuerza se queda corta. Equilibra ambas.',
  'Salta con propósito: no solo altura, sino timing y lectura del bloqueo rival.',
  'Un atleta disciplinado entrena incluso cuando no tiene ganas; ahí se forja el carácter.',
  'Cuida tu recuperación: dormir y estirar también son parte del entrenamiento.',
  'La actitud en el calentamiento marca el tono de toda la sesión.',
  'Cada pelota defendida es un mensaje: aquí no se rinde el equipo.',
  'Pide retroalimentación a tu entrenador; el ego frena, la humildad acelera.',
  'Hoy enfócate en la calidad del contacto, no solo en la potencia del golpe.',
  'El buen colocador lee el partido; entrena tu visión periférica y tu memoria de jugadas.',
  'La presión en el partido se domina entrenando situaciones difíciles en la práctica.',
  'Tu cuerpo puede más de lo que tu cabeza cree cuando la técnica está afilada.',
  'Celebra el progreso pequeño: un mejor paso, un saque más estable, una defensa más limpia.',
  'Entrena la salida de red con la misma seriedad que tu ataque: el juego es completo.',
  'La confianza no cae del cielo; se construye repetición tras repetición.',
  'Hoy deja huella en el entrenamiento: esfuerzo visible, actitud positiva, enfoque total.',
  'Un equipo unido comunica en silencio: miradas, gestos y confianza en cada rotación.',
  'No persigas la perfección; persigue la mejora constante en cada sesión.',
  'Cuando el cansancio aparezca, recuerda por qué elegiste esta cancha y este deporte.',
  'Cierra el mes con orgullo: lo que entrenas hoy es el jugador que serás mañana.',
];

const getAlumnoDailyMotivation = (date = new Date()) => {
  const day = date.getDate();
  const idx = Math.min(Math.max(day, 1), 31) - 1;
  return ALUMNO_DAILY_MOTIVATION[idx] || ALUMNO_DAILY_MOTIVATION[0];
};

const AlumnoDailyMotivation = ({ theme }) => {
  const now = new Date();
  const phrase = getAlumnoDailyMotivation(now);
  const dayNum = now.getDate();
  const monthLabel = now.toLocaleDateString('es-MX', { month: 'long' });
  return (
    <section
      className="tecos-alumno-daily-quote"
      role="note"
      aria-label="Frase motivacional del día"
      style={{
        '--tecos-quote-primary': theme.primary,
        '--tecos-quote-primary-dark': theme.primaryDark || theme.primary,
        '--tecos-quote-accent': theme.accent || theme.primary,
        '--tecos-quote-warning': theme.warning || '#f59e0b',
        '--tecos-quote-text': theme.text,
        '--tecos-quote-border': theme.border,
      }}
    >
      <div className="tecos-alumno-daily-quote__bg" aria-hidden="true">
        <CourtLines color="#fff" opacity={0.12} />
        <div className="tecos-alumno-daily-quote__ball">
          <Volleyball size={120} color="#fff" />
        </div>
      </div>
      <div className="tecos-alumno-daily-quote__inner">
        <div className="tecos-alumno-daily-quote__date" aria-label={`Día ${dayNum} de ${monthLabel}`}>
          <span className="tecos-alumno-daily-quote__date-num">{dayNum}</span>
          <span className="tecos-alumno-daily-quote__date-month">{monthLabel}</span>
        </div>
        <blockquote className="tecos-alumno-daily-quote__text">
          {phrase}
        </blockquote>
      </div>
    </section>
  );
};

const notifIconColor = (icon, theme) => {
  const i = String(icon || '').toLowerCase();
  if (i === 'money') return theme.warning;
  if (i === 'cart') return theme.success;
  if (i === 'cake') return theme.urgent;
  if (i === 'cal') return theme.primary;
  if (i === 'check') return theme.success;
  if (i === 'warning') return theme.danger;
  return theme.info;
};

/* Alumno Dashboard */
const AlumnoDashboard = ({
  theme, student, studentCode, onPayClick, months = [], orders = [], onOrderClick, onNav, onNotifNavigate,
  calendarYear, onCalendarYearPrev, onCalendarYearNext, calendarYearBounds, billingSettings,
}) => {
  const s = student || { name: 'Alumno', code: '—', cat: '—', joined: '—', status: 'activo', adeudo: 0 };
  const code = studentCode || s.code;
  const year = calendarYear ?? new Date().getFullYear();
  const bounds = calendarYearBounds || getPaymentCalendarYearBounds(s.joined_at || s.joinedAt);
  const { items: notifItems, loading: notifLoading, markRead: markNotifRead } = useNotifications({
    audience: 'student', studentCode: code,
  });
  const { data: eventRows, loading: eventsLoading } = useLiveData(() => fetchPublicEvents(), []);
  const { data: birthdays, loading: bdaysLoading } = useLiveData(
    () => (code ? fetchPortalUpcomingBirthdays(code, 12) : Promise.resolve([])),
    [code]
  );

  const upcomingEvents = React.useMemo(() => {
    const now = Date.now();
    const joinAt = s.joined_at ? new Date(s.joined_at) : null;
    const joinMs = joinAt && !Number.isNaN(joinAt.getTime()) ? joinAt.getTime() : null;
    return (eventRows || [])
      .filter((e) => {
        if (!e.starts_at || new Date(e.starts_at).getTime() < now) return false;
        if (joinMs != null && new Date(e.starts_at).getTime() < joinMs) return false;
        return true;
      })
      .sort((a, b) => new Date(a.starts_at) - new Date(b.starts_at))
      .slice(0, 3)
      .map((e) => {
        const d = new Date(e.starts_at);
        const monthsLbl = ['ENE', 'FEB', 'MAR', 'ABR', 'MAY', 'JUN', 'JUL', 'AGO', 'SEP', 'OCT', 'NOV', 'DIC'];
        return {
          id: e.id,
          d: String(d.getDate()),
          m: monthsLbl[d.getMonth()],
          n: e.title || 'Evento',
          t: d.toLocaleString('es-MX', { hour: '2-digit', minute: '2-digit' })
            + (e.location ? ` · ${e.location}` : ''),
          c: theme.primary,
        };
      });
  }, [eventRows, theme.primary, s.joined_at]);

  const sortedBirthdays = React.useMemo(() => (
    [...(birthdays || [])].sort((a, b) => (a.daysUntil ?? 99) - (b.daysUntil ?? 99))
  ), [birthdays]);

  const previewNotifs = React.useMemo(() => {
    const sorted = typeof sortNotificationItems === 'function'
      ? sortNotificationItems(notifItems)
      : (notifItems || []);
    return sorted.slice(0, 5);
  }, [notifItems]);
  const pending = typeof firstExigiblePendingMonth === 'function'
    ? firstExigiblePendingMonth(months)
    : months.find(m => ['pendiente', 'vencido', 'revision', 'en_revision', 'rechazado', 'urgente'].includes(m.status));
  const adeudo = Number(s.adeudo) || 0;
  const alCorriente = adeudo <= 0;
  const nextPay = alCorriente ? 'Al corriente' : `$${adeudo.toLocaleString('es-MX')}`;
  const nextDue = pending
    ? `Mensualidad ${pending.name} ${year}`
    : (alCorriente ? 'Sin adeudos' : 'Revisa el calendario de pagos');
  const pendientesCount = typeof countExigiblePendingMonths === 'function'
    ? countExigiblePendingMonths(months)
    : months.filter(m => ['pendiente', 'vencido', 'en_revision'].includes(m.status)).length;
  const billingHint = React.useMemo(() => (
    typeof portalBillingScheduleHint === 'function' && billingSettings
      ? portalBillingScheduleHint(billingSettings)
      : null
  ), [billingSettings]);
  const lastOrder = orders[0];
  return (
  <div className="tecos-page-shell tecos-alumno-portal" style={{ padding: 32, display: 'grid', gap: 18 }}>
    <AlumnoDailyMotivation theme={theme} />
    {/* Hero card */}
    <Card theme={theme} style={{
      padding: 28, position: 'relative', overflow: 'hidden',
      background: `linear-gradient(135deg, ${theme.primary} 0%, ${theme.primaryDark} 100%)`,
      color: '#fff',
    }}>
      <CourtLines color="#fff" opacity={0.15} />
      <div style={{ position: 'absolute', right: -40, top: -40, opacity: 0.2 }}>
        <Volleyball size={260} color="#fff" />
      </div>
      <div className="tecos-alumno-hero-inner" style={{ position: 'relative', display: 'flex', gap: 24, alignItems: 'center' }}>
        <StudentAvatar student={s} size={100} theme={theme} color="#fff" />
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 11, letterSpacing: 2, fontWeight: 700, textTransform: 'uppercase', opacity: 0.85, fontFamily: 'Inter, sans-serif' }}>Bienvenido, atleta</div>
          <h2 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 50, margin: '8px 0 4px', letterSpacing: 1.5, lineHeight: 1, fontWeight: 400 }}>{(s.name || '').toUpperCase()}</h2>
          <div style={{ display: 'flex', gap: 14, fontSize: 13, opacity: 0.9, fontFamily: 'Inter, sans-serif', flexWrap: 'wrap' }}>
            <span><strong>{s.code}</strong></span><span>·</span>
            <span>Categoría {s.cat}</span><span>·</span>
            <span>Ingreso {s.joined}</span><span>·</span>
            <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
              <span style={{
                width: 8, height: 8, borderRadius: '50%',
                background: alCorriente ? '#86efac' : '#fbbf24',
                boxShadow: alCorriente ? '0 0 10px #86efac' : '0 0 10px #fbbf24',
              }}></span>
              {alCorriente ? 'Al corriente' : `Adeudo $${adeudo.toLocaleString('es-MX')}`}
            </span>
          </div>
        </div>
      </div>
    </Card>

    {/* Quick stats */}
    <div className="tecos-grid-4 tecos-alumno-stats-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14 }}>
      {[
        { l: 'Próximo pago', v: nextPay, d: nextDue, c: theme.primary, i: 'money' },
        { l: 'Pagadas', v: String(months.filter(m => m.status === 'pagado' && m.inCycle !== false).length), d: 'Desde ingreso', c: theme.success, i: 'flame' },
        { l: 'Pendientes', v: String(pendientesCount), d: 'Meses con cobro activo', c: theme.warning, i: 'trophy' },
        { l: 'Compras', v: String(orders.length), d: 'Tienda', c: theme.accent, i: 'cart' },
      ].map(m => <MetricCard key={m.l} m={m} theme={theme} variant="gradient" />)}
    </div>

    {/* Two columns */}
    <div className="tecos-alumno-split-grid" style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18 }}>
      {/* Calendar of pages (year view) */}
      <Card theme={theme}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, marginBottom: 18, flexWrap: 'wrap' }}>
          <div style={{ flex: 1, minWidth: 200 }}>
            <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 24, color: theme.text, letterSpacing: 1, margin: 0, fontWeight: 400 }}>CALENDARIO DE PAGOS</h3>
            <p style={{ color: theme.textDim, fontSize: 12, fontFamily: 'Inter, sans-serif', marginTop: 2 }}>
              {billingHint?.calendar || 'Toca un mes con monto para transferir y subir comprobante. Los adelantos usan tarifa base sin recargo; el pago se confirma cuando el administrador lo apruebe.'}
            </p>
          </div>
          <PaymentCalendarYearNav
            theme={theme}
            year={year}
            onPrev={onCalendarYearPrev}
            onNext={onCalendarYearNext}
            minYear={bounds.minYear}
            maxYear={bounds.maxYear}
          />
          <div style={{ display: 'flex', gap: 10, fontSize: 11, color: theme.textDim, fontFamily: 'Inter, sans-serif', flexWrap: 'wrap', width: '100%' }}>
            <LegendDot c={theme.success} l="Pagado" />
            <LegendDot c={theme.warning} l="Pend." />
            <LegendDot c={theme.danger} l="Venc." />
            <LegendDot c={theme.info} l="Revisión" />
            <LegendDot c={theme.info} l="Adelanto" />
            <LegendDot c={theme.primary} l="Proporcional" />
          </div>
        </div>
        <PaymentGrid theme={theme} onPayClick={onPayClick} months={months} />
      </Card>

      {/* Notifications */}
      <Card theme={theme}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
          <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 24, color: theme.text, letterSpacing: 1, margin: 0, fontWeight: 400 }}>NOTIFICACIONES</h3>
          <Btn theme={theme} kind="ghost" size="sm" onClick={() => onNav?.('a-notificaciones')}>Ver todas</Btn>
        </div>
        {notifLoading ? (
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif' }}>Cargando…</p>
        ) : previewNotifs.length === 0 ? (
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif' }}>Sin notificaciones por ahora.</p>
        ) : (
          <div style={{ display: 'grid', gap: 8 }}>
            {previewNotifs.map((n) => {
              const c = notifIconColor(n.icon, theme);
              const target = typeof resolveStudentNotificationTarget === 'function'
                ? resolveStudentNotificationTarget(n)
                : null;
              return (
                <button key={n.id} type="button" onClick={() => {
                  if (typeof markNotifRead === 'function') markNotifRead(n);
                  if (target && onNotifNavigate) onNotifNavigate(target, n);
                  else onNav?.('a-notificaciones');
                }} style={{
                  display: 'flex', alignItems: 'center', gap: 12, padding: 10,
                  borderRadius: 10, background: n.read_at ? theme.bgInput : `${theme.primary}12`,
                  border: `1px solid ${n.read_at ? theme.border : `${theme.primary}44`}`,
                  position: 'relative', width: '100%', textAlign: 'left', cursor: 'pointer',
                  opacity: n.read_at ? 0.75 : 1,
                }}>
                  {!n.read_at && <span style={{ position: 'absolute', top: 8, right: 10, width: 7, height: 7, borderRadius: '50%', background: theme.danger, boxShadow: `0 0 6px ${theme.danger}` }}></span>}
                  <div style={{ width: 34, height: 34, borderRadius: 8, background: `${c}22`, color: c, display: 'grid', placeItems: 'center' }}>
                    <Icon name={n.icon || 'bell'} size={14} />
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 12, color: theme.text, fontWeight: 600, fontFamily: 'Inter, sans-serif' }}>{n.title}</div>
                    <div style={{ fontSize: 10, color: theme.textMute, fontFamily: 'Inter, sans-serif', marginTop: 2 }}>
                      {typeof formatNotifTime === 'function' ? formatNotifTime(n.created_at) : ''}
                    </div>
                  </div>
                </button>
              );
            })}
          </div>
        )}
      </Card>
    </div>

    {/* Próximos eventos y avisos */}
    <div className="tecos-alumno-triple-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 18 }}>
      <Card theme={theme}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }}>
          <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 22, color: theme.text, letterSpacing: 1, margin: 0, fontWeight: 400 }}>PRÓXIMOS EVENTOS</h3>
          <Btn theme={theme} kind="ghost" size="sm" onClick={() => onNav?.('a-eventos')}>Calendario</Btn>
        </div>
        {eventsLoading ? (
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif' }}>Cargando…</p>
        ) : upcomingEvents.length === 0 ? (
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif' }}>No hay eventos programados.</p>
        ) : upcomingEvents.map((e, i) => (
          <div key={e.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', borderBottom: i < upcomingEvents.length - 1 ? `1px solid ${theme.border}` : 'none' }}>
            <div style={{
              width: 50, height: 50, borderRadius: 10, background: `${e.c}22`, border: `1px solid ${e.c}55`,
              display: 'grid', placeItems: 'center', textAlign: 'center', flexShrink: 0,
            }}>
              <div>
                <div style={{ fontSize: 9, color: e.c, fontWeight: 700, letterSpacing: 1 }}>{e.m}</div>
                <div style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 22, color: e.c, lineHeight: 1, letterSpacing: 0.5 }}>{e.d}</div>
              </div>
            </div>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 13, color: theme.text, fontWeight: 600, fontFamily: 'Inter, sans-serif' }}>{e.n}</div>
              <div style={{ fontSize: 11, color: theme.textMute, fontFamily: 'Inter, sans-serif', marginTop: 2 }}>{e.t}</div>
            </div>
          </div>
        ))}
      </Card>

      <Card theme={theme}>
        <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 22, color: theme.text, letterSpacing: 1, margin: '0 0 14px', fontWeight: 400 }}>CUMPLEAÑOS</h3>
        {bdaysLoading ? (
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif' }}>Cargando…</p>
        ) : sortedBirthdays.length === 0 ? (
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif' }}>Sin cumpleaños próximos en 14 días.</p>
        ) : sortedBirthdays.map((c, i) => (
          <div key={c.id || i} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', borderBottom: i < sortedBirthdays.length - 1 ? `1px solid ${theme.border}` : 'none' }}>
            <Avatar name={c.name} size={40} theme={theme} />
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, color: theme.text, fontWeight: 600, fontFamily: 'Inter, sans-serif' }}>
                {c.name}{c.isSelf ? ' (tú)' : ''}
              </div>
              <div style={{ fontSize: 11, color: c.isSelf ? theme.urgent : theme.primary, fontFamily: 'Inter, sans-serif', fontWeight: 600, marginTop: 2 }}>
                🎂 {c.label}
              </div>
            </div>
          </div>
        ))}
      </Card>

      <Card theme={theme}>
        <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 22, color: theme.text, letterSpacing: 1, margin: '0 0 14px', fontWeight: 400 }}>ÚLTIMA COMPRA</h3>
        {!lastOrder ? (
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif', marginBottom: 14 }}>Aún no tienes compras con tu ID.</p>
        ) : (
          <div style={{ padding: 14, borderRadius: 10, background: theme.bgInput, border: `1px solid ${theme.border}` }}>
            <div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 12 }}>
              <div style={{ width: 56, height: 56, borderRadius: 10, background: `${theme.primary}22`, color: theme.primary, display: 'grid', placeItems: 'center' }}>
                <Icon name={lastOrder.i || 'cart'} size={24} />
              </div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, color: theme.text, fontWeight: 700, fontFamily: 'Inter, sans-serif' }}>{lastOrder.p}</div>
                <div style={{ fontSize: 11, color: theme.textMute, fontFamily: 'Inter, sans-serif', marginTop: 2 }}>{lastOrder.n} · {lastOrder.d}</div>
              </div>
            </div>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 22, color: theme.text, letterSpacing: 0.5 }}>${lastOrder.a}</span>
              <StatusPill status={lastOrder.s} theme={theme} />
            </div>
          </div>
        )}
        <Btn theme={theme} kind="soft" size="sm" icon="history" style={{ marginTop: 14, width: '100%' }} onClick={() => onNav?.('a-compras')}>Ver todas las compras</Btn>
      </Card>
    </div>
  </div>
  );
};

const LegendDot = ({ c, l }) => (
  <span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
    <span style={{ width: 8, height: 8, borderRadius: '50%', background: c }}></span>{l}
  </span>
);

/** Límites del calendario de pagos (desde ingreso hasta años futuros para adelantos). */
const getPaymentCalendarYearBounds = (joinedAt) => {
  const now = new Date();
  const maxYear = now.getFullYear() + 8;
  let minYear = now.getFullYear() - 2;
  if (joinedAt) {
    const join = new Date(joinedAt);
    if (!Number.isNaN(join.getTime())) minYear = Math.min(minYear, join.getFullYear());
  }
  return { minYear, maxYear };
};

/** Flechas de año en esquina del calendario de pagos. */
const PaymentCalendarYearNav = ({ theme, year, onPrev, onNext, minYear, maxYear }) => {
  const canPrev = year > minYear;
  const canNext = year < maxYear;
  const btnStyle = (disabled) => ({
    width: 34, height: 34, borderRadius: 8,
    background: theme.bgInput, border: `1px solid ${theme.border}`,
    color: disabled ? theme.textMute : theme.text,
    cursor: disabled ? 'default' : 'pointer',
    opacity: disabled ? 0.4 : 1,
    display: 'grid', placeItems: 'center', padding: 0,
  });
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
      <button type="button" style={btnStyle(!canPrev)} onClick={onPrev} disabled={!canPrev} aria-label="Año anterior">
        <Icon name="arrowLeft" size={16} />
      </button>
      <span style={{
        fontFamily: 'Bebas Neue, sans-serif', fontSize: 26, color: theme.primary,
        letterSpacing: 1, minWidth: 56, textAlign: 'center', lineHeight: 1,
      }}>{year}</span>
      <button type="button" style={btnStyle(!canNext)} onClick={onNext} disabled={!canNext} aria-label="Año siguiente">
        <Icon name="arrowRight" size={16} />
      </button>
    </div>
  );
};

/* Payment grid (12 months) */
const PaymentGrid = ({ theme, onPayClick, months = [], adminCalendar = false }) => {
  const colorFor = (s) => {
    if (s === 'sin_registro' || s === 'futuro') return theme.textMute;
    if (s === 'pagado') return theme.success;
    if (s === 'revision' || s === 'en_revision') return theme.warning;
    if (s === 'urgente') return theme.urgent;
    if (s === 'vencido') return theme.danger;
    if (s === 'rechazado') return theme.warning;
    return theme.warning;
  };
  const labelFor = (m, s) => {
    if (m?.advancePay && s === 'pendiente') return 'adelanto';
    if (s === 'futuro') return 'Próximo';
    if (s === 'sin_registro') return '—';
    if (s === 'revision' || s === 'en_revision') return 'pendiente';
    if (s === 'rechazado') return 'pendiente';
    return s;
  };
  return (
    <div className="tecos-alumno-payment-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: 10 }}>
      {months.map((m, i) => {
        const c = colorFor(m.status);
        const inactive = m.status === 'sin_registro';
        const amt = Number(m.amt) || 0;
        const hideMoney = m.hideAmount || (m.status === 'pendiente' && amt <= 0);
        const clickable = adminCalendar
          ? !inactive
          : (!inactive && (!hideMoney || m.advancePay));
        return (
          <button key={i} onClick={() => clickable && onPayClick?.(m)} disabled={!clickable} style={{
            padding: 12, borderRadius: 10, cursor: inactive ? 'default' : 'pointer',
            background: inactive ? theme.bgInput : `${c}11`,
            border: `1.5px solid ${inactive ? theme.border : `${c}44`}`,
            display: 'flex', flexDirection: 'column', gap: 6, textAlign: 'left',
            transition: 'all .2s', opacity: inactive ? 0.45 : 1,
          }}
            onMouseEnter={e => { if (!inactive) { e.currentTarget.style.background = `${c}22`; e.currentTarget.style.transform = 'translateY(-2px)'; } }}
            onMouseLeave={e => { if (!inactive) { e.currentTarget.style.background = `${c}11`; e.currentTarget.style.transform = 'translateY(0)'; } }}
          >
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 18, color: theme.text, letterSpacing: 0.5 }}>{m.name}</span>
              <span style={{ width: 8, height: 8, borderRadius: '50%', background: c, boxShadow: `0 0 8px ${c}` }}></span>
            </div>
            <div style={{ fontSize: 11, color: c, fontWeight: 700, textTransform: 'capitalize', fontFamily: 'Inter, sans-serif' }}>{labelFor(m, m.status)}</div>
            {!hideMoney ? (
              <>
                <div style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 16, color: theme.text, letterSpacing: 0.5 }}>
                  ${amt.toLocaleString('es-MX')}
                </div>
                {m.prorated && m.prorationLabel && (
                  <div style={{
                    fontSize: 10,
                    fontWeight: 700,
                    color: theme.primary,
                    fontFamily: 'Inter, sans-serif',
                    lineHeight: 1.35,
                  }}>
                    Proporcional · {m.prorationLabel}
                  </div>
                )}
                {m.isCurrentMonth && m.billingTierLabel && m.status !== 'pagado' && m.status !== 'sin_registro' && (
                  <div style={{
                    fontSize: 10,
                    color: theme.textDim,
                    fontFamily: 'Inter, sans-serif',
                    lineHeight: 1.35,
                  }}>
                    Hoy día {m.billingDayUsed}: {m.billingTierLabel}
                    {m.billingRangeLabel ? ` (${m.billingRangeLabel})` : ''}
                  </div>
                )}
              </>
            ) : (
              <div style={{ fontSize: 10, color: theme.textMute, fontFamily: 'Inter, sans-serif' }}>Tarifa al iniciar mes</div>
            )}
          </button>
        );
      })}
    </div>
  );
};

/* Other alumno views — concise stubs */
const AlumnoPerfilSection = ({ theme, student, onPhotoUpdated }) => {
  const s = student || {};
  const fileRef = React.useRef(null);
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState('');

  const uploadPhoto = async (file) => {
    if (!file || !s.code) return;
    setBusy(true); setMsg('');
    try {
      await uploadStudentPhotoByCode(s.code, file);
      setMsg('Foto actualizada.');
      onPhotoUpdated?.();
    } catch (e) { setMsg(e.message); }
    setBusy(false);
  };

  return (
    <div className="tecos-page-shell tecos-grid-2-fr tecos-alumno-portal tecos-alumno-perfil-grid" style={{ padding: 32, display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 18 }}>
      <Card theme={theme} style={{ padding: 24, textAlign: 'center' }}>
        <div style={{ display: 'flex', justifyContent: 'center' }}>
          <StudentAvatar student={s} size={140} theme={theme} color={theme.primary} />
        </div>
        <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 28, color: theme.text, letterSpacing: 1, margin: '14px 0 4px', fontWeight: 400 }}>{(s.name || '').toUpperCase()}</h3>
        <Badge theme={theme} color="info">Categoría {s.cat}</Badge>
        <input ref={fileRef} type="file" accept="image/jpeg,image/png,image/webp" hidden
          onChange={e => uploadPhoto(e.target.files?.[0])} />
        <div style={{ display: 'grid', gap: 8, marginTop: 20 }}>
          <Btn theme={theme} kind="soft" icon="upload" disabled={busy} onClick={() => fileRef.current?.click()}>
            {busy ? 'Subiendo…' : 'Cambiar foto'}
          </Btn>
        </div>
        {msg && <p style={{ fontSize: 12, color: theme.success, marginTop: 10 }}>{msg}</p>}
      </Card>
      <Card theme={theme}>
        <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 24, color: theme.text, letterSpacing: 1, margin: '0 0 16px', fontWeight: 400 }}>MIS DATOS</h3>
        <div className="tecos-alumno-perfil-fields" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
          <Input theme={theme} label="Nombre completo" value={s.name || ''} readOnly icon="user" />
          <Input theme={theme} label="ID" value={s.code || ''} readOnly icon="medal" />
          <Input theme={theme} label="Edad" value={String(s.age || '—')} readOnly />
          <Input theme={theme} label="Categoría" value={s.cat || ''} readOnly />
          <Input theme={theme} label="Tutor" value={s.tutor || ''} readOnly icon="user" />
          <Input theme={theme} label="Teléfono tutor" value={s.phone || ''} readOnly icon="wa" />
          <Input theme={theme} label="Correo" value={s.email || ''} readOnly />
          <Input theme={theme} label="Fecha ingreso" value={s.joined || ''} readOnly icon="cal" />
        </div>
        <p style={{ fontSize: 12, color: theme.textDim, marginTop: 16 }}>Para cambiar datos de contacto, solicítalo en la academia.</p>
      </Card>
    </div>
  );
};

const AlumnoSubview = ({
  theme, view, student, onPayClick, months, orders = [], expediente = [], documents = [],
  documentRequirements, onDocumentsUpdated, onOrderClick, onPhotoUpdated, onNotifNavigate,
  calendarYear, onCalendarYearPrev, onCalendarYearNext, calendarYearBounds, billingSettings,
}) => {
  const [expPdfBusy, setExpPdfBusy] = React.useState(false);
  const [expRowPdfBusy, setExpRowPdfBusy] = React.useState(null);
  const [orderPdfBusy, setOrderPdfBusy] = React.useState(null);
  const [portalEventModal, setPortalEventModal] = React.useState(null);
  const [portalAvisoModal, setPortalAvisoModal] = React.useState(null);
  const [portalBirthdayModal, setPortalBirthdayModal] = React.useState(null);
  const payYear = calendarYear ?? new Date().getFullYear();
  const payYearBounds = calendarYearBounds || getPaymentCalendarYearBounds(student?.joined_at || student?.joinedAt);
  const billingHint = React.useMemo(() => (
    typeof portalBillingScheduleHint === 'function' && billingSettings
      ? portalBillingScheduleHint(billingSettings)
      : null
  ), [billingSettings]);
  const expedienteRows = expediente.length ? expediente : [];

  const downloadMonthPdf = async (row) => {
    if (typeof exportPortalMonthReceiptPdf !== 'function') {
      alert('Módulo PDF no cargado. Recarga la página (F5).');
      return;
    }
    setExpRowPdfBusy(row.id);
    try {
      const settings = await fetchAcademySettings();
      await exportPortalMonthReceiptPdf(student, row, settings, theme);
    } catch (e) { alert(e.message || 'No se pudo generar el PDF'); }
    setExpRowPdfBusy(null);
  };

  const downloadExpedienteAllPdf = async () => {
    if (typeof exportStudentPaymentHistoryPdf !== 'function') {
      alert('Módulo PDF no cargado. Recarga la página (F5).');
      return;
    }
    setExpPdfBusy(true);
    try {
      const settings = await fetchAcademySettings();
      const payments = expedienteRows.map((r) => r.raw).filter((p) => p && p.id);
      await exportStudentPaymentHistoryPdf(student, {
        payments,
        settings,
        year: payYear,
      }, theme);
    } catch (e) { alert(e.message || 'No se pudo generar el PDF'); }
    setExpPdfBusy(false);
  };

  const downloadOrderPdf = async (order) => {
    if (typeof exportStoreOrderReceiptPdf !== 'function') {
      alert('Módulo PDF no cargado. Recarga la página (F5).');
      return;
    }
    setOrderPdfBusy(order.id);
    try {
      const settings = await fetchAcademySettings();
      await exportStoreOrderReceiptPdf(student, order, settings, theme);
    } catch (e) { alert(e.message || 'No se pudo generar el PDF'); }
    setOrderPdfBusy(null);
  };

  if (view === 'a-pagos') {
    return (
      <div className="tecos-page-shell tecos-alumno-portal" style={{ padding: 32 }}>
        <Card theme={theme}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, marginBottom: 8, flexWrap: 'wrap' }}>
            <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 28, color: theme.text, letterSpacing: 1, margin: 0, fontWeight: 400 }}>
              CALENDARIO DE PAGOS
            </h3>
            <PaymentCalendarYearNav
              theme={theme}
              year={payYear}
              onPrev={onCalendarYearPrev}
              onNext={onCalendarYearNext}
              minYear={payYearBounds.minYear}
              maxYear={payYearBounds.maxYear}
            />
          </div>
          <p style={{ fontSize: 12, color: theme.textDim, margin: '0 0 14px', fontFamily: 'Inter, sans-serif', lineHeight: 1.5 }}>
            Desde tu mes de ingreso en adelante puedes pagar cada mensualidad.{' '}
            {billingHint
              ? `${billingHint.activeMonths} ${billingHint.advanceMonths}`
              : 'Mes en curso y atrasados: tarifa según el día del mes. Meses futuros: adelanto con tarifa base sin recargo.'}
          </p>
          <PaymentGrid theme={theme} onPayClick={onPayClick} months={months} />
          <div style={{ display: 'flex', gap: 14, marginTop: 14, flexWrap: 'wrap', fontSize: 11, color: theme.textDim, fontFamily: 'Inter, sans-serif' }}>
            <LegendDot c={theme.success} l="Pagado" />
            <LegendDot c={theme.warning} l="Pendiente / en verificación" />
            <LegendDot c={theme.danger} l="Vencido" />
            <LegendDot c={theme.urgent} l="Urgente" />
            <LegendDot c={theme.info} l="Adelanto (tarifa base)" />
            <LegendDot c={theme.primary} l="Proporcional (mes de ingreso)" />
          </div>
        </Card>
      </div>
    );
  }
  if (view === 'a-papeleria') {
    const code = student?.code;
    return (
      <div className="tecos-page-shell tecos-alumno-portal" style={{ padding: 32 }}>
        <Card theme={theme}>
          <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 28, color: theme.text, letterSpacing: 1, margin: '0 0 8px', fontWeight: 400 }}>
            MI PAPELERÍA Y EXPEDIENTE
          </h3>
          <p style={{ fontSize: 13, color: theme.textDim, margin: '0 0 20px', fontFamily: 'Inter, sans-serif', lineHeight: 1.5 }}>
            Sube cada documento solicitado por la academia (JPG, PNG, WebP o PDF, máx. 5 MB).
            La administración verá en tu ficha si ya está cargado o sigue pendiente.
          </p>
          {typeof StudentDocumentsChecklist === 'function' ? (
            <StudentDocumentsChecklist
              theme={theme}
              docs={documents}
              requirements={documentRequirements}
              canUpload={!!code}
              studentCode={code}
              onUploaded={onDocumentsUpdated}
            />
          ) : (
            <p style={{ color: theme.warning, fontSize: 13, fontFamily: 'Inter, sans-serif' }}>
              Módulo de documentos no cargado. Recarga la página (F5).
            </p>
          )}
        </Card>
      </div>
    );
  }
  if (view === 'a-expediente') {
    return (
      <div className="tecos-page-shell tecos-alumno-portal" style={{ padding: 32 }}>
        <Card theme={theme} style={{ padding: 0, overflow: 'hidden' }}>
          <div style={{ padding: '16px 20px', borderBottom: `1px solid ${theme.border}`, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: 12 }}>
            <div>
              <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 24, color: theme.text, letterSpacing: 1, margin: 0, fontWeight: 400 }}>EXPEDIENTE DE PAGOS</h3>
              <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif', marginTop: 4 }}>
                Descarga el comprobante de cada mes. Si el estado es <strong>Por pagar</strong>, el PDF lo indica así (no como pagado).
              </p>
            </div>
            <Btn theme={theme} kind="soft" icon="download" onClick={downloadExpedienteAllPdf} disabled={expPdfBusy || !expedienteRows.length}>
              {expPdfBusy ? 'Generando…' : 'Generar todos juntos (PDF)'}
            </Btn>
          </div>
          {!expedienteRows.length ? (
            <div className="tecos-page-shell" style={{ padding: 32, textAlign: 'center', color: theme.textDim, fontFamily: 'Inter, sans-serif', fontSize: 13 }}>
              Sin movimientos en el expediente.
            </div>
          ) : (
          <table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'Inter, sans-serif' }}>
            <thead><tr style={{ background: theme.bgInput }}>{['Mes', 'Concepto', 'Monto', 'Fecha pago', 'Estado', 'Comprobante'].map(h => (
              <th key={h} style={{ padding: '12px 16px', textAlign: 'left', fontSize: 10, fontWeight: 700, color: theme.textMute, letterSpacing: 1, textTransform: 'uppercase' }}>{h}</th>
            ))}</tr></thead>
            <tbody>
              {expedienteRows.map((r, i) => (
                <tr key={r.id || i} style={{ borderBottom: i < expedienteRows.length - 1 ? `1px solid ${theme.border}` : 'none' }}>
                  <td style={tdStyle(theme)}><span style={{ fontWeight: 700 }}>{r.m}</span></td>
                  <td style={tdStyle(theme)}>
                    <div>{r.c}</div>
                    {r.prorated && r.prorationLabel && (
                      <div style={{ fontSize: 11, color: theme.primary, fontWeight: 600, marginTop: 4, fontFamily: 'Inter, sans-serif' }}>
                        Cobro proporcional: {r.prorationLabel}
                      </div>
                    )}
                  </td>
                  <td style={tdStyle(theme)}>
                    {r.hideAmount ? (
                      <span style={{ fontSize: 11, color: theme.textMute, fontWeight: 600 }}>Tarifa al iniciar mes</span>
                    ) : (
                      <>
                        <span style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 20, color: theme.text, letterSpacing: 0.5 }}>
                          ${Number(r.a).toLocaleString('es-MX')}
                        </span>
                        {r.prorated && r.baseMonthly > Number(r.a) && (
                          <div style={{ fontSize: 10, color: theme.textMute, marginTop: 2, fontFamily: 'Inter, sans-serif' }}>
                            Mensualidad completa: ${Number(r.baseMonthly).toLocaleString('es-MX')}
                          </div>
                        )}
                      </>
                    )}
                  </td>
                  <td style={tdStyle(theme)}>{r.d}</td>
                  <td style={tdStyle(theme)}><StatusPill status={r.s} theme={theme} /></td>
                  <td style={tdStyle(theme)}>
                    <Btn theme={theme} kind="ghost" size="sm" icon="download"
                      onClick={() => downloadMonthPdf(r)}
                      disabled={expRowPdfBusy === r.id}>
                      {expRowPdfBusy === r.id ? '…' : 'PDF'}
                    </Btn>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
          )}
        </Card>
      </div>
    );
  }
  if (view === 'a-compras') {
    return (
      <div className="tecos-page-shell tecos-alumno-portal" style={{ padding: 32 }}>
        <Card theme={theme}>
          <h3 style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 24, color: theme.text, letterSpacing: 1, margin: 0, marginBottom: 8, fontWeight: 400 }}>MIS COMPRAS</h3>
          <p style={{ color: theme.textDim, fontSize: 13, fontFamily: 'Inter, sans-serif', margin: '0 0 16px' }}>
            Descarga el comprobante PDF de cada compra con el mismo formato que las mensualidades.
          </p>
          <div style={{ display: 'grid', gap: 10 }}>
            {orders.map((c) => (
              <div key={c.id} style={{
                display: 'flex', alignItems: 'center', gap: 14, padding: 14,
                borderRadius: 10, background: theme.bgInput, border: `1px solid ${theme.border}`,
              }}>
                <div style={{ width: 50, height: 50, borderRadius: 10, background: `${theme.primary}22`, color: theme.primary, display: 'grid', placeItems: 'center' }}>
                  <Icon name={c.i} size={22} />
                </div>
                <div style={{ flex: 1 }}>
                  <div style={{ fontWeight: 700, color: theme.text, fontFamily: 'Inter, sans-serif', fontSize: 14 }}>{c.p}</div>
                  <div style={{ fontSize: 12, color: theme.textMute, fontFamily: 'Inter, sans-serif', marginTop: 2 }}>{c.n} · {c.d}</div>
                </div>
                <div style={{ fontFamily: 'Bebas Neue, sans-serif', fontSize: 24, color: theme.text, letterSpacing: 0.5 }}>${c.a}</div>
                <StatusPill status={c.s} theme={theme} />
                <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
                  <Btn theme={theme} kind="ghost" size="sm" icon="download"
                    onClick={() => downloadOrderPdf(c)}
                    disabled={orderPdfBusy === c.id}>
                    {orderPdfBusy === c.id ? '…' : 'PDF'}
                  </Btn>
                  {(c.s === 'rechazado' || c.s === 'pendiente') && (
                    <Btn theme={theme} kind="soft" size="sm" icon="upload" onClick={() => onOrderClick?.(c)}>
                      Subir comprobante
                    </Btn>
                  )}
                  <Btn theme={theme} kind="ghost" size="sm" icon="eye" onClick={() => onOrderClick?.(c)}>Detalle</Btn>
                </div>
              </div>
            ))}
          </div>
        </Card>
      </div>
    );
  }
  if (view === 'a-eventos') {
    return (
      <>
        <CalendarioSection
          theme={theme}
          variant="portal"
          onEventClick={setPortalEventModal}
          onAnnouncementClick={setPortalAvisoModal}
          onBirthdayClick={setPortalBirthdayModal}
        />
        <EventModal open={!!portalEventModal} onClose={() => setPortalEventModal(null)} ev={portalEventModal} theme={theme} />
        <AvisoModal open={!!portalAvisoModal} onClose={() => setPortalAvisoModal(null)} aviso={portalAvisoModal} theme={theme} />
        <BirthdayCongratsModal
          open={!!portalBirthdayModal}
          onClose={() => setPortalBirthdayModal(null)}
          item={portalBirthdayModal}
          theme={theme}
          viewerStudent={student}
        />
      </>
    );
  }
  if (view === 'a-avisos') {
    return <AvisosSection theme={theme} />;
  }
  if (view === 'a-notificaciones') {
    return (
      <NotificationsFullPage
        theme={theme}
        audience="student"
        studentCode={student?.code}
        onStudentNavigate={onNotifNavigate}
      />
    );
  }

  if (view === 'a-perfil') {
    return <AlumnoPerfilSection theme={theme} student={student} onPhotoUpdated={onPhotoUpdated} />;
  }
  return null;
};

Object.assign(window, {
  AlumnoSidebar, AlumnoDashboard, AlumnoSubview, AlumnoPerfilSection,
  PaymentCalendarYearNav, getPaymentCalendarYearBounds, PaymentGrid, LegendDot,
});
