import React, { useState, useEffect, useMemo, useRef, useId } from "react";
import { Plus, Trash2, TrendingUp, TrendingDown, Users, RefreshCw, Receipt, UserPlus, X, Pencil, Settings, Camera, Calendar, History, Bell, Volume2, BellRing, Moon, Sun, ChevronDown, ChevronUp, ShieldCheck, KeyRound, LogOut, ArrowUpRight, ArrowDownRight, CircleDollarSign, Sparkles, Activity, Database, Wifi, Undo2, CreditCard, Wallet, Landmark, Banknote, Gift, Clock, Rocket, Mic, MicOff, Wand2, Bot, Lightbulb, Target, AlertTriangle, BrainCircuit, Tag, Download, Share2, FileSpreadsheet, Eye, EyeOff } from "lucide-react";

// Tipografía y fondo globales — "libro de cuentas": papel, tinta y una hoja
// de oro (verde bosque → latón) solo en los acentos y en la firma visual: la
// doble raya de los totales, tal como se subraya una suma en un libro mayor.
// Fraunces (serif con carácter, ink-traps) lleva las cifras protagonistas;
// Inter se queda en la interfaz para que siga siendo legible y neutra.
const FONTS = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700;9..144,800&display=swap');
* { box-sizing: border-box; }
html { color-scheme: light; }
body {
  font-family: 'Inter', -apple-system, 'Segoe UI', sans-serif;
  background-color: #F5F4EE;
  background-image:
    radial-gradient(1200px 600px at 50% -10%, rgba(15,74,50,0.09), transparent 60%),
    radial-gradient(900px 520px at 108% 110%, rgba(201,163,76,0.14), transparent 55%),
    radial-gradient(700px 420px at -8% 108%, rgba(15,74,50,0.05), transparent 55%);
  background-attachment: fixed;
  color: #0F172A;
  margin: 0;
  padding: 0;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
::selection { background: rgba(201,163,76,0.28); }
/* Foco visible (teclado / toque) en toda la app */
:focus-visible { outline: 2px solid rgba(15,74,50,0.65); outline-offset: 2px; border-radius: 6px; }
/* Scrollbar fina y discreta (paneles y listas) */
* { scrollbar-width: thin; scrollbar-color: rgba(100,116,139,0.45) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-thumb { background: rgba(100,116,139,0.4); border-radius: 999px; }
*::-webkit-scrollbar-track { background: transparent; }
button, input, select, textarea {
  transition: background-color .16s ease, border-color .16s ease, color .16s ease, box-shadow .18s ease, transform .14s ease, opacity .16s ease, filter .16s ease;
}
input, select, textarea { font-variant-numeric: tabular-nums; }
.cc-display { font-family: 'Fraunces', 'Iowan Old Style', serif; font-optical-sizing: auto; }
/* Firma visual: doble raya bajo un total, como el subrayado de una suma en
   un libro mayor de cuentas. Se usa solo bajo cifras que cierran una cuenta. */
.cc-ledger-total { border-bottom: 3px double currentColor; padding-bottom: 0.15rem; display: inline-block; white-space: nowrap; }
/* Ocultar badge flotante y drawer de Netlify */
#netlify-drawer,
[data-netlify-drawer],
.netlify-badge,
iframe[src*="netlify"],
iframe[title*="Netlify"],
div[class*="netlify-drawer"],
div[id*="netlify-drawer"],
div[class*="netlify-feedback"],
div[id*="netlify-feedback"],
div[class*="netlify-badge"],
div[id*="netlify-badge"] {
  display: none !important;
  visibility: hidden !important;
  opacity: 0 !important;
  pointer-events: none !important;
  width: 0 !important;
  height: 0 !important;
}
@keyframes cc-spin { to { transform: rotate(360deg); } }
@keyframes cc-fade-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes cc-pop { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
@keyframes cc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes cc-slide-in { from { opacity: 0; transform: translateY(14px) scale(0.99); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes cc-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@keyframes cc-glow { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
/* --- Personaje de la cuenta: reacciones --- */
@keyframes cc-mascot-bounce { 0%,100% { transform: translateY(0) scale(1,1); } 25% { transform: translateY(-8px) scale(0.96,1.05); } 55% { transform: translateY(0) scale(1.05,0.95); } 75% { transform: translateY(-3px) scale(1,1); } }
@keyframes cc-mascot-droop { 0%,100% { transform: translateY(0) rotate(0); } 30% { transform: translateY(5px) rotate(-2.5deg); } 65% { transform: translateY(1px) rotate(2deg); } }
@keyframes cc-tear-fall { 0% { transform: translate(0,0) scale(0.5); opacity: 0; } 12% { opacity: 1; transform: translate(0,2px) scale(0.9); } 100% { transform: translate(-3px,42px) scale(1.05); opacity: 0; } }
@keyframes cc-sparkle { 0%,100% { opacity: 0; transform: scale(0.4) rotate(0deg); } 50% { opacity: 1; transform: scale(1) rotate(25deg); } }
@keyframes cc-bubble-in { 0% { opacity: 0; transform: translateY(-100%) translateY(8px) scale(0.92); } 70% { opacity: 1; transform: translateY(-100%) translateY(-2px) scale(1.02); } 100% { opacity: 1; transform: translateY(-100%) translateY(0) scale(1); } }
@keyframes cc-heart-up { 0% { opacity: 0; transform: translateY(6px) scale(0.5); } 18% { opacity: 1; transform: translateY(0) scale(1.15); } 100% { opacity: 0; transform: translateY(-28px) scale(0.85); } }
@keyframes cc-confetti-fall { 0% { opacity: 0; transform: translateY(-10px) rotate(0deg); } 15% { opacity: 1; } 100% { opacity: 0; transform: translateY(54px) rotate(220deg); } }
@keyframes cc-rain-drop { 0% { opacity: 0; transform: translateY(0) scale(0.7); } 25% { opacity: 1; } 100% { opacity: 0; transform: translateY(30px) scale(1); } }
@keyframes cc-cloud-drift { 0%,100% { transform: translateX(0); } 50% { transform: translateX(-4px); } }
@keyframes cc-idle-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
@keyframes cc-mascot-greet { 0% { opacity: 0; transform: scale(0.4) rotate(-8deg); } 45% { opacity: 1; transform: scale(1.12) rotate(3deg); } 65% { transform: scale(0.97, 0.94) rotate(-1deg); } 80% { transform: scale(1.04, 1.02) rotate(1deg); } 100% { opacity: 1; transform: scale(1) rotate(0deg); } }
/* Baile de bienvenida del personaje en la tarjeta (escritorio): primero el pop
   (como cc-mascot-greet) y después una sacudida alegre de lado a lado con giros
   y rebotes, sincronizada con el arpegio del tono de bienvenida. */
@keyframes cc-mascot-dance { 0% { opacity: 0; transform: scale(0.4) rotate(-8deg); } 18% { opacity: 1; transform: scale(1.12) rotate(3deg); } 28% { transform: scale(0.97, 0.94) rotate(-1deg); } 36% { transform: scale(1.04, 1.02) rotate(1deg); } 42% { transform: scale(1) rotate(0deg); } 50% { transform: scale(1.08, 0.94) rotate(-10deg); } 58% { transform: scale(0.96, 1.06) rotate(10deg); } 66% { transform: scale(1.06, 0.96) rotate(-8deg); } 74% { transform: scale(0.97, 1.04) rotate(8deg); } 82% { transform: scale(1.03, 0.98) rotate(-5deg); } 90% { transform: scale(0.99, 1.01) rotate(4deg); } 100% { opacity: 1; transform: scale(1) rotate(0deg); } }
/* Entrada de la tarjeta de saldo al abrir la app en escritorio (sube y aparece) */
@keyframes cc-card-enter { from { opacity: 0; transform: translateY(16px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
.cc-card-enter { animation: cc-card-enter .55s cubic-bezier(.2,.7,.3,1) both; }
/* Versión suave del pop para móvil: más pequeño y sutil (solo el personaje, sin
   animar la tarjeta). Menos rebote y menos giro que cc-mascot-greet. */
@keyframes cc-mascot-greet-soft { 0% { opacity: 0; transform: scale(0.7) rotate(-3deg); } 50% { opacity: 1; transform: scale(1.05) rotate(1.5deg); } 75% { transform: scale(0.98); } 100% { opacity: 1; transform: scale(1) rotate(0deg); } }
/* Baile suave de bienvenida para móvil: pop pequeño y una sacudida alegre más
   discreta (menos giro y menos rebote que cc-mascot-dance). */
@keyframes cc-mascot-dance-soft { 0% { opacity: 0; transform: scale(0.7) rotate(-3deg); } 16% { opacity: 1; transform: scale(1.05) rotate(1.5deg); } 26% { transform: scale(0.98); } 34% { transform: scale(1) rotate(0deg); } 42% { transform: scale(1.03, 0.97) rotate(-6deg); } 50% { transform: scale(0.98, 1.03) rotate(6deg); } 58% { transform: scale(1.02, 0.98) rotate(-5deg); } 66% { transform: scale(0.99, 1.02) rotate(5deg); } 74% { transform: scale(1.01, 0.99) rotate(-3deg); } 86% { transform: scale(1) rotate(2deg); } 100% { opacity: 1; transform: scale(1) rotate(0deg); } }
.cc-rise { animation: cc-fade-up .35s ease both; }
.cc-pop { animation: cc-pop .25s ease both; }
.cc-slide { animation: cc-slide-in .28s cubic-bezier(.2,.7,.3,1) both; }
/* Entrada de la hoja modal de anotar movimiento: se desliza desde abajo del
   todo (translateY 100%) con la misma curva suave del resto de la app. */
@keyframes cc-sheet-in { from { opacity: 0; transform: translateY(100%); } to { opacity: 1; transform: translateY(0); } }
.cc-sheet-in { animation: cc-sheet-in .45s cubic-bezier(.2,.7,.3,1) both; }
/* Entrada de los paneles desplegables (Ajustes): suben un tramo y aparecen. */
@keyframes cc-panel-in { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: translateY(0); } }
.cc-panel-in { animation: cc-panel-in .5s cubic-bezier(.2,.7,.3,1) both; }
/* Fondo de la hoja modal: solo fundido (sin desplazamiento). */
@keyframes cc-fade-in { from { opacity: 0; } to { opacity: 1; } }
.cc-fade-in { animation: cc-fade-in .25s ease both; }
.cc-card-hover { transition: transform .18s ease, box-shadow .2s ease, border-color .2s ease; }
.cc-card-hover:hover { transform: translateY(-3px) !important; box-shadow: 0 20px 44px -18px rgb(2 6 23 / 0.28) !important; border-color: rgba(201,163,76,0.35) !important; }
/* Filas de movimientos: tinte suave al pasar el cursor */
.dotted-row { transition: background-color .25s ease, border-radius .25s ease, box-shadow .25s ease; }
.dotted-row:hover { background-color: rgba(201,163,76,0.08); border-radius: 0.6rem; }
/* Fila resaltada al venir de una notificación del sistema (click → su fila) */
.cc-tx-highlight { background-color: rgba(201,163,76,0.20) !important; box-shadow: 0 0 0 3px rgba(201,163,76,0.55) !important; }
.cc-btn-press:active { transform: scale(0.96) !important; filter: brightness(0.97) !important; }
/* Tooltip de los mini personajes del historial: explica la reacción al pasar el cursor.
   Anclado al borde IZQUIERDO de la miniatura (no centrado): la miniatura está pegada al
   borde del panel (que recorta con overflow:hidden) y un tooltip centrado se cortaría. */
.cc-mascot-wrap { position: relative; cursor: help; }
.cc-mascot-wrap .cc-mascot-tip { position: absolute; bottom: calc(100% + 5px); left: 0; transform: translateY(3px); background: #0F172A; color: #FFFFFF; font-family: 'Inter', sans-serif; font-size: 0.62rem; font-weight: 600; line-height: 1.25; white-space: nowrap; padding: 0.3rem 0.55rem; border-radius: 0.5rem; box-shadow: 0 8px 18px -8px rgb(0 0 0 / 0.45); opacity: 0; pointer-events: none; transition: opacity .15s ease, transform .15s ease; z-index: 12; }
.cc-mascot-wrap .cc-mascot-tip::after { content: ""; position: absolute; top: 100%; left: 14px; transform: translateX(-50%); border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #0F172A; }
.cc-mascot-wrap:hover .cc-mascot-tip { opacity: 1; transform: translateY(0); }
/* Accesibilidad: si el sistema pide menos movimiento (prefers-reduced-motion:
   reduce), todas las animaciones de entrada (hoja del modal, paneles, lightbox,
   pops del personaje, tarjeta de saldo…) y las transiciones se colapsan a un
   instante: el contenido aparece directamente, sin deslizarse, fundirse ni
   rebotar. Los tiempos se reducen a 0.01 ms en vez de poner animation:none
   para que los estados finales (fill-mode both) se apliquen igualmente. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
`;

// Precarga las fuentes para que no bloqueen el render (compatible con Claude Artifacts)
const FONTS_LINKS = (
  <>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
    {/* Tailwind: activa las utilidades (w-full, flex, gap…) que ya usa la app.
        Si el CDN no responde, los estilos inline siguen funcionando igual.
        Forma autocerrada a propósito: un cierre de script literal dentro del
        bloque babel inline de test-rename.html cortaría el script y rompería
        la copia, así que este comentario tampoco puede contener esa secuencia. */}
    <script src="https://cdn.tailwindcss.com" />
    <style>{FONTS}</style>
  </>
);

// ---- Paleta (claro por defecto; se alterna a oscuro desde Ajustes) ----
// Se lee con getters en cada render: al cambiar darkMode y re-renderizar,
// toda la app se repinta sin tocar cientos de estilos.
// Verde bosque + latón: el verde sigue marcando "dinero que entra" en las
// cifras (convención que no conviene romper), pero la identidad de marca
// (logo, hero, botones principales) pasa de esmeralda-cielo a un verde de
// tinta de contable con acentos de hoja de oro.
let darkMode = false;
const C = {
  get paper() { return darkMode ? "#1E251F" : "#FFFFFF"; },
  get paper2() { return darkMode ? "#26302A" : "#FBFAF5"; },
  get ink() { return darkMode ? "#F1F0E8" : "#161A16"; },
  get inkSoft() { return darkMode ? "#9CA69C" : "#5B6459"; },
  get inkFaint() { return darkMode ? "#6B756A" : "#98A096"; },
  get red() { return darkMode ? "#F0897C" : "#B3392B"; },
  get green() { return darkMode ? "#4FAE84" : "#0F5132"; },
  get gold() { return darkMode ? "#E4C077" : "#B4863A"; },
  get violet() { return darkMode ? "#A78BFA" : "#8B5CF6"; },
  get line() { return darkMode ? "#37413A" : "#E4E1D6"; },
  get lineSoft() { return darkMode ? "#2C362F" : "#EFEDE3"; },
  get bg() { return darkMode ? "#12160F" : "#F5F4EE"; },
  get field() { return darkMode ? "#232C24" : "#F1EFE5"; },
  get dangerBg() { return darkMode ? "#3A1712" : "#FBEEEA"; },
  get successBg() { return darkMode ? "#0F2A1E" : "#EAF3EC"; },
  get warningBg() { return darkMode ? "#332608" : "#FBF0DA"; },
  get chipBg() { return darkMode ? "#26302A" : "#F1EFE5"; },
  get ring() { return darkMode ? "rgba(228,192,119,0.55)" : "rgba(180,134,58,0.55)"; },
  get cardShadow() { return darkMode ? "0 14px 34px -16px rgb(0 0 0 / 0.55)" : "0 14px 32px -18px rgb(22 26 22 / 0.14)"; },
  get overlay() { return darkMode ? "rgba(8,10,7,0.75)" : "rgba(22,26,22,0.45)"; },
};
// Gradiente de marca (verde bosque → latón, "hoja de oro sobre tinta"): solo
// en el hero, el logo y las acciones principales. El resto de la app se
// mantiene en tinta sobre papel.
const BRAND_GRADIENT = "linear-gradient(135deg, #0F5132 0%, #B4863A 100%)";
const BRAND_GRADIENT_DEEP = "linear-gradient(135deg, #0B3D26 0%, #1F5C3F 55%, #C9A34C 100%)";
function setDarkMode(v) {
  darkMode = !!v;
  if (typeof document !== "undefined") {
    const bg = darkMode ? "#12160F" : "#F5F4EE";
    const fg = darkMode ? "#F1F0E8" : "#161A16";
    document.documentElement.style.backgroundColor = bg;
    document.documentElement.style.setProperty("--cc-bg", bg);
    document.body.style.background = bg;
    document.body.style.backgroundColor = bg;
    document.body.style.color = fg;
    document.documentElement.style.colorScheme = darkMode ? "dark" : "light";
    try {
      const meta = document.querySelector('meta[name="theme-color"]');
      if (meta) meta.setAttribute("content", bg);
    } catch (e) {}
  }
}

// Interruptor tipo switch de los Ajustes: mismo comportamiento que un checkbox
// pero con aspecto táctil moderno. Tema-aware: usa C y darkMode en cada render.
function Toggle({ on, onToggle, label, describedBy }) {
  return (
    <button
      type="button"
      role="switch"
      aria-checked={!!on}
      aria-label={label}
      aria-describedby={describedBy}
      title={label}
      onClick={() => onToggle(!on)}
      style={{
        width: "2.7rem",
        height: "1.55rem",
        borderRadius: "999px",
        border: "none",
        cursor: "pointer",
        padding: 0,
        position: "relative",
        flexShrink: 0,
        outline: "none",
        background: on ? BRAND_GRADIENT : darkMode ? "#475569" : "#CBD5E1",
        boxShadow: "inset 0 1px 2px rgba(0,0,0,0.18)",
        transition: "background 0.2s ease",
      }}
    >
      <span
        style={{
          position: "absolute",
          top: "0.15rem",
          left: on ? "1.3rem" : "0.15rem",
          width: "1.25rem",
          height: "1.25rem",
          borderRadius: "50%",
          background: "#FFFFFF",
          boxShadow: "0 1px 3px rgba(0,0,0,0.3)",
          transition: "left 0.2s ease",
        }}
      />
    </button>
  );
}

// Categorías completas por defecto con sus emojis e iconos
const DEFAULT_CATEGORIES = [
  "Restaurantes y Comida",
  "Supermercado",
  "Transporte",
  "Taxis",
  "Servicios del Hogar",
  "Salud y Farmacia",
  "Mascotas",
  "Entretenimiento y Ocio",
  "Ropa y Compras",
  "Educación",
  "Viajes",
  "Gimnasio y Deporte",
  "Hogar",
  "Varios",
];

const DEFAULT_CATEGORY_ICONS = {
  "Restaurantes y Comida": "🍽️",
  "Comida": "🍔",
  "Supermercado": "🛒",
  "Transporte": "🚕",
  "Taxis": "🚖",
  "Gasolina": "⛽",
  "Servicios del Hogar": "💡",
  "Salud y Farmacia": "💊",
  "Mascotas": "🐾",
  "Entretenimiento y Ocio": "🎬",
  "Ropa y Compras": "🛍️",
  "Educación": "📚",
  "Viajes": "✈️",
  "Gimnasio y Deporte": "🏋️",
  "Hogar": "🏠",
  "Trabajo": "💼",
  "Varios": "📦",
};

const categoryIconCache = new Map();
function getCategoryIcon(catName) {
  if (!catName) return "🏷️";
  const key = String(catName).trim().toLowerCase();
  if (categoryIconCache.has(key)) return categoryIconCache.get(key);

  const norm = removeAccents(key);
  let icon = "🏷️";

  for (const [k, ic] of Object.entries(DEFAULT_CATEGORY_ICONS)) {
    if (norm === removeAccents(k).toLowerCase() || norm.includes(removeAccents(k).toLowerCase())) {
      icon = ic;
      break;
    }
  }

  if (icon === "🏷️") {
    if (/(taxi|uber|cabify|didi|remis)/i.test(norm)) icon = "🚖";
    else if (/(perro|gato|mascota|veterinaria|pet|croqueta)/i.test(norm)) icon = "🐾";
    else if (/(viaje|vuelo|hotel|aeropuerto|turismo|avion)/i.test(norm)) icon = "✈️";
    else if (/(cafe|café|starbucks|desayuno|almuerzo|cena|pizza|pollo|hamburguesa)/i.test(norm)) icon = "🍽️";
    else if (/(luz|agua|internet|telefono|celular|gas|cable|casa|depa|hogar)/i.test(norm)) icon = "💡";
    else if (/(gym|gimnasio|deporte|fitness|futbol|ciclismo)/i.test(norm)) icon = "🏋️";
    else if (/(curso|estudio|universidad|colegio|libro|clase|taller)/i.test(norm)) icon = "📚";
    else if (/(cine|netflix|spotify|juego|fiesta|bar|cerveza|disco|ocio)/i.test(norm)) icon = "🎬";
    else if (/(farmacia|medicina|doctor|clinica|salud|pastilla)/i.test(norm)) icon = "💊";
    else if (/(super|mercado|bodega|abarrotes)/i.test(norm)) icon = "🛒";
    else if (/(ropa|zapatilla|pantalon|camisa|mall|tienda)/i.test(norm)) icon = "🛍️";
  }

  categoryIconCache.set(key, icon);
  return icon;
}

function cleanCategoryName(rawName) {
  if (!rawName) return "";
  let name = String(rawName).trim();
  // Cortar en conjunciones 'y', 'luego', 'después' o disparadores de gasto
  name = name.split(/\s+\b(?:y\b|luego|despu[eé]s|con\s+(?:el\s+)?gasto|por\s+un\s+valor|de\s+\d+|por\s+\d+)/i)[0];
  name = name.split(/\b(?:agregues?|agregar|agrega|anotes?|anotar|anota|pon|poner|registra|registrar|sumes?|sumar)\b/i)[0];
  name = name.replace(/\b(por favor|gracias|plis|para los|para las|para el|para la|para|de los|de las|de el|de la|de|llamada|llamado|con nombre|titulada|sobre|los|las|el|la)\b/gi, " ").replace(/\s+/g, " ").trim();
  if (!name) return "";
  name = name.replace(/[.,;:\-_!¡?¿]/g, "").trim();
  // Limitar a máximo 3 palabras para evitar oraciones enteras como categoría
  const words = name.split(/\s+/).filter(Boolean).slice(0, 3).join(" ");
  if (!words) return "";
  return words.charAt(0).toUpperCase() + words.slice(1);
}

// Cabecera común de los paneles: icono en la tarjeta de marca (gradiente con
// anillo interior), título en negrita, subtítulo y una acción a la derecha
// (cerrar con X o abrir/cerrar con chevron). La comparten el modal de anotar
// movimiento, Ajustes, el historial y los avisos de duplicados y huérfanos,
// para que todas tengan la misma identidad y los ajustes se hagan en un solo
// sitio.
//  - icon / title / subtitle: contenido de cada parte.
//  - subtitleColor: color del subtítulo (por defecto C.inkSoft; los avisos
//    tintados pasan su propio tono).
//  - onClick (+ onClickLabel): si se pasa, el bloque título+subtítulo es un
//    botón (el historial lo usa para abrir/cerrar al pulsarlo).
//  - onAction / actionLabel / actionTitle / actionIcon: botón de la derecha
//    (X para cerrar, chevron para abrir/cerrar).
//  - actionBg / actionColor / actionBorder: estilo del botón de la derecha.
//  - className / style / titleStyle / subtitleStyle / tileStyle: ajustes finos.
function PanelHeader({
  icon,
  title,
  subtitle,
  subtitleColor,
  onClick,
  onClickLabel,
  expanded,
  onAction,
  actionLabel,
  actionTitle,
  actionIcon,
  actionBg,
  actionColor,
  actionBorder,
  className,
  style,
  titleStyle,
  subtitleStyle,
  tileStyle,
}) {
  const tile = (
    <div
      style={{
        width: "2.4rem",
        height: "2.4rem",
        borderRadius: "0.8rem",
        background: BRAND_GRADIENT,
        color: "#FFFFFF",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        flexShrink: 0,
        boxShadow: "0 8px 16px -8px rgba(15,81,50,0.55), inset 0 0 0 1px rgba(255,255,255,0.18)",
        ...tileStyle,
      }}
    >
      {icon}
    </div>
  );
  const text = (
    <>
      <p style={{ margin: 0, fontFamily: "'Inter', sans-serif", color: C.ink, fontWeight: 700, fontSize: "1rem", ...titleStyle }}>
        {title}
      </p>
      <p style={{ margin: "0.1rem 0 0", fontFamily: "'Inter', sans-serif", color: subtitleColor || C.inkSoft, fontSize: "0.72rem", ...subtitleStyle }}>
        {subtitle}
      </p>
    </>
  );
  return (
    <div className={className} style={{ display: "flex", alignItems: "center", gap: "0.75rem", ...style }}>
      {tile}
      {onClick ? (
        <button
          type="button"
          onClick={onClick}
          aria-label={onClickLabel}
          aria-expanded={expanded}
          style={{ flex: 1, minWidth: 0, textAlign: "left", background: "none", border: "none", padding: 0, cursor: "pointer" }}
          className="cc-btn-press"
        >
          {text}
        </button>
      ) : (
        <div style={{ flex: 1, minWidth: 0 }}>{text}</div>
      )}
      {onAction && (
        <button
          type="button"
          onClick={onAction}
          aria-label={actionLabel}
          aria-expanded={expanded}
          title={actionTitle || actionLabel}
          style={{
            fontFamily: "'Inter', sans-serif",
            background: actionBg || C.chipBg,
            border: actionBorder ? `1px solid ${C.line}` : "none",
            borderRadius: "0.6rem",
            width: "2.1rem",
            height: "2.1rem",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            cursor: "pointer",
            color: actionColor || C.inkSoft,
            flexShrink: 0,
          }}
          className="cc-btn-press"
        >
          {actionIcon}
        </button>
      )}
    </div>
  );
}
// Color estable de cada persona (derivado de su nombre: el mismo en todos los
// dispositivos sin necesidad de guardarlo).
const PERSON_COLORS = ["#10B981", "#3B82F6", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899", "#14B8A6", "#F97316", "#6366F1", "#84CC16"];
function personColor(name) {
  const n = String(name || "").trim();
  if (!n) return PERSON_COLORS[0];
  let h = 0;
  for (let i = 0; i < n.length; i++) h = (h * 31 + n.charCodeAt(i)) >>> 0;
  return PERSON_COLORS[h % PERSON_COLORS.length];
}

// Máximo de personas en la cuenta (coincide con db.js)
const MAX_PEOPLE = 20;

function uid() {
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}

function todayISO() {
  // Fecha local (no UTC): evita que a primera hora del día aparezca el día anterior
  const d = new Date();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${d.getFullYear()}-${m}-${day}`;
}

// ---- 1. Exportación Universal a CSV compatible con Microsoft Excel y Google Sheets ----
function exportTransactionsToCsv(transactions, currency, accountName, filterMonth, onlyPerson) {
  let filtered = filterMonth
    ? (transactions || []).filter(t => t.date && t.date.slice(0, 7) === filterMonth)
    : (transactions || []);

  if (onlyPerson && String(onlyPerson).trim()) {
    const targetPerson = String(onlyPerson).trim().toLowerCase();
    filtered = filtered.filter(t => (t.person && t.person.trim().toLowerCase() === targetPerson) || (t.recipient && t.recipient.trim().toLowerCase() === targetPerson));
  }

  const headers = ["Fecha", "Tipo", "Descripción", "Monto", "Moneda", "Categoría", "Persona", "Medio de Pago", "Tarjeta/Cuota", "Nota"];
  
  const rows = filtered.map(t => {
    const typeLabel = t.type === "gasto" ? "Gasto" : t.type === "ingreso" ? "Ingreso" : t.type === "pago_tarjeta" ? "Pago Tarjeta" : "Movimiento";
    const paymentLabel = t.paymentMethod || (t.cardId ? "Tarjeta de Crédito" : "Efectivo/Transferencia");
    const extraLabel = t.cardId ? `Tarjeta: ${t.cardId}` : t.installmentId ? `Cuota: ${t.installmentId}` : "";
    const noteClean = (t.note || "").replace(/"/g, '""');
    const descClean = (t.description || "").replace(/"/g, '""');

    return [
      t.date || "",
      typeLabel,
      `"${descClean}"`,
      (Number(t.amount) || 0).toFixed(2),
      t.currency || currency || "S/",
      t.category || "Varios",
      t.person || "",
      paymentLabel,
      extraLabel,
      `"${noteClean}"`
    ].join(";");
  });

  const csvContent = "\uFEFF" + [headers.join(";"), ...rows].join("\r\n");
  const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  const userTag = onlyPerson ? `mis_datos_${onlyPerson.toLowerCase().replace(/\s+/g, "_")}_` : "";
  const filename = `${userTag}${(accountName || "cuenta_compartida").toLowerCase().replace(/\s+/g, "_")}_${filterMonth || "historico"}_${new Date().toISOString().slice(0, 10)}.csv`;
  link.setAttribute("href", url);
  link.setAttribute("download", filename);
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

// ---- 2. Generador de Resumen Formateado para WhatsApp ----
function generateWhatsAppSummary(monthKey, monthLabel, transactions, people, totals, peerBalances, currency, accountName) {
  const currentMonthTx = (transactions || []).filter(t => t.date && t.date.slice(0, 7) === monthKey);
  const totalGastos = currentMonthTx.filter(t => t.type === "gasto").reduce((sum, t) => sum + (Number(t.amount) || 0), 0);
  const totalIngresos = currentMonthTx.filter(t => t.type === "ingreso").reduce((sum, t) => sum + (Number(t.amount) || 0), 0);

  let msg = `📊 *Resumen Mensual — ${accountName || "Cuenta compartida"}*\n`;
  msg += `🗓️ *Período:* ${monthLabel || monthKey}\n\n`;
  msg += `🔴 *Total Gastos:* ${totalGastos.toFixed(2)} ${currency}\n`;
  if (totalIngresos > 0) msg += `🟢 *Total Ingresos:* ${totalIngresos.toFixed(2)} ${currency}\n`;
  msg += `\n👥 *Gastos por persona:*\n`;

  (people || []).forEach(p => {
    const personSpent = currentMonthTx.filter(t => t.type === "gasto" && t.person === p).reduce((sum, t) => sum + (Number(t.amount) || 0), 0);
    msg += `• ${p}: ${personSpent.toFixed(2)} ${currency}\n`;
  });

  if (Array.isArray(peerBalances) && peerBalances.length > 0) {
    msg += `\n🤝 *Estado de Balances:*\n`;
    peerBalances.forEach(b => {
      if (b && b.from && b.to && b.amount > 0.01) {
        msg += `• ${b.from} le debe a ${b.to}: ${Number(b.amount).toFixed(2)} ${currency}\n`;
      }
    });
  } else {
    msg += `\n✨ *¡Cuentas al día! Todo saldado.*\n`;
  }

  msg += `\n📱 _Generado desde Cuenta Compartida App_`;
  return msg;
}

// ---- 3. Algoritmo de Simplificación de Deudas (Min-Cash Flow) ----
function calculateMinimalTransfers(netBalances, currency) {
  const debtors = [];
  const creditors = [];

  Object.entries(netBalances || {}).forEach(([person, net]) => {
    const val = Number(net) || 0;
    if (val < -0.01) debtors.push({ person, amount: Math.abs(val) });
    else if (val > 0.01) creditors.push({ person, amount: val });
  });

  debtors.sort((a, b) => b.amount - a.amount);
  creditors.sort((a, b) => b.amount - a.amount);

  const transfers = [];
  let dIdx = 0;
  let cIdx = 0;

  while (dIdx < debtors.length && cIdx < creditors.length) {
    const debtor = debtors[dIdx];
    const creditor = creditors[cIdx];
    const transferAmount = Math.min(debtor.amount, creditor.amount);

    if (transferAmount > 0.01) {
      transfers.push({
        from: debtor.person,
        to: creditor.person,
        amount: Number(transferAmount.toFixed(2)),
        currency: currency || "S/",
      });
    }

    debtor.amount -= transferAmount;
    creditor.amount -= transferAmount;

    if (debtor.amount < 0.01) dIdx++;
    if (creditor.amount < 0.01) cIdx++;
  }

  return transfers;
}

function fmtMoney(n, currency = "S/") {
  const sign = n < 0 ? "-" : "";
  return sign + Math.abs(n).toFixed(2).replace(".", ",") + " " + currency;
}

// Monedas rápidas al anotar un movimiento (la base es la de los ajustes).
const CURRENCY_OPTIONS = ["S/", "$", "€", "£", "¥"];

// Moneda de un movimiento: la suya si la tiene, si no la base de la cuenta.
function txCurrency(t, base) {
  const cur = t && t.currency ? String(t.currency).trim() : "";
  return cur || base || "S/";
}

// Convierte un movimiento a la moneda base (S/ por defecto). Si es en la base
// o no tiene tipo de cambio válido, se usa el importe tal cual.
function txToBase(t, base) {
  const amount = Number(t && t.amount) || 0;
  if (txCurrency(t, base) === (base || "S/")) return amount;
  const rate = Number(t && t.rate);
  if (isFinite(rate) && rate > 0) return amount * rate;
  return amount;
}

// Valor FIRMADO de un movimiento en la moneda base: positivo para un ingreso
// y negativo para un gasto (lo que el movimiento aporta o resta al saldo). Lo
// usan el personaje (reacción al deshacer) y el historial de acciones.
function signedTx(t, base) {
  return t ? (t.type === "gasto" ? -txToBase(t, base) : txToBase(t, base)) : 0;
}

// ---- Tipo de cambio automático ----
// Código ISO de las monedas rápidas para consultar el tipo de cambio en la API.
const CURRENCY_CODES = {
  "S/": "PEN",
  "S/.": "PEN",
  "PEN": "PEN",
  "SOLES": "PEN",
  "SOL": "PEN",
  "$": "USD",
  "USD": "USD",
  "US$": "USD",
  "DOLARES": "USD",
  "DOLAR": "USD",
  "€": "EUR",
  "EUR": "EUR",
  "EUROS": "EUR",
  "EURO": "EUR",
  "£": "GBP",
  "GBP": "GBP",
  "¥": "JPY",
  "JPY": "JPY",
  "CLP": "CLP",
  "COP": "COP",
  "MXN": "MXN",
  "MX$": "MXN",
  "BRL": "BRL",
  "R$": "BRL",
  "ARS": "ARS",
  "BOB": "BOB",
};

function toIsoCurrency(sym) {
  if (!sym) return null;
  const s = String(sym).trim();
  const up = s.toUpperCase();
  if (CURRENCY_CODES[s]) return CURRENCY_CODES[s];
  if (CURRENCY_CODES[up]) return CURRENCY_CODES[up];
  if (/^[A-Z]{3}$/.test(up)) return up;
  return null;
}

// Caché en memoria con caducidad para no repetir llamadas a la API.
let rateCache = {};
const RATE_CACHE_TTL = 30 * 60 * 1000; // 30 minutos

function fetchWithTimeout(url, ms) {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), ms);
  return fetch(url, { signal: ctrl.signal }).finally(() => clearTimeout(t));
}

// Devuelve a cuánto equivale 1 unidad de fromSym en la moneda toSym
// (ej. 1 $ = 3,35 S/). Primero intenta open.er-api.com (gratis, sin clave,
// con CORS) y si falla usa el respaldo de cdn.jsdelivr.net.
// Devuelve null si no hay código conocido o si es la misma moneda.
async function fetchRate(fromSym, toSym) {
  const from = toIsoCurrency(fromSym);
  const to = toIsoCurrency(toSym);
  if (!from || !to || from === to) return null;
  const key = from + "->" + to;
  const hit = rateCache[key];
  if (hit && Date.now() - hit.at < RATE_CACHE_TTL) return hit.rate;
  const attempts = [
    async () => {
      const res = await fetchWithTimeout("https://open.er-api.com/v6/latest/" + from, 6000);
      const j = await res.json();
      return Number(j && j.rates && j.rates[to]);
    },
    async () => {
      const res = await fetchWithTimeout(
        "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/" + from.toLowerCase() + ".json",
        6000
      );
      const j = await res.json();
      const inner = j && j[from.toLowerCase()];
      return Number(inner && inner[to.toLowerCase()]);
    },
  ];
  let lastErr;
  for (const fn of attempts) {
    try {
      const r = await fn();
      if (isFinite(r) && r > 0) {
        rateCache[key] = { at: Date.now(), rate: r };
        return r;
      }
      lastErr = new Error("tipo de cambio no válido");
    } catch (e) {
      lastErr = e;
    }
  }
  throw lastErr || new Error("sin tipo de cambio");
}

// ---- Foto del recibo ----
// Comprime una imagen (la que elige el usuario en el formulario) a un JPEG en
// miniatura (data URL). Mantenerla pequeña es clave: así cabe en la nube sin
// saturar la base de datos ni el localStorage, y se sincroniza igual de rápido
// que un texto. Devuelve una promesa con el data URL o lanza un Error.
const PHOTO_MAX_SIDE = 900; // px por lado como máximo
const PHOTO_QUALITY = 0.6;
function fileToCompressedDataUrl(file) {
  return new Promise((resolve, reject) => {
    if (!file || !/^image\//.test(file.type)) {
      reject(new Error("Solo se permiten imágenes."));
      return;
    }
    const reader = new FileReader();
    reader.onerror = () => reject(new Error("No se pudo leer el archivo."));
    reader.onload = () => {
      const img = new Image();
      img.onerror = () => reject(new Error("La imagen no es válida."));
      img.onload = () => {
        const scale = Math.min(1, PHOTO_MAX_SIDE / Math.max(img.width, img.height));
        const w = Math.max(1, Math.round(img.width * scale));
        const h = Math.max(1, Math.round(img.height * scale));
        const canvas = document.createElement("canvas");
        canvas.width = w;
        canvas.height = h;
        try {
          canvas.getContext("2d").drawImage(img, 0, 0, w, h);
        } catch (e) {
          reject(new Error("No se pudo procesar la imagen."));
          return;
        }
        resolve(canvas.toDataURL("image/jpeg", PHOTO_QUALITY));
      };
      img.src = reader.result;
    };
    reader.readAsDataURL(file);
  });
}

// ---- Notificaciones push (Web Push: avisan aunque la app esté cerrada) ----
// Convierte la clave VAPID (base64url) a un Uint8Array como espera el navegador.
function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
  const raw = atob(base64);
  const arr = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
  return arr;
}

// Suscribe ESTE dispositivo al push (solo si hay service worker y clave VAPID).
// Devuelve la suscripción o null si no se puede (sin permiso o sin soporte).
async function subscribePush() {
  if (!HAS_PUSH) return null;
  try {
    const reg = await navigator.serviceWorker.getRegistration();
    if (!reg || !reg.pushManager) return null;
    let sub = await reg.pushManager.getSubscription();
    if (!sub) {
      sub = await reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
      });
    }
    return sub;
  } catch (e) {
    return null;
  }
}

// Guarda (o renueva) la suscripción de este dispositivo en la nube.
async function savePushSubscription(subscription, myName) {
  if (!HAS_SUPABASE || !subscription || !subscription.endpoint) return;
  try {
    const body = { endpoint: subscription.endpoint, keys: subscription.toJSON().keys, user_name: myName || "" };
    await fetch(SUPABASE_URL + "/rest/v1/push_subscriptions", {
      method: "POST",
      headers: { "Content-Type": "application/json", apikey: SUPABASE_KEY, Authorization: "Bearer " + SUPABASE_KEY, Prefer: "resolution=merge-duplicates" },
      body: JSON.stringify(body),
    });
  } catch (e) {
    // Se reintentará la próxima vez; sin esto el push local sigue funcionando.
  }
}

// Quita de la nube la suscripción de este dispositivo.
async function deletePushSubscription(subscription) {
  if (!HAS_SUPABASE || !subscription || !subscription.endpoint) return;
  try {
    await fetch(SUPABASE_URL + "/rest/v1/push_subscriptions?endpoint=eq." + encodeURIComponent(subscription.endpoint), {
      method: "DELETE",
      headers: { apikey: SUPABASE_KEY, Authorization: "Bearer " + SUPABASE_KEY },
    });
  } catch (e) {
    /* no pasa nada */
  }
}

// Reclama el aviso de un cambio en la nube (tabla push_log, dedupe con el
// Database Webhook de Supabase). Cuando la app escribe algo, reclama su clave
// ANTES de que llegue el webhook: así el webhook ve que ya hay aviso y no
// duplica la notificación. Si la app muere justo después de escribir (o el
// cambio viene de otra vía), no hay reclamo y el webhook avisa él solo.
// La clave es la misma que usa la Edge Function: el id del movimiento o
// "people:" + nombre. Prefer ignore-duplicates: reclamar dos veces es inocuo.
async function claimPushLocal(key) {
  if (!HAS_SUPABASE || !key) return;
  try {
    await fetch(SUPABASE_URL + "/rest/v1/push_log", {
      method: "POST",
      headers: { "Content-Type": "application/json", apikey: SUPABASE_KEY, Authorization: "Bearer " + SUPABASE_KEY, Prefer: "resolution=ignore-duplicates" },
      body: JSON.stringify({ tx_id: String(key) }),
    });
  } catch (e) {
    // Sin internet o sin la tabla (schema aún no ejecutado): el webhook, si
    // está activo, avisará por su cuenta.
  }
}

// Pide a la Edge Function de Supabase que avise a los demás dispositivos (los
// que no son el que acaba de hacer el cambio). Si no hay función desplegada o
// no hay clave VAPID, no hace nada (la app sigue igual).
async function notifyOthers(senderName, message, undoId) {
  if (!HAS_SUPABASE || !VAPID_PUBLIC_KEY) return;
  try {
    let senderEndpoint = "";
    try {
      const reg = await navigator.serviceWorker.getRegistration();
      if (reg && reg.pushManager) {
        const sub = await reg.pushManager.getSubscription();
        if (sub) senderEndpoint = sub.endpoint;
      }
    } catch (e) {
      /* sin service worker */
    }
    const activeAccId = getActiveAccountId();
    await fetch(SUPABASE_URL + "/functions/v1/notify", {
      method: "POST",
      headers: { "Content-Type": "application/json", apikey: SUPABASE_KEY, Authorization: "Bearer " + SUPABASE_KEY },
      body: JSON.stringify({
        title: "Cuenta compartida",
        body: message,
        senderName: senderName || "",
        senderEndpoint: senderEndpoint,
        undoId: undoId || "",
        accountId: activeAccId,
      }),
    });
  } catch (e) {
    // Sin Edge Function desplegada (o sin red): la sincronización normal sigue.
  }
}

function fmtDate(iso) {
  if (!iso) return "";
  const [y, m, d] = iso.split("-");
  return `${d}/${m}/${y.slice(2)}`;
}

// "hace 3 min": formato relativo para la marca de tiempo de cada acción del historial.
// El segundo parámetro (_tick) no se usa: solo fuerza la re-renderización cuando
// el ticker de 30 s de App cambia, para que las etiquetas se refresquen solas.
function timeAgo(at, _tick) {
  if (!at) return "";
  const diff = Date.now() - Number(at);
  if (isNaN(diff)) return "";
  if (diff < 3000) return "ahora mismo";
  const s = Math.floor(diff / 1000);
  if (s < 60) return "hace " + s + " s";
  const m = Math.floor(s / 60);
  if (m < 60) return "hace " + m + " min";
  const h = Math.floor(m / 60);
  if (h < 24) return "hace " + h + " h";
  const d = Math.floor(h / 24);
  return "hace " + d + " día" + (d > 1 ? "s" : "");
}

// Hora exacta HH:MM de una marca de tiempo, para mostrarla junto al "hace X"
// en el historial. El día (Hoy/Ayer/fecha) lo muestra el separador de día del
// panel, así que aquí basta la hora; si la marca no es válida devuelve "".
function fmtClock(at) {
  if (!at) return "";
  const d = new Date(Number(at));
  if (isNaN(d.getTime())) return "";
  return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
}

// Día (YYYY-MM-DD) de una marca de tiempo, para agrupar el historial por día.
// Devuelve "" si la marca no es válida (esas acciones van al grupo "Sin fecha").
function dayKey(at) {
  if (!at) return "";
  const d = new Date(Number(at));
  if (isNaN(d.getTime())) return "";
  return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}

// Etiqueta del separador de día del historial: "Hoy", "Ayer" o fecha dd/mm/aa.
function dayLabel(at) {
  if (!at) return "Sin fecha";
  const d = new Date(Number(at));
  if (isNaN(d.getTime())) return "Sin fecha";
  const now = new Date();
  const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  const startDay = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
  const diffDays = Math.round((startToday - startDay) / 86400000);
  if (diffDays === 0) return "Hoy";
  if (diffDays === 1) return "Ayer";
  return d.getDate() + "/" + (d.getMonth() + 1) + "/" + String(d.getFullYear()).slice(2);
}

// Tiempo transcurrido en formato corto (para el aviso de pendientes):
// 12 s, 3 min, 1 h 5 min, 2 días…
function fmtElapsed(ms) {
  if (!ms || ms < 0 || isNaN(ms)) return "";
  const total = Math.floor(ms / 1000);
  if (total < 60) return total + " s";
  const m = Math.floor(total / 60);
  const s = total % 60;
  if (m < 60) return m + " min" + (s ? " " + s + " s" : "");
  const h = Math.floor(m / 60);
  const rem = m % 60;
  if (h < 24) return h + " h" + (rem ? " " + rem + " min" : "");
  const d = Math.floor(h / 24);
  return d + " día" + (d > 1 ? "s" : "");
}

// Etiqueta del aviso de pendientes: "(lleva 12 s intentándolo)". Devuelve ""
// si no hay marca o si el tiempo no es calculable (marca futura/negativa) para
// no mostrar un "(lleva  intentándolo)" con hueco.
function pendingElapsedLabel(since) {
  if (!since) return "";
  const el = fmtElapsed(Date.now() - since);
  return el ? " (lleva " + el + " intentándolo)" : "";
}

// Reintenta una operación async hasta 2 veces antes de darse por vencida
async function withRetry(fn, attempts = 2) {
  let lastErr;
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e) {
      lastErr = e;
      if (i < attempts - 1) await new Promise((r) => setTimeout(r, 400));
    }
  }
  throw lastErr;
}

// Lee y parsea un valor almacenado de forma segura
function safeParse(value, fallback) {
  try {
    return value ? JSON.parse(value) : fallback;
  } catch (e) {
    return fallback;
  }
}

// Compara dos valores por su JSON (evita re-renders si no hay cambios).
// Chequeo previo barato para listas: si cambia la longitud, no hace falta stringify.
function sameJson(a, b) {
  if (Array.isArray(a) && Array.isArray(b) && a.length !== b.length) return false;
  return JSON.stringify(a) === JSON.stringify(b);
}

// Fusiona listas de movimientos por id (evita duplicados al sincronizar).
// OJO: en un choque de ids gana el argumento `existing` (el primero): la
// función construye [...incoming, ...existing] y el último set() prevalece.
function mergeById(existing, incoming) {
  const map = new Map();
  [...(incoming || []), ...(existing || [])].forEach((t) => {
    if (t && t.id != null) map.set(t.id, t);
  });
  return Array.from(map.values());
}

// Asegura que una entrada de historial tenga identidad, autor y marca de tiempo
// (compatibilidad con entradas antiguas guardadas solo en local).
function normalizeHistoryEntry(e, fallbackBy) {
  const src = e && typeof e === "object" ? e : {};
  return {
    ...src,
    eid: src.eid || uid(),
    by: src.by || fallbackBy || "Alguien",
    at: src.at || Date.now(),
  };
}

// Fusiona listas de historial por eid, ordena por at (más reciente primero) y
// recorta a las últimas `limit` acciones (10 por defecto; la nube guarda 30
// para que los tombstones de borrados no se caigan tan pronto del historial).
function mergeHistory(list, limit) {
  const max = limit && limit > 0 ? limit : 10;
  const map = new Map();
  (Array.isArray(list) ? list : []).forEach((e) => {
    if (e && e.eid) map.set(e.eid, e);
  });
  return Array.from(map.values())
    .sort((a, b) => (b.at || 0) - (a.at || 0))
    .slice(0, max);
}

// Personajes disponibles (Ajustes → "Personaje de la cuenta"): el orden del
// array es el orden del selector. Es un ajuste COMPARTIDO de la cuenta: todas
// las personas ven el mismo personaje.
const MASCOT_KINDS = [
  { id: "coin", label: "Moneda" },
  { id: "piggy", label: "Cerdito" },
  { id: "cat", label: "Gatito" },
  { id: "plant", label: "Plantita" },
  { id: "owl", label: "Búho" },
];

// Paletas visuales para las tarjetas de crédito
const CARD_COLORS = [
  { id: "emerald", label: "Esmeralda", gradient: "linear-gradient(135deg, #0F4A32 0%, #15803D 100%)", border: "#166534", text: "#FFFFFF" },
  { id: "gold", label: "Dorado / Oro", gradient: "linear-gradient(135deg, #78350F 0%, #B45309 100%)", border: "#92400E", text: "#FFFFFF" },
  { id: "indigo", label: "Índigo / Azul", gradient: "linear-gradient(135deg, #1E1B4B 0%, #4338CA 100%)", border: "#3730A3", text: "#FFFFFF" },
  { id: "rose", label: "Rubí / Carmesí", gradient: "linear-gradient(135deg, #881337 0%, #BE123C 100%)", border: "#9F1239", text: "#FFFFFF" },
  { id: "slate", label: "Titanio / Grafito", gradient: "linear-gradient(135deg, #0F172A 0%, #334155 100%)", border: "#1E293B", text: "#FFFFFF" },
  { id: "violet", label: "Amatista", gradient: "linear-gradient(135deg, #3B0764 0%, #7E22CE 100%)", border: "#6B21A8", text: "#FFFFFF" },
  { id: "black", label: "Black Card / Obsidiana", gradient: "linear-gradient(135deg, #09090B 0%, #27272A 100%)", border: "#52525B", text: "#FFFFFF" },
  { id: "sapphire", label: "Zafiro / Azul Marino", gradient: "linear-gradient(135deg, #0C4A6E 0%, #0284C7 100%)", border: "#0369A1", text: "#FFFFFF" },
  { id: "amber", label: "Ámbar / Naranja", gradient: "linear-gradient(135deg, #9A3412 0%, #EA580C 100%)", border: "#C2410C", text: "#FFFFFF" },
  { id: "teal", label: "Turquesa / Teal", gradient: "linear-gradient(135deg, #134E4A 0%, #0D9488 100%)", border: "#0F766E", text: "#FFFFFF" },
  { id: "fuchsia", label: "Magenta / Fucsia", gradient: "linear-gradient(135deg, #701A75 0%, #C026D3 100%)", border: "#A21CAF", text: "#FFFFFF" },
  { id: "silver", label: "Platino / Silver", gradient: "linear-gradient(135deg, #334155 0%, #64748B 100%)", border: "#94A3B8", text: "#FFFFFF" },
  { id: "copper", label: "Cobre / Rose Gold", gradient: "linear-gradient(135deg, #831843 0%, #BE185D 100%)", border: "#9D174D", text: "#FFFFFF" },
  { id: "cyan", label: "Cian / Aqua", gradient: "linear-gradient(135deg, #083344 0%, #06B6D4 100%)", border: "#0891B2", text: "#FFFFFF" },
  { id: "crimson", label: "Rojo Pasión / Crimson", gradient: "linear-gradient(135deg, #7F1D1D 0%, #EF4444 100%)", border: "#DC2626", text: "#FFFFFF" },
  { id: "midnight", label: "Noche Profunda", gradient: "linear-gradient(135deg, #030712 0%, #1E1B4B 100%)", border: "#312E81", text: "#FFFFFF" },
];

// Detección inteligente de pagos de servicios (luz, agua, internet, telefonía, etc.)
// Excluye estos consumos del cashback según las políticas bancarias habituales
function isServicePayment(description, category) {
  const text = `${description || ""} ${category || ""}`.toLowerCase();
  if (/\b(gasolina|gasolinera|grifo)\b/i.test(text)) return false;
  const serviceKeywords = [
    "\\bluz\\b", "\\bagua\\b", "\\bgas\\b", "\\bcalidda\\b", "sedapal", "enel", "electrodunas", "hidrandina", "pluz",
    "osiptel", "telefon", "celular", "internet", "claro", "movistar", "entel", "bitel",
    "\\bwin\\b", "fibra", "recibo", "servicio", "tributo", "impuesto", "arbitrio", "\\bsat\\b",
    "\\bsunat\\b", "colegio", "pension", "universidad", "matricula"
  ];
  return serviceKeywords.some((kw) => {
    const reg = new RegExp(kw, "i");
    return reg.test(text);
  });
}

function removeAccents(str) {
  return String(str || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}

function normalizeSpanishNumbers(text) {
  if (!text) return "";
  let s = String(text).toLowerCase();

  const wordsToNumbers = [
    { regex: /\b(un|uno|una)\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?|pesos?)\b/gi, rep: "1 $2" },
    { regex: /\bdos\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "2 $1" },
    { regex: /\btres\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "3 $1" },
    { regex: /\bcuatro\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "4 $1" },
    { regex: /\bcinco\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "5 $1" },
    { regex: /\bseis\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "6 $1" },
    { regex: /\bsiete\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "7 $1" },
    { regex: /\bocho\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "8 $1" },
    { regex: /\bnueve\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "9 $1" },
    { regex: /\bdiez\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "10 $1" },
    { regex: /\bquince\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "15 $1" },
    { regex: /\bveinte\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "20 $1" },
    { regex: /\bveinticinco\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "25 $1" },
    { regex: /\btreinta\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "30 $1" },
    { regex: /\bcuarenta\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "40 $1" },
    { regex: /\bcincuenta\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "50 $1" },
    { regex: /\bcien\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros|lucas?)\b/gi, rep: "100 $1" },
    { regex: /\bciento\s+(\d+)\s*(sol|soles|dolar|dolares|dólar|dólares|euro|euros)?\b/gi, rep: "1$1 $2" },
    { regex: /\bdoscientos\s+(\d+)?\s*(sol|soles|dolar|dolares|dólar|dólares|euro|euros)?\b/gi, rep: "200 $2" },
    { regex: /\bquinientos\s*(sol|soles|dolar|dolares|dólar|dólares|euro|euros)?\b/gi, rep: "500 $1" },
    { regex: /\bmil\s+(sol|soles|dolar|dolares|dólar|dólares|euro|euros)?\b/gi, rep: "1000 $1" },
    { regex: /\b(\d+)\s+lucas?\b/gi, rep: "$1 soles" },
    { regex: /\buna\s+luca\b/gi, rep: "1 sol" }
  ];

  for (const { regex, rep } of wordsToNumbers) {
    s = s.replace(regex, rep);
  }

  return s;
}

// Motor de interpretación de gastos en lenguaje natural (Smart NLP)
function parseNaturalExpenseText(text, context = {}) {
  const rawInput = String(text || "").trim();
  if (!rawInput) return null;
  const raw = normalizeSpanishNumbers(rawInput);

  const people = (context.people || []).map((p) => String(p).trim()).filter(Boolean);
  const myName = String(context.myName || (people[0] || "")).trim();
  const cards = Array.isArray(context.cards) ? context.cards : [];
  const baseCurrency = String(context.currency || "S/").trim();

  const normText = removeAccents(raw).toLowerCase();

  // Extraer si se menciona a una persona específica en la orden (ej: "de Hans", "que tuvo Hans", "para Solange")
  let targetPersonIntent = "";
  for (const p of people) {
    const pNorm = removeAccents(p).toLowerCase();
    if (new RegExp(`\\b(?:de|que\\s+tuvo|que\\s+hizo|que\\s+anot[oó]|para|por)\\s+${pNorm}\\b|\\b${pNorm}\\b`, "i").test(normText)) {
      targetPersonIntent = p;
      break;
    }
  }

  // 1. INTENCIÓN: RESTAURAR / VOLVER A AGREGAR LO QUE SE BORRÓ O SE TUVO
  // Ej: "quiero que vuelvas a agregar el ultimo gasto", "que vuelvas a agregar el ultimo gasto que tuve", "vuelve a poner el ultimo gasto"
  const hasVolver = /(?:volv|vuelv)/i.test(normText); // volver, vuelve, vuelvas, vuelvan, volvemos, volviste...
  const hasActionAdd = /(?:agreg|pon|anot|registr|met|traer|trae)/i.test(normText);
  const hasRestoreWord = /(?:recuper|restaur|reagreg|repon|desborr|rehaz)/i.test(normText);
  const hasLast = /(?:ultim|anterior|pasad|que\s+(?:borr|quit|elimin|tuv|saqu|hic|anot|met))/i.test(normText);

  if (
    hasRestoreWord ||
    (hasVolver && hasActionAdd) ||
    (hasVolver && hasLast) ||
    ((hasActionAdd || /(?:deja)/i.test(normText)) && /(?:de\s+nuevo|otra\s+vez)/i.test(normText) && hasLast)
  ) {
    return {
      action: "restore_last_deleted",
      targetPerson: targetPersonIntent || undefined,
      thought: targetPersonIntent
        ? `Entendido, voy a restaurar o re-agregar el último movimiento de ${targetPersonIntent}.`
        : "Entendido, voy a restaurar o re-agregar el último movimiento registrado.",
    };
  }

  // 2. INTENCIÓN: REPETIR / DUPLICAR EL ÚLTIMO MOVIMIENTO
  // Ej: "repite el ultimo gasto", "duplica el gasto", "anota lo mismo de nuevo", "otra vez lo mismo"
  const hasRepeat = /(?:repit|repet|duplic|clon|copi|haz\s+lo\s+mismo|otra\s+vez\s+lo\s+mismo|lo\s+mismo\s+de\s+nuevo)/i.test(normText);
  if (hasRepeat) {
    return {
      action: "repeat_last_tx",
      targetPerson: targetPersonIntent || undefined,
      thought: targetPersonIntent
        ? `Entendido, voy a duplicar el último movimiento de ${targetPersonIntent}.`
        : "Entendido, voy a duplicar el último movimiento registrado.",
    };
  }

  // 3. INTENCIÓN: CONSULTA DE DEUDAS / SALDOS ENTRE PERSONAS
  if (/(?:cuanto|cuánto|cómo|quién|quien)\s+(?:debe|debemos|le\s+debo|me\s+debe|quedamos|saldar|ajustar\s+cuentas|deuda)/i.test(normText)) {
    return {
      action: "query_debt",
      thought: "Calculando saldos y cuentas pendientes entre miembros...",
    };
  }

  // 4. INTENCIÓN: CONSULTA DE GASTOS POR CATEGORÍA
  const queryCatMatch = raw.match(/(?:cuanto|cuánto)\s+(?:hemos\s+gastado|se\s+gast[oó]|gastamos|va|gasto)\s+en\s+([a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+)/i);
  if (queryCatMatch && queryCatMatch[1]) {
    return {
      action: "query_category",
      targetCategory: queryCatMatch[1].trim(),
      thought: `Consultando el total gastado en ${queryCatMatch[1].trim()}...`,
    };
  }

  // 5. INTENCIÓN: CAMBIO DE MONEDA
  if (/(?:cambia|cambiar|pon|poner)\s+(?:la\s+)?moneda\s+(?:a|en)\s+(soles|dolares|dólares|euros|usd|eur|pen|s\/\.?|\$|€)/i.test(normText)) {
    const curMatch = raw.match(/(?:cambia|cambiar|pon|poner)\s+(?:la\s+)?moneda\s+(?:a|en)\s+(soles|dolares|dólares|euros|usd|eur|pen|s\/\.?|\$|€)/i);
    let sym = "S/";
    if (/(dolar|dólar|usd|\$)/i.test(curMatch[1])) sym = "$";
    else if (/(euro|eur|€)/i.test(curMatch[1])) sym = "€";
    return {
      action: "set_currency",
      currency: sym,
      thought: `Cambiando la moneda principal a ${sym}...`,
    };
  }

  // 6. INTENCIÓN: MODO OSCURO / CLARO
  if (/(?:modo|tema)\s+(oscuro|claro|noche|dia|día)|(?:activa|activar|pon|poner)\s+(?:el\s+)?(?:modo|tema)\s+(oscuro|claro|noche|dia|día)/i.test(normText)) {
    const isDark = /(oscuro|noche)/i.test(normText);
    return {
      action: "toggle_dark_mode",
      targetState: isDark,
      thought: isDark ? "Activando modo oscuro..." : "Activando modo claro...",
    };
  }

  // 7. INTENCIÓN: BORRAR / ELIMINAR
  const isDeleteIntent = /(?:borr|elimin|quit|tach|anul|cancel|deshaz|sacar|bota|chau)/i.test(normText);
  if (isDeleteIntent) {
    if (
      /(?:[uú]ltim[oa]|deshaz|anterior|que\s+(?:puse|anot|hice|ingres|tuvo|registr))/i.test(normText) ||
      /^(?:borra|elimina|quita|deshaz|cancela)(?:\s+el|\s+la)?(?:\s+gasto|\s+movimiento|\s+ingreso)?$/i.test(normText)
    ) {
      return {
        action: "delete_last_tx",
        targetPerson: targetPersonIntent || undefined,
        thought: targetPersonIntent
          ? `Entendido, voy a eliminar el último movimiento registrado de ${targetPersonIntent}.`
          : "Entendido, voy a eliminar el último movimiento registrado.",
      };
    }
  }

  // 8. INTENCIÓN: ABRIR FORMULARIO DE INGRESO / GASTO / PRÉSTAMO
  // Ej: "que le agregue un ingreso", "agrega un ingreso", "quiero anotar un ingreso", "nuevo ingreso", "anota un prestamo"
  const hasActionVerb = /(?:agreg|pon|registr|anot|met|abr|cre|nuev|inici)/i.test(normText);
  const isIngresoIntent = /(?:ingreso|sueldo|cobro|ganancia|pago\s+que\s+recibi)/i.test(normText);
  const isPrestamoIntent = /(?:prestamo|deuda)/i.test(normText);
  const isGastoIntent = /(?:gasto|compra|salida)/i.test(normText);
  const hasAmountInText = /\d/.test(normText);

  if (hasActionVerb && !hasAmountInText) {
    if (isIngresoIntent) {
      return {
        action: "open_form",
        formType: "ingreso",
        person: targetPersonIntent || myName || (people[0] || ""),
        thought: "Abriendo formulario para registrar tu nuevo ingreso...",
      };
    }
    if (isPrestamoIntent) {
      return {
        action: "open_form",
        formType: "prestamo",
        person: targetPersonIntent || myName || (people[0] || ""),
        thought: "Abriendo formulario para registrar un préstamo...",
      };
    }
    if (isGastoIntent) {
      return {
        action: "open_form",
        formType: "gasto",
        person: targetPersonIntent || myName || (people[0] || ""),
        thought: "Abriendo formulario para registrar un gasto...",
      };
    }
  }

  // 0c. Detectar borrar un gasto específico (ej: "elimina el gasto de taxi", "borra el gasto de 59 soles")
  if (/(?:borra|borrar|borres?|elimina|eliminar|elimines?|quitar|quita|quites?)\s+(?:el\s+)?(?:gasto|movimiento|ingreso|pago)?\s*(?:de\s+|para\s+|en\s+)?(.+)/i.test(raw)) {
    const mSpec = raw.match(/(?:borra|borrar|borres?|elimina|eliminar|elimines?|quitar|quita|quites?)\s+(?:el\s+)?(?:gasto|movimiento|ingreso|pago)?\s*(?:de\s+|para\s+|en\s+)?(.+)/i);
    if (mSpec && mSpec[1] && !/(?:categor[ií]a|rubro)/i.test(raw)) {
      const targetStr = mSpec[1].trim();
      const numMatch = targetStr.match(/(\d+(?:[.,]\d+)?)/);
      const cleanDesc = targetStr.replace(/\b(\d+(?:[.,]\d+)?|soles|sol|dolares|usd|\$|con|la|el|de|tarjeta|efectivo|que|tuvo|hizo)\b/gi, "").trim();
      return {
        action: "delete_specific_tx",
        targetDescription: cleanDesc || undefined,
        targetAmount: numMatch ? parseFloat(numMatch[1].replace(",", ".")) : undefined,
        targetPerson: targetPersonIntent || undefined,
        thought: `Entendido, voy a buscar y eliminar el gasto solicitado.`,
      };
    }
  }

  // 0d. Detectar navegación y apertura de paneles (ej: "abre los ajustes", "abre el consejero")
  if (/(?:abre|abrir|mostrar|muestra|ver|ir\s+a)\s+(?:los\s+|las\s+|el\s+|la\s+)?(ajustes|configuracion|consejero|resumen|estadisticas|duplicados|huerfanos|tarjetas|cuotas)/i.test(raw)) {
    const mPanel = raw.match(/(?:abre|abrir|mostrar|muestra|ver|ir\s+a)\s+(?:los\s+|las\s+|el\s+|la\s+)?(ajustes|configuracion|consejero|resumen|estadisticas|duplicados|huerfanos|tarjetas|cuotas)/i);
    const panelName = mPanel ? mPanel[1].toLowerCase() : "";
    return {
      action: "open_panel",
      panel: panelName,
      thought: `Abriendo el panel de ${panelName}...`,
    };
  }

  let newCategoryRequested = null;

  // 0d. Detectar comandos de gestión de categorías (ej: "quiero que crees una categoría para los taxis", "crea categoría Mascotas")
  const catCmdRegex = /(?:(?:quiero|deseo|puedes|por\s+favor)\s+(?:que\s+)?(?:crees|agregues|anadas|añadas|pongas)\s+)?(?:crea|crear|crees|agrega|agregar|anade|añade|nueva|nuevo|anadir|añadir|pon|poner|registra|registrar|creame)\s+(?:una\s+|un\s+|la\s+|el\s+)?(?:categor[ií]a|rubro)\s+(?:para\s+(?:los|las|el|la)?\s*|de\s*|llamada\s*|con\s+nombre\s*|titulada\s*|:\s*)?([a-zA-ZáéíóúÁÉÍÓÚñÑ\s]{2,35})/i;
  const matchCat = raw.match(catCmdRegex);
  if (matchCat && matchCat[1]) {
    let catName = cleanCategoryName(matchCat[1]);
    if (catName.length >= 2) {
      if (!/\d/.test(raw)) {
        // Solo pide crear la categoría (sin gasto adjunto)
        return {
          action: "create_category",
          categoryName: catName,
        };
      } else {
        // Frase compuesta: pide crear la categoría Y anotar el gasto
        newCategoryRequested = catName;
      }
    }
  }

  // Patrón directo "categoría [nombre]" o "rubro [nombre]" (cuando no contiene números de monto)
  if (!/\d/.test(raw)) {
    const directCatMatch = raw.match(/^(?:categor[ií]a|rubro)\s+(?:para\s+(?:los|las|el|la)?\s*|de\s*|llamada\s*)?([a-zA-ZáéíóúÁÉÍÓÚñÑ\s]{2,35})$/i);
    if (directCatMatch && directCatMatch[1]) {
      let catName = cleanCategoryName(directCatMatch[1]);
      if (catName.length >= 2) {
        return {
          action: "create_category",
          categoryName: catName,
        };
      }
    }
  }

  const deleteCatRegex = /(?:elimina|eliminar|borra|borrar|quitar|quita)\s+(?:la\s+|el\s+|una\s+)?(?:categor[ií]a|rubro)\s+([a-zA-ZáéíóúÁÉÍÓÚñÑ\s]{2,35})/i;
  const deleteCatMatch = raw.match(deleteCatRegex);
  if (deleteCatMatch && deleteCatMatch[1]) {
    let catName = cleanCategoryName(deleteCatMatch[1]);
    if (catName.length >= 2) {
      return {
        action: "delete_category",
        categoryName: catName,
      };
    }
  }

  let type = "gasto";
  let person = myName || (people[0] || "");
  let borrower = "";
  let paymentMethod = "efectivo";
  let cardId = "";
  let currency = baseCurrency;
  let amount = 0;
  let description = "";

  // 1. Detectar tipo (Ingreso, Préstamo, Gasto)
  if (/(ingres[oóe]|ingresar|ingreso de dinero|sueldo|salario|cobro|cobr[eé]|me pagaron|le pagaron|ganancia|gan[eé]|recib[ií]|recibi[oó]|abono|abon[oó]|deposit[oó]|deposito|depósito|honorarios|propina|ventas|transferencia recibida|dinero que entro|plata que entro)/i.test(normText)) {
    type = "ingreso";
  } else if (/(prestamo|préstamo|prest[eé]|prest[oó]|deuda|le prest[eé]|le debo|me prest[oó]|prestame|préstame)/i.test(normText)) {
    type = "prestamo";
  } else {
    type = "gasto";
  }

  // 2. Detectar Moneda
  if (/(dolar|dolares|usd|\$)/i.test(normText)) {
    currency = "$";
  } else if (/(euro|euros|eur|€)/i.test(normText)) {
    currency = "€";
  } else if (/(sol|soles|pen|s\/\.?)/i.test(normText)) {
    currency = "S/";
  }

  // 3. Detectar Monto numérico
  const amountRegex = /(?:s\/\.?|\$|€)?\s*(\d+(?:[.,]\d{1,2})?)\s*(?:soles|sol|dolares|dolar|euros|euro|usd|pen|eur)?/i;
  const matchAmount = raw.match(amountRegex);
  if (matchAmount && matchAmount[1]) {
    const parsedNum = parseFloat(matchAmount[1].replace(",", "."));
    if (!isNaN(parsedNum) && parsedNum > 0) {
      amount = parsedNum;
    }
  }

  // 4. Detectar Personas
  for (const p of people) {
    const pNorm = removeAccents(p).toLowerCase();
    const reg = new RegExp(`\\b${pNorm}\\b`, "i");
    if (reg.test(normText)) {
      if (type === "prestamo") {
        if (new RegExp(`\\b(a|para)\\s+${pNorm}\\b`, "i").test(normText)) {
          borrower = p;
        } else if (new RegExp(`\\b(de)\\s+${pNorm}\\b`, "i").test(normText)) {
          person = p;
        } else {
          borrower = p;
        }
      } else {
        person = p;
      }
      break;
    }
  }

  if (type === "prestamo" && !borrower) {
    const other = people.find((p) => removeAccents(p).toLowerCase() !== removeAccents(person).toLowerCase()) || "";
    borrower = other;
  }

  // 5. Detectar Método de Pago y Tarjetas
  for (const c of cards) {
    if (c && c.name) {
      const cNorm = removeAccents(c.name).toLowerCase();
      const reg = new RegExp(`\\b${cNorm}\\b`, "i");
      if (reg.test(normText)) {
        paymentMethod = "tarjeta";
        cardId = c.id;
        break;
      }
    }
  }

  if (!cardId) {
    if (/(tarjeta|credito|debito|card|pos|visa|mastercard|amex)/i.test(normText)) {
      paymentMethod = "tarjeta";
      if (cards.length > 0) {
        const userCard = cards.find((c) => c.person && removeAccents(c.person).toLowerCase() === removeAccents(person).toLowerCase()) || cards[0];
        cardId = userCard.id;
      }
    } else if (/(transferencia|transfer|yape|yapee|plin|bcp|bbva|interbank|scotia)/i.test(normText)) {
      paymentMethod = "transferencia";
    } else if (/(efectivo|cash|efec|billete|moneda)/i.test(normText)) {
      paymentMethod = "efectivo";
    }
  }

  // 6. Detectar categoría explícita o inferida
  let category = "";
  const catExplicit = raw.match(/(?:en\s+la\s+categor[ií]a|en\s+categor[ií]a|categor[ií]a|en\s+el\s+rubro|rubro|ponlo\s+en|categorizar\s+en)\s+([a-zA-ZáéíóúÁÉÍÓÚñÑ]{3,25})/i);
  if (catExplicit && catExplicit[1]) {
    const rawCat = catExplicit[1].trim();
    const cleanCat = rawCat.charAt(0).toUpperCase() + rawCat.slice(1).toLowerCase();
    const reserved = ["Efectivo", "Tarjeta", "Transferencia", "Soles", "Dolares", "Euros", ...people];
    if (!reserved.some((r) => r.toLowerCase() === cleanCat.toLowerCase())) {
      category = cleanCat.charAt(0).toUpperCase() + cleanCat.slice(1);
    }
  }

  if (!category) {
    if (/(perro|gato|mascota|veterinaria|veterinario|croquetas|pet)/i.test(normText)) {
      category = "Mascotas";
    } else if (/(uber|taxi|gasolina|grifo|combustible|pasaje|metropolitano|corredor|peaje|estacionamiento|auto)/i.test(normText)) {
      category = "Transporte";
    } else if (/(medicina|farmacia|inkafarma|mifarma|doctor|consulta|clinica|salud|pastillas|dentista)/i.test(normText)) {
      category = "Salud y Farmacia";
    } else if (/(luz|agua|internet|telefono|celular|gas|cable|enel|sedapal|claro|movistar|entel)/i.test(normText)) {
      category = "Servicios del Hogar";
    } else if (/(frutas|verduras|mercado|supermercado|metro|plaza vea|tottus|wong|vivanda|bodega|abarrotes)/i.test(normText)) {
      category = "Supermercado";
    } else if (/(almuerzo|cena|desayuno|comida|restaurante|pizza|hamburguesa|ceviche|pollo|chifa|cafe|café|starbucks)/i.test(normText)) {
      category = "Restaurantes y Comida";
    } else if (/(cine|netflix|spotify|disney|juego|salida|bar|cerveza|discoteca|fiesta|viaje)/i.test(normText)) {
      category = "Entretenimiento y Ocio";
    } else if (/(ropa|zapatillas|pantalon|camisa|polo|compras|tienda|mall|falabella|ripley|zara)/i.test(normText)) {
      category = "Ropa y Compras";
    } else if (/(curso|libro|universidad|colegio|estudio|taller|clase|matricula|matrícula)/i.test(normText)) {
      category = "Educación";
    }
  }

  // 7. Limpiar descripción
  const stopWords = new Set([
    "gasto", "gaste", "compro", "compre", "pago", "pague", "anotar", "anota", "registra", "registrar",
    "ingreso", "ingrese", "ingresar", "ingreso de dinero", "prestamo", "preste", "presto", "prestame", "yape", "yapee", "plin",
    "en", "por", "de", "con", "para", "el", "la", "los", "las", "un", "una", "unos", "unas", "a", "ayer", "hoy", "mi", "mis", "del", "al",
    "tarjeta", "efectivo", "transferencia", "cash", "credito", "debito", "sol", "soles", "dolar", "dolares", "euro", "euros", "usd", "pen", "eur",
    "categoria", "categoría", "rubro", "ponlo", "dinero", "plata"
  ]);

  for (const p of people) stopWords.add(removeAccents(p).toLowerCase());
  for (const c of cards) if (c && c.name) stopWords.add(removeAccents(c.name).toLowerCase());

  const words = raw.split(/\s+/);
  const cleanWords = words.filter((w) => {
    if (/\d/.test(w) || /^[\$€]$/i.test(w) || /^s\/\.?$/i.test(w)) return false;
    const cleanW = removeAccents(w).toLowerCase().replace(/[^a-z0-9]/g, "");
    return cleanW && !stopWords.has(cleanW);
  });

  description = cleanWords.join(" ");
  if (newCategoryRequested) {
    category = newCategoryRequested;
    if (!description || description === "Gasto variado" || description === "Ingreso de dinero") {
      description = newCategoryRequested;
    }
  } else if (!description) {
    if (type === "ingreso") {
      description = category || "Ingreso de dinero";
    } else if (type === "prestamo") {
      description = "Préstamo";
    } else {
      description = category || "Gasto variado";
    }
  }

  const typeLabel = type === "ingreso" ? "un ingreso" : (type === "prestamo" ? "un préstamo" : "un gasto");
  const thought = newCategoryRequested && amount > 0
    ? `Creé la categoría "${newCategoryRequested}" y preparé ${typeLabel} de ${currency} ${amount.toFixed(2)} (${description}) por ${person}.`
    : (amount > 0 ? `Interpreté ${typeLabel} de ${currency} ${amount.toFixed(2)} (${description}) por ${person}.` : undefined);

  return {
    raw,
    type,
    amount,
    currency,
    person,
    borrower,
    paymentMethod,
    cardId,
    description,
    category,
    createCategory: newCategoryRequested || undefined,
    thought,
  };
}

// Modelos compatibles con Google Gemini optimizados para respuesta inmediata
const GEMINI_MODELS = [
  "gemini-2.5-flash",
  "gemini-2.0-flash",
  "gemini-2.5-pro",
  "gemini-2.0-flash-thinking-exp-01-21",
  "gemini-3.7-flash",
  "gemini-3.7-pro",
  "gemini-1.5-flash",
  "gemini-1.5-pro",
];

async function callGeminiApi(prompt, apiKey, jsonMode = true) {
  if (!apiKey || !prompt) return null;
  const key = apiKey.trim();
  for (const model of GEMINI_MODELS) {
    try {
      const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
      const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
      const timer = controller ? setTimeout(() => controller.abort(), 6000) : null;
      const resp = await fetch(url, {
        method: "POST",
        signal: controller ? controller.signal : undefined,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          contents: [{ parts: [{ text: prompt }] }],
          generationConfig: jsonMode
            ? { temperature: 0.1, responseMimeType: "application/json", maxOutputTokens: 1024 }
            : { temperature: 0.1, maxOutputTokens: 1024 },
        }),
      });
      if (timer) clearTimeout(timer);
      if (resp.ok) {
        const data = await resp.json();
        const textRes = data?.candidates?.[0]?.content?.parts?.[0]?.text;
        if (textRes) {
          return jsonMode ? JSON.parse(textRes) : textRes;
        }
      }
    } catch (e) {
      // Siguiente modelo en caso de timeout o error
    }
  }
  return null;
}

// Consulta a Google Gemini API (directa con API Key o a través de la Edge Function en Supabase como proxy secreto)
async function parseWithGeminiApi(raw, context = {}, apiKey = "") {
  if (!raw) return null;
  const key = (apiKey || "").trim();
  const people = context.people || ["Hans", "Solange"];
  const cards = context.cards || [];
  const categories = context.categories || [];
  const baseCurrency = context.currency || "S/";

  // 1. Si hay clave en el dispositivo / configuración, llamada directa a Gemini
  if (key) {
    try {
      const prompt = `
Eres la Inteligencia Artificial analítica y financiera de "Cuenta Compartida".
Tu trabajo es razonar y entender profundamente cualquier mensaje, orden, gasto, consulta o instrucción del usuario en lenguaje natural.

Contexto de la cuenta:
- Miembros del grupo: ${people.join(", ")}
- Usuario que habla/escribe: ${context.myName || people[0]}
- Tarjetas configuradas: ${cards.map((c) => `${c.name} (${c.person || "compartida"})`).join(", ") || "Ninguna"}
- Categorías existentes: ${categories.join(", ")}
- Moneda base: ${baseCurrency}

Mensaje del usuario: "${raw}"

Instrucciones de Razonamiento Autónomo:
Razona como un asistente inteligente y clasifica la intención en un JSON estructurado según el caso:

1. Si pide restaurar, re-agregar o volver a poner un gasto borrado o anterior (ej: "vuelve a agregar el último gasto que tuve", "reagrega el gasto", "recupera lo que borré", "pon de nuevo el último gasto"):
{
  "action": "restore_last_deleted",
  "targetPerson": "nombre si lo menciona",
  "thought": "Restaurando o re-agregando el último movimiento."
}

2. Si pide repetir o duplicar un gasto (ej: "repite el último gasto", "duplica lo último", "anota lo mismo de nuevo"):
{
  "action": "repeat_last_tx",
  "targetPerson": "nombre si lo menciona",
  "thought": "Duplicando el último gasto."
}

3. Si pide borrar el último gasto (ej: "borra el último gasto", "elimina lo último que puse", "borra el último gasto que tuvo Hans"):
{
  "action": "delete_last_tx",
  "targetPerson": "Hans si menciona a Hans, o undefined",
  "thought": "Eliminando el último movimiento."
}

4. Si pide borrar un gasto específico (ej: "elimina el gasto de 59 soles en taxi", "borra el gasto de uber"):
{
  "action": "delete_specific_tx",
  "targetDescription": "concepto (ej. Taxi)",
  "targetAmount": número si lo menciona,
  "thought": "Eliminando el gasto solicitado."
}

5. Si pide crear una categoría (ej: "crea categoría Mascotas"):
{
  "action": "create_category",
  "categoryName": "Mascotas",
  "thought": "Creando categoría."
}

6. Si pide eliminar una categoría:
{
  "action": "delete_category",
  "categoryName": "Nombre",
  "thought": "Eliminando categoría."
}

7. Si consulta deudas o saldos (ej: "¿cuánto debemos?", "¿quién debe a quién?", "¿cómo quedamos?"):
{
  "action": "query_debt",
  "thought": "Calculando saldos pendientes."
}

8. Si consulta cuánto se gastó en un rubro (ej: "¿cuánto gastamos en comida?"):
{
  "action": "query_category",
  "targetCategory": "Comida",
  "thought": "Consultando gastos de esta categoría."
}

9. Si pide cambiar moneda (ej: "cambia la moneda a dólares"):
{
  "action": "set_currency",
  "currency": "$" | "S/" | "€",
  "thought": "Cambiando moneda."
}

10. Si pide activar o desactivar modo oscuro (ej: "modo oscuro", "modo claro"):
{
  "action": "toggle_dark_mode",
  "targetState": true | false,
  "thought": "Cambiando tema visual."
}

11. Si pide abrir un panel (ej: "abre los ajustes", "abre el consejero", "ver duplicados"):
{
  "action": "open_panel",
  "panel": "ajustes" | "consejero" | "duplicados" | "tarjetas" | "cuotas",
  "thought": "Abriendo panel solicitado."
}

12. Si registra un gasto, ingreso o préstamo:
- IMPORTANTE para el tipo ("type"):
  * Si el usuario dice "ingreso", "ingreso de dinero", "sueldo", "salario", "cobré", "cobre", "me pagaron", "recibí", "recibió", "ganancia", "abono", "depósito", "venta", etc. -> "type": "ingreso" y "description": concepto indicado (ej. "Sueldo", "Ingreso de dinero", "Cobro de trabajo").
  * Si dice "préstamo", "le presté", "me prestó", "deuda" -> "type": "prestamo".
  * Si es un gasto o compra -> "type": "gasto".
{
  "type": "gasto" | "ingreso" | "prestamo",
  "amount": number,
  "currency": "${baseCurrency}" | "$" | "€",
  "person": string,
  "borrower": string,
  "paymentMethod": "efectivo" | "tarjeta" | "transferencia",
  "cardId": string,
  "description": string,
  "category": string,
  "thought": "Explicación clara del movimiento (ej. Registrando ingreso de dinero de S/ 100)."
}

13. Si es una consulta o saludo general:
{
  "action": "chat",
  "thought": "Respuesta concisa y servicial."
}
`;
      const res = await callGeminiApi(prompt, key, true);
      if (res && (res.amount || res.action || res.thought)) return res;
    } catch (err) {
      console.warn("Fallo llamada directa a Gemini:", err);
    }
  }

  // 2. Si no hay clave en este dispositivo pero hay Supabase, consultar Edge Function proxy (clave 100% secreta en el servidor)
  if (HAS_SUPABASE) {
    try {
      const fnUrl = `${SUPABASE_URL}/functions/v1/webhook-ai`;
      const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
      const timer = controller ? setTimeout(() => controller.abort(), 4000) : null;
      const resp = await fetch(fnUrl, {
        method: "POST",
        signal: controller ? controller.signal : undefined,
        headers: {
          "Content-Type": "application/json",
          apikey: SUPABASE_KEY,
          Authorization: `Bearer ${SUPABASE_KEY}`,
        },
        body: JSON.stringify({
          action: "parse",
          text: raw,
          people,
          cards,
          categories,
          currency: baseCurrency,
        }),
      });
      if (timer) clearTimeout(timer);
      if (resp.ok) {
        const data = await resp.json();
        if (data && data.parsed && (data.parsed.amount || data.parsed.action)) {
          return data.parsed;
        }
      }
    } catch (e) {
      console.warn("Fallo proxy Edge Function de Supabase:", e);
    }
  }

  return null;
}

// Cálculo local ultrarrápido (0 ms) de métricas financieras del Consejero
function computeLocalFinancialMetrics(transactions, monthKey, settings, people = []) {
  const baseCurrency = settings.currency || "S/";
  const txList = (transactions || []).filter((t) => {
    if (!t || !t.date) return false;
    return t.date.startsWith(monthKey);
  });

  const totalGastos = txList.filter((t) => t.type === "gasto").reduce((sum, t) => sum + (Number(t.amount) || 0), 0);
  const totalIngresos = txList.filter((t) => t.type === "ingreso").reduce((sum, t) => sum + (Number(t.amount) || 0), 0);
  const balance = totalIngresos - totalGastos;

  const byCategory = {};
  const byPerson = {};
  (people || []).forEach((p) => { byPerson[p] = { gastado: 0, aportado: 0 }; });

  txList.forEach((t) => {
    const amt = Number(t.amount) || 0;
    const cat = (t.category || t.description || "Varios").trim();
    if (t.type === "gasto") {
      byCategory[cat] = (byCategory[cat] || 0) + amt;
      if (t.person && byPerson[t.person]) byPerson[t.person].gastado += amt;
    } else if (t.type === "ingreso") {
      if (t.person && byPerson[t.person]) byPerson[t.person].aportado += amt;
    }
  });

  const sortedCategories = Object.entries(byCategory).sort((a, b) => b[1] - a[1]);
  const topCategory = sortedCategories[0] ? { name: sortedCategories[0][0], amount: sortedCategories[0][1] } : null;

  const now = new Date();
  const parts = String(monthKey || "").split("-");
  const year = Number(parts[0]) || now.getFullYear();
  const month = Number(parts[1]) || (now.getMonth() + 1);
  const totalDaysInMonth = new Date(year, month, 0).getDate();
  const isCurrentMonth = now.getFullYear() === year && (now.getMonth() + 1) === month;
  const currentDay = isCurrentMonth ? Math.max(1, now.getDate()) : totalDaysInMonth;

  const dailyAvg = currentDay > 0 ? totalGastos / currentDay : 0;
  const projectedGastos = isCurrentMonth ? dailyAvg * totalDaysInMonth : totalGastos;

  const microGastos = txList.filter((t) => t.type === "gasto" && Number(t.amount) <= 30);
  const totalMicro = microGastos.reduce((s, t) => s + (Number(t.amount) || 0), 0);

  const savingsRate = totalIngresos > 0 ? (balance / totalIngresos) * 100 : 0;
  const healthScore = totalGastos === 0 && totalIngresos === 0
    ? "Sin movimientos registrados"
    : balance >= 0 && savingsRate >= 20
    ? "Excelente (Buen Ahorro)"
    : balance >= 0
    ? "Ajustada (Poco Ahorro)"
    : "Precaución (Déficit de gasto)";

  const healthColor = healthScore.includes("Excelente") ? "#10B981" : healthScore.includes("Ajustada") ? "#F59E0B" : "#EF4444";

  const topPct = totalGastos > 0 && topCategory ? Math.round((topCategory.amount / totalGastos) * 100) : 0;
  const summaryText = totalGastos > 0
    ? `Has gastado ${baseCurrency} ${totalGastos.toFixed(2)} este mes. El mayor gasto está en ${topCategory?.name || "consumos"} (${topPct}% del total).`
    : "No se registran gastos en este periodo.";

  const tips = [];
  if (topCategory && topPct >= 35) {
    tips.push({
      title: `Controlar ${topCategory.name}`,
      desc: `Representa el ${topPct}% de tus salidas. Intenta toparlo o buscar alternativas más económicas esta semana.`,
    });
  }
  if (microGastos.length >= 3) {
    tips.push({
      title: "Atención a los gastos hormiga",
      desc: `Tienes ${microGastos.length} compras menores de 30 que suman ${baseCurrency} ${totalMicro.toFixed(2)}. Reducirlas aumentará tu saldo disponible.`,
    });
  }
  if (isCurrentMonth && projectedGastos > totalIngresos && totalIngresos > 0) {
    tips.push({
      title: "Proyección a fin de mes",
      desc: `A este ritmo proyectas gastar ${baseCurrency} ${projectedGastos.toFixed(2)}, superando tus ingresos. Modera los gastos variables.`,
    });
  } else {
    tips.push({
      title: "Fondo de tranquilidad",
      desc: "Destina al menos el 10% del balance positivo directamente a ahorros antes de planear nuevos consumos.",
    });
  }

  return {
    monthKey,
    totalGastos,
    totalIngresos,
    balance,
    healthScore,
    healthColor,
    summaryText,
    topCategory,
    dailyAvg,
    projectedGastos,
    totalMicro,
    microCount: microGastos.length,
    tips: tips.slice(0, 3),
    isAi: false,
  };
}

// Caché en memoria para apertura instantánea (0 ms) del Consejero IA
const advisorCache = new Map();

// Motor del Consejero Financiero Inteligente (Modo 3)
async function generateFinancialAdvice(transactions, monthKey, settings, people = [], apiKey = "") {
  const localAdvice = computeLocalFinancialMetrics(transactions, monthKey, settings, people);
  const baseCurrency = settings.currency || "S/";
  const txList = (transactions || []).filter((t) => t && t.date && t.date.startsWith(monthKey));

  if (txList.length === 0) {
    return localAdvice;
  }

  const cacheKey = `${monthKey}_${txList.length}_${localAdvice.totalGastos}_${localAdvice.totalIngresos}`;
  if (advisorCache.has(cacheKey)) {
    return advisorCache.get(cacheKey);
  }

  // 1. Consulta directa ultrarrápida con Gemini si hay clave
  let aiJson = null;
  if (apiKey) {
    try {
      const prompt = `
Consejero financiero de "Cuenta Compartida".
Mes: ${monthKey} | Moneda: ${baseCurrency}
Gastos: ${localAdvice.totalGastos} | Ingresos: ${localAdvice.totalIngresos} | Balance: ${localAdvice.balance}
Top Categoría: ${localAdvice.topCategory ? `${localAdvice.topCategory.name} (${localAdvice.topCategory.amount})` : "N/A"}
Hormiga: ${localAdvice.microCount} compras (${localAdvice.totalMicro})

Genera un JSON conciso con 3 consejos prácticos y breves:
{
  "healthScore": "Excelente" | "Ajustada" | "Precaución",
  "summaryText": "1 frase concisa de resumen",
  "tips": [
    { "title": "título corto 1", "desc": "consejo directo 1" },
    { "title": "título corto 2", "desc": "consejo directo 2" },
    { "title": "título corto 3", "desc": "consejo directo 3" }
  ]
}
`;
      aiJson = await callGeminiApi(prompt, apiKey, true);
    } catch (e) {}
  }

  // 2. Si no hay clave en este dispositivo, consultar Edge Function proxy
  if (!aiJson && HAS_SUPABASE) {
    try {
      const fnUrl = `${SUPABASE_URL}/functions/v1/webhook-ai`;
      const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
      const timer = controller ? setTimeout(() => controller.abort(), 3500) : null;
      const resp = await fetch(fnUrl, {
        method: "POST",
        signal: controller ? controller.signal : undefined,
        headers: {
          "Content-Type": "application/json",
          apikey: SUPABASE_KEY,
          Authorization: `Bearer ${SUPABASE_KEY}`,
        },
        body: JSON.stringify({
          action: "advise",
          monthKey,
          totalGastos: localAdvice.totalGastos,
          totalIngresos: localAdvice.totalIngresos,
          topCategory: localAdvice.topCategory,
          microCount: localAdvice.microCount,
          totalMicro: localAdvice.totalMicro,
          people,
        }),
      });
      if (timer) clearTimeout(timer);
      if (resp.ok) {
        const data = await resp.json();
        if (data && data.aiJson) {
          aiJson = data.aiJson;
        }
      }
    } catch (e) {}
  }

  if (aiJson && Array.isArray(aiJson.tips) && aiJson.tips.length > 0) {
    const enriched = {
      ...localAdvice,
      healthScore: aiJson.healthScore || localAdvice.healthScore,
      healthColor: aiJson.healthScore?.includes("Precaución") ? "#EF4444" : aiJson.healthScore?.includes("Ajustada") ? "#F59E0B" : "#10B981",
      summaryText: aiJson.summaryText || localAdvice.summaryText,
      tips: aiJson.tips.slice(0, 3),
      isAi: true,
    };
    advisorCache.set(cacheKey, enriched);
    return enriched;
  }

  advisorCache.set(cacheKey, localAdvice);
  return localAdvice;
}

// Registro de versiones y cambios de la aplicación (Changelog)
const APP_CHANGELOG = [
  {
    version: "v26",
    date: "30 de Agosto, 2026",
    badge: "IA & Voz",
    title: "Asistente de Voz con IA (Gemini Spark & NLP) + Automatizaciones",
    description: "Dicta o escribe tus gastos con lenguaje natural desde el micrófono, integración inteligente con Google Gemini y conexión en segundo plano con Telegram Bot y Webhooks.",
    items: [
      {
        tag: "Nuevo",
        text: "Dictado por Voz y Smart NLP: Toca el micrófono o escribe 'Hans gastó 40 soles en Uber con tarjeta' y el formulario se autocompleta al instante sin tipear números.",
      },
      {
        tag: "Nuevo",
        text: "Integración Google Gemini AI: Conecta tu clave de Gemini en Ajustes para análisis y comprensión semántica avanzada para todo el grupo.",
      },
      {
        tag: "Nuevo",
        text: "Automatización con Telegram y Webhook: Registra gastos automáticamente enviando mensajes a tu Bot de Telegram o vía Gemini Spark / Webhooks en segundo plano.",
      },
    ],
  },
  {
    version: "v25",
    date: "30 de Agosto, 2026",
    badge: "Más reciente",
    title: "Resumen por Persona Claro, Historial Ordenado y Splash Oscuro",
    description: "Rediseño completo de la sección por persona para máxima claridad, historial de acciones más estructurado y compacto, ajustes simplificados y pantalla de inicio oscura en celular.",
    items: [
      {
        tag: "Nuevo",
        text: "Resumen por Persona Simplificado: tarjetas limpias con etiquetas directas de 'Aportó', 'Gastó', deudas y saldo individual destacado en grande sin números confusos.",
      },
      {
        tag: "Mejora",
        text: "Historial de Acciones y Lista de Movimientos: diseño en feed con avatares de autor, reacción de mascota, tiempo relativo y balance neto diario.",
      },
      {
        tag: "Mejora",
        text: "Ajustes Minimalistas y Compactos: menú de configuración más directo, sin elementos innecesarios y con personajes más chicos.",
      },
      {
        tag: "Nuevo",
        text: "Pantalla de Carga Oscura en Móvil: bienvenida nativa en modo oscuro (#12160F) para evitar deslumbramientos al abrir la app de noche.",
      },
    ],
  },
  {
    version: "v24",
    date: "30 de Agosto, 2026",
    badge: "Minimalista",
    title: "Modo Minimalista y Comentarios Estilizados",
    description: "Nueva opción de interfaz limpia y minimalista sin distracciones, alternancia rápida desde la cabecera o Ajustes, y notas/comentarios reubicados debajo de cada descripción.",
    items: [
      {
        tag: "Nuevo",
        text: "Modo Minimalista: simplifica la pantalla ocultando módulos secundarios densos y concentrándose en el saldo, acciones principales y movimientos.",
      },
      {
        tag: "Nuevo",
        text: "Botón de Alternancia Rápida: cambia entre Modo Minimalista y Modo Completo con un solo toque desde la barra superior o en Ajustes > Preferencias.",
      },
      {
        tag: "Diseño",
        text: "Botones de Acción Directa: acceso instantáneo a [+ Anotar Ingreso] y [− Anotar Gasto] optimizado para rapidez.",
      },
      {
        tag: "Diseño",
        text: "Notas debajo de la Descripción: los comentarios de cada movimiento ahora se muestran elegantemente en su propia línea inferior.",
      },
    ],
  },
  {
    version: "v23",
    date: "29 de Agosto, 2026",
    badge: "Notificaciones",
    title: "Notificaciones Flotantes y Corrección de Identidad de Usuario",
    description: "Mejoras en la entrega de notificaciones en móvil y PC, avisos flotantes interactivos, y preservación de la identidad activa al editar o registrar movimientos de otra persona.",
    items: [
      {
        tag: "Mejora",
        text: "Notificaciones Flotantes y Persistentes: nuevo toast visible en todo momento con botón directo para ver y resaltar el movimiento.",
      },
      {
        tag: "Mejora",
        text: "Entrega Robusta en Celular y PC: notificaciones inmediatas mediante Service Worker y vibración táctil optimizada.",
      },
      {
        tag: "Corrección",
        text: "Identidad Protegida: tu usuario activo se mantiene intacto al consultar, editar o registrar gastos a nombre de otra persona.",
      },
      {
        tag: "Corrección",
        text: "Atribución Real en Avisos: las notificaciones ahora indican con claridad quién realizó el cambio y para quién.",
      },
    ],
  },
  {
    version: "v22",
    date: "29 de Agosto, 2026",
    badge: "Cashback",
    title: "Cashback en Tarjetas de Crédito y Exclusión de Servicios",
    description: "Configura porcentajes de cashback personalizados por tarjeta, exclusión inteligente en pagos de servicios, cálculo automático al comprar y canje hacia la tarjeta o saldo.",
    items: [
      {
        tag: "Nuevo",
        text: "Programa de Cashback por tarjeta: activa y define el % de cashback (1%, 1.5%, 2%, 3%, 5% o personalizado) para cada tarjeta de crédito.",
      },
      {
        tag: "Nuevo",
        text: "Exclusión de Servicios: las compras detectadas como pagos de servicios (luz, agua, internet, telefonía) se excluyen automáticamente de generar cashback.",
      },
      {
        tag: "Nuevo",
        text: "Cálculo en vivo al anotar consumos: visualiza el cashback generado antes de guardar con opción de personalizar o desactivar.",
      },
      {
        tag: "Nuevo",
        text: "Módulo de Cashback acumulado y botón de Canje para abonar directamente a la deuda de la tarjeta o cobrarlo como ingreso.",
      },
    ],
  },
  {
    version: "v21",
    date: "29 de Agosto, 2026",
    badge: "Préstamos",
    title: "Préstamos y Deudas entre Nosotros (con Resaltado Visual)",
    description: "Registra préstamos y deudas directas entre los miembros del grupo con módulo destacado, seguimiento en cuotas y saldado rápido.",
    items: [
      {
        tag: "Nuevo",
        text: "Opción «🤝 Préstamo / Deuda» en el formulario para registrar préstamos directos de una persona a otra.",
      },
      {
        tag: "Nuevo",
        text: "Módulo destacado en el panel principal: «🤝 Préstamos y Deudas entre nosotros» con tarjetas visuales y botón de saldado en 1 toque.",
      },
      {
        tag: "Nuevo",
        text: "Soporte de préstamo entre miembros en Cuotas y Préstamos con seguimiento a plazos o pago único.",
      },
      {
        tag: "Mejora",
        text: "Historial de movimientos con distintivo e icono 🤝 para identificar préstamos entre personas al instante.",
      },
    ],
  },
  {
    version: "v20",
    date: "28 de Agosto, 2026",
    badge: "Deudas",
    title: "Deuda General Integrada (Tarjetas + Cuotas)",
    description: "Ahora los préstamos y compromisos en cuotas se suman automáticamente a la deuda general de la cuenta y al patrimonio neto.",
    items: [
      {
        tag: "Nuevo",
        text: "Integración total en el balance principal: las cuotas pendientes se suman a la deuda global de la cuenta y descuentan del patrimonio neto.",
      },
      {
        tag: "Mejora",
        text: "Desglose claro de patrimonio: visualiza en el saldo principal cuánto corresponde a tarjetas y cuánto a cuotas pendientes.",
      },
      {
        tag: "Mejora",
        text: "Saldos por persona: el resumen individual descuenta las cuotas activas de cada titular para mostrar su saldo neto real.",
      },
    ],
  },
  {
    version: "v19",
    date: "28 de Agosto, 2026",
    badge: "Tarjetas",
    title: "Selectores de Mes Independientes y Frecuencia de Ciclo",
    description: "Control preciso de fechas de corte y vencimiento de tarjetas de crédito con desfase entre meses.",
    items: [
      {
        tag: "Nuevo",
        text: "Selectores de mes independientes para Corte y Pago con historial de 2 meses pasados y 12 meses futuros.",
      },
      {
        tag: "Nuevo",
        text: "Frecuencia de ciclo configurable: soporte para facturación Mensual (30 días) y Quincenal (15 días).",
      },
      {
        tag: "Mejora",
        text: "Botones de control rápido [+1m] y [Auto] por separado para corte y pago en la tarjeta del panel principal.",
      },
    ],
  },
  {
    version: "v18",
    date: "27 de Agosto, 2026",
    badge: "Abonos",
    title: "Desglose Inteligente en Pagos de Tarjeta",
    description: "Visualización automática de cuotas del mes al pagar o abonar tu estado de cuenta.",
    items: [
      {
        tag: "Nuevo",
        text: "Desglose automático al registrar un abono: muestra cuánto corresponde a cuotas del mes y cuánto a otros consumos corrientes.",
      },
      {
        tag: "Mejora",
        text: "Abono rápido en 1 toque del importe pendiente exacto sin duplicar gastos del mes.",
      },
    ],
  },
  {
    version: "v17",
    date: "26 de Agosto, 2026",
    badge: "Módulos",
    title: "Gestor de Cuotas, Préstamos y Tarjetas de Crédito",
    description: "Llegada del módulo completo de tarjetas y seguimiento de préstamos y cuotas a plazos.",
    items: [
      {
        tag: "Nuevo",
        text: "Módulo de Cuotas y Préstamos: seguimiento de cuotas pagadas/restantes, importe mensual y barra de progreso.",
      },
      {
        tag: "Nuevo",
        text: "Vinculación directa de préstamos y cuotas a tarjetas de crédito específicas.",
      },
      {
        tag: "Nuevo",
        text: "Tarjetas de crédito con colores personalizables, cupo disponible y recordatorios de corte y pago.",
      },
    ],
  },
  {
    version: "v16",
    date: "25 de Agosto, 2026",
    badge: "Nube",
    title: "Sincronización en Tiempo Real y Colaboración",
    description: "Actualizaciones instantáneas entre dispositivos y notificaciones del sistema.",
    items: [
      {
        tag: "Nuevo",
        text: "Notificaciones Push para avisarte cuando otra persona añade un gasto o ingreso.",
      },
      {
        tag: "Mejora",
        text: "Canal en vivo Supabase Realtime con detección instantánea de cambios y guardado sin conexión.",
      },
      {
        tag: "Mejora",
        text: "Historial colaborativo de acciones con posibilidad de deshacer cualquier movimiento.",
      },
    ],
  },
  {
    version: "v15",
    date: "24 de Agosto, 2026",
    badge: "Diseño",
    title: "Modo Oscuro y Multi-Moneda",
    description: "Experiencia visual personalizable para cualquier hora del día y monedas internacionales.",
    items: [
      {
        tag: "Nuevo",
        text: "Tema Oscuro / Claro automático con persistencia y sin parpadeos.",
      },
      {
        tag: "Nuevo",
        text: "Soporte multi-moneda (Soles S/, Dólares $, Euros €, etc.) con conversor y tipos de cambio.",
      },
    ],
  },
];

// Genera la lista de meses para selectores de anclaje de ciclo (meses recientes + futuros)
function getUpcomingMonthsList(pastMonths = 2, futureMonths = 12) {
  const list = [];
  const now = new Date();
  const MONTHS_FULL = [
    "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
    "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
  ];
  let d = new Date(now.getFullYear(), now.getMonth() - pastMonths, 1);
  const total = pastMonths + 1 + futureMonths;
  for (let i = 0; i < total; i++) {
    const y = d.getFullYear();
    const m = d.getMonth();
    const key = `${y}-${String(m + 1).padStart(2, "0")}`;
    const isCurrent = y === now.getFullYear() && m === now.getMonth();
    const label = `${MONTHS_FULL[m]} ${y}${isCurrent ? " (Este mes)" : ""}`;
    list.push({ key, label, year: y, month: m, isCurrent });
    d.setMonth(d.getMonth() + 1);
  }
  return list;
}

// Calcula los días restantes para la fecha de corte y fecha límite de pago
function getCardCycleInfo(card, refDate = new Date()) {
  if (!card || card.cardType === "debito") return null;
  const now = new Date(refDate);
  now.setHours(0, 0, 0, 0);

  const year = now.getFullYear();
  const month = now.getMonth();
  const today = now.getDate();

  const isQuincenal = card.billingCycle === "quincenal" || Number(card.cycleDays) === 15;
  const MONTHS = ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Set", "Oct", "Nov", "Dic"];
  const msPerDay = 1000 * 60 * 60 * 24;

  const cutoff = Math.min(31, Math.max(1, Number(card.cutoffDay) || 20));
  const due = Math.min(31, Math.max(1, Number(card.dueDay) || 10));

  // Mes de corte personalizado independiente (targetCutoffMonth: "YYYY-MM")
  let customCutoffDate = null;
  const rawCutMonth = card.targetCutoffMonth || (card.targetMonth && card.targetMonth !== "auto" ? card.targetMonth : null);
  if (rawCutMonth && typeof rawCutMonth === "string" && rawCutMonth !== "auto" && /^\d{4}-\d{2}$/.test(rawCutMonth)) {
    const [cYear, cMonth] = rawCutMonth.split("-").map(Number);
    if (cYear && cMonth >= 1 && cMonth <= 12) {
      customCutoffDate = { year: cYear, month: cMonth - 1 };
    }
  }

  // Mes de pago personalizado independiente (targetDueMonth: "YYYY-MM")
  let customDueDate = null;
  const rawDueMonth = card.targetDueMonth || (card.targetMonth && card.targetMonth !== "auto" ? card.targetMonth : null);
  if (rawDueMonth && typeof rawDueMonth === "string" && rawDueMonth !== "auto" && /^\d{4}-\d{2}$/.test(rawDueMonth)) {
    const [dYear, dMonth] = rawDueMonth.split("-").map(Number);
    if (dYear && dMonth >= 1 && dMonth <= 12) {
      customDueDate = { year: dYear, month: dMonth - 1 };
    }
  }

  if (isQuincenal) {
    // Tarjeta quincenal (15 días): dos cortes y pagos al mes (ej. día 15 y fin de mes)
    const cutoff1 = Math.min(31, Math.max(1, Number(card.cutoffDay) || 15));
    const due1 = Math.min(31, Math.max(1, Number(card.dueDay) || 15));

    let baseCutYear = customCutoffDate ? customCutoffDate.year : year;
    let baseCutMonth = customCutoffDate ? customCutoffDate.month : month;
    const lastDayCutMonth = new Date(baseCutYear, baseCutMonth + 1, 0).getDate();
    const cutoff2 = Math.min(lastDayCutMonth, cutoff1 <= 15 ? lastDayCutMonth : Math.min(31, cutoff1 + 15));

    let nextCutoff;
    if (!customCutoffDate && baseCutYear === year && baseCutMonth === month) {
      if (today <= cutoff1) nextCutoff = new Date(baseCutYear, baseCutMonth, cutoff1);
      else if (today <= cutoff2) nextCutoff = new Date(baseCutYear, baseCutMonth, cutoff2);
      else nextCutoff = new Date(baseCutYear, baseCutMonth + 1, cutoff1);
    } else {
      nextCutoff = new Date(baseCutYear, baseCutMonth, cutoff1);
    }
    nextCutoff.setHours(0, 0, 0, 0);

    let baseDueYear = customDueDate ? customDueDate.year : year;
    let baseDueMonth = customDueDate ? customDueDate.month : month;
    const lastDayDueMonth = new Date(baseDueYear, baseDueMonth + 1, 0).getDate();
    const due2 = Math.min(lastDayDueMonth, due1 <= 15 ? lastDayDueMonth : Math.min(31, due1 + 15));

    let nextDue;
    if (!customDueDate && baseDueYear === year && baseDueMonth === month) {
      if (today <= due1) nextDue = new Date(baseDueYear, baseDueMonth, due1);
      else if (today <= due2) nextDue = new Date(baseDueYear, baseDueMonth, due2);
      else nextDue = new Date(baseDueYear, baseDueMonth + 1, due1);
    } else {
      nextDue = new Date(baseDueYear, baseDueMonth, due1);
    }
    nextDue.setHours(0, 0, 0, 0);

    const diffCutoff = Math.round((nextCutoff.getTime() - now.getTime()) / msPerDay);
    const diffDue = Math.round((nextDue.getTime() - now.getTime()) / msPerDay);

    const cutoffRelative = diffCutoff === 0 ? "¡Hoy!" : diffCutoff > 0 ? `en ${diffCutoff}d` : `hace ${Math.abs(diffCutoff)}d`;
    const dueRelative = diffDue === 0 ? "¡Hoy!" : diffDue > 0 ? `en ${diffDue}d` : `hace ${Math.abs(diffDue)}d`;

    return {
      billingCycle: "quincenal",
      isQuincenal: true,
      cutoffDay: cutoff1,
      dueDay: due1,
      targetCutoffMonth: card.targetCutoffMonth || "auto",
      targetDueMonth: card.targetDueMonth || "auto",
      currentCutoffMonthKey: `${nextCutoff.getFullYear()}-${String(nextCutoff.getMonth() + 1).padStart(2, "0")}`,
      currentDueMonthKey: `${nextDue.getFullYear()}-${String(nextDue.getMonth() + 1).padStart(2, "0")}`,
      daysToCutoff: diffCutoff,
      daysToDue: diffDue,
      cutoffRelative,
      dueRelative,
      cutoffLabel: `${nextCutoff.getDate()} ${MONTHS[nextCutoff.getMonth()]}`,
      dueLabel: `${nextDue.getDate()} ${MONTHS[nextDue.getMonth()]}`,
      cutoffFullLabel: `${nextCutoff.getDate()} de ${MONTHS[nextCutoff.getMonth()]} ${nextCutoff.getFullYear()}`,
      dueFullLabel: `${nextDue.getDate()} de ${MONTHS[nextDue.getMonth()]} ${nextDue.getFullYear()}`,
    };
  }

  // Tarjeta mensual estándar (30 días)
  let nextCutoff;
  if (customCutoffDate) {
    const maxDayTarget = new Date(customCutoffDate.year, customCutoffDate.month + 1, 0).getDate();
    nextCutoff = new Date(customCutoffDate.year, customCutoffDate.month, Math.min(cutoff, maxDayTarget));
  } else {
    const maxDayThisMonth = new Date(year, month + 1, 0).getDate();
    const effectiveCutoffThisMonth = Math.min(cutoff, maxDayThisMonth);
    if (today <= effectiveCutoffThisMonth) {
      nextCutoff = new Date(year, month, effectiveCutoffThisMonth);
    } else {
      const maxDayNextMonth = new Date(year, month + 2, 0).getDate();
      nextCutoff = new Date(year, month + 1, Math.min(cutoff, maxDayNextMonth));
    }
  }
  nextCutoff.setHours(0, 0, 0, 0);

  let nextDue;
  if (customDueDate) {
    const maxDayTarget = new Date(customDueDate.year, customDueDate.month + 1, 0).getDate();
    nextDue = new Date(customDueDate.year, customDueDate.month, Math.min(due, maxDayTarget));
  } else {
    const maxDayThisMonth = new Date(year, month + 1, 0).getDate();
    const effectiveDueThisMonth = Math.min(due, maxDayThisMonth);
    if (today <= effectiveDueThisMonth) {
      nextDue = new Date(year, month, effectiveDueThisMonth);
    } else {
      const maxDayNextMonth = new Date(year, month + 2, 0).getDate();
      nextDue = new Date(year, month + 1, Math.min(due, maxDayNextMonth));
    }
  }
  nextDue.setHours(0, 0, 0, 0);

  const diffCutoff = Math.round((nextCutoff.getTime() - now.getTime()) / msPerDay);
  const diffDue = Math.round((nextDue.getTime() - now.getTime()) / msPerDay);

  const cutoffRelative = diffCutoff === 0 ? "¡Hoy!" : diffCutoff > 0 ? `en ${diffCutoff}d` : `hace ${Math.abs(diffCutoff)}d`;
  const dueRelative = diffDue === 0 ? "¡Hoy!" : diffDue > 0 ? `en ${diffDue}d` : `hace ${Math.abs(diffDue)}d`;

  const cutoffLabel = `${nextCutoff.getDate()} ${MONTHS[nextCutoff.getMonth()]}`;
  const dueLabel = `${nextDue.getDate()} ${MONTHS[nextDue.getMonth()]}`;
  const cutoffFullLabel = `${nextCutoff.getDate()} de ${MONTHS[nextDue.getMonth()]} ${nextCutoff.getFullYear()}`;
  const dueFullLabel = `${nextDue.getDate()} de ${MONTHS[nextDue.getMonth()]} ${nextDue.getFullYear()}`;

  return {
    billingCycle: "mensual",
    isQuincenal: false,
    cutoffDay: cutoff,
    dueDay: due,
    targetCutoffMonth: card.targetCutoffMonth || "auto",
    targetDueMonth: card.targetDueMonth || "auto",
    currentCutoffMonthKey: `${nextCutoff.getFullYear()}-${String(nextCutoff.getMonth() + 1).padStart(2, "0")}`,
    currentDueMonthKey: `${nextDue.getFullYear()}-${String(nextDue.getMonth() + 1).padStart(2, "0")}`,
    daysToCutoff: diffCutoff,
    daysToDue: diffDue,
    cutoffRelative,
    dueRelative,
    cutoffLabel,
    dueLabel,
    cutoffFullLabel,
    dueFullLabel,
  };
}

// Calcula los días restantes y la etiqueta de vencimiento de una cuota mensual
function getInstallmentCycleInfo(inst, refDate = new Date()) {
  if (!inst) return null;
  const now = new Date(refDate);
  now.setHours(0, 0, 0, 0);

  const year = now.getFullYear();
  const month = now.getMonth();
  const today = now.getDate();

  const due = Math.min(31, Math.max(1, Number(inst.dueDay) || 15));
  const MONTHS = ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Set", "Oct", "Nov", "Dic"];
  const msPerDay = 1000 * 60 * 60 * 24;

  let customTargetDate = null;
  const rawDueMonth = inst.targetDueMonth || inst.targetMonth;
  if (rawDueMonth && typeof rawDueMonth === "string" && rawDueMonth !== "auto" && /^\d{4}-\d{2}$/.test(rawDueMonth)) {
    const [tYear, tMonth] = rawDueMonth.split("-").map(Number);
    if (tYear && tMonth >= 1 && tMonth <= 12) {
      customTargetDate = { year: tYear, month: tMonth - 1 };
    }
  }

  let nextDue;
  if (customTargetDate) {
    const maxDayTarget = new Date(customTargetDate.year, customTargetDate.month + 1, 0).getDate();
    nextDue = new Date(customTargetDate.year, customTargetDate.month, Math.min(due, maxDayTarget));
  } else {
    const maxDayThisMonth = new Date(year, month + 1, 0).getDate();
    const effectiveDueThisMonth = Math.min(due, maxDayThisMonth);
    if (today <= effectiveDueThisMonth) {
      nextDue = new Date(year, month, effectiveDueThisMonth);
    } else {
      const maxDayNextMonth = new Date(year, month + 2, 0).getDate();
      nextDue = new Date(year, month + 1, Math.min(due, maxDayNextMonth));
    }
  }
  nextDue.setHours(0, 0, 0, 0);

  const diffDue = Math.round((nextDue.getTime() - now.getTime()) / msPerDay);
  const dueRelative = diffDue === 0 ? "¡Hoy!" : diffDue > 0 ? `en ${diffDue}d` : `hace ${Math.abs(diffDue)}d`;

  const dueLabel = `${due} ${MONTHS[nextDue.getMonth()]}`;
  const dueFullLabel = `${due} de ${MONTHS[nextDue.getMonth()]} ${nextDue.getFullYear()}`;

  return {
    dueDay: due,
    targetDueMonth: inst.targetDueMonth || inst.targetMonth || "auto",
    targetMonth: inst.targetDueMonth || inst.targetMonth || "auto",
    currentDueMonthKey: `${nextDue.getFullYear()}-${String(nextDue.getMonth() + 1).padStart(2, "0")}`,
    daysToDue: diffDue,
    dueRelative,
    dueLabel,
    dueFullLabel,
    isToday: diffDue === 0,
    isSoon: diffDue > 0 && diffDue <= 3,
  };
}

// Normaliza los ajustes compartidos (compatibilidad con versiones antiguas).
function normalizeSettings(s) {
  const src = s && typeof s === "object" ? s : {};
  // Si el modo o el interruptor están desactivados, queda en modo "sin"
  const isSplitOff = src.splitEnabled === false || src.split_enabled === false || src.splitMode === "sin" || src.split_mode === "sin";
  const splitMode = isSplitOff ? "sin" : "igual";
  return {
    splitEnabled: !isSplitOff,
    currency: typeof src.currency === "string" && src.currency.trim() ? src.currency : "S/",
    splitMode,
    categories: Array.isArray(src.categories) ? src.categories.map(String) : [],
    admin: typeof src.admin === "string" ? src.admin : undefined,
    allowMemberInvites: src.allowMemberInvites === true,
    invitations: Array.isArray(src.invitations) ? src.invitations : [],
    categoryBudgets: typeof src.categoryBudgets === "object" && src.categoryBudgets !== null ? src.categoryBudgets : (typeof src.category_budgets === "object" && src.category_budgets !== null ? src.category_budgets : {}),
    savingsGoals: Array.isArray(src.savingsGoals) ? src.savingsGoals : (Array.isArray(src.savings_goals) ? src.savings_goals : []),
    cards: Array.isArray(src.cards)
      ? src.cards
          .filter((c) => c && typeof c === "object")
          .map((c) => ({
            id: c.id ? String(c.id) : (c.name ? "card-" + String(c.name).toLowerCase().replace(/[^a-z0-9]/g, "-") : "card-0"),
            name: String(c.name || "Tarjeta").trim(),
            cardType: c.cardType === "debito" ? "debito" : "credito",
            person: String(c.person || "").trim(),
            billingCycle: c.billingCycle === "quincenal" || Number(c.cycleDays) === 15 ? "quincenal" : "mensual",
            cutoffDay: Math.min(31, Math.max(1, Number(c.cutoffDay) || 20)),
            dueDay: Math.min(31, Math.max(1, Number(c.dueDay) || 10)),
            targetCutoffMonth: typeof c.targetCutoffMonth === "string" && c.targetCutoffMonth.trim() ? c.targetCutoffMonth.trim() : undefined,
            targetDueMonth: typeof c.targetDueMonth === "string" && c.targetDueMonth.trim() ? c.targetDueMonth.trim() : (typeof c.targetMonth === "string" && c.targetMonth.trim() ? c.targetMonth.trim() : undefined),
            creditLimit: c.creditLimit != null && isFinite(Number(c.creditLimit)) && Number(c.creditLimit) > 0 ? Number(c.creditLimit) : undefined,
            currency: typeof c.currency === "string" && c.currency.trim() ? c.currency.trim() : undefined,
            color: CARD_COLORS.some((col) => col.id === c.color) ? c.color : "emerald",
            hasCashback: c.hasCashback === true || (c.cashbackPercent != null && Number(c.cashbackPercent) > 0),
            cashbackPercent: c.cashbackPercent != null && isFinite(Number(c.cashbackPercent)) && Number(c.cashbackPercent) > 0 ? Number(c.cashbackPercent) : (c.hasCashback ? 1 : undefined),
            excludeServices: c.excludeServices !== false,
            minAmount: c.minAmount != null && isFinite(Number(c.minAmount)) && Number(c.minAmount) > 0 ? Number(c.minAmount) : undefined,
            monthlyCap: c.monthlyCap != null && isFinite(Number(c.monthlyCap)) && Number(c.monthlyCap) > 0 ? Number(c.monthlyCap) : undefined,
          }))
      : [],
    installments: Array.isArray(src.installments)
      ? src.installments
          .filter((i) => i && typeof i === "object")
          .map((i) => {
            const amt = Number(i.amount) > 0 ? Number(i.amount) : 0;
            const totalInst = i.totalInstallments && Number(i.totalInstallments) > 0 ? Math.round(Number(i.totalInstallments)) : undefined;
            const totalDebt = Number(i.totalAmount || i.totalDebt) > 0
              ? Number(i.totalAmount || i.totalDebt)
              : (amt > 0 && totalInst ? amt * totalInst : amt);
            return {
              id: i.id ? String(i.id) : (i.name ? "inst-" + String(i.name).toLowerCase().replace(/[^a-z0-9]/g, "-") : "inst-0"),
              name: String(i.name || "Cuota").trim(),
              person: String(i.person || "").trim(),
              recipient: String(i.recipient || "").trim(),
              totalAmount: totalDebt > 0 ? totalDebt : undefined,
              amount: amt,
              currency: typeof i.currency === "string" && i.currency.trim() ? i.currency.trim() : "S/",
              dueDay: Math.min(31, Math.max(1, Number(i.dueDay) || 15)),
              targetDueMonth: typeof i.targetDueMonth === "string" && i.targetDueMonth.trim() ? i.targetDueMonth.trim() : (typeof i.targetMonth === "string" && i.targetMonth.trim() ? i.targetMonth.trim() : undefined),
              totalInstallments: totalInst,
              paidInstallments: Number(i.paidInstallments) >= 0 ? Math.round(Number(i.paidInstallments)) : 0,
              // Tarjeta vinculada (opcional): cada pago de cuota se anota como
              // consumo en esa tarjeta y se liquida al abonarla. Si la tarjeta
              // se borró después, el pago cae en efectivo (ver Pagar cuota).
              cardId: i.cardId ? String(i.cardId) : undefined,
              notes: String(i.notes || "").trim(),
              color: CARD_COLORS.some((col) => col.id === i.color) ? i.color : "gold",
            };
          })
      : [],
    accountName: typeof src.accountName === "string" && src.accountName.trim()
      ? src.accountName
      : typeof src.account_name === "string" && src.account_name.trim()
      ? src.account_name
      : "Cuenta compartida",
    mascot: MASCOT_KINDS.some((k) => k.id === src.mascot) ? src.mascot : "coin",
    geminiApiKey: typeof src.geminiApiKey === "string" ? src.geminiApiKey.trim() : typeof src.gemini_api_key === "string" ? src.gemini_api_key.trim() : "",
  };
}

// ---- Login: hash de contraseñas (SHA-256 + salt) ----
// La contraseña NUNCA se guarda: solo su hash con un salt aleatorio por
// persona. Usa Web Crypto cuando está disponible (https) y un hash
// determinista de respaldo en contextos sin crypto (p. ej. file://), para que
// crear y verificar coincidan siempre en el mismo dispositivo.
// Hash de contraseña determinista: el MISMO resultado para la misma contraseña
// en cualquier dispositivo y contexto. Se guarda con un prefijo que indica el
// algoritmo usado, para poder verificarla después aunque el contexto cambie
// (la app se abre en HTTPS con Web Crypto, pero también en un archivo local
// file:// donde Web Crypto no existe y se usa el respaldo cyrb53).
//   "sha:" + SHA-256 hex (Web Crypto, HTTPS)
//   "fb:"  + cyrb53 hex (respaldo determinista, funciona en cualquier contexto)
async function hashPassword(password, salt) {
  const input = salt + "::" + password;
  // PBKDF2 (Web Crypto) cuando está disponible: es mucho más resistente a la
  // fuerza bruta que SHA-256 (el estándar para contraseñas). El prefijo guarda
  // las iteraciones para poder subirlas en el futuro sin romper las cuentas.
  //   "pbkdf2:<iteraciones>:<hex>" (Web Crypto, HTTPS)
  if (typeof crypto !== "undefined" && crypto.subtle && crypto.subtle.importKey && crypto.subtle.deriveBits) {
    try {
      const iterations = 150000;
      const key = await crypto.subtle.importKey(
        "raw",
        new TextEncoder().encode(input),
        "PBKDF2",
        false,
        ["deriveBits"]
      );
      const bits = await crypto.subtle.deriveBits(
        { name: "PBKDF2", hash: "SHA-256", salt: new TextEncoder().encode("cuenta-compartida"), iterations },
        key,
        256
      );
      return "pbkdf2:" + iterations + ":" + Array.from(new Uint8Array(bits)).map((b) => b.toString(16).padStart(2, "0")).join("");
    } catch (e) {
      // cae al respaldo determinista
    }
  }
  // cyrb53 (respaldo sin Web Crypto, p. ej. archivo local file://)
  let h1 = 0xdeadbeef;
  let h2 = 0x41c6ce57;
  for (let i = 0; i < input.length; i++) {
    const ch = input.charCodeAt(i);
    h1 = Math.imul(h1 ^ ch, 2654435761);
    h2 = Math.imul(h2 ^ ch, 1597334677);
  }
  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
  return "fb:" + (h2 >>> 0).toString(16) + (h1 >>> 0).toString(16);
}

// Verifica una contraseña contra el hash guardado usando el MISMO algoritmo con
// el que se guardó (lo dice el prefijo del hash). Así, una cuenta creada en
// HTTPS (SHA-256) se puede verificar desde un archivo local (cyrb53) y al
// revés, sin falsos "contraseña incorrecta".
async function checkPassword(password, salt, storedHash) {
  const stored = String(storedHash || "");
  if (!stored) return false;
  const input = salt + "::" + password;
  // Formas "en bruto" (sin prefijo): SHA-256 hex y cyrb53 hex. Los formatos con
  // prefijo se derivan añadiendo "sha:"/"fb:" sobre estas.
  const shaRaw = async () => {
    if (typeof crypto !== "undefined" && crypto.subtle && crypto.subtle.digest) {
      try {
        const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
        return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
      } catch (e) {
        return null;
      }
    }
    return null;
  };
  const cyrbRaw = () => {
    let h1 = 0xdeadbeef;
    let h2 = 0x41c6ce57;
    for (let i = 0; i < input.length; i++) {
      const ch = input.charCodeAt(i);
      h1 = Math.imul(h1 ^ ch, 2654435761);
      h2 = Math.imul(h2 ^ ch, 1597334677);
    }
    h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
    h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
    return (h2 >>> 0).toString(16) + (h1 >>> 0).toString(16);
  };
  if (stored.startsWith("pbkdf2:")) {
    // PBKDF2: el prefijo lleva las iteraciones usadas ("pbkdf2:<iter>:<hex>").
    const parts = stored.split(":");
    const iterations = parseInt(parts[1], 10);
    const want = parts.slice(2).join(":");
    if (!isFinite(iterations) || iterations <= 0 || !want) return false;
    if (typeof crypto !== "undefined" && crypto.subtle && crypto.subtle.importKey && crypto.subtle.deriveBits) {
      try {
        const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(input), "PBKDF2", false, ["deriveBits"]);
        const bits = await crypto.subtle.deriveBits(
          { name: "PBKDF2", hash: "SHA-256", salt: new TextEncoder().encode("cuenta-compartida"), iterations },
          key,
          256
        );
        const hex = Array.from(new Uint8Array(bits)).map((b) => b.toString(16).padStart(2, "0")).join("");
        return hex === want;
      } catch (e) {
        return false;
      }
    }
    return false;
  }
  if (stored.startsWith("fb:")) return "fb:" + cyrbRaw() === stored;
  if (stored.startsWith("sha:")) {
    const raw = await shaRaw();
    return raw !== null && "sha:" + raw === stored;
  }
  // Hash heredado SIN prefijo: la primera versión guardaba SHA-256 en hex puro.
  // Se comprueba contra las formas en bruto (sin añadir prefijos).
  const raw = await shaRaw();
  if (raw !== null && raw === stored) return true;
  return cyrbRaw() === stored;
}

function randomSalt(len) {
  const n = len || 16;
  const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  let s = "";
  if (typeof crypto !== "undefined" && crypto.getRandomValues) {
    const a = new Uint32Array(n);
    crypto.getRandomValues(a);
    for (let i = 0; i < n; i++) s += chars[a[i] % chars.length];
    return s;
  }
  for (let i = 0; i < n; i++) s += chars[Math.floor(Math.random() * chars.length)];
  return s;
}

// Detecta un cambio de nombre en la lista de personas: exactamente un nombre
// sale y exactamente uno entra → se trata de la misma persona renombrada.
// Devuelve { oldName, newName } o null si es ambiguo (añadir/quitar de más).
function detectRename(oldList, newList) {
  const removed = oldList.filter((n) => !newList.includes(n));
  const added = newList.filter((n) => !oldList.includes(n));
  if (removed.length === 1 && added.length === 1) {
    return { oldName: removed[0], newName: added[0] };
  }
  return null;
}

// Construye los mensajes del aviso de novedades: qué cambió en la cuenta desde
// el último estado que este dispositivo mostró (prev) hasta el nuevo estado
// compartido (next). Devuelve [] si no hay cambios. Solo se anuncian cambios
// que NO hizo este dispositivo (el ref lastShownRef se actualiza en cada
// acción local, así que aquí solo llegan los cambios de otras personas).
function buildNotices(prev, next, activeUserName) {
  const lines = [];
  const prevTx = prev.transactions || [];
  const nextTx = next.transactions || [];
  const prevIds = new Set(prevTx.map((t) => t.id));
  const nextIds = new Set(nextTx.map((t) => t.id));
  const currency = (next.settings && next.settings.currency) || "S/";
  const activeUserLower = String(activeUserName || "").trim().toLowerCase();

  // Movimientos nuevos (solo de OTRAS personas, nunca los propios ni los suprimidos)
  nextTx.forEach((t) => {
    if (!prevIds.has(t.id)) {
      if (suppressedTxIdSet.has(String(t.id))) return;
      const authorLower = String(t.person || "").trim().toLowerCase();
      if (activeUserLower && authorLower === activeUserLower) return; // No auto-notificarme a mí mismo
      const sign = t.type === "gasto" ? "−" : "+";
      lines.push(`${t.person || "Alguien"} anotó: ${t.description} (${sign}${fmtMoney(t.amount, txCurrency(t, currency))})`);
    }
  });
  // Movimientos borrados
  prevTx.forEach((t) => {
    if (!nextIds.has(t.id)) {
      lines.push(`${t.person || "Alguien"} borró: ${t.description}`);
    }
  });
  // Movimientos editados: mismo id, contenido distinto → otra persona lo editó.
  const prevById = new Map(prevTx.map((t) => [t.id, t]));
  const sigTx = (t) => [t.type, t.amount, t.description, t.person, t.date, t.currency, t.rate, t.note, t.photo].join("|");
  nextTx.forEach((t) => {
    const old = prevById.get(t.id);
    if (old && sigTx(old) !== sigTx(t)) {
      lines.push(`${t.person || "Alguien"} editó: ${t.description}`);
    }
  });
  // Personas: renombradas (1 sale + 1 entra) o nuevas
  const prevPeople = prev.people || [];
  const nextPeople = next.people || [];
  const rename = detectRename(prevPeople, nextPeople);
  if (rename) {
    lines.push(`${rename.oldName} cambió su nombre a ${rename.newName}`);
  } else {
    nextPeople.forEach((p) => {
      if (!prevPeople.includes(p)) lines.push(`${p} se unió a la cuenta`);
    });
  }
  // Ajustes compartidos: SOLO si NO hay movimientos ni personas en este aviso
  // y solo para cambios principales de cuenta (nombre, moneda o mascota).
  if (lines.length === 0 && prev.settings && next.settings) {
    const prevSt = normalizeSettings(prev.settings);
    const nextSt = normalizeSettings(next.settings);

    if (prevSt.currency !== nextSt.currency) {
      lines.push(`Se cambió la moneda principal a ${nextSt.currency}`);
    } else if (prevSt.accountName !== nextSt.accountName) {
      lines.push(`Se cambió el nombre de la cuenta a «${nextSt.accountName}»`);
    } else if (prevSt.mascot !== nextSt.mascot) {
      const m = MASCOT_KINDS.find((k) => k.id === nextSt.mascot);
      lines.push(`Se cambió el personaje de la cuenta a ${m ? m.label : nextSt.mascot}`);
    }
  }
  // Máximo 4 líneas para no saturar el aviso
  if (lines.length > 4) {
    const extra = lines.length - 4;
    lines.length = 4;
    lines.push(`… y ${extra} cambio${extra > 1 ? "s" : ""} más`);
  }
  return lines;
}

// Id del primer movimiento NUEVO o EDITADO entre dos estados: sirve para que al
// pulsar la notificación del sistema (camino del sondeo) la app salte a esa
// fila concreta. Un borrado no tiene fila que resaltar: se devuelve null.
function firstChangedTxId(prev, next) {
  const prevTx = (prev && prev.transactions) || [];
  const nextTx = (next && next.transactions) || [];
  const prevById = new Map(prevTx.map((t) => [t.id, t]));
  const prevIds = new Set(prevTx.map((t) => t.id));
  const sig = (x) => [x.type, x.amount, x.description, x.person, x.date, x.currency, x.rate, x.note, x.photo].join("|");
  for (const t of nextTx) {
    if (!prevIds.has(t.id)) return t.id; // nuevo
    const old = prevById.get(t.id);
    if (old && sig(old) !== sig(t)) return t.id; // editado
  }
  return null;
}

// ---- Limpieza de movimientos duplicados ----
// Firma de un movimiento para detectar duplicados: mismo tipo, importe,
// descripción, fecha y moneda. La persona NO forma parte de la firma a
// propósito: las copias fantasma de antes del arreglo de sincronización
// suelen tener el nombre antiguo y deben detectarse igualmente.
function dupSignature(t) {
  if (!t) return "";
  return [
    t.type || "",
    (Number(t.amount) || 0).toFixed(2),
    String(t.description || "").trim().toLowerCase(),
    t.date || "",
    t.currency || "",
  ].join("|");
}

// Devuelve los grupos de movimientos con la MISMA firma y distinto id
// (duplicados o copias fantasma). Cada grupo: { sig, items }. La elección de
// cuál conservar se hace en el componente (prefiere el que pertenezca a una
// persona de la lista actual).
function findDuplicates(transactions) {
  const groups = new Map();
  (Array.isArray(transactions) ? transactions : []).forEach((t) => {
    if (!t || t.id == null) return;
    const sig = dupSignature(t);
    if (!sig) return;
    if (!groups.has(sig)) groups.set(sig, []);
    groups.get(sig).push(t);
  });
  const out = [];
  groups.forEach((items, sig) => {
    if (items.length > 1) {
      out.push({ sig, items: items.slice().sort((a, b) => String(a.id).localeCompare(String(b.id))) });
    }
  });
  return out;
}

// Devuelve grupos de movimientos HUÉRFANOS: su person ya no está en la lista
// de la cuenta (fantasmas puros, sin copia gemela). Cada grupo:
// { person, items }. La clave normaliza mayúsculas para no duplicar grupos.
function findOrphans(transactions, people) {
  const inPeople = new Set((people || []).map((p) => String(p || "").trim().toLowerCase()));
  const groups = new Map();
  (Array.isArray(transactions) ? transactions : []).forEach((t) => {
    if (!t || t.id == null) return;
    const name = String(t.person || "").trim();
    if (!name || inPeople.has(name.toLowerCase())) return;
    const key = name.toLowerCase();
    if (!groups.has(key)) groups.set(key, { person: name, items: [] });
    groups.get(key).items.push(t);
  });
  return Array.from(groups.values());
}

// ---------------------------------------------------------------------------
// Sonido y vibración al llegar novedades.
// Se genera un pitido con la Web Audio API (sin archivos externos) y se vibra
// en móvil si el navegador lo permite. Todo va envuelto en try/catch: si el
// navegador bloquea el audio, simplemente no suena y la app sigue igual.
let audioCtx = null;
function ensureAudioCtx() {
  if (typeof window === "undefined") return null;
  if (!audioCtx) {
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return null;
    try {
      audioCtx = new AC();
    } catch (e) {
      return null;
    }
  }
  // Los navegadores silencian el audio hasta el primer gesto del usuario:
  // si está suspendido, intentamos reanudarlo; si no se puede, no suena.
  if (audioCtx.state === "suspended") {
    try {
      audioCtx.resume();
    } catch (e) {}
  }
  return audioCtx;
}

// Pitido corto y agradable: dos tonos ascendentes (nota y nota + 3ª mayor).
function playNoticeSound() {
  const ctx = ensureAudioCtx();
  if (!ctx) return;
  try {
    const t0 = ctx.currentTime;
    [0, 0.16].forEach((offset, i) => {
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = "sine";
      osc.frequency.value = i === 0 ? 880 : 1318.5;
      gain.gain.setValueAtTime(0.0001, t0 + offset);
      gain.gain.exponentialRampToValueAtTime(0.22, t0 + offset + 0.02);
      gain.gain.exponentialRampToValueAtTime(0.0001, t0 + offset + 0.16);
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.start(t0 + offset);
      osc.stop(t0 + offset + 0.18);
    });
  } catch (e) {
    // Silencio si el navegador lo bloquea.
  }
}

// Vibración corta en móviles que la soporten (Android/Chrome; iOS Safari no).
function vibrateNotice() {
  try {
    if (typeof navigator !== "undefined" && navigator.vibrate) navigator.vibrate(120);
  } catch (e) {}
}

// Tono del personaje al reaccionar a un movimiento, con la misma Web Audio
// API (sin archivos externos): un arpegio que SUBE (do-mi-sol) cuando entra
// dinero y una melodía corta que BAJA (mi-do) cuando hay un gasto. Va envuelto
// en try/catch: si el navegador bloquea el audio, no suena y todo sigue igual.
function playMascotSound(mood) {
  const ctx = ensureAudioCtx();
  if (!ctx) return;
  try {
    const t0 = ctx.currentTime;
    const happy = mood === "happy";
    const notes = happy ? [523.25, 659.25, 783.99] : [329.63, 261.63];
    notes.forEach((freq, i) => {
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = happy ? "triangle" : "sine";
      osc.frequency.value = freq;
      const start = t0 + i * 0.11;
      gain.gain.setValueAtTime(0.0001, start);
      gain.gain.exponentialRampToValueAtTime(0.16, start + 0.02);
      gain.gain.exponentialRampToValueAtTime(0.0001, start + 0.3);
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.start(start);
      osc.stop(start + 0.32);
    });
  } catch (e) {
    // Silencio si el navegador lo bloquea.
  }
}

// Desbloqueo global de audio con el primer toque o tecla del usuario
if (typeof window !== "undefined") {
  const unlockAudio = () => {
    ensureAudioCtx();
    window.removeEventListener("click", unlockAudio);
    window.removeEventListener("touchstart", unlockAudio);
    window.removeEventListener("keydown", unlockAudio);
  };
  window.addEventListener("click", unlockAudio, { passive: true });
  window.addEventListener("touchstart", unlockAudio, { passive: true });
  window.addEventListener("keydown", unlockAudio, { passive: true });
}

// Notificación del sistema (Service Worker showNotification en móviles + Notification API en PC)
// Avisa de forma fiable cuando otra persona realiza un cambio en la cuenta compartida
let appFocusTxHandler = null;
function setAppFocusTxHandler(fn) {
  appFocusTxHandler = typeof fn === "function" ? fn : null;
}
function requestTxFocus(txId) {
  if (txId && typeof appFocusTxHandler === "function") {
    try {
      appFocusTxHandler(String(txId));
    } catch (e) {
      // Si el manejador falla (p. ej. la app aún no está lista), no pasa nada.
    }
  }
}

async function showSystemNotice(lines, txId) {
  try {
    if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
    const body = (Array.isArray(lines) ? lines : []).join("\n") || "Hay novedades en la cuenta compartida.";

    // 1. En móviles Android, iOS PWA y navegadores con Service Worker activo:
    if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
      try {
        const reg = await navigator.serviceWorker.getRegistration();
        if (reg && typeof reg.showNotification === "function") {
          await reg.showNotification("Cuenta compartida", {
            body: body,
            icon: "./icons/icon.svg",
            badge: "./icons/icon.svg",
            tag: "cc-novedades-" + (txId || "general"),
            renotify: true,
            vibrate: [250, 100, 250, 100, 250],
            data: { url: "./", txId: txId || "" },
          });
          return;
        }
      } catch (e) {}
    }

    // 2. Fallback a Notification API estándar (escritorio / PC):
    const n = new Notification("Cuenta compartida", {
      body: body,
      icon: "./icons/icon.svg",
      tag: "cc-novedades",
      renotify: true,
    });
    n.onclick = () => {
      try {
        window.focus();
        n.close();
        if (txId) requestTxFocus(txId);
      } catch (e) {}
    };
  } catch (e) {
    // Sin notificaciones (bloqueadas o no soportadas): no pasa nada.
  }
}

// ---------------------------------------------------------------------------
// Notificación del sistema INMEDIATA desde Realtime.
// ---------------------------------------------------------------------------
// Últimas líneas anunciadas al instante y cuándo: sirven para que el refresh
// que llega justo después (debounce de 400 ms) NO duplique la notificación.
let lastRealtimeNoticeSig = "";
let lastRealtimeNoticeAt = 0;
let suppressNoticesUntil = 0;
const suppressedTxIdSet = new Set();

function noticeSig(lines) {
  return (Array.isArray(lines) ? lines : []).join("|");
}

// Convierte un evento Realtime (INSERT/UPDATE/DELETE de transactions, o alta de
// people) en líneas legibles con el MISMO formato que buildNotices. Devuelve []
// si el evento no es anunciable (p. ej. borrado de persona: el refresh lo avisa).
function realtimeNoticeLines(payload, baseCurrency) {
  const table = payload && payload.table;
  const ev = payload && payload.eventType;
  const base = baseCurrency || "S/";
  if (table === "transactions") {
    const row = payload.new || payload.old;
    if (!row) return [];
    if (ev === "INSERT") {
      const sign = row.type === "gasto" ? "−" : "+";
      return [`${row.person || "Alguien"} anotó: ${row.description || ""} (${sign}${fmtMoney(Number(row.amount) || 0, txCurrency(row, base))})`];
    }
    if (ev === "UPDATE") return [`${row.person || "Alguien"} editó: ${row.description || ""}`];
    if (ev === "DELETE") return [`${row.person || "Alguien"} borró: ${row.description || ""}`];
    return [];
  }
  if (table === "people" && ev === "INSERT") {
    const name = String((payload.new && payload.new.name) || "").trim();
    return name ? [`${name} se unió a la cuenta`] : [];
  }
  return [];
}

// ---------------------------------------------------------------------------
// Adaptador de almacenamiento.
// Orden de preferencia:
//   1. Supabase (nube, si APP_SUPABASE_URL + APP_SUPABASE_KEY están definidas)
//   2. Backend REST local (SQLite, si APP_API_BASE)
//   3. window.storage (Claude Artifacts)
//   4. localStorage (último recurso)
// La interfaz imita a window.storage: get(key, shared) → { value } | null,
// set(key, value, shared) → resultado con ok. value siempre es un JSON string.
// ---------------------------------------------------------------------------
const API_BASE = (typeof window !== "undefined" && window.APP_API_BASE) || "";
const SUPABASE_URL = (typeof window !== "undefined" && window.APP_SUPABASE_URL) || "";
const SUPABASE_KEY = (typeof window !== "undefined" && window.APP_SUPABASE_KEY) || "";
const HAS_SUPABASE = !!(SUPABASE_URL && SUPABASE_KEY);
// En Claude Artifacts existe window.storage y no hay APP_API_BASE configurada:
// usamos el puente local directamente sin golpear un backend que no existe.
const HAS_LOCAL_BRIDGE = typeof window !== "undefined" && !!window.storage;
// Clave VAPID pública para las notificaciones push (la privada vive en la Edge
// Function de Supabase). Sin clave no hay push: el toggle lo explica.
const VAPID_PUBLIC_KEY = (typeof window !== "undefined" && window.APP_VAPID_PUBLIC_KEY) || "";
const HAS_PUSH = !!(VAPID_PUBLIC_KEY && typeof navigator !== "undefined" && "serviceWorker" in navigator);

// ============================================================================
//  Supabase Realtime — cambios al instante, sin el sondeo de 2 s
// ----------------------------------------------------------------------------
//  Se suscribe a los cambios de la base de datos (people, transactions,
//  settings, history). Cuando OTRA persona anota, edita o borra algo, llega un
//  evento por WebSocket y la app refresca al momento (en vez de esperar al
//  siguiente sondeo). El sondeo de 2 s se convierte en un "heartbeat" lento de
//  seguridad por si Realtime se cae o no está disponible (backend local,
//  Claude, sin WebSocket). Todo lo de abajo es opcional y con fallback: si
//  algo falla, la app sigue funcionando con el sondeo clásico.
// ============================================================================
let realtimeClientPromise = null; // promesa del cliente supabase-js (se crea una vez)
let realtimeSubscribed = false;   // true mientras el canal está suscrito
let realtimeFailed = false;       // si falla, no reintentar a cada segundo

async function getRealtimeClient() {
  if (!HAS_SUPABASE || typeof WebSocket === "undefined") return null;
  if (realtimeClientPromise) return realtimeClientPromise;
  realtimeClientPromise = (async () => {
    try {
      const mod = await import("https://esm.sh/@supabase/supabase-js@2");
      return mod.createClient(SUPABASE_URL, SUPABASE_KEY, {
        auth: { persistSession: false },
      });
    } catch (e) {
      // SDK no cargó (sin red, CDN bloqueado...): se mantiene el sondeo clásico.
      realtimeFailed = true;
      return null;
    }
  })();
  return realtimeClientPromise;
}

function realtimeAvailable() {
  return HAS_SUPABASE && !realtimeFailed && typeof WebSocket !== "undefined";
}

// Compara dos movimientos por contenido (para reconocer el eco propio de
// Realtime: un cambio que YA está reflejado en nuestra pantalla no se refresca).
// ============================================================================
//  Gestor de Cuentas & Invitaciones (Multi-Cuenta / Multi-Tenant)
// ============================================================================
function generateAccountCode(prefix = "CC") {
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
  let randomPart = "";
  for (let i = 0; i < 4; i++) {
    randomPart += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  const cleanPrefix = String(prefix || "CC").trim().toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 3) || "CC";
  return `${cleanPrefix}-${randomPart}`;
}

function getActiveAccountId() {
  if (typeof window !== "undefined") {
    try {
      const params = new URLSearchParams(window.location.search);
      const urlCode = params.get("cuenta") || params.get("code") || params.get("acc");
      if (urlCode && urlCode.trim()) {
        const clean = urlCode.trim();
        localStorage.setItem("cc:active_account_id", clean);
        return clean;
      }
      const saved = localStorage.getItem("cc:active_account_id");
      if (saved && saved.trim()) return saved.trim();
    } catch (e) {}
  }
  return "default";
}

function setActiveAccountId(accId) {
  if (typeof window !== "undefined") {
    try {
      localStorage.setItem("cc:active_account_id", String(accId || "default").trim());
    } catch (e) {}
  }
}

function getKnownAccounts() {
  const defaultAcc = { id: "default", name: "Cuenta compartida", code: "DEFAULT" };
  if (typeof window === "undefined") return [defaultAcc];
  try {
    const raw = localStorage.getItem("cc:known_accounts");
    const parsed = raw ? JSON.parse(raw) : [];
    const list = Array.isArray(parsed) ? parsed : [];
    const hasDefault = list.some(a => a && (a.id === "default" || a.code === "DEFAULT"));
    const fullList = hasDefault ? list : [defaultAcc, ...list];
    return fullList
      .filter(a => a && (a.id || a.code))
      .map(a => {
        if (a && (a.id === "default" || a.code === "DEFAULT")) {
          return { ...a, name: "Cuenta compartida", code: "DEFAULT" };
        }
        return a;
      });
  } catch (e) {
    return [defaultAcc];
  }
}

function saveKnownAccount(acc) {
  if (!acc || (!acc.id && !acc.code)) return;
  try {
    const list = getKnownAccounts();
    const cleanId = String(acc.id || acc.code).trim();
    const cleanCode = String(acc.code || acc.id).trim();
    const filtered = list.filter(a => a && a.id !== cleanId && a.code !== cleanCode);
    const updated = [{ id: cleanId, name: acc.name || "Cuenta compartida", code: cleanCode }, ...filtered];
    localStorage.setItem("cc:known_accounts", JSON.stringify(updated.slice(0, 15)));
  } catch (e) {}
}

function removeKnownAccount(targetIdOrCode) {
  if (!targetIdOrCode) return;
  try {
    const clean = String(targetIdOrCode).trim().toLowerCase();
    const list = getKnownAccounts();
    const filtered = list.filter(a => a && String(a.id || "").toLowerCase() !== clean && String(a.code || "").toLowerCase() !== clean);
    localStorage.setItem("cc:known_accounts", JSON.stringify(filtered));
  } catch (e) {}
}

function txSame(a, b) {
  if (!a || !b) return false;
  return (
    String(a.id) === String(b.id) &&
    String(a.type) === String(b.type) &&
    Number(a.amount) === Number(b.amount) &&
    String(a.description || "") === String(b.description || "") &&
    String(a.person || "") === String(b.person || "") &&
    String(a.date || "") === String(b.date || "") &&
    // Moneda, tipo de cambio y nota también cuentan: si OTRA persona edita
    // solo esos campos, el cambio debe refrescarse (no tratarse como eco
    // propio, que lo filtraría hasta el siguiente heartbeat).
    String((a.currency || "").trim()) === String((b.currency || "").trim()) &&
    Number(a.rate || 0) === Number(b.rate || 0) &&
    String(a.note || "").trim() === String(b.note || "").trim()
  );
}

// Suscribe un canal a los cambios de las tablas de la cuenta. onChange(payload)
// se llama por cada evento (INSERT/UPDATE/DELETE) con { table, eventType, new, old }.
async function subscribeRealtime(onChange, onStatus) {
  if (!realtimeAvailable()) {
    if (onStatus) onStatus("polling");
    return null;
  }
  const client = await getRealtimeClient();
  if (!client) {
    if (onStatus) onStatus("polling");
    return null;
  }
  try {
    const channel = client
      .channel("db-changes")
      .on("postgres_changes", { event: "*", schema: "public" }, (payload) => {
        const table = payload && payload.table;
        if (table === "people" || table === "transactions" || table === "settings" || table === "history") {
          onChange(payload);
        }
      })
      .subscribe((status) => {
        realtimeSubscribed = status === "SUBSCRIBED";
        if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") realtimeFailed = true;
        // Avisa al indicador de la cabecera: en vivo, reconectando o caído.
        if (onStatus) {
          if (status === "SUBSCRIBED") onStatus("connected");
          else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") onStatus("reconnecting");
        }
      });
    return channel;
  } catch (e) {
    realtimeFailed = true;
    if (onStatus) onStatus("polling");
    return null;
  }
}

function unsubscribeRealtime(channel) {
  if (channel) {
    try {
      channel.unsubscribe();
    } catch (e) {
      /* sin conexión */
    }
  }
  realtimeSubscribed = false;
}

// ---- Diagnóstico Realtime (Ajustes → Diagnóstico) ----
// Consulta la base: si la publicación supabase_realtime existe y cuántos
// dispositivos hay suscritos a push. Usa la función cc_diag_realtime de
// schema.sql (RPC); si aún no está instalada en la base, cuenta
// push_subscriptions directamente por REST y deja la publicación en
// "sin verificar" para recordar que falta ejecutar el bloque de schema.sql.
async function diagRealtimeInfo() {
  const out = { publicationExists: null, pushDevices: null, rpcOk: false };
  if (!HAS_SUPABASE) return out;
  try {
    const r = await supabaseFetch("rpc/cc_diag_realtime", {
      method: "POST",
      body: "{}",
    });
    if (r && typeof r === "object") {
      out.rpcOk = true;
      out.publicationExists = !!r.publication_exists;
      out.pushDevices = Number(r.push_devices) || 0;
      return out;
    }
  } catch (e) {
    // La función aún no está en la base: se cuenta por REST debajo.
  }
  try {
    const rows = await supabaseFetch("push_subscriptions?select=endpoint");
    out.pushDevices = Array.isArray(rows) ? rows.length : null;
  } catch (e) {
    out.pushDevices = null;
  }
  return out;
}

class ApiError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "ApiError";
    this.status = status;
  }
}

// Enfriamientos independientes para no golpear un servicio caído en cada sondeo
let backendDownUntil = 0;
let supabaseDownUntil = 0;
// Se marca cuando Supabase responde que faltan columnas o tablas (esquema de
// base de datos desactualizado): la app avisa al usuario para que re-ejecute
// el schema.sql y no revierte en pantalla lo que solo quedó guardado en local.
let supabaseSchemaOutdated = false;
// Se marca si la nube rechaza la columna installments (base de datos creada
// con un schema anterior): los reintentos de guardado la omiten para no
// perder el resto de los ajustes. Se ejecuta el SQL de migración para
// recuperarla (alter table settings add column if not exists installments).
let supabaseSettingsNoInstallments = false;
let supabaseNoAccountId = typeof localStorage !== "undefined" && localStorage.getItem("cc:supabase_no_account_id") === "1";
// Último origen real de los datos: "supabase" | "backend" | "local".
// La app lo expone para avisar si la nube no responde y quedó en modo local.
let lastSyncSource = "local";

function markBackendDown() {
  backendDownUntil = Date.now() + 10000;
}
function markSupabaseDown() {
  supabaseDownUntil = Date.now() + 10000;
}

function backendOk() {
  if (typeof fetch !== "function") return false;
  // Sin APP_API_BASE el backend REST no existe; si hay Supabase, lo usamos solo
  // si alguien configuró una URL REST explícita.
  if (HAS_SUPABASE && API_BASE === "") return false;
  if (HAS_LOCAL_BRIDGE && API_BASE === "") return false;
  return true;
}

function supabaseOk() {
  if (!HAS_SUPABASE || typeof fetch !== "function") return false;
  return Date.now() >= supabaseDownUntil;
}

// El servicio está caído con error de red/timeout (status 0) o error del
// servidor (5xx). Un 4xx significa petición inválida: el servicio sigue sano.
// Un error que NO es ApiError (p. ej. el de negocio "cuenta llena") tampoco
// debe marcar el servicio como caído: eso haría que el reintento se saltara
// Supabase, cayera al fallback local y devolviera éxito (entrando en la app
// aunque la cuenta esté llena).
function serviceIsDown(e) {
  if (!(e instanceof ApiError)) return false;
  return e.status === 0 || e.status >= 500;
}

async function apiFetch(base, path, options) {
  const opts = options || {};
  let res;
  try {
    res = await fetch(base + path, {
      ...opts,
      method: opts.method || "GET",
      headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
      signal: typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(8000) : undefined,
    });
  } catch (e) {
    throw new ApiError(0, "red: " + (e && e.message ? e.message : ""));
  }
  if (!res.ok) throw new ApiError(res.status, "HTTP " + res.status);
  if (res.status === 204) return null;
  const text = await res.text();
  if (!text || !text.trim()) return null;
  try {
    return JSON.parse(text);
  } catch (e) {
    // Respuesta 2xx sin JSON válido: no la tratamos como "servicio caído"
    throw new ApiError(res.status, "JSON inválido");
  }
}

async function supabaseFetch(path, options) {
  if (Date.now() < supabaseDownUntil) throw new ApiError(0, "supabase no disponible");
  const opts = options || {};
  let res;
  try {
    res = await fetch(SUPABASE_URL + "/rest/v1/" + path, {
      ...opts,
      method: opts.method || "GET",
      headers: {
        apikey: SUPABASE_KEY,
        Authorization: "Bearer " + SUPABASE_KEY,
        "Content-Type": "application/json",
        ...(opts.headers || {}),
      },
      signal: typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(8000) : undefined,
    });
  } catch (e) {
    throw new ApiError(0, "red supabase: " + (e && e.message ? e.message : ""));
  }
  if (!res.ok) {
    // PostgREST detalla el fallo en el body JSON: si el código indica que
    // faltan columnas o tablas (esquema de base de datos desactualizado),
    // lo marcamos para que la app avise al usuario y no borre en pantalla
    // los cambios que solo quedaron guardados en este dispositivo.
    const err = new ApiError(res.status, "supabase HTTP " + res.status);
    try {
      const body = await res.json();
      const code = body && body.code ? String(body.code) : "";
      if (code === "42703" || code === "42P01" || code === "PGRST204" || code === "PGRST205") {
        supabaseSchemaOutdated = true;
      }
    } catch (e) {
      // Sin body JSON: no se puede clasificar el error.
    }
    throw err;
  }
  if (res.status === 204) return null;
  const text = await res.text();
  if (!text || !text.trim()) return null;
  try {
    return JSON.parse(text);
  } catch (e) {
    throw new ApiError(res.status, "JSON inválido");
  }
}

// Mapea una fila de transactions (SQLite o Supabase) al formato de la app
function rowToTx(r) {
  let type = r.type;
  let recipient = r.recipient || undefined;
  let isInternal = r.isInternal || undefined;
  let note = r.note != null ? String(r.note) : undefined;
  let description = r.description || "";
  let isSettled = r.isSettled === true;
  let cashbackAmount = r.cashbackAmount != null ? Number(r.cashbackAmount) : (r.cashback_amount != null ? Number(r.cashback_amount) : undefined);
  let cashbackPercent = r.cashbackPercent != null ? Number(r.cashbackPercent) : (r.cashback_percent != null ? Number(r.cashback_percent) : undefined);

  // Detección de marca de saldado
  if (note && /\[saldado\]/i.test(note)) {
    isSettled = true;
    note = note.replace(/\[saldado\]/gi, "").trim() || undefined;
  }

  // Detección de marca de cashback
  if (note && /\[cashback:([0-9.]+):?([0-9.]*)%?\]/i.test(note)) {
    const match = note.match(/\[cashback:([0-9.]+):?([0-9.]*)%?\]/i);
    if (match) {
      cashbackAmount = Number(match[1]);
      if (match[2]) cashbackPercent = Number(match[2]);
      note = note.replace(/\[cashback:[^\]]+\]/gi, "").trim() || undefined;
    }
  }

  // Detección de préstamo entre personas (soporta tags de compatibilidad y descripciones)
  if (note && /\[prestamo:([^\]]+)\]/.test(note)) {
    const match = note.match(/\[prestamo:([^\]]+)\]/);
    if (match && match[1]) {
      type = "prestamo";
      recipient = match[1].trim();
      isInternal = true;
      note = note.replace(/\[prestamo:[^\]]+\]/, "").trim() || undefined;
    }
  } else if (r.payment_method === "prestamo" || r.paymentMethod === "prestamo" || type === "prestamo") {
    type = "prestamo";
    isInternal = true;
    if (!recipient && description && /prestó a\s+([^·\(\)]+)/i.test(description)) {
      const match = description.match(/prestó a\s+([^·\(\)]+)/i);
      if (match && match[1]) recipient = match[1].trim();
    }
  } else if (description && /prestó a\s+([^·\(\)]+)/i.test(description)) {
    type = "prestamo";
    isInternal = true;
    const match = description.match(/prestó a\s+([^·\(\)]+)/i);
    if (match && match[1]) recipient = match[1].trim();
  }

  return {
    id: r.id,
    type,
    amount: Number(r.amount),
    description,
    person: r.person,
    recipient,
    isInternal,
    isSettled,
    cashbackAmount,
    cashbackPercent,
    date: r.date,
    currency: r.currency,
    rate: r.rate != null ? Number(r.rate) : undefined,
    note,
    photo: r.photo != null ? String(r.photo) : undefined,
    paymentMethod: r.paymentMethod || r.payment_method || (type === "prestamo" ? "prestamo" : undefined),
    cardId: r.cardId || r.card_id || undefined,
    installmentId: r.installmentId || r.installment_id || undefined,
    loanId: r.loanId || r.loan_id || undefined,
  };
}

// Convierte un movimiento al formato exacto de columnas de Supabase (sin propiedades camelCase ni extras)
function txToSupabaseRow(t, customAccId) {
  const accId = customAccId || t.accountId || t.account_id || getActiveAccountId();
  if (!t) return null;
  const isPrestamo = t.type === "prestamo" || t.paymentMethod === "prestamo";
  const recipient = t.recipient || "";
  let finalNote = t.note ? String(t.note) : undefined;
  if (isPrestamo && recipient) {
    if (!finalNote || !finalNote.includes(`[prestamo:${recipient}]`)) {
      finalNote = (finalNote ? finalNote + " " : "") + `[prestamo:${recipient}]`;
    }
  }
  if (t.isSettled) {
    if (!finalNote || !finalNote.includes("[saldado]")) {
      finalNote = (finalNote ? finalNote + " " : "") + "[saldado]";
    }
  }
  if (t.cashbackAmount != null && Number(t.cashbackAmount) > 0) {
    const cbTag = `[cashback:${Number(t.cashbackAmount)}${t.cashbackPercent != null ? ":" + t.cashbackPercent + "%" : ""}]`;
    if (!finalNote || !finalNote.includes(cbTag)) {
      finalNote = (finalNote ? finalNote + " " : "") + cbTag;
    }
  }

  const row = {
    account_id: accId,
    category: t.category ? String(t.category).trim() : undefined,
    id: String(t.id),
    type: t.type === "pago_tarjeta" ? "pago_tarjeta" : t.type === "ingreso" ? "ingreso" : "gasto",
    amount: Number(t.amount) || 0,
    description: String(t.description || ""),
    person: String(t.person || ""),
    date: String(t.date || ""),
    currency: t.currency ? String(t.currency) : undefined,
    rate: t.rate != null ? Number(t.rate) : undefined,
    note: finalNote,
    created_at: t.created_at || new Date().toISOString(),
  };
  if (isPrestamo) {
    row.payment_method = "prestamo";
  } else if (t.paymentMethod || t.payment_method) {
    row.payment_method = String(t.paymentMethod || t.payment_method);
  }
  if (t.cardId || t.card_id) {
    row.card_id = String(t.cardId || t.card_id);
  }
  if (t.installmentId || t.installment_id) {
    row.installment_id = String(t.installmentId || t.installment_id);
  }
  if (t.photo) {
    row.photo = String(t.photo);
  }
  return row;
}

// ---- Espejo local de movimientos ----
// Mantiene cc:transactions alineado con la nube tras CADA operación que toca
// la nube (añadir, borrar, editar, renombrar). Sin esto, la recuperación del
// arranque releería copias viejas y volvería a subir movimientos borrados
// (reaparecen) o con el nombre antiguo (duplican la persona).
function writeTxMirror(list) {
  try {
    // Conserva las fotos que ya estaban en el espejo (los refrescos ligeros no
    // descargan las fotos por rendimiento): así la recuperación del arranque no
    // pierde los recibos de los movimientos que este dispositivo ya conocía.
    let prev = [];
    try {
      prev = JSON.parse(localStorage.getItem("cc:transactions") || "[]");
    } catch (e) {
      prev = [];
    }
    const prevById = new Map(
      (Array.isArray(prev) ? prev : []).filter((t) => t && t.id != null).map((t) => [t.id, t])
    );
    const merged = (Array.isArray(list) ? list : []).map((t) => {
      const old = t && t.id != null ? prevById.get(t.id) : null;
      if (t && t.photo == null && old && old.photo) return { ...t, photo: old.photo };
      return t;
    });
    localStorage.setItem("cc:transactions", JSON.stringify(merged));
  } catch (e) {
    // Sin localStorage: la recuperación no tendrá copias locales que re-subir.
  }
}

// Tombstone local de movimientos borrados en este dispositivo: la recuperación
// no re-subirá estos ids aunque sigan en el espejo viejo.
function txDeletedIds() {
  try {
    return new Set(safeParse(localStorage.getItem("cc:deletedTx"), []));
  } catch (e) {
    return new Set();
  }
}
function markTxDeleted(id) {
  try {
    const s = txDeletedIds();
    s.add(String(id));
    localStorage.setItem("cc:deletedTx", JSON.stringify(Array.from(s).slice(-300)));
    // Si este id se había restaurado (deshecho), ya no está restaurado.
    unmarkTxRestored(id);
  } catch (e) {}
}
function unmarkTxDeleted(id) {
  try {
    const s = txDeletedIds();
    s.delete(String(id));
    localStorage.setItem("cc:deletedTx", JSON.stringify(Array.from(s)));
  } catch (e) {}
}

// ---- Restaurados localmente (al deshacer un borrado) ----
// Al deshacer un borrado sin conexión, la restauración solo queda en este
// dispositivo (espejo + tombstone limpio), pero la entrada "delTx" puede
// seguir en el historial de la nube. Estos ids se excluyen del tombstone de
// la recuperación del arranque para que el movimiento se re-subir en vez de
// desaparecer de pantalla.
function txRestoredIds() {
  try {
    const v = safeParse(localStorage.getItem("cc:restoredTx"), []);
    return new Set(Array.isArray(v) ? v : []);
  } catch (e) {
    return new Set();
  }
}
function markTxRestored(id) {
  try {
    const s = txRestoredIds();
    s.add(String(id));
    localStorage.setItem("cc:restoredTx", JSON.stringify(Array.from(s).slice(-300)));
  } catch (e) {}
}
function unmarkTxRestored(id) {
  try {
    const s = txRestoredIds();
    s.delete(String(id));
    localStorage.setItem("cc:restoredTx", JSON.stringify(Array.from(s)));
  } catch (e) {}
}

// ---- Cola de escrituras pendientes de subir (nube caída o sin conexión) ----
// Cuando un guardado solo pudo hacerse en LOCAL (p. ej. con la nube caída),
// se encola aquí y el sondeo la reintenta en cuanto el servicio vuelve. Sin
// esta cola, un movimiento "guardado" sin conexión desaparecía de la nube y
// de la pantalla de los demás cuando el sondeo sobreescribía el estado local.
function readPendingWrites() {
  try {
    const v = safeParse(localStorage.getItem("cc:pendingWrites"), []);
    const list = Array.isArray(v) ? v : [];
    // Los pendientes que envejecen demasiado (p. ej. un teléfono apagado un
    // mes) se descartan: re-subirlos tras tanto tiempo causaría más líos que
    // beneficios. También descarta entradas malformadas de versiones viejas.
    const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 días
    const now = Date.now();
    return list.filter((w) => w && typeof w === "object" && (!w.at || now - Number(w.at) < maxAge));
  } catch (e) {
    return [];
  }
}
function writePendingWrites(list) {
  try {
    // Tope de seguridad: nunca más de 50 pendientes (las más recientes).
    localStorage.setItem("cc:pendingWrites", JSON.stringify(list.slice(-50)));
  } catch (e) {}
}
function enqueuePendingWrite(op) {
  const pending = readPendingWrites();
  const accId = getActiveAccountId();
  writePendingWrites([...pending, { ...op, accountId: op.accountId || accId, at: Date.now() }]);
  // Se sube enseguida: no se espera al sondeo (que con Realtime va a 30 s).
  schedulePendingFlush();
}
// Solo tiene sentido encolar pendientes si hay una NUBE real configurada
// (Supabase o backend REST): en modo 100% local (Claude Artifacts, test sin
// Supabase) no hay nada que re-subir y el aviso quedaría pegado para siempre.
function shouldQueuePending() {
  return !!HAS_SUPABASE || (typeof API_BASE === "string" && API_BASE.trim() !== "");
}

// ---------------------------------------------------------------------------
// Reintento RÁPIDO de escrituras pendientes.
// Cuando una escritura no llega a la nube se encola, pero antes solo se
// reintentaba en el siguiente ciclo del sondeo — que con Realtime activo va a
// 30 s (60 s en segundo plano). Por eso tardaba tanto en subir. Ahora, al
// encolar algo se programa un reintento a los ~1,5 s y, si sigue sin poder, se
// repite cada ~4 s hasta que la nube responde, sin esperar al sondeo. La UI se
// entera por onPendingWritesChanged para actualizar el aviso al instante.
let pendingFlushTimer = null;
let pendingFlushBusy = false; // un flush ya está en curso: no se duplica
let onPendingWritesChanged = null; // lo registra la app (useEffect)
let onHistoryFlushed = null; // lo registra la app: refresca el panel al re-subir

function schedulePendingFlush(delayMs) {
  if (pendingFlushTimer || pendingFlushBusy) return; // ya hay uno programado o en curso
  pendingFlushTimer = setTimeout(async () => {
    pendingFlushTimer = null;
    let remaining = 0;
    try {
      remaining = await storage.flushPendingWrites();
    } catch (e) {
      remaining = -1; // error de red: se reintenta en un momento
    }
    if (typeof onPendingWritesChanged === "function") {
      try {
        onPendingWritesChanged(remaining < 0 ? storage.pendingWritesCount() : remaining);
      } catch (e) {}
    }
    // Si aún quedan sin subir (nube caída, enfriamiento o error), se reintenta
    // pronto. El flag pendingFlushBusy lo gestiona flushPendingWrites y ya está
    // liberado al llegar aquí, así que el guard de arriba no se traga este
    // reintento (bug: antes el bucle rápido moría tras el primer intento y todo
    // volvía a depender del sondeo de 30 s).
    if (remaining !== 0) schedulePendingFlush(4000);
  }, delayMs == null ? 1500 : delayMs);
}

const storage = {
  // Estado completo de la cuenta (people + transactions + settings) en una llamada
  
  // Crea un nuevo espacio / cuenta independiente en el mismo servidor
  async createAccount(name, customPrefix) {
    const accId = "acc_" + uid();
    const code = generateAccountCode(customPrefix || "CC");
    const accountName = String(name || "Cuenta compartida").trim();
    const newAcc = { id: accId, name: accountName, code };
    
    if (supabaseOk()) {
      try {
        await supabaseFetch("accounts", {
          method: "POST",
          headers: { Prefer: "resolution=merge-duplicates" },
          body: JSON.stringify({ id: accId, name: accountName, code }),
        });
        await supabaseFetch("settings", {
          method: "POST",
          headers: { Prefer: "resolution=merge-duplicates" },
          body: JSON.stringify({
            id: 1,
            account_id: accId,
            account_name: accountName,
            admin: String(name || "").trim(),
            allowMemberInvites: false,
            invitations: [],
            split_enabled: true,
            currency: "S/",
            mascot: "coin",
            categories: DEFAULT_CATEGORIES,
          }),
        });
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    setActiveAccountId(accId);
    saveKnownAccount(newAcc);
    return newAcc;
  },

  // Busca una cuenta por ID o Código de invitación
  async findAccount(codeOrId) {
    const clean = String(codeOrId || "").trim();
    if (!clean) return null;
    if (clean.toLowerCase() === "default" || clean.toUpperCase() === "DEFAULT") {
      return { id: "default", name: "Cuenta compartida", code: "DEFAULT" };
    }
    if (supabaseOk()) {
      try {
        const rows = (await supabaseFetch(`accounts?or=(id.eq.${encodeURIComponent(clean)},code.ilike.${encodeURIComponent(clean)})&select=*`)) || [];
        if (Array.isArray(rows) && rows.length > 0) {
          const acc = rows[0];
          saveKnownAccount(acc);
          return acc;
        }
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    const known = getKnownAccounts().find(a => a && (String(a.id).toLowerCase() === clean.toLowerCase() || (a.code && String(a.code).toLowerCase() === clean.toLowerCase())));
    return known || null;
  },

  async getState() {
    if (supabaseOk()) {
      try {
        lastSyncSource = "supabase";
        // Las fotos NO se descargan en el sondeo (son base64 grandes: cada 2 s y
        // por cada dispositivo sería un gasto enorme de datos). Solo se piden los
        // ids que tienen foto (consulta ligera) y la imagen se carga bajo demanda
        // al abrirla (storage.getTxPhoto). El rendimiento gana muchísimo.
        let isOutdated = false;
        let peopleRes = [];
        let txRes = [];
        let settingsRes = [];
        let photoIdsRes = [];

        const curAccId = getActiveAccountId();
        const isDefaultSpace = curAccId === "default" || curAccId === "DEFAULT";

        try {
          // Consultar datos de la cuenta activa (soporta tanto account_id como fallback mono-cuenta)
          const filterAcc = `account_id=eq.${encodeURIComponent(curAccId)}`;
          [peopleRes, txRes, photoIdsRes] = await Promise.all([
            supabaseFetch(`people?${filterAcc}&select=name`).catch(() => isDefaultSpace ? supabaseFetch(`people?select=name`).catch(() => []) : []),
            supabaseFetch(`transactions?${filterAcc}&select=id,type,amount,description,category,person,date,currency,rate,note,payment_method,card_id&order=created_at.desc`).catch(() => isDefaultSpace ? supabaseFetch(`transactions?select=id,type,amount,description,category,person,date,currency,rate,note,payment_method,card_id&order=created_at.desc`).catch(() => []) : []),
            supabaseFetch(`transactions?${filterAcc}&select=id&photo=not.is.null`).catch(() => isDefaultSpace ? supabaseFetch(`transactions?select=id&photo=not.is.null`).catch(() => []) : []),
          ]);
          try {
            settingsRes = await supabaseFetch(`settings?${filterAcc}&select=*`);
            if ((!settingsRes || settingsRes.length === 0) && isDefaultSpace) {
              settingsRes = await supabaseFetch(`settings?select=*`).catch(() => []);
            }
          } catch (e) {
            settingsRes = isDefaultSpace ? await supabaseFetch(`settings?select=*`).catch(() => []) : [];
          }
          supabaseSchemaOutdated = false;
        } catch (e) {
          if (serviceIsDown(e)) markSupabaseDown();
          if (isDefaultSpace) {
            try {
              [peopleRes, txRes, settingsRes, photoIdsRes] = await Promise.all([
                supabaseFetch(`people?select=name`).catch(() => []),
                supabaseFetch(`transactions?select=id,type,amount,description,category,person,date,currency,rate,note,payment_method,card_id&order=created_at.desc`).catch(() => []),
                supabaseFetch(`settings?select=*`).catch(() => []),
                supabaseFetch(`transactions?select=id&photo=not.is.null`).catch(() => []),
              ]);
            } catch (e2) {}
          }
        }
        let histRes = null;
        if (!supabaseNoAccountId) {
          try {
            histRes = await supabaseFetch(`history?account_id=eq.${encodeURIComponent(getActiveAccountId())}&select=items`);
          } catch (e) {
            histRes = null;
          }
        } else if (isDefaultSpace) {
          try {
            histRes = await supabaseFetch(`history?id=eq.1&select=items`).catch(() => null);
          } catch (e) {
            histRes = null;
          }
        }
        const people = (peopleRes || [])
          .map((r) => (typeof r === "string" ? r : r && r.name ? r.name : ""))
          .map((s) => String(s || "").trim())
          .filter(Boolean);
        // Marca con hasPhoto=true los movimientos que tienen foto en la nube
        const photoIds = new Set((photoIdsRes || []).map((r) => String(r.id)));
        const transactions = (txRes || []).map((r) => {
          const t = rowToTx(r);
          if (photoIds.has(String(t.id))) t.hasPhoto = true;
          return t;
        });
        let s = (settingsRes || [])[0];
        // Si no es el espacio default y no hay settings en la nube, leer del storage local aislado
        if (!isDefaultSpace && !s) {
          try {
            const rawSaved = localStorage.getItem(`cc:settings:${curAccId}`);
            if (rawSaved) s = JSON.parse(rawSaved);
          } catch (e) {}
        }
        if (!isDefaultSpace && !s) {
          const known = getKnownAccounts().find(a => a && (a.id === curAccId || a.code === curAccId));
          s = {
            accountName: known ? known.name : "Nuevo Espacio",
            currency: "S/",
            splitEnabled: true,
            splitMode: "igual",
            mascot: "coin",
            categories: DEFAULT_CATEGORIES,
            cards: [],
            installments: [],
          };
        }
        let categories = [];
        if (s && s.categories) {
          if (Array.isArray(s.categories)) {
            categories = s.categories;
          } else {
            try {
              categories = JSON.parse(s.categories);
            } catch (e) {
              categories = [];
            }
          }
        }
        let cards = [];
        if (s && s.cards) {
          if (Array.isArray(s.cards)) {
            cards = s.cards;
          } else {
            try {
              cards = JSON.parse(s.cards);
            } catch (e) {
              cards = [];
            }
          }
        }
        let installments = [];
        if (s && s.installments) {
          if (Array.isArray(s.installments)) {
            installments = s.installments;
          } else {
            try {
              installments = JSON.parse(s.installments);
            } catch (e) {
              installments = [];
            }
          }
        }
        let history = [];
        if (histRes && histRes[0] && histRes[0].items) {
          if (Array.isArray(histRes[0].items)) {
            history = histRes[0].items;
          } else {
            try {
              history = JSON.parse(histRes[0].items);
            } catch (e) {
              history = [];
            }
          }
        }
        return {
          people,
          transactions,
          settings: {
            splitEnabled: s ? (s.split_enabled !== false && s.split_mode !== "sin") : true,
            currency: s && s.currency ? s.currency : "S/",
            splitMode: s && (s.split_mode === "sin" || s.split_enabled === false) ? "sin" : (s && s.split_mode ? s.split_mode : "igual"),
            accountName: (s && (s.account_name || s.accountName)) || "Cuenta compartida",
            mascot: (s && s.mascot) || "coin",
            categories: Array.isArray(categories) ? categories.map(String) : [],
            cards: Array.isArray(cards) ? cards : [],
            installments: Array.isArray(installments) ? installments : [],
            geminiApiKey: (s && (s.gemini_api_key || s.geminiApiKey)) || "",
          },
          schemaOutdated: supabaseSchemaOutdated,
          history: Array.isArray(history) ? history : [],
        };
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    if (backendOk()) {
      try {
        lastSyncSource = "backend";
        const s = await apiFetch(API_BASE, "/api/state");
        const rawP = Array.isArray(s.people) ? s.people : [];
        return {
          people: rawP.map((r) => (typeof r === "string" ? r : r && r.name ? r.name : "")).map((x) => String(x || "").trim()).filter(Boolean),
          transactions: Array.isArray(s.transactions) ? s.transactions : [],
          settings: normalizeSettings(s.settings),
          schemaOutdated: supabaseSchemaOutdated,
          history: Array.isArray(s.history) ? s.history : [],
        };
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
      }
    }
    // Fallback local (Claude Artifacts o localStorage)
    lastSyncSource = "local";
    const [rp, rt, rs, rh] = await Promise.all([
      this.get("people", true),
      this.get("transactions", true),
      this.get("settings", true),
      this.get("history", true),
    ]);
    const rawLocalP = rp ? safeParse(rp.value, []) : [];
    return {
      people: (Array.isArray(rawLocalP) ? rawLocalP : []).map((r) => (typeof r === "string" ? r : r && r.name ? r.name : "")).map((x) => String(x || "").trim()).filter(Boolean),
      transactions: rt ? safeParse(rt.value, []) : [],
      settings: normalizeSettings(rs ? safeParse(rs.value, {}) : {}),
      schemaOutdated: supabaseSchemaOutdated,
      history: rh ? safeParse(rh.value, []) : [],
    };
  },

  async get(key, shared) {
    if (shared && supabaseOk()) {
      try {
        const st = await this.getState();
        const val = st[key];
        if (val === undefined) return null;
        return { value: JSON.stringify(val) };
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    if (shared && backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const val = s[key];
        if (val === undefined) return null;
        return { value: JSON.stringify(val) };
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
      }
    }
    // Claude Artifacts
    if (typeof window !== "undefined" && window.storage) {
      try {
        return await window.storage.get(key, shared);
      } catch (e) {
        // si falla, usamos localStorage
      }
    }
    const raw = localStorage.getItem("cc:" + key);
    return raw === null ? null : { value: raw };
  },

  async set(key, value, shared) {
    if (shared && supabaseOk()) {
      try {
        const parsed = JSON.parse(value);
        if (key === "people") {
          // SOLO upsert por nombre: NUNCA borrado en masa. Borrar los nombres que
          // no estén en la copia local podía eliminar a alguien que acababa de
          // entrar desde otro dispositivo (carrera de escrituras). Las bajas se
          // hacen dirigidas con removePerson desde Ajustes.
          for (const n of parsed) {
            try {
              await supabaseFetch("people?on_conflict=account_id,name", {
                method: "POST",
                headers: { Prefer: "resolution=merge-duplicates" },
                body: JSON.stringify({ name: n, account_id: getActiveAccountId() }),
              });
            } catch (e) {
              await supabaseFetch("people?on_conflict=name", {
                method: "POST",
                headers: { Prefer: "resolution=merge-duplicates" },
                body: JSON.stringify({ name: n }),
              }).catch(() => {});
            }
          }
        } else if (key === "settings") {
          const ns = normalizeSettings(parsed);
          const curAccId = getActiveAccountId();
          const isDef = curAccId === "default" || curAccId === "DEFAULT";
          
          // Guardar copia local aislada por espacio
          try {
            localStorage.setItem(`cc:settings:${curAccId}`, JSON.stringify(ns));
          } catch (e) {}

          const settingsBody = {
            split_enabled: ns.splitEnabled,
            currency: ns.currency,
            split_mode: ns.splitMode,
            categories: ns.categories,
            cards: ns.cards,
            account_name: ns.accountName || "Cuenta compartida",
            mascot: ns.mascot || "coin",
          };
          if (isDef) {
            settingsBody.id = 1;
          } else {
            settingsBody.account_id = curAccId;
          }
          // La columna installments es más nueva que algunas bases de datos ya
          // creadas: si la nube aún no la tiene, guardar con ella tumbaba TODO
          // el guardado de ajustes (PGRST204). Reintentamos sin la columna para
          // no perder moneda, personas, tarjetas, etc. Las cuotas se conservan
          // solo en este dispositivo hasta ejecutar la migración SQL.
          if (!supabaseSettingsNoInstallments) settingsBody.installments = ns.installments;
          try {
            await supabaseFetch("settings?on_conflict=account_id", {
              method: "POST",
              headers: { Prefer: "resolution=merge-duplicates" },
              body: JSON.stringify(settingsBody),
            });
          } catch (e) {
            if (settingsBody.installments !== undefined) {
              supabaseSettingsNoInstallments = true;
              delete settingsBody.installments;
              await supabaseFetch("settings?on_conflict=account_id", {
                method: "POST",
                headers: { Prefer: "resolution=merge-duplicates" },
                body: JSON.stringify(settingsBody),
              });
            } else {
              throw e;
            }
          }
        } else if (key === "transactions") {
          // SOLO upsert por id, NUNCA borrar en masa: los borrados van dirigidos
          // por deleteTransaction (con su tombstone). El patrón antiguo (leer
          // los ids y borrar los que no estuvieran en la copia local) podía
          // eliminar movimientos que otra persona anotaba a la vez.
          for (const t of parsed) {
            await supabaseFetch("transactions?on_conflict=id", {
              method: "POST",
              headers: { Prefer: "resolution=merge-duplicates" },
              body: JSON.stringify(txToSupabaseRow(t)),
            });
          }
          writeTxMirror(parsed);
        } else if (key === "history") {
          const parsed = JSON.parse(value);
          await supabaseFetch("history?on_conflict=account_id", {
            method: "POST",
            headers: { Prefer: "resolution=merge-duplicates" },
            body: JSON.stringify({ id: 1, account_id: getActiveAccountId(), items: Array.isArray(parsed) ? parsed : [], updated_at: new Date().toISOString() }),
          });
        } else {
          throw new Error("clave no soportada");
        }
        // Escritura en la nube OK: si el esquema estaba desactualizado, ya no
        // lo está (el usuario re-ejecutó el schema.sql) → se quita el aviso.
        supabaseSchemaOutdated = false;
        return { ok: true };
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    if (shared && backendOk()) {
      try {
        const parsed = JSON.parse(value);
        if (key === "people") {
          await apiFetch(API_BASE, "/api/people", { method: "PUT", body: JSON.stringify({ people: parsed }) });
        } else if (key === "settings") {
          await apiFetch(API_BASE, "/api/settings", { method: "PUT", body: JSON.stringify(parsed) });
        } else if (key === "transactions") {
          await apiFetch(API_BASE, "/api/transactions", { method: "PUT", body: JSON.stringify({ transactions: parsed }) });
          writeTxMirror(parsed);
        } else if (key === "history") {
          await apiFetch(API_BASE, "/api/history", { method: "PUT", body: JSON.stringify({ history: parsed }) });
        } else {
          throw new Error("clave no soportada");
        }
        return { ok: true };
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
      }
    }
    // Claude Artifacts
    if (typeof window !== "undefined" && window.storage) {
      try {
        return await window.storage.set(key, value, shared);
      } catch (e) {
        // si falla, usamos localStorage
      }
    }
    localStorage.setItem("cc:" + key, value);
    return { ok: true };
  },

  // Reintenta subir a la nube las escrituras que quedaron solo en LOCAL (nube
  // caída o sin conexión). Se llama desde el sondeo cuando un servicio vuelve
  // a estar disponible. Devuelve cuántas quedaron sin subir (0 = todo listo).
  async flushPendingWrites() {
    // Guard de re-entrada: si ya hay un flush corriendo (p. ej. el del sondeo
    // y el del reintento rápido a la vez), no duplicar el trabajo.
    if (pendingFlushBusy) return readPendingWrites().length;
    pendingFlushBusy = true;
    try {
      return await this._flushPendingWritesImpl();
    } finally {
      pendingFlushBusy = false;
    }
  },
  async _flushPendingWritesImpl() {
    const pending = readPendingWrites();
    if (!pending.length) return 0;
    if (supabaseOk()) {
      const remaining = [];
      for (const w of pending) {
        try {
          if (w.op === "add" && w.tx) {
            try {
              await supabaseFetch("transactions?on_conflict=id", {
                method: "POST",
                headers: { Prefer: "resolution=merge-duplicates" },
                body: JSON.stringify(txToSupabaseRow(w.tx)),
              });
              unmarkTxDeleted(w.tx.id);
            } catch (e) {
              // El POST pudo haber llegado al servidor aunque la respuesta se
              // perdiera (timeout de 8 s): si el movimiento ya existe en la
              // nube, el pendiente está subido de verdad y se limpia — evita
              // el contador clavado con datos que YA están en la nube.
              const exists = await this.txExistsInCloud(w.tx.id);
              if (exists === true) unmarkTxDeleted(w.tx.id);
              else remaining.push(w);
            }
          } else if (w.op === "delete" && w.id != null) {
            try {
              await supabaseFetch("transactions?id=eq." + encodeURIComponent(w.id), { method: "DELETE" });
              markTxDeleted(w.id);
            } catch (e) {
              // Si el DELETE falló pero el movimiento ya no está en la nube,
              // el borrado ya se completó: se limpia el pendiente.
              const exists = await this.txExistsInCloud(w.id);
              if (exists === false) markTxDeleted(w.id);
              else remaining.push(w);
            }
          } else if (w.op === "rename" && w.oldName && w.newName) {
            const res = await this.renamePerson(w.oldName, w.newName);
            if (!res) throw new Error("reintentar");
          } else if (w.op === "history" && w.entry) {
            // Historial compartido: read-modify-write con merge por eid. Si la
            // entrada YA está en la nube (el push anterior llegó pero la
            // respuesta se perdió por timeout), el merge la deja igual y el
            // guardado funciona: el pendiente se limpia sin duplicar ni
            // quedarse clavado en la cola local.
            // Lectura dirigida (1 petición) en vez del getState completo
            // (5 peticiones) que dispararía storage.get("history", true).
            const histRes = await supabaseFetch("history?id=eq.1&select=items");
            let base = [];
            if (histRes && histRes[0] && histRes[0].items) {
              base = Array.isArray(histRes[0].items)
                ? histRes[0].items
                : safeParse(histRes[0].items, []);
            }
            const merged = mergeHistory([w.entry, ...(Array.isArray(base) ? base : [])], 30);
            await this.set("history", JSON.stringify(merged), true);
            // El panel se entera al instante (no espera al siguiente sondeo).
            if (typeof onHistoryFlushed === "function") onHistoryFlushed(merged);
          } else {
            remaining.push(w);
          }
        } catch (e) {
          remaining.push(w); // sigue sin poder: se reintenta en el próximo sondeo
        }
      }
      writePendingWrites(remaining);
      return remaining.length;
    }
    if (backendOk()) {
      const remaining = [];
      for (const w of pending) {
        try {
          if (w.op === "add" && w.tx) {
            const r = await apiFetch(API_BASE, "/api/transactions", {
              method: "POST",
              body: JSON.stringify({ transaction: w.tx }),
            });
            if (Array.isArray(r.transactions)) writeTxMirror(r.transactions);
            unmarkTxDeleted(w.tx.id);
          } else if (w.op === "delete" && w.id != null) {
            const r = await apiFetch(API_BASE, "/api/transactions/" + encodeURIComponent(w.id), { method: "DELETE" });
            if (Array.isArray(r.transactions)) writeTxMirror(r.transactions);
            markTxDeleted(w.id);
          } else if (w.op === "rename" && w.oldName && w.newName) {
            const res = await this.renamePerson(w.oldName, w.newName);
            if (!res) throw new Error("reintentar");
          } else if (w.op === "history" && w.entry) {
            // Historial compartido: read-modify-write con merge por eid (misma
            // reconciliación que en Supabase): si la entrada ya está en la
            // nube, el merge la deja igual y el pendiente se limpia.
            const rh = await this.get("history", true);
            const base = rh ? safeParse(rh.value, []) : [];
            const merged = mergeHistory([w.entry, ...(Array.isArray(base) ? base : [])], 30);
            await this.set("history", JSON.stringify(merged), true);
            // El panel se entera al instante (no espera al siguiente sondeo).
            if (typeof onHistoryFlushed === "function") onHistoryFlushed(merged);
          } else {
            remaining.push(w);
          }
        } catch (e) {
          remaining.push(w);
        }
      }
      writePendingWrites(remaining);
      return remaining.length;
    }
    return pending.length;
  },
  // ¿Existe un movimiento con este id en la nube? Reconciliación de la cola de
  // pendientes: si un POST falló por timeout pero el servidor SÍ lo guardó, el
  // pendiente ya está subido y no debe quedarse clavado en el contador "N
  // cambios sin subir". Devuelve true/false, o null si no se puede saber.
  async txExistsInCloud(id) {
    try {
      const rows = (await supabaseFetch("transactions?id=eq." + encodeURIComponent(id) + "&select=id")) || [];
      return Array.isArray(rows) && rows.length > 0;
    } catch (e) {
      return null;
    }
  },
  // Cuántas escrituras quedaron sin subir a la nube (para el aviso en pantalla).
  pendingWritesCount() {
    return readPendingWrites().length;
  },
  // Marca de tiempo del pendiente más antiguo: cuánto lleva intentando subir
  // la cola. Devuelve 0 si no hay pendientes (o no tienen marca de tiempo).
  pendingWritesSince() {
    const list = readPendingWrites();
    let oldest = 0;
    list.forEach((w) => {
      const at = Number(w && w.at);
      if (at > 0 && (!oldest || at < oldest)) oldest = at;
    });
    return oldest;
  },

  // Foto de UN movimiento, bajo demanda (el sondeo no la descarga por
  // rendimiento). Devuelve el data URL o null si no tiene foto.
  async getTxPhoto(id) {
    if (supabaseOk()) {
      try {
        const rows = (await supabaseFetch("transactions?id=eq." + encodeURIComponent(id) + "&select=photo")) || [];
        const p = rows[0] && rows[0].photo;
        return p != null ? String(p) : null;
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    if (backendOk()) {
      try {
        // Endpoint dedicado: getState() ya no trae las fotos (solo hasPhoto),
        // la imagen se pide aquí bajo demanda, igual que en Supabase.
        const r = await apiFetch(API_BASE, "/api/transactions/" + encodeURIComponent(id) + "/photo");
        return r && r.photo != null ? String(r.photo) : null;
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
      }
    }
    const r = await this.get("transactions", true);
    const list = r ? safeParse(r.value, []) : [];
    const tx = list.find((t) => t && String(t.id) === String(id));
    return tx && tx.photo != null ? String(tx.photo) : null;
  },

  // Añade una persona a la lista compartida SIN borrar a nadie.
  // En Supabase es un upsert atómico por name (imposible perder a otros).
  // Si la lectura previa falla, propaga el error: nunca sobrescribe la lista
  // con datos vacíos (eso borraría los nombres de las demás personas).
  async addPerson(name) {
    if (supabaseOk()) {
      try {
        const curAccId = getActiveAccountId();
        const filterAcc = `account_id=eq.${encodeURIComponent(curAccId)}`;
        let existing = [];
        try {
          existing = (await supabaseFetch(`people?${filterAcc}&select=name`)) || [];
        } catch (e) {
          existing = (await supabaseFetch("people?select=name").catch(() => [])) || [];
        }
        const names = existing.map((r) => r.name);
        // Detecta mayúsculas: si "Hans" ya existe y llega "hans", usamos el
        // nombre existente (misma persona) en vez de crear un duplicado.
        const lower = String(name).trim().toLowerCase();
        const canonical = names.find((n) => String(n).toLowerCase() === lower) || name;
        if (!names.includes(canonical) && names.length >= MAX_PEOPLE) {
          throw new Error("cuenta llena");
        }
        try {
          await supabaseFetch("people?on_conflict=account_id,name", {
            method: "POST",
            headers: { Prefer: "resolution=merge-duplicates" },
            body: JSON.stringify({ name: canonical, account_id: curAccId }),
          });
        } catch (e) {
          await supabaseFetch("people?on_conflict=name", {
            method: "POST",
            headers: { Prefer: "resolution=merge-duplicates" },
            body: JSON.stringify({ name: canonical }),
          });
        }
        // Reclama el aviso de la nueva persona (misma clave que usa el webhook
        // de la tabla people): el aviso "se unió a la cuenta" no saldrá doble.
        claimPushLocal("people:" + canonical);
        supabaseSchemaOutdated = false;
        const st = await this.getState();
        return st.people;
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
        throw e;
      }
    }
    if (backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const list = Array.isArray(s.people) ? s.people.slice() : [];
        const lower = String(name).trim().toLowerCase();
        const canonical = list.find((n) => String(n).toLowerCase() === lower) || name;
        if (!list.includes(canonical)) {
          if (list.length >= MAX_PEOPLE) throw new Error("cuenta llena");
          list.push(canonical);
        }
        await apiFetch(API_BASE, "/api/people", { method: "PUT", body: JSON.stringify({ people: list }) });
        return list;
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
        throw e;
      }
    }
    // Fallback local (Claude Artifacts o localStorage)
    const r = await this.get("people", true);
    const list = r ? safeParse(r.value, []) : [];
    const lower = String(name).trim().toLowerCase();
    const canonical = list.find((n) => String(n).toLowerCase() === lower) || name;
    if (!list.includes(canonical)) {
      if (list.length >= MAX_PEOPLE) throw new Error("cuenta llena");
      list.push(canonical);
    }
    await this.set("people", JSON.stringify(list), true);
    return list;
  },

  // Quita UNA persona concreta de la lista compartida (baja dirigida por
  // nombre). NUNCA borra en masa: así no se pierde a nadie que entrara a la vez
  // desde otro dispositivo. No toca los movimientos (se quedan como huérfanos).
  async removePerson(name) {
    const key = String(name || "").trim();
    if (!key) return false;
    const curAccId = getActiveAccountId();
    if (supabaseOk()) {
      try {
        await supabaseFetch(`people?account_id=eq.${encodeURIComponent(curAccId)}&name=eq.${encodeURIComponent(key)}`, { method: "DELETE" });
        supabaseSchemaOutdated = false;
        return true;
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
        return false;
      }
    }
    if (backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const list = (Array.isArray(s.people) ? s.people : []).filter((n) => n !== key);
        await apiFetch(API_BASE, "/api/people", { method: "PUT", body: JSON.stringify({ people: list }) });
        return true;
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
        return false;
      }
    }
    // Fallback local (Claude Artifacts o localStorage)
    const r = await this.get("people", true);
    const list = r ? safeParse(r.value, []) : [];
    await this.set("people", JSON.stringify(list.filter((n) => n !== key)), true);
    return true;
  },

  // ---- Login: usuarios con contraseña ----
  // Busca un usuario por nombre SIN distinguir mayúsculas. Devuelve
  // { name, salt, hash } o null si no existe.
  async getUser(rawName) {
    const key = String(rawName || "").trim().toLowerCase();
    if (!key) return null;
    const curAccId = getActiveAccountId();
    if (supabaseOk()) {
      try {
        let rows = [];
        try {
          rows = (await supabaseFetch(`users?account_id=eq.${encodeURIComponent(curAccId)}&select=name,salt,hash`)) || [];
        } catch (e) {
          rows = (await supabaseFetch("users?select=name,salt,hash")) || [];
        }
        const hit = rows.find((r) => String(r.name || "").toLowerCase() === key);
        if (!hit) return null;
        return { name: hit.name, salt: hit.salt || "", hash: hit.hash || "" };
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
        // Error de red/servicio ≠ "usuario no existe": se PROPAGA para que
        // joinAccount aborte en vez de tratar el fallo como cuenta nueva y
        // pisar la contraseña de alguien que ya existe.
        throw e;
      }
    }
    if (backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const users = Array.isArray(s.users) ? s.users : [];
        const hit = users.find((u) => String(u.name || "").toLowerCase() === key);
        if (!hit) return null;
        return { name: hit.name, salt: hit.salt || "", hash: hit.hash || "" };
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
        // Igual que en Supabase: error de red se propaga, no se trata como
        // "no existe" (evita pisar contraseñas al entrar).
        throw e;
      }
    }
    const r = await this.get("users", true);
    const users = r ? safeParse(r.value, []) : [];
    const hit = users.find((u) => String(u.name || "").toLowerCase() === key);
    return hit || null;
  },

  // Crea o actualiza un usuario. Si ya existe uno con el mismo nombre (sin
  // distinguir mayúsculas), se actualiza su fila (la contraseña nueva).
  async saveUser(name, salt, hash) {
    const display = String(name || "").trim();
    if (!display) return false;
    const curAccId = getActiveAccountId();
    if (supabaseOk()) {
      try {
        let rows = [];
        try {
          rows = (await supabaseFetch(`users?account_id=eq.${encodeURIComponent(curAccId)}&select=name`)) || [];
        } catch (e) {
          rows = (await supabaseFetch("users?select=name")) || [];
        }
        const key = display.toLowerCase();
        const hit = rows.find((r) => String(r.name || "").toLowerCase() === key);
        const target = hit ? hit.name : display;
        await supabaseFetch("users?on_conflict=account_id,name", {
          method: "POST",
          headers: { Prefer: "resolution=merge-duplicates" },
          body: JSON.stringify({ account_id: curAccId, name: target, salt, hash }),
        }).catch(async () => {
          await supabaseFetch("users?on_conflict=name", {
            method: "POST",
            headers: { Prefer: "resolution=merge-duplicates" },
            body: JSON.stringify({ name: target, salt, hash }),
          });
        });
        supabaseSchemaOutdated = false;
        return true;
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
        return false;
      }
    }
    if (backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const users = Array.isArray(s.users) ? s.users : [];
        const updated = users.filter((u) => String(u.name || "").toLowerCase() !== display.toLowerCase());
        updated.push({ name: display, salt, hash });
        await apiFetch(API_BASE, "/api/users", { method: "PUT", body: JSON.stringify({ users: updated }) });
        return true;
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
        return false;
      }
    }
    const r = await this.get("users", true);
    const users = r ? safeParse(r.value, []) : [];
    const updated = users.filter((u) => String(u.name || "").toLowerCase() !== display.toLowerCase());
    updated.push({ name: display, salt, hash });
    await this.set("users", JSON.stringify(updated), true);
    return true;
  },

  // Quita un usuario (se usa al cambiar el nombre: la contraseña sigue a la
  // persona bajo el nombre nuevo).
  async removeUser(rawName) {
    const key = String(rawName || "").trim().toLowerCase();
    if (!key) return;
    if (supabaseOk()) {
      try {
        const rows = (await supabaseFetch("users?select=name")) || [];
        const hit = rows.find((r) => String(r.name || "").toLowerCase() === key);
        if (hit) await supabaseFetch("users?name=eq." + encodeURIComponent(hit.name), { method: "DELETE" });
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
      return;
    }
    if (backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const users = Array.isArray(s.users) ? s.users : [];
        const updated = users.filter((u) => String(u.name || "").toLowerCase() !== key);
        await apiFetch(API_BASE, "/api/users", { method: "PUT", body: JSON.stringify({ users: updated }) });
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
      }
      return;
    }
    const r = await this.get("users", true);
    const users = r ? safeParse(r.value, []) : [];
    const updated = users.filter((u) => String(u.name || "").toLowerCase() !== key);
    await this.set("users", JSON.stringify(updated), true);
  },

  // Quita un nombre de la lista compartida (borrado dirigido, sin tocar a los
  // demás). Se usa al deshacer un cambio de nombre, solo si ese nombre ya no
  // tiene movimientos. Devuelve la lista actualizada, o null si falla.
  async deletePerson(name) {
    const curAccId = getActiveAccountId();
    if (supabaseOk()) {
      try {
        await supabaseFetch(`people?account_id=eq.${encodeURIComponent(curAccId)}&name=eq.${encodeURIComponent(name)}`, { method: "DELETE" });
        supabaseSchemaOutdated = false;
        const st = await this.getState();
        return st.people;
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
        return null;
      }
    }
    if (backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const list = Array.isArray(s.people) ? s.people : [];
        const updated = list.filter((n) => n !== name);
        await apiFetch(API_BASE, "/api/people", { method: "PUT", body: JSON.stringify({ people: updated }) });
        return updated;
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
        return null;
      }
    }
    const r = await this.get("people", true);
    const list = r ? safeParse(r.value, []) : [];
    const updated = list.filter((n) => n !== name);
    await this.set("people", JSON.stringify(updated), true);
    return updated;
  },

  // Escritura atómica por id en el servicio (evita perder movimientos
  // cuando dos personas anotan a la vez). Cae a read-modify-write local.
  async addTransaction(entry) {
    if (supabaseOk()) {
      try {
        await supabaseFetch("transactions?on_conflict=id", {
          method: "POST",
          headers: { Prefer: "resolution=merge-duplicates" },
          body: JSON.stringify(txToSupabaseRow(entry)),
        });
        supabaseSchemaOutdated = false;
        unmarkTxDeleted(entry.id);
        // Reclama el aviso de este movimiento (anotado o editado): el webhook
        // no repetirá la notificación que ya va a enviar notifyOthers.
        claimPushLocal(String(entry.id));
        // Refresco ligero del espejo: solo transactions (1 petición) en vez de
        // un getState() completo (4 peticiones) que solo servía para el espejo.
        let txRes = [];
        try {
          txRes = (await supabaseFetch("transactions?select=id,type,amount,description,person,date,currency,rate,note,payment_method,card_id&order=created_at.desc")) || [];
        } catch (e2) {
          txRes = (await supabaseFetch("transactions?select=id,type,amount,description,person,date,currency,rate,note&order=created_at.desc")) || [];
        }
        const txs = txRes.map(rowToTx);
        // El refresco ligero no descarga las fotos: si el movimiento recién
        // añadido lleva foto, se marca hasPhoto para que el botón "Ver recibo"
        // aparezca ya y la imagen se cargue bajo demanda (getTxPhoto).
        if (entry && entry.photo) {
          const mine = txs.find((t) => t && String(t.id) === String(entry.id));
          if (mine) mine.hasPhoto = true;
        }
        writeTxMirror(txs);
        return txs;
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    if (backendOk()) {
      try {
        const r = await apiFetch(API_BASE, "/api/transactions", {
          method: "POST",
          body: JSON.stringify({ transaction: entry }),
        });
        unmarkTxDeleted(entry.id);
        writeTxMirror(Array.isArray(r.transactions) ? r.transactions : []);
        return Array.isArray(r.transactions) ? r.transactions : [];
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
      }
    }
    const r = await this.get("transactions", true);
    const current = r ? safeParse(r.value, []) : [];
    // El movimiento entrante (nuevo o EDITADO) debe ganar sobre el guardado:
    // mergeById da prioridad al argumento existing (el primero), así que pasamos
    // [entry] como existing para que el upsert por id actualice el valor.
    const updated = mergeById([entry], current);
    unmarkTxDeleted(entry.id);
    await this.set("transactions", JSON.stringify(updated), true);
    // Solo quedó en local (nube caída): se encola para subirlo al recuperarse.
    if (shouldQueuePending()) enqueuePendingWrite({ op: "add", tx: entry });
    return updated;
  },

  async deleteTransaction(id) {
    if (supabaseOk()) {
      try {
        await supabaseFetch("transactions?id=eq." + encodeURIComponent(id), { method: "DELETE" });
        supabaseSchemaOutdated = false;
        markTxDeleted(id);
        // Reclama el aviso del borrado: el webhook no repetirá la notificación.
        claimPushLocal(String(id));
        // Refresco ligero del espejo: solo transactions (1 petición).
        let txRes = [];
        try {
          txRes = (await supabaseFetch("transactions?select=id,type,amount,description,person,date,currency,rate,note,payment_method,card_id&order=created_at.desc")) || [];
        } catch (e2) {
          txRes = (await supabaseFetch("transactions?select=id,type,amount,description,person,date,currency,rate,note&order=created_at.desc")) || [];
        }
        const txs = txRes.map(rowToTx);
        writeTxMirror(txs);
        return txs;
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
      }
    }
    if (backendOk()) {
      try {
        const r = await apiFetch(API_BASE, "/api/transactions/" + encodeURIComponent(id), { method: "DELETE" });
        markTxDeleted(id);
        writeTxMirror(Array.isArray(r.transactions) ? r.transactions : []);
        return Array.isArray(r.transactions) ? r.transactions : [];
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
      }
    }
    const r = await this.get("transactions", true);
    const current = r ? safeParse(r.value, []) : [];
    const updated = current.filter((t) => t.id !== id);
    markTxDeleted(id);
    await this.set("transactions", JSON.stringify(updated), true);
    // El borrado solo quedó en local: se encola para subirlo al recuperarse.
    if (shouldQueuePending()) enqueuePendingWrite({ op: "delete", id });
    return updated;
  },

  // Renombra una persona en los movimientos compartidos SIN tocar a las demás:
  // solo actualiza las filas cuyo person === oldName (upsert atómico por id en
  // Supabase; nunca borra nada). Devuelve
  //   { changed: true, transactions: [...] }  éxito, hubo movimientos migrados
  //   { changed: false }                      éxito, no había nada que migrar
  //   null                                   fallo de red/servicio (reintentar)
  async renamePerson(oldName, newName) {
    if (oldName === newName) return { changed: false };
    if (supabaseOk()) {
      try {
        const rows = (await supabaseFetch("transactions?select=*")) || [];
        const affected = rows.filter((r) => r.person === oldName);
        if (affected.length === 0) return { changed: false };
        for (const r of affected) {
          await supabaseFetch("transactions?on_conflict=id", {
            method: "POST",
            headers: { Prefer: "resolution=merge-duplicates" },
            body: JSON.stringify(txToSupabaseRow({ ...rowToTx(r), person: newName })),
          });
        }
        supabaseSchemaOutdated = false;
        // Reclama el aviso de cada movimiento renombrado (una clave por id):
        // así el webhook no dispara una notificación por cada movimiento (una
        // ráfaga por un solo cambio de nombre sería spam).
        for (const r of affected) claimPushLocal(String(r.id));
        // Refresco ligero del espejo reutilizando las filas ya leídas (sin un
        // getState() completo), aplicando el renombrado a lo que se devuelve.
        const renamed = rows.map((r) => {
          const t = rowToTx(r);
          return t.person === oldName ? { ...t, person: newName } : t;
        });
        writeTxMirror(renamed);
        return { changed: true, transactions: renamed };
      } catch (e) {
        if (serviceIsDown(e)) markSupabaseDown();
        return null;
      }
    }
    if (backendOk()) {
      try {
        const s = await apiFetch(API_BASE, "/api/state");
        const txs = Array.isArray(s.transactions) ? s.transactions : [];
        let changed = false;
        const updated = txs.map((t) => {
          if (t.person === oldName) {
            changed = true;
            return { ...t, person: newName };
          }
          return t;
        });
        if (!changed) return { changed: false };
        await apiFetch(API_BASE, "/api/transactions", {
          method: "PUT",
          body: JSON.stringify({ transactions: updated }),
        });
        writeTxMirror(updated);
        return { changed: true, transactions: updated };
      } catch (e) {
        if (serviceIsDown(e)) markBackendDown();
        return null;
      }
    }
    // Fallback local (Claude Artifacts o localStorage)
    const r = await this.get("transactions", true);
    const txs = r ? safeParse(r.value, []) : [];
    let changed = false;
    const updated = txs.map((t) => {
      if (t.person === oldName) {
        changed = true;
        return { ...t, person: newName };
      }
      return t;
    });
    if (!changed) return { changed: false };
    await this.set("transactions", JSON.stringify(updated), true);
    writeTxMirror(updated);
    // El renombrado solo quedó en local: se encola para subirlo al recuperarse.
    if (shouldQueuePending()) enqueuePendingWrite({ op: "rename", oldName, newName });
    return { changed: true, transactions: updated };
  },
};

// ---------------------------------------------------------------------------
// Personaje de la cuenta (mascota)
// ---------------------------------------------------------------------------
// Los personajes se dibujan en SVG puro (sin imágenes externas: funciona sin
// red y en claro/oscuro) y comparten el mismo sistema de ánimo en CINCO
// niveles: se alegran con cada ingreso, se entristecen con cada gasto y, en
// reposo, muestran el ánimo que da el saldo (muy contento · contento · neutro
// · triste · abatido). El burst lleva la cara al extremo en cada reacción.
// - kind:  personaje elegido en Ajustes (id de MASCOT_KINDS)
// - mood:  ánimo de reposo derivado del saldo (joy | happy | neutral | sad | miserable)
// - burst: reacción momentánea al anotar/borrar/deshacer ("happy" | "sad" | null)
// - size:  tamaño en px (68 en la tarjeta, más pequeño en el selector)

// Lágrima que cae cuando el personaje está triste (se coloca junto a su ojo).
function MascotTear({ x, y }) {
  const d = "M " + x + " " + y + " C " + (x - 2) + " " + (y + 4) + " " + (x - 2) + " " + (y + 8) + " " + x + " " + (y + 10) + " C " + (x + 2) + " " + (y + 8) + " " + (x + 2) + " " + (y + 4) + " " + x + " " + y + " Z";
  return <path d={d} fill="#7EC8F7" stroke="#4A9FD8" strokeWidth="1" style={{ animation: "cc-tear-fall 1.4s ease-in 0.25s infinite" }} />;
}

// Anclas de la cara de cada personaje (coordenadas en el viewBox 120×120): los
// ojos se centran en 60±eyeGap a la altura eyeY. Todas las caras comparten el
// mismo dibujo (MascotFace); lo único que cambia es de dónde cuelga cada rasgo.
const MASCOT_FACES = {
  coin: { ink: "#6B4A12", eyeY: 56, eyeGap: 10, browY: 44, mouthY: 76, cheekY: 70, cheekX: 40, tear: [42, 50], mouth: "arc" },
  piggy: { ink: "#7C4256", eyeY: 50, eyeGap: 10, browY: 38, mouthY: 92, cheekY: 66, cheekX: 36, tear: [40, 44], mouth: "arc" },
  cat: { ink: "#8A5A1B", eyeY: 40, eyeGap: 13, browY: 27, mouthY: 56, cheekY: 52, cheekX: 34, tear: [38, 34], mouth: "w" },
  plant: { ink: "#2E6B3A", eyeY: 38, eyeGap: 10, browY: 26, mouthY: 50, cheekY: 46, cheekX: 42, tear: [43, 32], mouth: "arc" },
  // El búho: ojos grandes y separados sobre sus discos oculares, boca pequeña.
  owl: { ink: "#5B3A1E", eyeY: 44, eyeGap: 14, browY: 32, mouthY: 66, cheekY: 62, cheekX: 34, tear: [38, 38], mouth: "arc" },
};

// Cejas, ojos, boca y mejillas según el ánimo. Hay CINCO niveles:
//   joy (muy feliz) · happy (feliz) · neutral · sad (triste) · miserable (abatido)
function MascotFace({ cfg, mood, detail }) {
  const { ink, eyeY, eyeGap, browY, mouthY, cheekY, cheekX, mouth } = cfg;
  const xL = 60 - eyeGap;
  const xR = 60 + eyeGap;
  const up = mood === "joy" || mood === "happy";
  const down = mood === "sad" || mood === "miserable";
  // En miniatura (historial, 28/36 px) los trazos se engrosan un poco —a 28 px
  // un trazo de 3 unidades del viewBox mide menos de un píxel y se vería roto—
  // y los ojos se dibujan más grandes para que la expresión se lea igual de
  // bien que en grande. El doble brillo del ojo se queda en un solo reflejo
  // más grande: el segundo sería más pequeño que un píxel y solo ensuciaría.
  const min = detail === "min";
  const sw = (n) => (min ? n + 0.5 : n);
  return (
    <>
      {/* Cejas: alzadas de alegría, caídas con pena */}
      {mood === "joy" && (
        <>
          <path d={`M ${xL - 8} ${browY + 3} Q ${xL} ${browY - 3} ${xL + 8} ${browY + 3}`} stroke={ink} strokeWidth={sw(3)} fill="none" strokeLinecap="round" />
          <path d={`M ${xR - 8} ${browY + 3} Q ${xR} ${browY - 3} ${xR + 8} ${browY + 3}`} stroke={ink} strokeWidth={sw(3)} fill="none" strokeLinecap="round" />
        </>
      )}
      {down && (
        <>
          <path d={`M ${xL - 8} ${browY - 2} Q ${xL} ${browY + 3} ${xL + 8} ${browY - 1}`} stroke={ink} strokeWidth={sw(3)} fill="none" strokeLinecap="round" />
          <path d={`M ${xR + 8} ${browY - 2} Q ${xR} ${browY + 3} ${xR - 8} ${browY - 1}`} stroke={ink} strokeWidth={sw(3)} fill="none" strokeLinecap="round" />
        </>
      )}
      {/* Ojos: cerrados de risa, abiertos con pupila y brillo, o caídos de pena */}
      {mood === "joy" && (
        <>
          <path d={`M ${xL - 7} ${eyeY} Q ${xL} ${eyeY - 8} ${xL + 7} ${eyeY}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />
          <path d={`M ${xR - 7} ${eyeY} Q ${xR} ${eyeY - 8} ${xR + 7} ${eyeY}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />
        </>
      )}
      {(mood === "happy" || mood === "neutral") && (
        <>
          {/* En miniatura: ojo más grande, pupila y brillo mayores, un solo
              reflejo (el doble brillo se perdería a 28 px) */}
          <circle cx={xL} cy={eyeY} r={min ? 5.5 : 5} fill="#FFFFFF" stroke={ink} strokeWidth={min ? 2.8 : 2.2} />
          <circle cx={xL} cy={eyeY} r={min ? 3 : 2.4} fill={ink} />
          <circle cx={xL + 1.2} cy={eyeY - 1.4} r={min ? 1.5 : 1.1} fill="#FFFFFF" />
          {!min && <circle cx={xL - 1.1} cy={eyeY + 1.2} r="0.6" fill="#FFFFFF" opacity="0.85" />}
          <circle cx={xR} cy={eyeY} r={min ? 5.5 : 5} fill="#FFFFFF" stroke={ink} strokeWidth={min ? 2.8 : 2.2} />
          <circle cx={xR} cy={eyeY} r={min ? 3 : 2.4} fill={ink} />
          <circle cx={xR + 1.2} cy={eyeY - 1.4} r={min ? 1.5 : 1.1} fill="#FFFFFF" />
          {!min && <circle cx={xR - 1.1} cy={eyeY + 1.2} r="0.6" fill="#FFFFFF" opacity="0.85" />}
        </>
      )}
      {mood === "sad" && (
        <>
          <path d={`M ${xL - 7} ${eyeY + 3} Q ${xL} ${eyeY + 9} ${xL + 7} ${eyeY + 3}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />
          <path d={`M ${xR - 7} ${eyeY + 3} Q ${xR} ${eyeY + 9} ${xR + 7} ${eyeY + 3}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />
        </>
      )}
      {mood === "miserable" && (
        <>
          <path d={`M ${xL - 7} ${eyeY + 5} Q ${xL} ${eyeY + 11} ${xL + 7} ${eyeY + 5}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />
          <path d={`M ${xR - 7} ${eyeY + 5} Q ${xR} ${eyeY + 11} ${xR + 7} ${eyeY + 5}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />
        </>
      )}
      {/* Boca: risa abierta, sonrisa, línea, mueca o puchero */}
      {mouth === "w" ? (
        <>
          {(mood === "joy" || mood === "happy") && <path d={`M ${xL - 3} ${mouthY} Q ${60 - 4} ${mouthY + 6} ${60} ${mouthY} Q ${60 + 4} ${mouthY + 6} ${xR + 3} ${mouthY}`} stroke={ink} strokeWidth={sw(4.5)} fill="none" strokeLinecap="round" />}
          {mood === "neutral" && <path d={`M ${60 - 6} ${mouthY} Q ${60} ${mouthY + 4} ${60 + 6} ${mouthY}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />}
          {mood === "sad" && <path d={`M ${xL - 3} ${mouthY + 5} Q ${60 - 4} ${mouthY - 1} ${60} ${mouthY + 5} Q ${60 + 4} ${mouthY - 1} ${xR + 3} ${mouthY + 5}`} stroke={ink} strokeWidth={sw(4)} fill="none" strokeLinecap="round" />}
          {mood === "miserable" && <path d={`M ${xL - 3} ${mouthY + 6} Q ${60 - 4} ${mouthY - 2} ${60} ${mouthY + 6} Q ${60 + 4} ${mouthY - 2} ${xR + 3} ${mouthY + 6}`} stroke={ink} strokeWidth={sw(4.5)} fill="none" strokeLinecap="round" />}
        </>
      ) : (
        <>
          {mood === "joy" && <path d={`M ${60 - 11} ${mouthY} Q ${60} ${mouthY + 11} ${60 + 11} ${mouthY}`} stroke={ink} strokeWidth={sw(5)} fill="none" strokeLinecap="round" />}
          {mood === "happy" && <path d={`M ${60 - 10} ${mouthY} Q ${60} ${mouthY + 8} ${60 + 10} ${mouthY}`} stroke={ink} strokeWidth={sw(4.5)} fill="none" strokeLinecap="round" />}
          {mood === "neutral" && <path d={`M ${60 - 6} ${mouthY} H ${60 + 6}`} stroke={ink} strokeWidth={sw(4)} strokeLinecap="round" />}
          {mood === "sad" && <path d={`M ${60 - 10} ${mouthY + 4} Q ${60} ${mouthY - 3} ${60 + 10} ${mouthY + 4}`} stroke={ink} strokeWidth={sw(4.5)} fill="none" strokeLinecap="round" />}
          {mood === "miserable" && <path d={`M ${60 - 12} ${mouthY + 6} Q ${60} ${mouthY - 3} ${60 + 12} ${mouthY + 6}`} stroke={ink} strokeWidth={sw(5)} fill="none" strokeLinecap="round" />}
        </>
      )}
      {/* Mejillas (más rojas cuando está contento) */}
      <ellipse cx={cheekX} cy={cheekY} rx="5" ry="3.4" fill={up ? "#E8786B" : "#DFA06A"} opacity="0.6" />
      <ellipse cx={120 - cheekX} cy={cheekY} rx="5" ry="3.4" fill={up ? "#E8786B" : "#DFA06A"} opacity="0.6" />
      {/* Lágrimas: una cuando está triste, dos cuando está abatido */}
      {down && <MascotTear x={cfg.tear[0]} y={cfg.tear[1]} />}
      {mood === "miserable" && <MascotTear x={120 - cfg.tear[0]} y={cfg.tear[1]} />}
    </>
  );
}

// Cuerpo + cara del personaje según su tipo y su ánimo.
function MascotSvg({ kind, mood, gid, detail }) {
  const min = detail === "min";
  // Contorno de la silueta: también se engrosa en miniatura para que el cuerpo
  // no se vea más fino que la cara (que ya va engrosada con sw() en MascotFace).
  const swB = (n) => (min ? n + 0.5 : n);
  // Si llegara un id desconocido (dato viejo), caemos a la moneda (el defecto).
  if (kind !== "coin" && kind !== "piggy" && kind !== "cat" && kind !== "plant" && kind !== "owl") kind = "coin";
  const cfg = MASCOT_FACES[kind];
  const up = mood === "joy" || mood === "happy";

  // ---- Moneda de oro (por defecto) ----
  if (kind === "coin") {
    return (
      <>
        {/* Bracitos: se alzan contentos o cuelgan con pena */}
        <ellipse cx="30" cy="74" rx="7" ry="12" fill="#D9A860" stroke="#96621C" strokeWidth="2.5" transform={up ? "rotate(-38 30 74)" : "rotate(16 30 74)"} />
        <ellipse cx="90" cy="74" rx="7" ry="12" fill="#D9A860" stroke="#96621C" strokeWidth="2.5" transform={up ? "rotate(38 90 74)" : "rotate(-16 90 74)"} />
        {/* Canto de la moneda: anillo más oscuro desplazado detrás para dar
            grosor 3D visible (asoma ~2px por abajo-derecha) */}
        <circle cx="61" cy="63" r="48.5" fill="#96621C" />
        {/* Cara con gradiente de volumen (luz arriba-izquierda) */}
        <circle cx="60" cy="62" r="46" fill={"url(#" + gid + ")"} stroke="#8A5A1B" strokeWidth={swB(3)} />
        {/* Estriado del canto y bisel: en miniatura son líneas de menos de un
            píxel (ruido), así que solo se dibujan en tamaño completo */}
        {!min && (
          <>
            <circle cx="60" cy="62" r="43.5" fill="none" stroke="rgba(138,90,27,0.45)" strokeWidth="2" strokeDasharray="1.6 5.4" strokeLinecap="round" />
            <circle cx="60" cy="62" r="33" fill="none" stroke="rgba(150,98,28,0.45)" strokeWidth="2.5" strokeDasharray="1 7" strokeLinecap="round" />
          </>
        )}
        {/* Cel-shading suave: sombra en la parte inferior de la cara */}
        <path d="M 27 74 A 35 35 0 0 0 93 74 A 40 40 0 0 1 27 74 Z" fill="rgba(138,90,27,0.22)" />
        {/* Brillos especulares del metal pulido (en miniatura basta el principal) */}
        <path d="M 32 40 A 30 30 0 0 1 52 26" stroke="rgba(255,255,255,0.7)" strokeWidth="5" fill="none" strokeLinecap="round" />
        {!min && <path d="M 78 34 l 4 -5 M 86 42 l 5 -3" stroke="rgba(255,255,255,0.5)" strokeWidth="3.5" fill="none" strokeLinecap="round" />}
        <MascotFace cfg={cfg} mood={mood} detail={detail} />
      </>
    );
  }

  // ---- Cerdito (hucha) ----
  if (kind === "piggy") {
    return (
      <>
        {/* Rabito rizado (con punta enrollada) */}
        <path d="M 103 82 C 112 82 115 72 110 66 C 106 61 100 62 98 66" fill="none" stroke="#D98DA2" strokeWidth="5" strokeLinecap="round" />
        {/* Orejas (con interior más oscuro para profundidad) */}
        <path d="M 32 40 L 21 16 L 45 29 Z" fill="#F6C3CF" stroke="#C98A9C" strokeWidth="3" strokeLinejoin="round" />
        <path d="M 27 33 L 23 22 L 37 28 Z" fill="#EAA9BB" />
        <path d="M 88 40 L 99 16 L 75 29 Z" fill="#F6C3CF" stroke="#C98A9C" strokeWidth="3" strokeLinejoin="round" />
        <path d="M 93 33 L 97 22 L 83 28 Z" fill="#EAA9BB" />
        {/* Cuerpo con gradiente de volumen (luz arriba-izquierda) */}
        <ellipse cx="60" cy="66" rx="47" ry="38" fill={"url(#" + gid + "B)"} stroke="#C98A9C" strokeWidth={swB(3)} />
        {/* Cel-shading suave en la panza y bajo el hocico */}
        <ellipse cx="60" cy="93" rx="26" ry="10" fill="rgba(169,107,124,0.25)" />
        {/* Brillo especular de la cerámica (hucha esmaltada) */}
        <ellipse cx="42" cy="44" rx="15" ry="7" fill="rgba(255,255,255,0.4)" transform="rotate(-16 42 44)" />
        {/* Ranura para monedas: con moneda a medias cuando está contento */}
        <rect x="49" y="27" width="22" height="5" rx="2.5" fill="#A85A70" stroke="#8A4B5C" strokeWidth="1.2" />
        {up && (
          <>
            <ellipse cx="56" cy="26" rx="9" ry="4.5" fill="#B4863A" />
            <ellipse cx="56" cy="23.5" rx="9" ry="4.5" fill="#FFD966" stroke="#B4863A" strokeWidth="1.5" />
            {/* Brillito en la moneda para que se vea metálica (se pierde en miniatura) */}
            {!min && <path d="M 51 21 l 3 -2.5" stroke="rgba(255,255,255,0.85)" strokeWidth="2" strokeLinecap="round" />}
          </>
        )}
        {/* Patitas y bracitos */}
        <rect x="41" y="100" width="13" height="10" rx="5" fill="#E8A7B6" stroke="#C98A9C" strokeWidth="2.5" />
        <rect x="66" y="100" width="13" height="10" rx="5" fill="#E8A7B6" stroke="#C98A9C" strokeWidth="2.5" />
        <ellipse cx="24" cy="78" rx="7" ry="11" fill="#F6C3CF" stroke="#C98A9C" strokeWidth="2.5" transform={up ? "rotate(-38 24 78)" : "rotate(14 24 78)"} />
        <ellipse cx="96" cy="78" rx="7" ry="11" fill="#F6C3CF" stroke="#C98A9C" strokeWidth="2.5" transform={up ? "rotate(38 96 78)" : "rotate(-14 96 78)"} />
        {/* Hocico (con sombra inferior) */}
        <ellipse cx="60" cy="72" rx="17" ry="12" fill="#FBE3EA" stroke="#C98A9C" strokeWidth={swB(3)} />
        <ellipse cx="60" cy="76" rx="12" ry="6" fill="rgba(169,107,124,0.18)" />
        <ellipse cx="54" cy="72" rx="2.4" ry="3" fill="#A85A70" />
        <ellipse cx="66" cy="72" rx="2.4" ry="3" fill="#A85A70" />
        <MascotFace cfg={cfg} mood={mood} detail={detail} />
      </>
    );
  }

  // ---- Gatito sentado (con cuerpo, pechito, patitas y cola) ----
  if (kind === "cat") {
    return (
      <>
        {/* Cola enroscada (bicolor, con punta que se riza) */}
        <path d="M 92 84 C 105 84 109 76 105 66 C 102 59 94 57 90 62" fill="none" stroke="#C9831F" strokeWidth="9" strokeLinecap="round" />
        <path d="M 92 84 C 105 84 109 76 105 66 C 102 59 94 57 90 62" fill="none" stroke="#F8C774" strokeWidth="5" strokeLinecap="round" />
        {/* Cuerpo con gradiente de volumen (luz arriba-izquierda) */}
        <ellipse cx="60" cy="86" rx="31" ry="22" fill={"url(#" + gid + "B)"} stroke="#C9831F" strokeWidth={swB(3)} />
        {/* Pechito crema (con sombra suave bajo la cabeza) */}
        <ellipse cx="60" cy="88" rx="17" ry="13" fill="#FBE7C8" opacity="0.9" />
        <ellipse cx="60" cy="74" rx="14" ry="4.5" fill="rgba(201,131,31,0.22)" />
        {/* Patitas delanteras */}
        <ellipse cx="44" cy="104" rx="8" ry="6" fill="#F8C774" stroke="#C9831F" strokeWidth="2.5" />
        <ellipse cx="76" cy="104" rx="8" ry="6" fill="#F8C774" stroke="#C9831F" strokeWidth="2.5" />
        {/* Orejas (con la parte rosa) */}
        <path d="M 28 34 L 26 10 L 47 25 Z" fill="#F8C774" stroke="#C9831F" strokeWidth="3.5" strokeLinejoin="round" />
        <path d="M 33 29 L 31 16 L 43 25 Z" fill="#F2A7B5" />
        <path d="M 92 34 L 94 10 L 73 25 Z" fill="#F8C774" stroke="#C9831F" strokeWidth="3.5" strokeLinejoin="round" />
        <path d="M 87 29 L 89 16 L 77 25 Z" fill="#F2A7B5" />
        {/* Mechones de pelo en las mejillas (rompen el círculo perfecto) */}
        <path d="M 26 46 L 19 41 L 25 53 Z" fill="#F8C774" />
        <path d="M 94 46 L 101 41 L 95 53 Z" fill="#F8C774" />
        {/* Cabeza con gradiente */}
        <circle cx="60" cy="44" r="33" fill={"url(#" + gid + "B)"} stroke="#C9831F" strokeWidth={swB(3)} />
        {/* Bigotes: en miniatura (28 px) son trazos de medio píxel que se ven
            como motas, así que solo se dibujan en tamaño completo */}
        {!min && (
          <>
            <path d="M 24 44 H 12" stroke="#C98A3A" strokeWidth="2.2" strokeLinecap="round" />
            <path d="M 23 52 H 10" stroke="#C98A3A" strokeWidth="2.2" strokeLinecap="round" />
            <path d="M 24 59 H 13" stroke="#C98A3A" strokeWidth="2" strokeLinecap="round" />
            <path d="M 96 44 H 108" stroke="#C98A3A" strokeWidth="2.2" strokeLinecap="round" />
            <path d="M 97 52 H 110" stroke="#C98A3A" strokeWidth="2.2" strokeLinecap="round" />
            <path d="M 96 59 H 107" stroke="#C98A3A" strokeWidth="2" strokeLinecap="round" />
          </>
        )}
        <MascotFace cfg={cfg} mood={mood} detail={detail} />
      </>
    );
  }

  // ---- Plantita en maceta (hoja pequeña que se alza como un bracito) ----
  if (kind === "plant") {
    return (
      <>
      {/* Borde de la maceta (latón, el acento de la marca) */}
      <rect x="33" y="78" width="54" height="10" rx="4" fill="#C99A4B" stroke="#8A6226" strokeWidth="2.5" />
      <rect x="33" y="78" width="54" height="5" rx="4" fill="rgba(255,255,255,0.22)" />
      {/* Cuerpo de la maceta (trapecio, más ancho arriba) con gradiente */}
      <path d="M 40 88 L 80 88 L 73 112 L 47 112 Z" fill={"url(#" + gid + "P)"} stroke="#8A6226" strokeWidth={swB(3)} strokeLinejoin="round" />
      {/* Tierra visible sobre el tallo */}
      <ellipse cx="60" cy="79" rx="19" ry="4" fill="#6B4A2E" />
      {/* Tallo */}
      <path d="M 60 80 C 60 70 58 62 60 50" stroke="#3F7F4F" strokeWidth="5" fill="none" strokeLinecap="round" />
      {/* Hoja pequeña: se alza contenta y se inclina con pena (con veta) */}
      <ellipse cx="38" cy="60" rx="12" ry="7" fill="#8BC98B" stroke="#3F7F4F" strokeWidth="2.5" transform={up ? "rotate(-26 38 60)" : "rotate(-6 38 60)"} />
      {!min && <path d="M 38 60 L 47 60" stroke="rgba(63,127,79,0.5)" strokeWidth="1.8" strokeLinecap="round" transform={up ? "rotate(-26 38 60)" : "rotate(-6 38 60)"} />}
      {/* Flor (pétalos con centro) */}
      <circle cx="60" cy="4" r="4" fill="#FFE9A8" stroke="#D9A25F" strokeWidth="1.5" />
      <circle cx="66" cy="10" r="4" fill="#FFE9A8" stroke="#D9A25F" strokeWidth="1.5" />
      <circle cx="60" cy="16" r="4" fill="#FFE9A8" stroke="#D9A25F" strokeWidth="1.5" />
      <circle cx="54" cy="10" r="4" fill="#FFE9A8" stroke="#D9A25F" strokeWidth="1.5" />
      <circle cx="60" cy="10" r="3.5" fill="#F8C774" />
      {/* Hoja principal (con la cara) con gradiente de volumen y veta */}
      <circle cx="60" cy="42" r="29" fill={"url(#" + gid + "B)"} stroke="#3F7F4F" strokeWidth={swB(3)} />
      {!min && <path d="M 60 15 C 59 26 61 36 60 46" stroke="rgba(63,127,79,0.5)" strokeWidth="3" fill="none" strokeLinecap="round" />}
      {/* Cel-shading: sombra suave bajo la cara, donde la hoja se une al tallo */}
      <path d="M 34 56 Q 60 68 86 56 Q 60 62 34 56 Z" fill="rgba(63,127,79,0.18)" />
        <MascotFace cfg={cfg} mood={mood} detail={detail} />
      </>
    );
  }

  // ---- Búho guardián del dinero (cuerpo redondeado, discos oculares y alas) ----
  return (
    <>
      {/* Orejas de plumas en las esquinas de la cabeza */}
      <path d="M 26 42 L 20 12 L 40 30 Z" fill="#A9744F" stroke="#7A4E28" strokeWidth="2.5" strokeLinejoin="round" />
      <path d="M 94 42 L 100 12 L 80 30 Z" fill="#A9744F" stroke="#7A4E28" strokeWidth="2.5" strokeLinejoin="round" />
      {/* Cuerpo redondeado con gradiente de volumen (luz arriba-izquierda) */}
      <ellipse cx="60" cy="78" rx="40" ry="36" fill={"url(#" + gid + "B)"} stroke="#7A4E28" strokeWidth={swB(3)} />
      {/* Cel-shading suave en la base del cuerpo */}
      <ellipse cx="60" cy="104" rx="22" ry="10" fill="rgba(122,78,40,0.25)" />
      {/* Brillo especular de las plumas superiores */}
      <ellipse cx="44" cy="52" rx="14" ry="7" fill="rgba(255,255,255,0.28)" transform="rotate(-18 44 52)" />
      {/* Plumas del pecho (líneas en V, textura de plumaje): en miniatura se
          pierden y ensucian, solo se dibujan en tamaño completo */}
      {!min && <path d="M 44 92 l 6 5 M 52 96 l 6 5 M 60 99 l 6 -4 M 68 96 l 6 -4 M 76 92 l 6 -5" stroke="rgba(122,78,40,0.4)" strokeWidth="2.4" fill="none" strokeLinecap="round" />}
      {/* Patitas */}
      <rect x="42" y="111" width="12" height="7" rx="3.5" fill="#E8A33D" stroke="#B4863A" strokeWidth="2" />
      <rect x="66" y="111" width="12" height="7" rx="3.5" fill="#E8A33D" stroke="#B4863A" strokeWidth="2" />
      {/* Alas a los lados (pliegue de plumas) */}
      <ellipse cx="21" cy="84" rx="8" ry="17" fill="#8A5A2B" stroke="#6E4422" strokeWidth="2.5" transform="rotate(12 21 84)" />
      <ellipse cx="99" cy="84" rx="8" ry="17" fill="#8A5A2B" stroke="#6E4422" strokeWidth="2.5" transform="rotate(-12 99 84)" />
      {/* Discos oculares (la marca del búho: anillos claros alrededor de los ojos) */}
      <circle cx="46" cy="48" r="13" fill="#F3E7CE" stroke="#7A4E28" strokeWidth="2.5" />
      <circle cx="74" cy="48" r="13" fill="#F3E7CE" stroke="#7A4E28" strokeWidth="2.5" />
      {/* Pico pequeño entre los discos */}
      <path d="M 56 58 L 60 55 L 64 58 L 60 64 Z" fill="#E8A33D" stroke="#B4863A" strokeWidth="1.8" strokeLinejoin="round" />
        <MascotFace cfg={cfg} mood={mood} detail={detail} />
    </>
  );
}

// Pop de bienvenida del personaje: el mismo keyframe para las miniaturas del
// historial (al abrir el panel) y para la tarjeta de saldo en escritorio (al
// abrir la app); solo cambia el retardo de cada uso.
const MASCOT_GREET = "cc-mascot-greet 0.55s cubic-bezier(.34,1.56,.64,1) ";
// Baile de bienvenida del personaje en la tarjeta de saldo: el pop + sacudida
// alegre, sincronizado con el tono de bienvenida. Escritorio con más energía,
// móvil con la versión suave (solo el personaje, sin animar la tarjeta).
// Duración pensada para que la sacudida caiga dentro de la ventana del tono de
// bienvenida (arpegio de ~0.55 s que arranca con el mismo retardo de la animación).
const MASCOT_DANCE = "cc-mascot-dance 0.9s cubic-bezier(.34,1.56,.64,1) 0.2s both";
const MASCOT_DANCE_SOFT = "cc-mascot-dance-soft 0.75s cubic-bezier(.34,1.56,.64,1) 0.1s both";
// Tamaño máximo para dibujar el personaje con el nivel de detalle reducido
// ("min"): por debajo de 40 px los detalles finos del viewBox 120×120 miden
// menos de un píxel y se vuelven ruido. Solo lo usan las miniaturas del
// historial (28/36 px); el selector (42-68) y la tarjeta (68/96) van "full".
const MIN_DETAIL_SIZE = 40;
// Tarjeta de saldo y paneles (Ajustes, historial): en escritorio entran con la
// misma animación al abrir la app o el panel.
const CARD_ENTER_CLASS = "cc-card-hover cc-card-enter";

function Mascot({ kind = "coin", mood, burst, size = 68, animate = true, effects = true, halo = true }) {
  // Nivel de detalle: el dibujo se hace en un viewBox 120×120 y se escala.
  // Abajo de MIN_DETAIL_SIZE (las miniaturas del historial, 28/36 px) los
  // detalles finos —bigotes, vetas, doble brillo del ojo, estriado de la
  // moneda— se convierten en ruido y desaparecen. En "min" se simplifican y se
  // agrandan los rasgos que importan (ojos, brillos, trazos) para que se lea
  // igual de bien.
  const detail = size <= MIN_DETAIL_SIZE ? "min" : "full";
  // El burst (reacción momentánea) lleva la cara al extremo: la alegría se
  // vuelve éxtasis y la pena, abatimiento. En reposo manda el ánimo del saldo.
  const m = burst === "happy" ? "joy" : burst === "sad" ? "miserable" : mood || "neutral";
  const up = m === "joy" || m === "happy";
  const down = m === "sad" || m === "miserable";
  const gid = "ccCoin" + useId().replace(/[^a-zA-Z0-9]/g, "");
  const bubbleText = burst === "happy" ? "¡Qué alegría! 🥳" : burst === "sad" ? "¡Ay, no… 💧" : null;
  // Los efectos más llamativos (corazones, confeti, lluvia) solo durante la
  // reacción; en reposo bastan las chispitas y la cara extrema.
  const showParty = burst === "happy";
  const showRain = burst === "sad";
  const showSparkles = !burst && (m === "happy" || m === "joy");
  // Escala de los efectos: siguen al personaje cuando cambia de tamaño (en
  // escritorio es más grande y los corazones, el confeti, la nube y la burbuja
  // crecen con él para mantener sus proporciones). Con el tamaño de móvil
  // (68 px) s = 1 y no cambia nada.
  const s = size / 68;
  // La capa solo se monta si hay algún efecto que dibujar (en el historial y en
  // el selector no hay ninguno: se ahorra un div escalado vacío por miniatura).
  const hasFx = (effects && (showSparkles || showParty || showRain)) || burst;
  // Sombra de contacto: ancho y alto según el personaje (proporciones del
  // viewBox 120×120). Se dibuja FUERA del div animado, así se queda en el suelo
  // mientras el personaje brinca o baila (una sombra real no se eleva con él).
  const GROUND_SHADOW = {
    coin: { w: 68, h: 12 },
    piggy: { w: 70, h: 11 },
    cat: { w: 56, h: 10 },
    plant: { w: 48, h: 9 },
    owl: { w: 64, h: 11 },
  };
  const gs = GROUND_SHADOW[kind] || GROUND_SHADOW.coin;
  return (
    <div style={{ position: "relative", width: size, height: size + 4, flexShrink: 0 }} aria-hidden="true">
      {/* Sombra de contacto en el suelo (suave, no negra dura): elipse aplanada
          debajo del personaje que no participa en sus animaciones */}
      <div
        style={{
          position: "absolute",
          left: "50%",
          bottom: 1,
          transform: "translateX(-50%)",
          width: (gs.w / 120) * size,
          height: (gs.h / 120) * size,
          borderRadius: "50%",
          background: "radial-gradient(ellipse at center, rgba(30,20,8,0.20) 0%, rgba(30,20,8,0) 68%)",
          pointerEvents: "none",
        }}
      />
      {/* Capa de efectos: se escala con el tamaño del personaje (transform scale) */}
      {hasFx && (
      <div style={{ position: "absolute", inset: 0, transform: "scale(" + s + ")", transformOrigin: "0 0", pointerEvents: "none" }}>
      {/* Chispitas de alegría (ánimo feliz o muy feliz en reposo) */}
      {effects && showSparkles && (
        <div style={{ position: "absolute", inset: -6, pointerEvents: "none" }}>
          {[[8, 10, "0s"], [84, 4, "0.22s"], [92, 58, "0.1s"], [2, 50, "0.34s"]].map(([l, t, d], i) => (
            <span
              key={i}
              style={{
                position: "absolute",
                left: l + "%",
                top: t + "%",
                fontSize: "0.7rem",
                color: "#FFE9A8",
                animation: "cc-sparkle 1.1s ease-in-out " + d + " infinite",
              }}
            >
              ✦
            </span>
          ))}
        </div>
      )}
      {/* Corazones flotando + confeti de colores (reacción feliz) */}
      {effects && showParty && (
        <div style={{ position: "absolute", inset: -8, pointerEvents: "none" }}>
          {[[10, 28, "0s", "0.85rem"], [82, 22, "0.18s", "0.7rem"], [88, 58, "0.36s", "0.95rem"], [2, 60, "0.5s", "0.65rem"]].map(([l, t, d, f], i) => (
            <span key={"h" + i} style={{ position: "absolute", left: l + "%", top: t + "%", fontSize: f, animation: "cc-heart-up 1.1s ease-out " + d + " infinite" }}>❤️</span>
          ))}
          {["#F2C94C", "#E8786B", "#8BC98B", "#7EC8F7", "#C99A4B"].map((c, i) => (
            <span
              key={"c" + i}
              style={{
                position: "absolute",
                left: [12, 30, 52, 70, 86][i] + "%",
                top: "-8%",
                width: "0.32rem",
                height: "0.55rem",
                borderRadius: "1px",
                background: c,
                animation: "cc-confetti-fall 1.2s linear " + i * 0.12 + "s infinite",
              }}
            />
          ))}
        </div>
      )}
      {/* Nube de lluvia con gotitas (reacción triste) */}
      {effects && showRain && (
        <div style={{ position: "absolute", top: "-1.4rem", left: "50%", transform: "translateX(-50%)", pointerEvents: "none", textAlign: "center", animation: "cc-cloud-drift 2.4s ease-in-out infinite" }}>
          <div style={{ fontSize: "0.95rem", lineHeight: 1 }}>☁️</div>
          <div style={{ display: "flex", justifyContent: "center", gap: "0.35rem", marginTop: "0.05rem" }}>
            {[0, 0.22, 0.45].map((d, i) => (
              <span key={i} style={{ fontSize: "0.5rem", animation: "cc-rain-drop 0.9s linear " + d + "s infinite" }}>💧</span>
            ))}
          </div>
        </div>
      )}
      {/* Burbuja de ánimo durante la reacción */}
      {burst && (
        <div
          style={{
            position: "absolute",
            top: "-0.5rem",
            right: "-0.55rem",
            transform: "translateY(-100%)",
            zIndex: 5,
            background: "#FFFFFF",
            color: up ? "#0F5132" : "#B3392B",
            fontSize: "0.6rem",
            fontWeight: 700,
            lineHeight: 1.2,
            padding: "0.28rem 0.5rem",
            borderRadius: "0.55rem",
            boxShadow: "0 8px 18px -8px rgb(0 0 0 / 0.45)",
            whiteSpace: "nowrap",
            animation: "cc-bubble-in .28s ease both",
          }}
        >
          {bubbleText}
          <span
            style={{
              position: "absolute",
              left: "0.9rem",
              bottom: "-0.24rem",
              width: "0.5rem",
              height: "0.5rem",
              background: "#FFFFFF",
              transform: "rotate(45deg)",
              borderRadius: "0.08rem",
            }}
          />
        </div>
      )}
      </div>
      )}
      {/* Cuerpo con su animación: brinca de alegría, se apaga con pena o respira en reposo */}
      <div
        style={{
          animation: animate ? (up ? "cc-mascot-bounce 1s ease-in-out infinite" : down ? "cc-mascot-droop 1.5s ease-in-out infinite" : "cc-idle-bob 3.2s ease-in-out infinite") : undefined,
          transformOrigin: "50% 100%",
        }}
      >
        {/* Halo suave detrás del personaje: un resplandor cálido que lo separa
            del fondo verde de la tarjeta. Es un efecto EXCLUSIVO del personaje
            principal (prop halo): en Ajustes y el historial se omite para no
            ensuciar las miniaturas y que el de la tarjeta se sienta especial. */}
        {halo && (
          <div
            style={{
              position: "absolute",
              inset: "-6% -14%",
              borderRadius: "50%",
              background: "radial-gradient(circle at 50% 45%, rgba(255,236,180,0.55) 0%, rgba(255,236,180,0.20) 45%, rgba(255,236,180,0) 70%)",
              pointerEvents: "none",
            }}
          />
        )}
        <svg width={size} height={size} viewBox="0 0 120 120">
          <defs>
            {/* Moneda: oro con luz arriba-izquierda (solo se define para la moneda) */}
            {kind === "coin" && (
              <radialGradient id={gid} cx="36%" cy="28%" r="80%">
                <stop offset="0%" stopColor="#F6DFA6" />
                <stop offset="55%" stopColor="#E0B463" />
                <stop offset="100%" stopColor="#B4863A" />
              </radialGradient>
            )}
            {/* Gradiente de volumen del cuerpo de cada personaje (mismo id gidB,
                solo se define el del personaje actual): luz arriba-izquierda. */}
            {kind === "piggy" && (
              <radialGradient id={gid + "B"} cx="38%" cy="28%" r="85%">
                <stop offset="0%" stopColor="#FDE6EE" />
                <stop offset="55%" stopColor="#F6C3CF" />
                <stop offset="100%" stopColor="#E4A0B1" />
              </radialGradient>
            )}
            {kind === "cat" && (
              <radialGradient id={gid + "B"} cx="38%" cy="28%" r="85%">
                <stop offset="0%" stopColor="#FBD894" />
                <stop offset="55%" stopColor="#F8C774" />
                <stop offset="100%" stopColor="#E0A94E" />
              </radialGradient>
            )}
            {kind === "plant" && (
              <radialGradient id={gid + "B"} cx="38%" cy="28%" r="85%">
                <stop offset="0%" stopColor="#9ED49A" />
                <stop offset="55%" stopColor="#7FBF7F" />
                <stop offset="100%" stopColor="#639E66" />
              </radialGradient>
            )}
            {kind === "plant" && (
              <radialGradient id={gid + "P"} cx="40%" cy="25%" r="90%">
                <stop offset="0%" stopColor="#D9AE63" />
                <stop offset="55%" stopColor="#B4863A" />
                <stop offset="100%" stopColor="#96621C" />
              </radialGradient>
            )}
            {kind === "owl" && (
              <radialGradient id={gid + "B"} cx="38%" cy="28%" r="85%">
                <stop offset="0%" stopColor="#C99B6F" />
                <stop offset="55%" stopColor="#A9744F" />
                <stop offset="100%" stopColor="#8A5A2B" />
              </radialGradient>
            )}
          </defs>
          <MascotSvg kind={kind} mood={m} gid={gid} detail={detail} />
        </svg>
      </div>
    </div>
  );
}

// Reacción del personaje a cada acción del historial: la cara que se muestra
// en miniatura junto a la entrada y el texto breve de su tooltip. Anotar un
// ingreso alegra, anotar un gasto entristece, borrar hace lo contrario, y
// editar depende de cómo cambió el saldo (la versión anterior de la entrada
// frente a la actual, si sigue). Las acciones sin movimiento (renombrar,
// ajustes…) quedan neutras.
function historyReaction(e, txs, base) {
  if (e.op === "addTx") {
    const t = txs.find((x) => x.id === e.id);
    if (!t) return { mood: "neutral", text: "Quedó neutro" };
    return t.type === "gasto"
      ? { mood: "sad", text: "Se entristeció: hubo un gasto" }
      : { mood: "happy", text: "Se alegró: entró dinero" };
  }
  if (e.op === "delTx" && e.tx) {
    return e.tx.type === "gasto"
      ? { mood: "happy", text: "Se alegró: se borró un gasto" }
      : { mood: "sad", text: "Se entristeció: se borró un ingreso" };
  }
  if (e.op === "editTx" && e.tx) {
    const old = signedTx(e.tx, base);
    const cur = txs.find((x) => x.id === e.tx.id);
    const delta = (cur ? signedTx(cur, base) : old) - old;
    if (delta > 0.005) return { mood: "happy", text: "Se alegró: el cambio subió el saldo" };
    if (delta < -0.005) return { mood: "sad", text: "Se entristeció: el cambio bajó el saldo" };
    return { mood: "neutral", text: "Quedó neutro" };
  }
  return { mood: "neutral", text: "Quedó neutro" };
}

// ¿Pantalla de escritorio? (ancho ≥ 768 px). El personaje y sus animaciones
// están pensados para móvil: en escritorio se dibujan más grandes (también la
// miniatura del historial y el selector de Ajustes) y la capa de efectos los
// sigue. Reacciona a los cambios de tamaño de la ventana.
function useDesktop() {
  const [on, setOn] = useState(
    () => typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(min-width: 768px)").matches
  );
  useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const mq = window.matchMedia("(min-width: 768px)");
    const fn = () => setOn(mq.matches);
    fn();
    if (mq.addEventListener) mq.addEventListener("change", fn);
    else if (mq.addListener) mq.addListener(fn);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", fn);
      else if (mq.removeListener) mq.removeListener(fn);
    };
  }, []);
  return on;
}

export default function CuentaCompartida() {
  const [myName, setMyName] = useState(() => {
    try {
      const raw = localStorage.getItem("cc:my-name");
      if (!raw) return "";
      if (typeof raw === "string" && raw.startsWith("{")) {
        const p = safeParse(raw, null);
        return p && p.value ? String(p.value).trim() : "";
      }
      return String(raw || "").trim();
    } catch (e) { return ""; }
  });
  const [person, setPerson] = useState(() => {
    try {
      const raw = localStorage.getItem("cc:my-name");
      if (!raw) return "";
      if (typeof raw === "string" && raw.startsWith("{")) {
        const p = safeParse(raw, null);
        return p && p.value ? String(p.value).trim() : "";
      }
      return String(raw || "").trim();
    } catch (e) { return ""; }
  });
  const [phase, setPhase] = useState(() => {
    try {
      const cached = localStorage.getItem("cc:my-name");
      if (cached && cached.trim()) return "app";
    } catch (e) {}
    return "loading";
  });
  const [nameDraft, setNameDraft] = useState("");
  // Contraseña del onboarding (entrar o crear la cuenta).
  const [passDraft, setPassDraft] = useState("");
  const [showPassDraft, setShowPassDraft] = useState(false);
  const [showForgotPass, setShowForgotPass] = useState(false);
  const [showNewPass, setShowNewPass] = useState(false);
  const [showNewPass2, setShowNewPass2] = useState(false);
  const [showGeminiKey, setShowGeminiKey] = useState(false);
  // ¿Existe ya un usuario con el nombre escrito (sin distinguir mayúsculas)?
  // null = sin comprobar · true = ya existe · false = nombre nuevo.
  const [userExists, setUserExists] = useState(null);
  // ---- "¿Olvidaste tu contraseña?": restablecer la de otra persona ----
  // La cuenta es compartida y no hay email: cualquier persona con acceso puede
  // restablecer la contraseña de otra (con confirmación antes de guardar).
  const [forgotOpen, setForgotOpen] = useState(false); // panel abierto/cerrado
  const [forgotName, setForgotName] = useState(""); // de quién
  const [forgotPass, setForgotPass] = useState(""); // contraseña nueva
  const [forgotConfirm, setForgotConfirm] = useState(null); // { name, pass } pendiente de confirmar
  const [forgotDone, setForgotDone] = useState(""); // mensaje de éxito (verde)
  const [people, setPeople] = useState(() => {
    try {
      const raw = localStorage.getItem("cc:people");
      const list = raw ? safeParse(raw, []) : [];
      return (Array.isArray(list) ? list : [])
        .map((r) => (typeof r === "string" ? r : r && r.name ? r.name : ""))
        .map((s) => String(s || "").trim())
        .filter(Boolean);
    } catch (e) { return []; }
  });
  const [transactions, setTransactions] = useState(() => {
    try {
      const raw = localStorage.getItem("cc:transactions");
      return raw ? safeParse(raw, []) : [];
    } catch (e) { return []; }
  });
  const [settings, setSettings] = useState(() => {
    try {
      const raw = localStorage.getItem("cc:settings");
      return raw ? normalizeSettings(safeParse(raw, {})) : normalizeSettings({});
    } catch (e) {
      return normalizeSettings({});
    }
  });
  const [error, setError] = useState("");
  const [saving, setSaving] = useState(false);
  // Cambio de nombre pendiente de confirmar: la migración de movimientos solo
  // se hace cuando el usuario pulsa "Sí, migrar" (evita migraciones no deseadas).
  const [pendingRename, setPendingRename] = useState(null);
  // Historial de las últimas 10 acciones de este dispositivo para poder deshacer.
  const [history, setHistory] = useState([]);

  const [showForm, setShowForm] = useState(false);
  const [formType, setFormType] = useState("gasto");
  const [amount, setAmount] = useState("");
  const [desc, setDesc] = useState("");
  const [category, setCategory] = useState("");
  const [note, setNote] = useState("");
  // Foto del recibo (opcional): data URL comprimido del movimiento.
  const [photo, setPhoto] = useState(null);
  const [photoBusy, setPhotoBusy] = useState(false);
  // Visor (lightbox): foto del recibo ampliada al tocarla en la lista.
  const [viewPhoto, setViewPhoto] = useState(null);
  const [date, setDate] = useState(todayISO());
  // Moneda del movimiento que se está anotando (S/ por defecto) y, si no es la
  // base, el tipo de cambio a cuánto equivale 1 unidad en la moneda base.
  const [formCurrency, setFormCurrency] = useState("S/");
  const [formRate, setFormRate] = useState("");
  // Método de pago: 'efectivo' | 'tarjeta' | 'transferencia'
  const [formPaymentMethod, setFormPaymentMethod] = useState("efectivo");
  // Tarjeta específica seleccionada si el método es tarjeta o al abonar
  const [formCardId, setFormCardId] = useState("");
  // Cuota o préstamo específico seleccionado al pagar cuota
  const [formInstallmentId, setFormInstallmentId] = useState("");
  // Préstamos entre nosotros: deudor y seguimiento en cuotas
  const [formBorrower, setFormBorrower] = useState("");
  const [formCreateInstallment, setFormCreateInstallment] = useState(false);
  const [formInstallmentCount, setFormInstallmentCount] = useState("1");
  // Buscando el tipo de cambio automático (cuando se elige una moneda distinta).
  const [rateLoading, setRateLoading] = useState(false);
  // Movimiento en modo edición (null = anotar uno nuevo)
  const [editingTx, setEditingTx] = useState(null);

  // ---- Personaje de la cuenta ----
  // "burst" es el ánimo momentáneo (null = en reposo): se dispara al anotar,
  // borrar o deshacer un movimiento y vuelve solo al ánimo que da el saldo a
  // los ~3 s.
  const [burst, setBurst] = useState(null);
  const burstTimerRef = useRef(null);
  useEffect(() => () => { if (burstTimerRef.current) clearTimeout(burstTimerRef.current); }, []);

  // Dispara la reacción del personaje y la revierte al ánimo del saldo.
  // También suena: tono alegre al entrar dinero, tono triste al gastar. Solo
  // con la pestaña visible y si el sonido de este dispositivo está activado
  // (mismo interruptor que los avisos de novedades).
  function reactMood(m) {
    setBurst(m);
    if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
    burstTimerRef.current = setTimeout(() => setBurst(null), 3200);
    if (!document.hidden && soundEnabledRef.current) playMascotSound(m);
  }

  // En escritorio (≥ 768 px) el personaje y sus miniaturas se dibujan más grandes.
  const isDesktop = useDesktop();

    const [showInviteModal, setShowInviteModal] = useState(false);
  const [inviteLimitType, setInviteLimitType] = useState("single"); // "single" | "count" | "time" | "unlimited"
  const [inviteMaxCount, setInviteMaxCount] = useState(2);
  const [inviteExpireHours, setInviteExpireHours] = useState(24);
  const [currentInviteToken, setCurrentInviteToken] = useState(null);
  const [showSpaceModal, setShowSpaceModal] = useState(false);
  const [spaceModalTab, setSpaceModalTab] = useState("create"); // 'create' | 'join' | 'switch'
  const [newSpaceName, setNewSpaceName] = useState("");
  const [joinCodeDraft, setJoinCodeDraft] = useState("");
  const [onboardingMode, setOnboardingMode] = useState(() => {
    try {
      if (typeof window !== "undefined") {
        const params = new URLSearchParams(window.location.search);
        if (params.get("cuenta") || params.get("code") || params.get("acc")) return "invite";
      }
    } catch (e) {}
    return "create";
  });
  const [onboardingSpaceName, setOnboardingSpaceName] = useState("");
  const [spaceFeedback, setSpaceFeedback] = useState(null);
  const [inviteCopied, setInviteCopied] = useState(false);
  const [activeAccountCode, setActiveAccountCode] = useState(() => {
    try {
      const known = getKnownAccounts();
      const curId = getActiveAccountId();
      const found = known.find(a => a && (a.id === curId || a.code === curId));
      return found ? (found.code || found.id) : (curId === "default" ? "DEFAULT" : curId);
    } catch (e) { return "DEFAULT"; }
  });
  const [showSettings, setShowSettings] = useState(false);
  const [settingsTab, setSettingsTab] = useState("cuenta");
  const [globalNotifsEnabled, setGlobalNotifsEnabled] = useState(() => {
    try {
      return localStorage.getItem("cc:global_notifications") === "1";
    } catch (e) {
      return false;
    }
  });
  const [showChangelog, setShowChangelog] = useState(false);
  const [pendingSettleDebt, setPendingSettleDebt] = useState(null);
  const [pendingCashbackRedeem, setPendingCashbackRedeem] = useState(null);
  const [quickCashbackCard, setQuickCashbackCard] = useState(null);
  const [formLoanId, setFormLoanId] = useState("");
  const [formCashbackEnabled, setFormCashbackEnabled] = useState(true);
  const [formCashbackPercent, setFormCashbackPercent] = useState("");
  const [formCashbackCustom, setFormCashbackCustom] = useState(false);
  const [lastSeenChangelog, setLastSeenChangelog] = useState(() => {
    try { return localStorage.getItem("cc:last_seen_changelog") || ""; } catch (e) { return ""; }
  });
  const hasUnseenChangelog = lastSeenChangelog !== (APP_CHANGELOG[0]?.version || "v25");
  const [appUpdateAvailable, setAppUpdateAvailable] = useState(false);
  const [updatingApp, setUpdatingApp] = useState(false);
  const [dismissedUpdateBanner, setDismissedUpdateBanner] = useState(false);

  useEffect(() => {
    const handleUpdate = () => setAppUpdateAvailable(true);
    window.addEventListener("cc:app-update-available", handleUpdate);
    return () => window.removeEventListener("cc:app-update-available", handleUpdate);
  }, []);

  // ---- Asistente de Voz y Lenguaje Natural con IA (Smart Prompt & Gemini) ----
  const [smartPromptText, setSmartPromptText] = useState("");
  const [isListening, setIsListening] = useState(false);
  const [voiceSupported, setVoiceSupported] = useState(false);
  const [smartProcessing, setSmartProcessing] = useState(false);
  const [smartFeedback, setSmartFeedback] = useState(null); // { type: 'success' | 'error', text: '' }
  const [editGeminiApiKey, setEditGeminiApiKey] = useState("");
  const [editGeminiIsPrivate, setEditGeminiIsPrivate] = useState(true);
  const [showEditGeminiKeyField, setShowEditGeminiKeyField] = useState(false);
  const [geminiTestStatus, setGeminiTestStatus] = useState("");

  function getEffectiveGeminiApiKey(curSettings) {
    try {
      const loc = localStorage.getItem("cc:local_gemini_api_key");
      if (loc && loc.trim()) return loc.trim();
    } catch (e) {}
    return (curSettings && curSettings.geminiApiKey) ? String(curSettings.geminiApiKey).trim() : "";
  }

  const isListeningRef = useRef(false);
  const speechRecognitionRef = useRef(null);
  const speechTimerRef = useRef(null);
  const latestSpokenRef = useRef("");
  const lastDeletedTxRef = useRef(null);
  const mediaRecorderRef = useRef(null);
  const audioChunksRef = useRef([]);

  useEffect(() => {
    const SpeechRec = typeof window !== "undefined" ? (window.SpeechRecognition || window.webkitSpeechRecognition) : null;
    if (SpeechRec) {
      setVoiceSupported(true);
    }
    return () => {
      isListeningRef.current = false;
      if (speechRecognitionRef.current) {
        try { speechRecognitionRef.current.abort(); } catch (e) {}
      }
      if (speechTimerRef.current) clearTimeout(speechTimerRef.current);
    };
  }, []);

  function stopAndProcessVoice() {
    isListeningRef.current = false;
    if (speechTimerRef.current) {
      clearTimeout(speechTimerRef.current);
      speechTimerRef.current = null;
    }
    if (speechRecognitionRef.current) {
      try {
        speechRecognitionRef.current.stop();
      } catch (e) {
        try { speechRecognitionRef.current.abort(); } catch (e2) {}
      }
      speechRecognitionRef.current = null;
    }
    setIsListening(false);

    const textToRun = (latestSpokenRef.current || "").trim();
    latestSpokenRef.current = "";

    if (textToRun) {
      processSmartExpense(textToRun);
    }
  }

  function deduplicateConsecutiveWords(str) {
    if (!str) return "";
    let clean = String(str).replace(/\s+/g, " ").trim();
    // 1. Limpiar repeticiones consecutivas de palabras individuales ("que que que" -> "que")
    const words = clean.split(/\s+/);
    const deduped = [];
    for (let i = 0; i < words.length; i++) {
      if (i === 0 || words[i].toLowerCase() !== words[i - 1].toLowerCase()) {
        deduped.push(words[i]);
      }
    }
    let res = deduped.join(" ");

    // 2. Limpiar repeticiones consecutivas de frases ("que elimines que elimines" -> "que elimines")
    for (let n = 4; n >= 2; n--) {
      const pattern = new RegExp(`\\b(${'\\w+\\s+'.repeat(n - 1)}\\w+)\\s+\\1\\b`, "gi");
      res = res.replace(pattern, "$1");
    }
    return res.replace(/\s+/g, " ").trim();
  }

  async function toggleVoiceRecording() {
    const SpeechRec = typeof window !== "undefined" ? (window.SpeechRecognition || window.webkitSpeechRecognition) : null;
    if (!SpeechRec) {
      alert("El reconocimiento de voz no está disponible en este navegador. Puedes escribir o dictar usando el micrófono de tu teclado.");
      return;
    }

    if (isListeningRef.current || isListening) {
      stopAndProcessVoice();
      return;
    }

    // Solicitar permiso de micrófono en navegadores de escritorio si es necesario
    if (typeof navigator !== "undefined" && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        stream.getTracks().forEach((t) => t.stop());
      } catch (err) {
        console.warn("Permiso de micrófono:", err);
        if (err.name === "NotAllowedError" || err.name === "PermissionDeniedError") {
          alert("Por favor permite el acceso al micrófono en los permisos de tu navegador.");
          setIsListening(false);
          isListeningRef.current = false;
          return;
        }
      }
    }

    try {
      if (speechRecognitionRef.current) {
        try { speechRecognitionRef.current.abort(); } catch (e) {}
        speechRecognitionRef.current = null;
      }
      if (speechTimerRef.current) {
        clearTimeout(speechTimerRef.current);
        speechTimerRef.current = null;
      }

      latestSpokenRef.current = "";
      setSmartPromptText("");
      setSmartFeedback(null);
      isListeningRef.current = true;
      setIsListening(true);

      const rec = new SpeechRec();
      rec.continuous = true;
      rec.interimResults = true;
      rec.maxAlternatives = 1;
      rec.lang = (typeof navigator !== "undefined" && navigator.language && navigator.language.startsWith("es")) ? navigator.language : "es-PE";

      rec.onstart = () => {
        setIsListening(true);
        isListeningRef.current = true;
      };

      rec.onresult = (event) => {
        let fullSpeech = "";
        for (let i = 0; i < event.results.length; ++i) {
          const item = event.results[i];
          if (item && item[0] && item[0].transcript) {
            fullSpeech += item[0].transcript + " ";
          }
        }
        const clean = deduplicateConsecutiveWords(fullSpeech);
        if (clean) {
          latestSpokenRef.current = clean;
          setSmartPromptText(clean);

          if (speechTimerRef.current) clearTimeout(speechTimerRef.current);
          speechTimerRef.current = setTimeout(() => {
            if (isListeningRef.current) {
              stopAndProcessVoice();
            }
          }, 2000);
        }
      };

      rec.onspeechend = () => {
        if (speechTimerRef.current) clearTimeout(speechTimerRef.current);
        speechTimerRef.current = setTimeout(() => {
          if (isListeningRef.current) {
            stopAndProcessVoice();
          }
        }, 1500);
      };

      rec.onerror = (event) => {
        console.warn("Aviso micrófono:", event.error);
        if (event.error === "no-speech") {
          return;
        }
        setIsListening(false);
        isListeningRef.current = false;
        if (event.error === "not-allowed" || event.error === "service-not-allowed") {
          alert("Por favor permite el acceso al micrófono en los permisos de tu navegador.");
        } else if (event.error === "network") {
          alert("El navegador no pudo conectar con el servicio de voz. Si usas Brave o un bloqueador, habilita el reconocimiento de voz en la configuración del navegador (brave://settings/privacy -> Google Speech Services).");
        }
      };

      rec.onend = () => {
        setIsListening(false);
        isListeningRef.current = false;
        if (speechTimerRef.current) {
          clearTimeout(speechTimerRef.current);
          speechTimerRef.current = null;
        }
        if (latestSpokenRef.current && latestSpokenRef.current.trim()) {
          const t = latestSpokenRef.current.trim();
          latestSpokenRef.current = "";
          processSmartExpense(t);
        }
      };

      speechRecognitionRef.current = rec;
      rec.start();
    } catch (err) {
      console.warn("No se pudo iniciar el micrófono:", err);
      setIsListening(false);
      isListeningRef.current = false;
    }
  }

  async function processSmartExpense(rawInput) {
    const raw = String(rawInput || smartPromptText).trim();
    if (!raw) return;
    const text = deduplicateConsecutiveWords(raw);
    if (!text) return;
    setSmartProcessing(true);
    setSmartFeedback(null);

    const context = {
      people,
      myName,
      cards: settings.cards || [],
      categories: settings.categories || [],
      currency: settings.currency || "S/",
    };

    let parsed = null;
    const apiKey = getEffectiveGeminiApiKey(settings);

    // 1. Procesar a través de Gemini API si hay clave configurada
    if (apiKey) {
      parsed = await parseWithGeminiApi(text, context, apiKey);
    }

    // 2. Respaldo por reglas naturales locales
    if (!parsed || (!parsed.amount && !parsed.action)) {
      parsed = parseNaturalExpenseText(text, context);
    }

    setSmartProcessing(false);

    // 1. Manejo de comando directo: Crear o agregar categoría
    if (parsed && parsed.action === "create_category" && parsed.categoryName) {
      const catName = cleanCategoryName(parsed.categoryName);
      const icon = getCategoryIcon(catName);
      const currentCats = Array.isArray(settings.categories) && settings.categories.length > 0 ? settings.categories : DEFAULT_CATEGORIES;
      if (currentCats.some((c) => c.toLowerCase() === catName.toLowerCase())) {
        setSmartFeedback({
          type: "success",
          text: `ℹ️ La categoría "${icon} ${catName}" ya está en tu lista de categorías.`,
        });
      } else {
        const newCats = [...currentCats, catName];
        setSettings((prev) => ({ ...prev, categories: newCats }));
        storage.set("settings", JSON.stringify({ ...settings, categories: newCats }), true).catch(() => {});
        setSmartFeedback({
          type: "success",
          text: `🎉 ¡Categoría "${icon} ${catName}" creada y guardada con éxito! Ya puedes verla y usarla en tus gastos.`,
        });
      }
      setTimeout(() => setSmartFeedback(null), 7000);
      setSmartPromptText("");
      return;
    }

    // 2. Manejo de comando directo: Eliminar o borrar categoría
    if (parsed && parsed.action === "delete_category" && parsed.categoryName) {
      const catName = cleanCategoryName(parsed.categoryName);
      const currentCats = Array.isArray(settings.categories) && settings.categories.length > 0 ? settings.categories : DEFAULT_CATEGORIES;
      const filtered = currentCats.filter((c) => c.toLowerCase() !== catName.toLowerCase());
      setSettings((prev) => ({ ...prev, categories: filtered }));
      storage.set("settings", JSON.stringify({ ...settings, categories: filtered }), true).catch(() => {});
      setSmartFeedback({
        type: "success",
        text: `🗑️ Categoría "${catName}" eliminada de tu cuenta.`,
      });
      setTimeout(() => setSmartFeedback(null), 6000);
      setSmartPromptText("");
      return;
    }

    // 3. Manejo de comando directo: Restaurar / re-agregar último gasto
    if (parsed && (parsed.action === "restore_last_deleted" || parsed.action === "restore" || parsed.action === "redo")) {
      const txToRestore = lastDeletedTxRef.current || (history.find((e) => e.op === "delTx")?.tx);
      if (txToRestore) {
        (async () => {
          const updated = await withRetry(() => storage.addTransaction(txToRestore));
          setTransactions(updated);
          if (txToRestore.id) markTxRestored(txToRestore.id);
          const undoEid = uid();
          pushHistory({ label: "Restaurar " + txToRestore.description, op: "addTx", id: txToRestore.id, eid: undoEid });
          lastDeletedTxRef.current = null;
          setSmartFeedback({
            type: "success",
            text: `🎉 Gasto restaurado: ${txToRestore.currency || "S/"} ${Number(txToRestore.amount).toFixed(2)} (${txToRestore.description}) de ${txToRestore.person}.`,
          });
          setTimeout(() => setSmartFeedback(null), 6000);
        })();
        setSmartPromptText("");
        return;
      }

      // Si no hay borrado previo en memoria pero hay gastos en la cuenta, re-anota el último para cumplir la orden
      if (transactions.length > 0) {
        let sorted = [...transactions].sort((a, b) => (b.date || "").localeCompare(a.date || "") || (b.id || "").localeCompare(a.id || ""));
        if (parsed.targetPerson) {
          const pNorm = removeAccents(parsed.targetPerson).toLowerCase();
          const pFiltered = sorted.filter((t) => t.person && removeAccents(t.person).toLowerCase() === pNorm);
          if (pFiltered.length > 0) sorted = pFiltered;
        }
        const latest = sorted[0];
        if (latest) {
          const cloned = {
            ...latest,
            id: uid(),
            date: todayISO(),
          };
          (async () => {
            const updated = await withRetry(() => storage.addTransaction(cloned));
            setTransactions(updated);
            const undoEid = uid();
            pushHistory({ label: "Re-agregar " + cloned.description, op: "addTx", id: cloned.id, eid: undoEid });
            setSmartFeedback({
              type: "success",
              text: `🎉 Gasto re-agregado: ${cloned.currency || "S/"} ${Number(cloned.amount).toFixed(2)} (${cloned.description}) de ${cloned.person}.`,
            });
            setTimeout(() => setSmartFeedback(null), 6000);
          })();
          setSmartPromptText("");
          return;
        }
      }

      setSmartFeedback({
        type: "error",
        text: "ℹ️ No hay movimientos registrados para volver a agregar.",
      });
      setTimeout(() => setSmartFeedback(null), 5000);
      setSmartPromptText("");
      return;
    }

    // 4. Manejo de comando directo: Repetir / duplicar último gasto
    if (parsed && (parsed.action === "repeat_last_tx" || parsed.action === "duplicate_last")) {
      if (transactions.length > 0) {
        const sorted = [...transactions].sort((a, b) => (b.date || "").localeCompare(a.date || "") || (b.id || "").localeCompare(a.id || ""));
        const latest = sorted[0];
        const cloned = {
          ...latest,
          id: uid(),
          date: todayISO(),
        };
        (async () => {
          const updated = await withRetry(() => storage.addTransaction(cloned));
          setTransactions(updated);
          const undoEid = uid();
          pushHistory({ label: "Repetir " + cloned.description, op: "addTx", id: cloned.id, eid: undoEid });
          setSmartFeedback({
            type: "success",
            text: `🔁 Gasto repetido: ${cloned.currency || "S/"} ${Number(cloned.amount).toFixed(2)} (${cloned.description}) de ${cloned.person}.`,
          });
          setTimeout(() => setSmartFeedback(null), 6000);
        })();
        setSmartPromptText("");
        return;
      }
    }

    // 5. Manejo de comando directo: Consultar deudas / saldos entre personas
    if (parsed && (parsed.action === "query_debt" || parsed.action === "query_balance" || parsed.action === "query_category")) {
      if (parsed.action === "query_debt") {
        if (settleUp && settleUp.length > 0) {
          const debtsText = settleUp.map((s) => `${s.from} le debe a ${s.to} ${settings.currency || "S/"} ${s.amount.toFixed(2)}`).join(" · ");
          setSmartFeedback({
            type: "success",
            text: `🤝 Cuentas pendientes: ${debtsText}.`,
          });
        } else {
          setSmartFeedback({
            type: "success",
            text: `🤝 ¡Las cuentas están al día! Nadie debe a nadie.`,
          });
        }
      } else if (parsed.targetCategory) {
        const catName = parsed.targetCategory.toLowerCase();
        const curMonth = todayISO().slice(0, 7);
        const matchingTxs = transactions.filter((t) => (t.date || "").startsWith(curMonth) && t.type === "gasto" && ((t.category && t.category.toLowerCase().includes(catName)) || (t.description && t.description.toLowerCase().includes(catName))));
        const totalCat = matchingTxs.reduce((sum, t) => sum + (Number(t.amount) || 0), 0);
        setSmartFeedback({
          type: "success",
          text: `📊 En ${parsed.targetCategory} este mes llevan gastado: ${settings.currency || "S/"} ${totalCat.toFixed(2)} (${matchingTxs.length} movimientos).`,
        });
      } else {
        const totalSpent = (totals && totals[settings.currency || "S/"]?.gasto) || 0;
        setSmartFeedback({
          type: "success",
          text: `💰 Total de gastos acumulados: ${settings.currency || "S/"} ${totalSpent.toFixed(2)}.`,
        });
      }
      setTimeout(() => setSmartFeedback(null), 8000);
      setSmartPromptText("");
      return;
    }

    // 6. Manejo de comando directo: Cambiar moneda
    if (parsed && parsed.action === "set_currency" && parsed.currency) {
      const newCur = parsed.currency;
      const next = { ...settings, currency: newCur };
      setSettings(next);
      storage.set("settings", JSON.stringify(next), true).catch(() => {});
      setSmartFeedback({
        type: "success",
        text: `💱 Moneda principal actualizada a ${newCur}.`,
      });
      setTimeout(() => setSmartFeedback(null), 5000);
      setSmartPromptText("");
      return;
    }

    // 7. Manejo de comando directo: Cambiar tema oscuro / claro
    if (parsed && parsed.action === "toggle_dark_mode") {
      const nextDark = parsed.targetState != null ? parsed.targetState : !darkMode;
      setDarkMode(nextDark);
      applyDarkModeToDOM(nextDark);
      try { localStorage.setItem("cc:dark_mode", nextDark ? "1" : "0"); } catch (e) {}
      setSmartFeedback({
        type: "success",
        text: nextDark ? "🌙 Modo oscuro activado." : "☀️ Modo claro activado.",
      });
      setTimeout(() => setSmartFeedback(null), 5000);
      setSmartPromptText("");
      return;
    }

    // 8. Manejo de comando directo: Borrar o deshacer el último movimiento
    if (parsed && (parsed.action === "delete_last_tx" || parsed.action === "undo")) {
      const targetP = parsed.targetPerson || (parsed.person && parsed.person !== myName ? parsed.person : "");
      if (transactions.length > 0) {
        let sorted = [...transactions].sort((a, b) => (b.date || "").localeCompare(a.date || "") || (b.id || "").localeCompare(a.id || ""));
        if (targetP) {
          const pNorm = removeAccents(targetP).toLowerCase();
          const pFiltered = sorted.filter((t) => t.person && removeAccents(t.person).toLowerCase() === pNorm);
          if (pFiltered.length > 0) sorted = pFiltered;
        }
        const latest = sorted[0];
        if (latest) {
          deleteTransaction(latest.id);
          setSmartFeedback({
            type: "success",
            text: `🗑️ Último gasto ${targetP ? `de ${latest.person} ` : ""}eliminado: ${latest.currency || "S/"} ${Number(latest.amount).toFixed(2)} (${latest.description}).`,
          });
          setTimeout(() => setSmartFeedback(null), 6000);
          setSmartPromptText("");
          return;
        }
      }
      if (history.length > 0) {
        const lastEntry = history[0];
        undoAction(lastEntry);
        setSmartFeedback({
          type: "success",
          text: `↩️ Deshecha última acción: "${lastEntry.label || "Movimiento"}".`,
        });
        setTimeout(() => setSmartFeedback(null), 6000);
        setSmartPromptText("");
        return;
      }
      setSmartFeedback({
        type: "error",
        text: targetP ? `ℹ️ No encontré movimientos recientes de ${targetP} para borrar.` : "ℹ️ No hay movimientos registrados para borrar.",
      });
      setTimeout(() => setSmartFeedback(null), 5000);
      setSmartPromptText("");
      return;
    }

    // 9. Manejo de comando directo: Borrar gasto específico
    if (parsed && parsed.action === "delete_specific_tx") {
      const qDesc = (parsed.targetDescription || parsed.description || "").toLowerCase().trim();
      const qAmt = parsed.targetAmount || parsed.amount;
      const match = transactions.find((t) => {
        const matchAmt = !qAmt || Math.abs(Number(t.amount) - Number(qAmt)) < 0.05;
        const matchDesc = !qDesc || (t.description && removeAccents(t.description).toLowerCase().includes(removeAccents(qDesc)));
        return (qAmt && qDesc) ? (matchAmt && matchDesc) : (matchAmt || matchDesc);
      });
      if (match) {
        deleteTransaction(match.id);
        setSmartFeedback({
          type: "success",
          text: `🗑️ Eliminé el movimiento: ${match.currency || "S/"} ${Number(match.amount).toFixed(2)} (${match.description}) de ${match.person}.`,
        });
      } else {
        setSmartFeedback({
          type: "error",
          text: `⚠️ No encontré ningún gasto que coincida con "${qDesc || qAmt}".`,
        });
      }
      setTimeout(() => setSmartFeedback(null), 6000);
      setSmartPromptText("");
      return;
    }

    // 10. Manejo de comando directo: Abrir paneles
    if (parsed && parsed.action === "open_panel" && parsed.panel) {
      const p = parsed.panel.toLowerCase();
      if (p.includes("ajuste") || p.includes("config")) {
        openSettings();
      } else if (p.includes("consejer") || p.includes("resumen") || p.includes("estadist")) {
        openFinancialAdvisor();
      } else if (p.includes("duplicad") || p.includes("huerfan")) {
        openDupReview();
      } else if (p.includes("tarjeta")) {
        setSettingsTab("tarjetas");
        openSettings();
      } else if (p.includes("cuota")) {
        setSettingsTab("cuotas");
        openSettings();
      }
      setSmartFeedback({
        type: "success",
        text: `✨ ${parsed.thought || "Abriendo panel solicitado..."}`,
      });
      setTimeout(() => setSmartFeedback(null), 5000);
      setSmartPromptText("");
      return;
    }

    // 11. Manejo de comando directo: Abrir formulario (Ingreso, Préstamo, Gasto)
    if (parsed && parsed.action === "open_form") {
      setEditingTx(null);
      setFormType(parsed.formType || "ingreso");
      setShowForm(true);
      setAmount("");
      setDesc(parsed.description || "");
      setCategory(parsed.category || "");
      setPerson(parsed.person || myName || (people[0] || ""));
      setSmartFeedback({
        type: "success",
        text: `✨ ${parsed.thought || `Abriendo formulario para ${parsed.formType || "ingreso"}...`}`,
      });
      setTimeout(() => setSmartFeedback(null), 5000);
      setSmartPromptText("");
      return;
    }

    // 12. Manejo de registro de gasto / ingreso / préstamo — GUARDADO DIRECTO SIN FORM
    if (parsed && (parsed.amount > 0 || parsed.type === "ingreso" || parsed.type === "prestamo")) {
      // Auto-creación de categoría si el gasto incluye una nueva
      let catCreatedMsg = "";
      let finalSettings = settings;
      if (parsed.category) {
        const cat = parsed.category.trim();
        const currentCats = Array.isArray(settings.categories) ? settings.categories : [];
        if (cat && !currentCats.some((c) => c.toLowerCase() === cat.toLowerCase())) {
          const newCats = [...currentCats, cat];
          finalSettings = { ...settings, categories: newCats };
          setSettings(finalSettings);
          storage.set("settings", JSON.stringify(finalSettings), true).catch(() => {});
          catCreatedMsg = ` [Nueva categoría: ${cat}]`;
        }
      }

      const defaultDesc = parsed.type === "ingreso" ? "Ingreso de dinero" : (parsed.type === "prestamo" ? "Préstamo" : "Gasto variado");
      const finalDesc = parsed.description ? parsed.description.trim() : (parsed.category ? parsed.category.trim() : defaultDesc);
      const parsedPerson = parsed.person || myName || (people[0] || "");
      const baseCur = finalSettings.currency || "S/";
      const cur = parsed.currency || baseCur;

      const entry = {
        id: uid(),
        type: parsed.type || "gasto",
        amount: parsed.amount || 0,
        description: finalDesc,
        category: parsed.category ? parsed.category.trim() : undefined,
        note: parsed.note ? parsed.note.trim() : (parsed.category ? `Categoría: ${parsed.category}` : undefined),
        person: parsedPerson,
        recipient: parsed.type === "prestamo" ? (parsed.borrower || undefined) : undefined,
        isInternal: parsed.type === "prestamo" ? true : undefined,
        date: todayISO(),
        currency: cur,
        paymentMethod: parsed.paymentMethod || "efectivo",
        cardId: parsed.paymentMethod === "tarjeta" ? (parsed.cardId || undefined) : undefined,
      };

      setSaving(true);
      setSmartFeedback({ type: "success", text: "⏳ Guardando..." });
      try {
        const updated = await withRetry(() => storage.addTransaction(entry));
        setTransactions(updated);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updated };
        const undoEid = uid();
        pushHistory({ label: "Anotar " + entry.description, op: "addTx", id: entry.id, eid: undoEid });
        const actor = myNameRef.current || parsedPerson;
        const isIngreso = entry.type === "ingreso";
        const isPrestamo = entry.type === "prestamo";
        const sign = isIngreso ? "+" : "−";
        const typeTag = isIngreso ? "Ingreso" : (isPrestamo ? "Préstamo" : "Gasto");
        const noticeText = actor === parsedPerson
          ? `${actor} anotó ${typeTag.toLowerCase()}: ${entry.description} (${sign}${fmtMoney(entry.amount, cur)})`
          : `${actor} anotó para ${parsedPerson}: ${entry.description} (${sign}${fmtMoney(entry.amount, cur)})`;
        notifyOthers(actor, noticeText, undoEid);
        reactMood(isIngreso ? "happy" : "sad");
        const feedbackText = isIngreso
          ? `🎉 ¡Ingreso registrado!: +${fmtMoney(entry.amount, cur)} — ${finalDesc}${catCreatedMsg} (${parsedPerson})`
          : isPrestamo
          ? `🤝 ¡Préstamo registrado!: ${fmtMoney(entry.amount, cur)} — ${finalDesc} (${parsedPerson} ➔ ${entry.recipient || "otro"})`
          : `✅ Gasto anotado: ${fmtMoney(entry.amount, cur)} — ${finalDesc}${catCreatedMsg} (${parsedPerson})`;
        setSmartFeedback({ type: "success", text: feedbackText });
        setTimeout(() => setSmartFeedback(null), 6000);
      } catch (err) {
        setSmartFeedback({ type: "error", text: "❌ No se pudo guardar. Revisa tu conexión." });
        setTimeout(() => setSmartFeedback(null), 6000);
      } finally {
        setSaving(false);
      }
      setSmartPromptText("");
    } else if (parsed && parsed.action === "chat" && parsed.thought) {
      setSmartFeedback({
        type: "success",
        text: `🤖 ${parsed.thought}`,
      });
      setTimeout(() => setSmartFeedback(null), 8000);
      setSmartPromptText("");
    } else {
      setSmartFeedback({
        type: "error",
        text: parsed && parsed.thought ? `⚠️ ${parsed.thought}` : "⚠️ No detecté el monto o la acción. Ej: 'Hans gastó 40 soles en pizza con tarjeta' o 'Crea una categoría para Taxis'",
      });
    }
  }

  // ---- Consejero Financiero & Resumen Inteligente (Modo 3) ----
  const [showAdvisorModal, setShowAdvisorModal] = useState(false);
  const [advisorData, setAdvisorData] = useState(null);
  const [advisorLoading, setAdvisorLoading] = useState(false);

  async function openFinancialAdvisor(monthTarget) {
    const target = monthTarget || summaryMonth || todayISO().slice(0, 7);
    // 1. Mostrar métricas locales de inmediato en 0 ms
    const instantData = computeLocalFinancialMetrics(transactions, target, settings, people);
    setAdvisorData(instantData);
    setShowAdvisorModal(true);
    setAdvisorLoading(true);

    // 2. Enriquecer con consejos de IA en paralelo sin bloquear la UI
    try {
      const apiKey = getEffectiveGeminiApiKey(settings);
      const enrichedData = await generateFinancialAdvice(transactions, target, settings, people, apiKey);
      if (enrichedData) {
        setAdvisorData(enrichedData);
      }
    } catch (e) {
      console.warn("Fallo enriquecimiento IA del consejero:", e);
    } finally {
      setAdvisorLoading(false);
    }
  }

  async function triggerAppUpdate() {
    setUpdatingApp(true);
    try {
      if (window.ccServiceWorkerReg && window.ccServiceWorkerReg.waiting) {
        window.ccServiceWorkerReg.waiting.postMessage({ type: "SKIP_WAITING" });
      }
      if (typeof caches !== "undefined") {
        const keys = await caches.keys();
        await Promise.all(keys.map((k) => caches.delete(k)));
      }
      try {
        localStorage.setItem("cc:last_seen_changelog", APP_CHANGELOG[0]?.version || "v25");
      } catch (e) {}
    } catch (err) {}
    setTimeout(() => {
      window.location.reload(true);
    }, 450);
  }

  function openChangelog() {
    setShowChangelog(true);
    try {
      localStorage.setItem("cc:last_seen_changelog", APP_CHANGELOG[0]?.version || "v25");
      setLastSeenChangelog(APP_CHANGELOG[0]?.version || "v25");
    } catch (e) {}
  }
  const [editPeople, setEditPeople] = useState([""]);
  const [editSplitEnabled, setEditSplitEnabled] = useState(true);

  async function handleToggleSplit(val) {
    const nextVal = Boolean(val);
    setEditSplitEnabled(nextVal);
    const updatedSettings = {
      ...settings,
      splitEnabled: nextVal,
      splitMode: nextVal ? "igual" : "sin",
    };
    setSettings(updatedSettings);
    try {
      await storage.set("settings", JSON.stringify(updatedSettings), true);
    } catch (e) {}
  }
  const [editCurrency, setEditCurrency] = useState("S/");
  // Filtro de persona activa en el módulo de tarjetas del dashboard
  const [cardTabPerson, setCardTabPerson] = useState("auto");
  // Estados para minimizar/plegar secciones en el dashboard
  const [peerLoansCollapsed, setPeerLoansCollapsed] = useState(() => {
    try { return localStorage.getItem("cc:collapse:peerloans") === "1"; } catch (e) { return false; }
  });
  const [cardsCollapsed, setCardsCollapsed] = useState(() => {
    try { return localStorage.getItem("cc:collapse:cards") === "1"; } catch (e) { return false; }
  });
  const [installmentsCollapsed, setInstallmentsCollapsed] = useState(() => {
    try { return localStorage.getItem("cc:collapse:installments") === "1"; } catch (e) { return false; }
  });
  const [selectedInstallmentPerson, setSelectedInstallmentPerson] = useState("todas");
  const [summaryCollapsed, setSummaryCollapsed] = useState(() => {
    try { return localStorage.getItem("cc:collapse:summary") === "1"; } catch (e) { return false; }
  });
  const [peopleCollapsed, setPeopleCollapsed] = useState(() => {
    try { return localStorage.getItem("cc:collapse:people") === "1"; } catch (e) { return false; }
  });
  const [savingsCollapsed, setSavingsCollapsed] = useState(() => {
    try { return localStorage.getItem("cc:collapse:savings") === "0" ? false : false; } catch (e) { return false; }
  });
  const [budgetsCollapsed, setBudgetsCollapsed] = useState(() => {
    try { return localStorage.getItem("cc:collapse:budgets") === "0" ? false : false; } catch (e) { return false; }
  });
  const [showSavingsModal, setShowSavingsModal] = useState(false);
  const [savingsDraft, setSavingsDraft] = useState({ id: "", name: "", targetAmount: "", currentAmount: "", targetDate: "", color: "emerald" });
  const [showMinimalTransfersModal, setShowMinimalTransfersModal] = useState(false);
  const [showTransferDataModal, setShowTransferDataModal] = useState(false);
  const [transferTargetSpace, setTransferTargetSpace] = useState("");
  const [transferMode, setTransferMode] = useState("copy"); // "copy" | "move"
  const [transferIncludeTx, setTransferIncludeTx] = useState(true);
  const [transferIncludeCards, setTransferIncludeCards] = useState(true);
  const [transferIncludeInstallments, setTransferIncludeInstallments] = useState(true);
  const [transferStatus, setTransferStatus] = useState(null);
  const [transferLoading, setTransferLoading] = useState(false);
  const [transferProgressText, setTransferProgressText] = useState("");
  const [showBudgetModal, setShowBudgetModal] = useState(false);
  // Tarjetas de crédito en edición en Ajustes
  const [editCards, setEditCards] = useState([]);
  const [showCardModal, setShowCardModal] = useState(false);
  const [settingsCardFilter, setSettingsCardFilter] = useState("todas");
  const [cardDraft, setCardDraft] = useState({
    id: "",
    name: "",
    cardType: "credito",
    person: "",
    billingCycle: "mensual",
    cutoffDay: 20,
    dueDay: 10,
    targetCutoffMonth: "auto",
    targetDueMonth: "auto",
    creditLimit: "",
    currency: "S/",
    color: "emerald",
    hasCashback: false,
    cashbackPercent: "1.5",
    excludeServices: true,
    minAmount: "",
    monthlyCap: "",
  });

  // Cuotas y préstamos en edición en Ajustes
  const [editInstallments, setEditInstallments] = useState([]);
  const [showInstallmentModal, setShowInstallmentModal] = useState(false);
  const [settingsInstallmentFilter, setSettingsInstallmentFilter] = useState("todas");
  const [installmentDraft, setInstallmentDraft] = useState({
    id: "",
    name: "",
    person: "",
    recipient: "",
    totalAmount: "",
    amount: "",
    currency: "S/",
    dueDay: 15,
    targetDueMonth: "auto",
    totalInstallments: "",
    paidInstallments: 0,
    cardId: "",
    notes: "",
    color: "gold",
    isInternal: false,
  });
  const [installmentPayingId, setInstallmentPayingId] = useState(null);

  // Personaje de la cuenta elegido en Ajustes (id de MASCOT_KINDS).
  const [editMascot, setEditMascot] = useState("coin");
  // Pop de bienvenida al PROBAR un personaje en el selector de Ajustes: guarda
  // qué personaje se pulsó y un contador (n) para poder repetir el pop cada vez
  // que se pulsa (incluso el ya seleccionado). Al terminar la animación se
  // limpia solo (onAnimationEnd) y el personaje vuelve a respirar.
  const [previewPop, setPreviewPop] = useState(null);
  // Si se cierra Ajustes a mitad del pop (antes de que termine la animación),
  // el estado quedaría vivo y el pop se repetiría al reabrir el panel sin haber
  // pulsado nada: se limpia al cerrar.
  useEffect(() => {
    if (!showSettings) {
      setPreviewPop(null);
      setShowCardModal(false);
      setShowInstallmentModal(false);
    }
  }, [showSettings]);
  const [editCategories, setEditCategories] = useState("");
  const [editCategoryList, setEditCategoryList] = useState(DEFAULT_CATEGORIES);
  const [newCategoryDraft, setNewCategoryDraft] = useState("");
  const [editAccountName, setEditAccountName] = useState("Cuenta compartida");
  // Cambio de contraseña desde Ajustes (actual + nueva + confirmación).
  const [currentPass, setCurrentPass] = useState("");
  const [newPass, setNewPass] = useState("");
  const [newPass2, setNewPass2] = useState("");

  // Reflejos de miNombre y la lista para usarlos dentro del sondeo (setInterval)
  // sin problemas de clausura obsoleta: el temporizador se crea una sola vez.
  const myNameRef = useRef("");
  const peopleRef = useRef([]);
  // Evita que una respuesta vieja de tipo de cambio pise una más nueva.
  const rateSeqRef = useRef(0);
  // Evita que una respuesta vieja del sondeo pise el estado de una más nueva:
  // cada refreshShared descarta su resultado si mientras tanto se lanzó otro.
  const refreshSeqRef = useRef(0);
  // Realtime: refs para que el listener use SIEMPRE el estado y la función de
  // refresco actuales (el efecto de suscripción corre una sola vez y no debe
  // quedarse con clausuras obsoletas).
  const realtimeTimerRef = useRef(null);
  const uiRef = useRef({ showForm: false, showSettings: false, saving: false });
  const refreshRef = useRef(() => {});
  const realtimeStateRef = useRef({ transactions: [], people: [], settings: {} });
  // Acumulador de avisos inmediatos de Realtime: las ráfagas (p. ej. renombrar
  // muchos movimientos) se agrupan en UNA notificación del sistema.
  const realtimeNoticeBufRef = useRef({ lines: [], timer: null, txId: "" });
  useEffect(() => {
    uiRef.current = { showForm, showSettings, saving };
    refreshRef.current = refreshShared;
    realtimeStateRef.current = { transactions, people, settings: normalizeSettings(settings) };
  });
  // El formulario vive en una ventana modal: mientras está abierta, bloquea el
  // scroll del fondo (para que el personaje no se mueva de sitio) y cierra con
  // Escape.
  useEffect(() => {
    if (!showForm) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const onKey = (e) => {
      if (e.key !== "Escape") return;
      // Si hay una foto ampliada (lightbox) dentro del formulario, Escape
      // cierra primero la foto y, si no, el modal.
      if (viewPhoto) {
        setViewPhoto(null);
        return;
      }
      setEditingTx(null);
      setShowForm(false);
    };
    window.addEventListener("keydown", onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener("keydown", onKey);
    };
  }, [showForm, viewPhoto]);
  useEffect(() => {
    myNameRef.current = myName;
  }, [myName]);
  useEffect(() => {
    peopleRef.current = people;
  }, [people]);
  // Detecta (sin distinguir mayúsculas) si el nombre escrito ya tiene cuenta,
  // para mostrar "Entrar" o "Crear mi cuenta" y el aviso de contraseña.
  useEffect(() => {
    const t = setTimeout(async () => {
      const typed = nameDraft.trim();
      if (!typed) {
        setUserExists(null);
        return;
      }
      try {
        const u = await storage.getUser(typed);
        setUserExists(!!u);
      } catch (e) {
        setUserExists(null);
      }
    }, 400);
    return () => clearTimeout(t);
  }, [nameDraft]);
  // Último estado compartido que este dispositivo mostró: se usa en el sondeo
  // para anunciar solo los cambios hechos por OTRAS personas.
  const lastShownRef = useRef(null);
  // Escrituras que solo pudieron guardarse en LOCAL (nube caída o sin red):
  // se muestran en un aviso y se reintentan automáticamente en cada sondeo.
  const [pendingWrites, setPendingWrites] = useState(0);
  // Cuándo empezó a intentarse subir la cola actual (marca del más antiguo).
  const [pendingSince, setPendingSince] = useState(0);
  // Ticker de 1 s mientras haya pendientes: re-renderiza el banner para que el
  // "lleva X intentándolo" cuente en vivo sin esperar al siguiente flush.
  const [pendingTick, setPendingTick] = useState(0);
  // Reintento rápido: el módulo avisa aquí cuando cambia el número de
  // escrituras sin subir, para actualizar el aviso al instante (sin esperar
  // al siguiente sondeo).
  useEffect(() => {
    onPendingWritesChanged = (count) => {
      setPendingWrites(count);
      setPendingSince(count > 0 ? storage.pendingWritesSince() || Date.now() : 0);
    };
    // Ticker de 1 s mientras haya pendientes: refresca el tiempo del aviso.
    // Cuando el reintento rápido re-subir una entrada del historial a la nube,
    // el panel se refresca al instante (sin esperar al siguiente sondeo).
    onHistoryFlushed = (merged) => {
      const next = mergeHistory(merged);
      setHistory((prev) => (sameJson(prev, next) ? prev : next));
    };
    return () => {
      onPendingWritesChanged = null;
      onHistoryFlushed = null;
      // Si el componente se desmonta, no dejar el reintento corriendo solo.
      if (pendingFlushTimer) {
        clearTimeout(pendingFlushTimer);
        pendingFlushTimer = null;
      }
      // Si se desmonta a mitad de un flush, que el próximo montaje pueda
      // volver a reintentar (sin esto el flag quedaría en true para siempre).
      pendingFlushBusy = false;
    };
  }, []);
  // Ticker de 1 s mientras haya pendientes: re-renderiza el banner para que el
  // "lleva X intentándolo" cuente en vivo sin esperar al siguiente flush.
  useEffect(() => {
    if (pendingWrites <= 0) return;
    const t = setInterval(() => setPendingTick((n) => n + 1), 1000);
    return () => clearInterval(t);
  }, [pendingWrites > 0]);
  // Aviso visible de novedades (cambios de otras personas), auto-ocultable.
  const [notice, setNotice] = useState(null);
  const noticeTimerRef = useRef(null);
  // Preferencia de ESTE dispositivo: sonido + vibración al llegar novedades.
  // No es compartida: cada persona la activa o desactiva en su propio teléfono.
  const [soundEnabled, setSoundEnabled] = useState(true);
  const soundEnabledRef = useRef(true);
  useEffect(() => {
    soundEnabledRef.current = soundEnabled;
  }, [soundEnabled]);
  // Pop de bienvenida al abrir la app: el personaje hace su saludo (animación
  // CSS en la tarjeta) y suena su tono alegre sincronizado con ese pop. El tono
  // arranca con el mismo retardo de la animación (0.2 s en escritorio, 0.1 s en
  // móvil) y respeta los mismos criterios que las reacciones: solo con la
  // pestaña visible y con el sonido de este dispositivo activado. Suena una
  // sola vez por carga (cuando la tarjeta aparece en fase app).
  const welcomePlayedRef = useRef(false);
  useEffect(() => {
    if (phase !== "app" || welcomePlayedRef.current) return;
    welcomePlayedRef.current = true;
    let removeGesture = null;
    const t = setTimeout(() => {
      // Política de autoplay: si el AudioContext nace suspendido (recarga sin
      // gesto previo), el navegador no deja sonar; el saludo se reproduce en
      // cuanto el usuario toque o teclee algo (el warmUp de abajo ya reanuda
      // el contexto en ese gesto). Si ya está running, suena con el pop.
      const ctx = ensureAudioCtx();
      const play = () => {
        if (!document.hidden && soundEnabledRef.current) playMascotSound("happy");
      };
      if (ctx && ctx.state === "suspended") {
        const EVENTS = ["pointerdown", "keydown", "touchstart"];
        const onGesture = () => {
          play();
          EVENTS.forEach((ev) => window.removeEventListener(ev, onGesture));
        };
        EVENTS.forEach((ev) =>
          window.addEventListener(ev, onGesture, { once: true, passive: true })
        );
        removeGesture = () =>
          EVENTS.forEach((ev) => window.removeEventListener(ev, onGesture));
      } else {
        play();
      }
    }, isDesktop ? 200 : 100);
    return () => {
      clearTimeout(t);
      if (removeGesture) removeGesture();
    };
  }, [phase]);
  // Preferencia de ESTE dispositivo: notificaciones del sistema cuando la
  // pestaña está en segundo plano (requiere permiso del navegador).
  const [notifyEnabled, setNotifyEnabled] = useState(false);
  const notifyEnabledRef = useRef(false);
  useEffect(() => {
    notifyEnabledRef.current = notifyEnabled;
  }, [notifyEnabled]);
  // Notificaciones push de verdad (con la app cerrada). Preferencia de ESTE
  // dispositivo: se guarda localmente y se re-suscribe al cargar. Se lee de
  // forma SINCRÓNICA igual que el tema oscuro: si no, el efecto de abajo
  // escribiría "0" en el primer render pisando el "1" guardado.
  const [pushEnabled, setPushEnabled] = useState(() => {
    let saved = false;
    try {
      saved = localStorage.getItem("cc:push") === "1";
    } catch (e) {}
    return saved;
  });
  const pushEnabledRef = useRef(false);
  useEffect(() => {
    pushEnabledRef.current = pushEnabled;
    storage.set("push", pushEnabled ? "1" : "0", false).catch(() => {});
  }, [pushEnabled]);
  // Preferencia de ESTE dispositivo: tema claro/oscuro (no compartido).
  // Se lee de forma SINCRÓNICA en el primer render: si se esperara a la carga
  // asíncrona, el efecto de abajo ya habría escrito "0" (claro) pisando el "1"
  // guardado y el modo oscuro se perdería al reiniciar.
  const [dark, setDark] = useState(() => {
    let saved = false;
    try {
      const stored = localStorage.getItem("cc:dark");
      if (stored !== null) {
        saved = localStorage.getItem("cc:dark") === "1";
      } else if (typeof window !== "undefined" && window.matchMedia) {
        saved = window.matchMedia("(prefers-color-scheme: dark)").matches;
      }
    } catch (e) {}
    // Aplica el tema antes del primer pintado para no destellar en claro.
    setDarkMode(saved);
    return saved;
  });
  useEffect(() => {
    setDarkMode(dark);
    try {
      localStorage.setItem("cc:dark", dark ? "1" : "0");
    } catch (e) {}
  }, [dark]);

  // Preferencia de ESTE dispositivo: Modo minimalista (por defecto true para nuevos usuarios)
  const [minimalMode, setMinimalMode] = useState(() => {
    try {
      const stored = localStorage.getItem("cc:minimal");
      if (stored === null) return true; // Inicia siempre en modo minimal para nuevos usuarios
      return stored === "1";
    } catch (e) {
      return true;
    }
  });
  useEffect(() => {
    try {
      localStorage.setItem("cc:minimal", minimalMode ? "1" : "0");
    } catch (e) {}
  }, [minimalMode]);

  // Movimiento resaltado al venir de una notificación del sistema: se pinta su
  // fila con un anillo unos segundos para localizarlo de un vistazo.
  const [highlightTx, setHighlightTx] = useState(null);
  const highlightTimerRef = useRef(null);
  const focusRetryTimerRef = useRef(null);
  // La notificación del sistema es global (fuera de React): al pulsarla con un
  // id de movimiento, la app cierra el formulario/ajustes, salta a la fila y la
  // resalta. Si el id aún no está en pantalla (el refresh debounceado tarda
  // 400 ms), se reintenta hasta 10 veces mientras llega.
  useEffect(() => {
    setAppFocusTxHandler((txId) => {
      setShowForm(false);
      setShowSettings(false);
      setHighlightTx(txId);
      if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
      highlightTimerRef.current = setTimeout(() => setHighlightTx(null), 5000);
      let tries = 0;
      const tryScroll = () => {
        const el = document.getElementById("tx-row-" + txId);
        if (el) {
          try {
            el.scrollIntoView({ behavior: "smooth", block: "center" });
          } catch (e) {
            try {
              el.scrollIntoView();
            } catch (e2) {}
          }
          return;
        }
        if (++tries < 10) focusRetryTimerRef.current = setTimeout(tryScroll, 250);
      };
      focusRetryTimerRef.current = setTimeout(tryScroll, 100);
    });
    return () => {
      setAppFocusTxHandler(null);
      if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
      if (focusRetryTimerRef.current) clearTimeout(focusRetryTimerRef.current);
    };
  }, []);

  // Activa/desactiva el push de este dispositivo: pide permiso al navegador,
  // guarda la suscripción en la nube y avisa del resultado.
  async function setPushPref(on) {
    try {
      if (on) {
        if (typeof Notification === "undefined") {
          setPushEnabled(false);
          setError("Este navegador no admite notificaciones push.");
          return;
        }
        let perm = Notification.permission;
        if (perm === "default") perm = await Notification.requestPermission();
        if (perm !== "granted") {
          setPushEnabled(false);
          setError("Permiso de notificaciones denegado. Actívalo en los ajustes del navegador.");
          return;
        }
        const sub = await subscribePush();
        if (!sub) {
          setPushEnabled(false);
          setError("Tu navegador no permite push aquí. Instala la app (PWA) e inténtalo de nuevo.");
          return;
        }
        await savePushSubscription(sub, myNameRef.current || myName);
        setPushEnabled(true);
      } else {
        const reg = await navigator.serviceWorker.getRegistration();
        if (reg) {
          const sub = await reg.pushManager.getSubscription();
          if (sub) {
            await deletePushSubscription(sub);
            await sub.unsubscribe();
          }
        }
        setPushEnabled(false);
      }
    } catch (err) {
      setPushEnabled(false);
      setError("No se pudo configurar el push: " + ((err && err.message) || err));
    }
  }

  // Al cargar la cuenta, si este dispositivo tenía el push activado se
  // re-suscribe (la suscripción puede haber cambiado) y refresca su nombre.
  useEffect(() => {
    if (phase !== "app" || !HAS_PUSH) return;
    let cancelled = false;
    (async () => {
      try {
        const pref = await storage.get("push", false);
        if (cancelled || !pref || pref.value !== "1") return;
        const sub = await subscribePush();
        if (sub && !cancelled) {
          await savePushSubscription(sub, myNameRef.current || myName);
          setPushEnabled(true);
        }
      } catch (e) {
        // Sin push: se activa desde Ajustes.
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [phase, myName]);
  // Origen real de los datos en esta sesión: "supabase" | "backend" | "local".
  // Si la nube está configurada pero falla, se avisa con un indicador visible.
  const [syncSource, setSyncSource] = useState("local");
  // Estado del canal Realtime para el indicador de la cabecera:
  // "connecting" | "connected" | "reconnecting" | "polling" (sondeo clásico).
  const [rtStatus, setRtStatus] = useState(realtimeAvailable() ? "connecting" : "polling");
  // Diagnóstico de Realtime para Ajustes: existencia de la publicación
  // supabase_realtime y nº de dispositivos con push. El estado del canal ya
  // vive en rtStatus (arriba); aquí solo se consulta la base al abrir Ajustes.
  const [rtDiag, setRtDiag] = useState(null);   // { publicationExists, pushDevices, rpcOk } | null
  const [rtDiagBusy, setRtDiagBusy] = useState(false);
  const [rtDiagError, setRtDiagError] = useState("");
  // Guard de respuestas fuera de orden: si se abre/cierra Ajustes rápido o se
  // pulsa Reintentar varias veces, solo gana la consulta más reciente.
  const rtDiagSeqRef = useRef(0);

  async function runRtDiag() {
    const seq = ++rtDiagSeqRef.current;
    if (!HAS_SUPABASE) {
      setRtDiag({ publicationExists: null, pushDevices: null, rpcOk: false });
      return;
    }
    setRtDiagBusy(true);
    setRtDiagError("");
    try {
      const info = await diagRealtimeInfo();
      if (seq !== rtDiagSeqRef.current) return; // llegó una consulta más nueva
      setRtDiag(info);
    } catch (e) {
      if (seq !== rtDiagSeqRef.current) return;
      setRtDiagError("No se pudo consultar: " + ((e && e.message) || "error"));
    } finally {
      if (seq === rtDiagSeqRef.current) setRtDiagBusy(false);
    }
  }

  // Al abrir Ajustes se consulta el diagnóstico (y con el botón "Reintentar").
  useEffect(() => {
    if (showSettings && phase === "app") runRtDiag();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [showSettings, phase]);
  // La base de datos de Supabase tiene un esquema desactualizado (faltan
  // columnas o tablas): se muestra un aviso y no se revierten los cambios
  // que solo se guardaron en este dispositivo.
  const [dbOutdated, setDbOutdated] = useState(false);
  // PWA: para poder instalar la app en el móvil (banner "Instalar app").
  const [installEvt, setInstallEvt] = useState(null);
  const [showInstallTip, setShowInstallTip] = useState(false);
  // PWA: detecta que Chrome/Android permite instalar la app (beforeinstallprompt)
  // o muestra una vez el consejo de instalación para Safari/iOS.
  useEffect(() => {
    const standalone =
      (window.matchMedia && window.matchMedia("(display-mode: standalone)").matches) ||
      window.navigator.standalone === true;
    if (standalone) return;
    const onBip = (e) => {
      e.preventDefault();
      setInstallEvt(e);
    };
    window.addEventListener("beforeinstallprompt", onBip);
    const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent || "");
    if (isIOS && !localStorage.getItem("cc_ios_tip")) {
      setShowInstallTip(true);
    }
    return () => window.removeEventListener("beforeinstallprompt", onBip);
  }, []);
  async function handleInstall() {
    if (!installEvt) return;
    installEvt.prompt();
    try {
      await installEvt.userChoice;
    } catch (e) {
      /* el usuario canceló la instalación */
    }
    setInstallEvt(null);
  }
  function dismissInstallTip() {
    setShowInstallTip(false);
    try {
      localStorage.setItem("cc_ios_tip", "1");
    } catch (e) {
      /* noop */
    }
  }
  // Política de autoplay del navegador: el AudioContext nace suspendido hasta
  // el primer gesto del usuario. Lo reanudamos en el primer toque/tecla para
  // que el primer aviso suene aunque la página se haya recargado y no se haya
  // tocado nada.
  useEffect(() => {
    const warmUp = () => {
      // No creamos el AudioContext si el sonido está desactivado.
      if (!soundEnabledRef.current) return;
      try {
        const ctx = ensureAudioCtx();
        if (ctx && ctx.state === "suspended") ctx.resume();
      } catch (e) {}
    };
    ["pointerdown", "keydown", "touchstart"].forEach((ev) =>
      window.addEventListener(ev, warmUp, { once: true, passive: true })
    );
    return () => {
      ["pointerdown", "keydown", "touchstart"].forEach((ev) =>
        window.removeEventListener(ev, warmUp)
      );
    };
  }, []);
  // Confirmación pendiente antes de deshacer una acción compartida.
  const [pendingUndo, setPendingUndo] = useState(null);
  // Panel de historial desplegado o plegado.
  const [historyOpen, setHistoryOpen] = useState(false);
  // Detalle de una acción del historial (al pulsar su hora exacta): muestra
  // quién, qué y cuándo exacto, con botón para copiar el resumen.
  const [historyDetail, setHistoryDetail] = useState(null);
  // Feedback "¡Copiado!" tras copiar el detalle de una acción.
  const [copiedDetail, setCopiedDetail] = useState(false);
  // Ticker de 30 s: refresca las etiquetas relativas del historial ("hace 3
  // min") mientras el panel está abierto, sin depender de un nuevo sondeo.
  const [nowTick, setNowTick] = useState(0);
  useEffect(() => {
    if (!historyOpen) return;
    const id = setInterval(() => setNowTick((t) => t + 1), 30000);
    return () => clearInterval(id);
  }, [historyOpen]);
  // Resumen mensual: mes visible en formato YYYY-MM (por defecto, el actual).
  const [summaryMonth, setSummaryMonth] = useState(() => {
    const d = new Date();
    return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0");
  });
  // Limpieza de movimientos duplicados/fantasma (copias de antes del arreglo
  // de sincronización). Las firmas omitidas se recuerdan en este dispositivo.
  const [dupDismissed, setDupDismissed] = useState(() => {
    try {
      const v = safeParse(localStorage.getItem("cc:dupDismissed"), []);
      return Array.isArray(v) ? v : [];
    } catch (e) {
      return [];
    }
  });
  // Panel de duplicados desplegado o plegado + limpieza en curso.
  const [showDupPanel, setShowDupPanel] = useState(false);
  const [dupBusy, setDupBusy] = useState(false);
  // Consolidar copias fantasma de un grupo de duplicados: destino elegido por
  // grupo (sig -> nombre) y grupo pendiente de confirmar (sig | null).
  const [dupTargets, setDupTargets] = useState({});
  const [dupGhostConfirm, setDupGhostConfirm] = useState(null);
  // Movimientos huérfanos (persona ya no está en la lista de la cuenta).
  // Las claves omitidas se recuerdan en este dispositivo.
  const [orphanDismissed, setOrphanDismissed] = useState(() => {
    try {
      const v = safeParse(localStorage.getItem("cc:orphanDismissed"), []);
      return Array.isArray(v) ? v : [];
    } catch (e) {
      return [];
    }
  });
  // Panel de huérfanos desplegado o plegado + migración/borrado en curso.
  const [showOrphanPanel, setShowOrphanPanel] = useState(false);
  const [orphanBusy, setOrphanBusy] = useState(false);
  // Persona elegida como destino de migración por grupo (clave -> nombre).
  const [orphanTargets, setOrphanTargets] = useState({});
  // Grupo pendiente de confirmar (borrar o migrar): { key, action } | null.
  const [orphanConfirm, setOrphanConfirm] = useState(null);
  // Vista previa expandida por grupo huérfano (clave -> abierto o cerrado):
  // muestra cada movimiento (descripción, fecha e importe) antes de decidir.
  const [orphanExpanded, setOrphanExpanded] = useState({});

  // ---- carga inicial ----
  useEffect(() => {
    (async () => {
      try {
        let mine = "";
        try {
          const r = await storage.get("my-name", false);
          mine = r ? r.value : "";
        } catch {
          mine = "";
        }
        // Preferencia de sonido de este dispositivo (local, no compartida).
        try {
          const rs = await storage.get("sound", false);
          if (rs && (rs.value === "0" || rs.value === "false")) setSoundEnabled(false);
        } catch {
          // Por defecto: sonido activado.
        }
        // Tema claro/oscuro de este dispositivo: ya se leyó de forma SINCRÓNICA
        // en el useState inicial (para no destellar y no pisar la preferencia),
        // así que aquí no hace falta volver a leerlo.
        // Preferencia de notificaciones del sistema de este dispositivo.
        try {
          const rn = await storage.get("notify", false);
          if (rn && rn.value === "1") setNotifyEnabled(true);
        } catch {
          // Por defecto: desactivadas.
        }

        // Historial de acciones COMPARTIDO (para que cualquiera pueda deshacer
        // lo que otro anotó). Se lee de la nube; si está vacío, se migra una vez
        // el historial local antiguo para no perder la posibilidad de deshacer.
        try {
          const rh = await storage.get("history", true);
          const parsed = rh ? safeParse(rh.value, []) : [];
          if (Array.isArray(parsed) && parsed.length) {
            setHistory(mergeHistory(parsed));
          } else {
            const rl = await storage.get("history", false);
            const local = rl ? safeParse(rl.value, []) : [];
            if (Array.isArray(local) && local.length) {
              const seeded = mergeHistory(local.map((e) => normalizeHistoryEntry(e, mine)));
              setHistory(seeded);
              try {
                await storage.set("history", JSON.stringify(seeded), true);
                // Tras migrar a la nube, limpiamos el historial local antiguo
                // para que la próxima carga use solo el compartido.
                await storage.set("history", JSON.stringify([]), false);
              } catch (e) {
                // Si la nube no está disponible, al menos queda en pantalla.
              }
            }
          }
        } catch (e) {
          // Sin historial: no pasa nada.
        }

        // Vino desde una notificación con "Deshacer": localiza la acción en el
        // historial compartido y abre la confirmación para deshacerla.
        try {
          const params = new URLSearchParams(window.location.search);
          const undoId = params.get("undo");
          if (undoId) {
            const rh = await storage.get("history", true);
            const parsed = rh ? safeParse(rh.value, []) : [];
            const found = mergeHistory(parsed).find((e) => e.eid === undoId);
            if (found) setPendingUndo(found);
            try {
              window.history.replaceState({}, "", window.location.pathname);
            } catch (e) {}
          }
        } catch (e) {
          // Sin historial o sin URL válida: no pasa nada.
        }

        const st = await storage.getState();
        setSyncSource(lastSyncSource || "local");
        setDbOutdated(!!st.schemaOutdated);
        const pwCount = storage.pendingWritesCount();
        setPendingWrites(pwCount);
        setPendingSince(pwCount > 0 ? storage.pendingWritesSince() || Date.now() : 0);
        // Si quedaron escrituras sin subir de una sesión anterior, se reintenta
        // subirlas enseguida (sin esperar al primer sondeo).
        if (pwCount > 0) schedulePendingFlush();
        setPeople(st.people);
        // Recuperación de movimientos que en una sesión anterior solo quedaron
        // en este dispositivo (p. ej. mientras la base de datos estaba
        // desactualizada y la nube rechazaba los guardados): se suben a la nube
        // si ya se puede y se muestran en pantalla para que no se pierdan.
        let txShown = st.transactions;
        let localTx = [];
        try {
          const raw = localStorage.getItem("cc:transactions");
          localTx = raw ? safeParse(raw, []) : [];
        } catch (e) {
          // localStorage no disponible: seguimos con la nube.
        }
        if (HAS_SUPABASE && Array.isArray(localTx) && localTx.length) {
          const cloudIds = new Set(st.transactions.map((t) => t.id));
          // No re-subir movimientos que YA se borraron: si un gasto eliminado
          // quedó en el espejo local (o figura como "Borrar…" en el historial
          // compartido), re-subirlo lo haría reaparecer con el nombre antiguo.
          const tomb = txDeletedIds();
          (Array.isArray(st.history) ? st.history : []).forEach((e) => {
            if (e && e.op === "delTx" && e.tx && e.tx.id) tomb.add(String(e.tx.id));
          });
          // Movimientos RESTAURADOS al deshacer un borrado (quizá sin conexión):
          // no se tratan como borrados aunque su delTx siga en el historial de la
          // nube; se re-suben. Si el id ya está en la nube, el marcador sobra.
          if (lastSyncSource === "supabase") {
            txRestoredIds().forEach((id) => {
              if (cloudIds.has(id)) unmarkTxRestored(id);
              else tomb.delete(String(id));
            });
          }
          const missing = localTx.filter(
            (t) => t && t.id != null && !cloudIds.has(t.id) && !tomb.has(String(t.id))
          );
          if (missing.length) {
            for (const t of missing) {
              try {
                await withRetry(() => storage.addTransaction(t));
              } catch (e) {
                // Se reintentará en una próxima recarga o sondeo.
              }
            }
            txShown = mergeById(missing, st.transactions);
          }
          // Alineamos el espejo con lo mostrado para no re-subir copias viejas.
          writeTxMirror(txShown);
        }
        // Si el esquema está desactualizado, además fusionamos todo lo local
        // para que lo anotado aquí no desaparezca de pantalla con el sondeo.
        if (st.schemaOutdated) txShown = mergeById(localTx, txShown);
        setTransactions(txShown);
        setSettings(normalizeSettings(st.settings));
        // Este es el estado que ya mostramos: el primer sondeo no debe
        // anunciar la carga inicial como si fueran novedades.
        lastShownRef.current = { people: st.people, transactions: txShown, settings: normalizeSettings(st.settings) };

        if (mine) {
          setMyName(mine);
          setPerson(mine);
          if (st.people.length && !st.people.includes(mine)) {
            // Mi nombre ya no está en la lista compartida: alguien lo cambió (o
            // lo quitó) mientras este dispositivo estaba apagado. En vez de
            // entrar como un "fantasma" que no está en la lista, pedimos que
            // confirme su nombre actual: al entrar, la app lo reconoce como la
            // misma persona (por su nombre guardado) y migra sus movimientos.
            setNameDraft(mine);
            setPhase("onboarding");
          } else {
            setPhase("app");
          }
        } else {
          setPhase("onboarding");
        }
      } catch (e) {
        setError("No se pudieron cargar los datos. Prueba a recargar.");
        setPhase("onboarding");
      }
    })();
  }, []);

  // Muestra el aviso de novedades (cambios de otras personas) y lo oculta tras 14 s o al pulsar la X.
  function showNotice(lines, quiet, focusTxId) {
    if (!lines || !lines.length) return;
    setNotice({ lines, at: Date.now(), txId: focusTxId });
    // Sonido + vibración
    if (!quiet && soundEnabledRef.current) {
      playNoticeSound();
      vibrateNotice();
    }
    // Notificación del sistema (móvil y PC)
    if (!quiet && notifyEnabledRef.current) {
      const justNow = lastRealtimeNoticeAt && Date.now() - lastRealtimeNoticeAt < 5000;
      if (!justNow || noticeSig(lines) !== lastRealtimeNoticeSig) showSystemNotice(lines, focusTxId);
    }
    if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current);
    noticeTimerRef.current = setTimeout(() => setNotice(null), 14000);
  }

  // Limpia el temporizador del aviso si la app se desmonta.
  useEffect(() => {
    return () => {
      if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current);
    };
  }, []);

  async function refreshShared(silent, isSwitchingSpace) {
    const seq = ++refreshSeqRef.current;
    try {
      const st = await storage.getState();
      if (seq !== refreshSeqRef.current) return; // llegó una respuesta más nueva

      // Si el usuario fue removido de la cuenta por el administrador:
      if (st.people && st.people.length > 0 && myNameRef.current && !st.people.some(p => p.toLowerCase() === myNameRef.current.toLowerCase())) {
        setTransactions([]);
        setPeople(st.people);
        setNotice(null);
        lastShownRef.current = null;
        removeKnownAccount(getActiveAccountId());
        setError("Has sido desvinculado de este espacio por el administrador.");
        setPhase("onboarding");
        return;
      }
      // Si la nube volvió, reintenta subir lo que quedó pendiente en local y
      // actualiza el aviso de "cambios sin subir".
      try {
        await storage.flushPendingWrites();
      } catch (e) {
        // El reintento rápido se encargará en unos segundos.
      }
      // Si quedaron sin subir, se reintenta pronto (sin esperar al sondeo).
      if (storage.pendingWritesCount() > 0) schedulePendingFlush();
      // El flush es asíncrono: si mientras tanto llegó un sondeo más nuevo,
      // descartamos esta respuesta para no pisar su estado con datos viejos.
      if (seq !== refreshSeqRef.current) return;
      const pwCount = storage.pendingWritesCount();
      setPendingWrites(pwCount);
      setPendingSince(pwCount > 0 ? storage.pendingWritesSince() || Date.now() : 0);
      setSyncSource(lastSyncSource || "local");
      setDbOutdated(!!st.schemaOutdated);
      // Cambio de nombre detectado: mi nombre desapareció de la lista y entró
      // exactamente uno nuevo → soy yo, renombrado. Migro mis movimientos y
      // actualizo mi identidad local para no dejar dos personas distintas.
      const rename = detectRename(peopleRef.current, st.people);
      if (rename && rename.oldName === myNameRef.current) {
        // La identidad sigue a la lista (misma persona), y se muestra un aviso
        // para CONFIRMAR la migración de movimientos antes de tocar nada.
        setPendingRename({ oldName: rename.oldName, newName: rename.newName });
        setMyName(rename.newName);
        setPerson(rename.newName);
        try {
          await storage.set("my-name", rename.newName, false);
        } catch (e) {
          // No pasa nada: la próxima pasada lo reintenta.
        }
        let finalPeople = Array.isArray(st.people) ? [...st.people] : [];
      if (myName && myName.trim() && !finalPeople.some(p => p.toLowerCase() === myName.trim().toLowerCase())) {
        const hasMyTx = (txShown || []).some(t => t.person && t.person.trim().toLowerCase() === myName.trim().toLowerCase());
        if (hasMyTx || finalPeople.length === 0) {
          finalPeople.push(myName.trim());
          // Registrar en la base de datos para que quede persistido
          storage.savePerson(myName.trim()).catch(() => {});
        }
      }
      setPeople((prev) => (sameJson(prev, finalPeople) ? prev : finalPeople));
        setTransactions((prev) => (sameJson(prev, st.transactions) ? prev : st.transactions));
        setSettings((prev) => {
          const next = normalizeSettings(st.settings);
          return sameJson(prev, next) ? prev : next;
        });
        setHistory((prev) => (sameJson(prev, mergeHistory(st.history)) ? prev : mergeHistory(st.history)));
        lastShownRef.current = { people: st.people, transactions: st.transactions, settings: normalizeSettings(st.settings) };
        return;
      }
      // En modo esquema desactualizado la nube no devuelve lo que solo se
      // guardó aquí: trabajamos con la FUSIÓN (local + nube) para que lo
      // anotado no desaparezca de pantalla ni se anuncie como "borrado".
      const txShown = st.schemaOutdated
        ? mergeById((lastShownRef.current && lastShownRef.current.transactions) || [], st.transactions)
        : st.transactions;
      // Novedades: cambios hechos por OTRA persona desde lo último que mostramos.
      // lastShownRef se actualiza también en cada acción local, así que aquí
      // solo se anuncian los cambios que no hizo este dispositivo.
      if (lastShownRef.current && !isSwitchingSpace) {
        const lines = buildNotices(lastShownRef.current, {
          people: st.people,
          transactions: txShown,
          settings: normalizeSettings(st.settings),
        }, myName);
        const focusTxId = firstChangedTxId(lastShownRef.current, {
          people: st.people,
          transactions: txShown,
          settings: normalizeSettings(st.settings),
        });
        // Solo mostrar aviso si son cambios reales de sondeo (máximo 8 cambios puntuales)
        if (lines.length > 0 && lines.length <= 8) {
          showNotice(lines, false, focusTxId || "");
        }
      }
      lastShownRef.current = { people: st.people, transactions: txShown, settings: normalizeSettings(st.settings) };
      setPeople((prev) => (sameJson(prev, st.people) ? prev : st.people));
      setTransactions((prev) => (sameJson(prev, txShown) ? prev : txShown));
      setSettings((prev) => {
        const next = normalizeSettings(st.settings);
        return sameJson(prev, next) ? prev : next;
      });
      setHistory((prev) => (sameJson(prev, mergeHistory(st.history)) ? prev : mergeHistory(st.history)));
    } catch (e) {
      if (seq !== refreshSeqRef.current) return; // respuesta obsoleta
      if (!silent) setError("No se pudo actualizar. Inténtalo de nuevo.");
    }
  }

  // Supabase Realtime: cuando OTRA persona anota, edita o borra algo, llega un
  // evento por WebSocket y se refresca al instante. Se suscribe una sola vez al
  // entrar en la app; si Realtime no está disponible, el sondeo de abajo sigue
  // siendo el que hace el trabajo (todo sigue funcionando igual que antes).
  useEffect(() => {
    if (phase !== "app") return;
    let disposed = false;
    let realtimeChannel = null;
    if (realtimeAvailable()) {
      setRtStatus("connecting");
      subscribeRealtime((payload) => {
        if (disposed) return;
        const cur = realtimeStateRef.current;
        const table = payload && payload.table;
        const ev = payload && payload.eventType;
        const activeAcc = getActiveAccountId();
        const rowData = payload && (payload.new || payload.old);
        const rowAcc = (rowData && (rowData.account_id || rowData.accountId)) || (table === "settings" && rowData && rowData.id === 1 ? "default" : null);

        // Comprobar si el evento pertenece al espacio activo o a alguna de las cuentas conocidas
        const isCurrentSpace = !rowAcc || rowAcc === activeAcc || (activeAcc === "DEFAULT" && rowAcc === "default");
        const knownSpaces = getKnownAccounts();
        const targetKnownAcc = knownSpaces.find(a => a && (a.id === rowAcc || a.code === rowAcc));
        const isKnownSpace = Boolean(targetKnownAcc);

        if (!isCurrentSpace) {
          if (!globalNotifsEnabled || !isKnownSpace) {
            return; // No es el espacio activo y no se han activado notificaciones globales multi-cuenta
          }
        }
        // Filtro de "eco propio": Realtime reenvía a todos los dispositivos,
        // incluido el que hizo el cambio. Si el cambio YA está reflejado en
        // nuestra pantalla (lo hicimos nosotros), no hace falta refrescar.
        if (table === "transactions") {
          const row = payload.new || payload.old;
          const id = row ? String(row.id) : "";
          const mine = cur.transactions.some((t) => String(t.id) === id);
          if (ev === "INSERT" && mine) return;
          if (ev === "UPDATE" && mine && cur.transactions.some((t) => txSame(t, row))) return;
          if (ev === "DELETE" && !mine) return;
        } else if (table === "people") {
          const name = String(((payload.new || {}).name) || ((payload.old || {}).name) || "").toLowerCase();
          const mine = cur.people.some((p) => String(p).toLowerCase() === name);
          if (ev === "INSERT" && mine) return;
          if (ev === "DELETE" && !mine) return;
        }
        // Notificación del sistema INMEDIATA: con la pestaña en segundo plano
        // avisamos YA con el detalle del evento, sin esperar al refresh completo
        // (que tarda una vuelta a la nube). Las ráfagas se agrupan en UN aviso y
        // se deduplican contra el del refresh (mismo contenido hace < 5 s).
        if (table === "transactions") {
          const row = payload.new || payload.old;
          const id = row ? String(row.id) : "";
          // Silenciamiento quirúrgico: ignorar solo movimientos clonados/transferidos masivamente
          if (id && suppressedTxIdSet.has(id)) {
            return;
          }
        }
        if (Date.now() < suppressNoticesUntil && table !== "transactions") {
          return;
        }
        let rtLines = realtimeNoticeLines(payload, (cur.settings && cur.settings.currency) || "S/");
        if (!isCurrentSpace && targetKnownAcc && rtLines.length) {
          const spacePrefix = `[${targetKnownAcc.name || "Espacio"}] `;
          rtLines = rtLines.map(l => spacePrefix + l);
        }
        if (rtLines.length && notifyEnabledRef.current) {
          const buf = realtimeNoticeBufRef.current;
          rtLines.forEach((l) => {
            if (!buf.lines.includes(l)) buf.lines.push(l);
          });
          if (buf.lines.length > 4) buf.lines.length = 4; // igual que buildNotices
          // Id del movimiento al que se refiere el aviso (INSERT/UPDATE): al
          // pulsar la notificación, la app salta y resalta esa fila concreta.
          if (table === "transactions" && (ev === "INSERT" || ev === "UPDATE")) {
            const row = payload.new;
            // En una ráfaga (p. ej. un renombrado), se conserva el PRIMER id:
            // es el cambio más reciente que anunció la notificación.
            if (row && row.id != null && !buf.txId) buf.txId = String(row.id);
          }
          if (!buf.timer) {
            buf.timer = setTimeout(() => {
              buf.timer = null;
              const lines = buf.lines.splice(0);
              const focusTxId = buf.txId || "";
              buf.txId = "";
              if (!disposed && lines.length && notifyEnabledRef.current) {
                lastRealtimeNoticeSig = noticeSig(lines);
                lastRealtimeNoticeAt = Date.now();
                showSystemNotice(lines, focusTxId);
              }
            }, 300);
          }
        }
        // Debounce: una ráfaga de cambios (p. ej. renombrar muchos movimientos)
        // se convierte en UN refresco, no en uno por fila.
        if (realtimeTimerRef.current) clearTimeout(realtimeTimerRef.current);
        realtimeTimerRef.current = setTimeout(() => {
          realtimeTimerRef.current = null;
          if (disposed) return;
          const ui = uiRef.current;
          if (!ui.showForm && !ui.showSettings && !ui.saving) refreshRef.current(true);
        }, 400);
      }, (status) => {
        if (disposed) return;
        setRtStatus(status);
      }).then((ch) => {
        if (disposed && ch) unsubscribeRealtime(ch);
        else realtimeChannel = ch;
      });
    } else {
      setRtStatus("polling");
    }
    return () => {
      disposed = true;
      if (realtimeNoticeBufRef.current.timer) {
        clearTimeout(realtimeNoticeBufRef.current.timer);
        realtimeNoticeBufRef.current.timer = null;
      }
      if (realtimeTimerRef.current) {
        clearTimeout(realtimeTimerRef.current);
        realtimeTimerRef.current = null;
      }
      if (realtimeChannel) unsubscribeRealtime(realtimeChannel);
    };
  }, [phase]);

  // Sondeo automático: red de seguridad. Con Realtime activo solo hace un
  // "heartbeat" lento (los cambios reales llegan por el canal); si Realtime no
  // está disponible (backend local, Claude, sin WebSocket), vuelve al sondeo
  // clásico de 2 s (activa) / 5 s (segundo plano).
  useEffect(() => {
    if (phase !== "app") return;
    let timer = null;
    const tick = () => {
      // No sondeamos mientras haya un guardado en curso para no pisar el estado
      if (!showForm && !showSettings && !saving) refreshShared(true);
      // Con Realtime suscrito, el sondeo solo vigila (menos batería y datos);
      // sin él, mantiene el ritmo clásico de 2 s / 5 s.
      const delay = realtimeSubscribed
        ? document.hidden ? 60000 : 30000
        : document.hidden ? 5000 : 2000;
      timer = setTimeout(tick, delay);
    };
    // Al volver a la pestaña, sincroniza de inmediato sin esperar al siguiente sondeo
    const onVisible = () => {
      if (!document.hidden && !showForm && !showSettings && !saving) {
        refreshShared(true);
        schedulePendingFlush();
      }
    };
    // Al recuperar la conexión, sube enseguida lo que quedó pendiente.
    const onOnline = () => {
      if (!showForm && !showSettings && !saving) {
        refreshShared(true);
        schedulePendingFlush();
      }
    };
    document.addEventListener("visibilitychange", onVisible);
    window.addEventListener("online", onOnline);
    timer = setTimeout(tick, 2000);
    return () => {
      if (timer) clearTimeout(timer);
      document.removeEventListener("visibilitychange", onVisible);
      window.removeEventListener("online", onOnline);
    };
  }, [phase, showForm, showSettings, saving]);

  // Si el nombre seleccionado ya no está en la lista (p. ej. tras cambiar nombres
  // en Ajustes), volvemos a un valor válido para el selector. Preferimos mi propio
  // nombre si sigue en la lista, para no atribuir gastos a otra persona sin avisar.
  useEffect(() => {
    if (!people.length || !person || people.includes(person)) return;
    const preferred = people.includes(myName) ? myName : people[0];
    setPerson(preferred);
  }, [people, person, myName]);

  // ---- onboarding: restablecer la contraseña de otra persona ----
  // La cuenta es compartida y no hay email: alguien con acceso puede restablecer
  // la contraseña de otra. Primero comprobamos que existe (sin distinguir
  // mayúsculas) y pedimos CONFIRMACIÓN; solo después se guarda el nuevo hash.
  async function resetPassword() {
    const name = forgotName.trim();
    const pass = forgotPass;
    if (!name || !pass) return;
    // Mismo mínimo que en el registro: 8 caracteres. La contraseña de otra
    // persona no debe poder ser más débil que la de una cuenta nueva.
    if (pass.length < 8) {
      setError("La contraseña nueva debe tener al menos 8 caracteres.");
      return;
    }
    // Paso 1: comprobar que la persona tiene cuenta y pedir confirmación.
    if (!forgotConfirm || forgotConfirm.name !== name) {
      try {
        const user = await storage.getUser(name);
        if (!user) {
          setError("Ese nombre no tiene cuenta todavía: entra con su nombre para crearla.");
          return;
        }
        // Usamos el nombre canónico (el guardado), no el escrito, para no
        // crear "hans" y "Hans" como registros distintos.
        setForgotConfirm({ name: user.name, pass });
        setError("");
        return;
      } catch (e) {
        setError("No se pudo comprobar la cuenta. Revisa tu conexión e inténtalo de nuevo.");
        return;
      }
    }
    // Paso 2: confirmado → guardar la nueva contraseña (hash + salt nuevos).
    setSaving(true);
    setError("");
    try {
      const salt = randomSalt();
      const hash = await hashPassword(forgotConfirm.pass, salt);
      const ok = await storage.saveUser(forgotConfirm.name, salt, hash);
      if (!ok) throw new Error("fallo al guardar");
      setForgotDone("Contraseña de " + forgotConfirm.name + " restablecida. Ya pueden entrar con ella.");
      setForgotName("");
      setForgotPass("");
      setForgotConfirm(null);
    } catch (e) {
      setError("No se pudo restablecer la contraseña. Revisa tu conexión e inténtalo de nuevo.");
    } finally {
      setSaving(false);
    }
  }

  // ---- onboarding: crear cuenta (nombre + contraseña) o entrar ----
  async function joinAccount(customMode) {
    const typed = nameDraft.trim();
    if (!typed) {
      setError("Ingresa tu nombre.");
      return;
    }
    const pass = passDraft;
    if (!pass) {
      setError("Escribe tu contraseña para entrar (o para crear tu cuenta).");
      return;
    }
    setSaving(true);
    setError("");

    const effectiveMode = customMode || onboardingMode;

    // Si está en modo crear nuevo espacio independiente
    if (effectiveMode === "create") {
      const spaceName = (onboardingSpaceName || "").trim() || "Mis Finanzas";
      if (pass.length < 8) {
        setError("La contraseña debe tener al menos 8 caracteres.");
        setSaving(false);
        return;
      }
      try {
        const newAcc = await storage.createAccount(spaceName);
        setActiveAccountId(newAcc.id);
        setActiveAccountCode(newAcc.code);
        saveKnownAccount(newAcc);
        
        const salt = randomSalt();
        const hash = await hashPassword(pass, salt);
        await storage.saveUser(typed, salt, hash);
        const list = await storage.addPerson(typed);
        setPeople(list);
        await storage.set("my-name", typed, false);
        setMyName(typed);
        await refreshShared(true);
        setPhase("app");
        setSaving(false);
        return;
      } catch (err) {
        setError("No se pudo crear tu espacio. Revisa tu conexión e inténtalo de nuevo.");
        setSaving(false);
        return;
      }
    }

    // Paso 1: comprobar la cuenta (sin distinguir mayúsculas) ANTES de tocar
    // la lista compartida: si el nombre ya tiene contraseña, debe coincidir;
    // si es nuevo, se crea la cuenta. Así, un nombre con contraseña
    // equivocada no añade ni modifica nada en la cuenta de otras personas.
    let user = null;
    try {
      user = await storage.getUser(typed);
    } catch (e) {
      // No se pudo comprobar (sin red o servicio caído): NO se trata como
      // cuenta nueva, eso pisaría la contraseña de alguien que ya existe.
      setError("No se pudo comprobar tu cuenta. Revisa tu conexión e inténtalo de nuevo.");
      setSaving(false);
      return;
    }
    if (user) {
      const ok = await checkPassword(pass, user.salt, user.hash);
      if (!ok) {
        setError(
          "Contraseña incorrecta. Si no la recuerdas, pide a alguien con acceso que la cambie en Ajustes → Mi cuenta."
        );
        setSaving(false);
        return;
      }
    } else {
      // Cuenta nueva: se crea con la contraseña elegida (mínimo 8 caracteres).
      if (pass.length < 8) {
        setError("La contraseña debe tener al menos 8 caracteres.");
        setSaving(false);
        return;
      }
      try {
        const salt = randomSalt();
        const hash = await hashPassword(pass, salt);
        const ok = await storage.saveUser(typed, salt, hash);
        if (!ok) {
          setError("No se pudo crear tu cuenta. Revisa tu conexión e inténtalo de nuevo.");
          setSaving(false);
          return;
        }
      } catch (e) {
        setError("No se pudo crear tu cuenta. Revisa tu conexión e inténtalo de nuevo.");
        setSaving(false);
        return;
      }
    }

    // Paso 2: nombre canónico SIN distinguir mayúsculas. addPerson devuelve la
    // lista ya deduplicada: si escribes "hans" y existe "Hans", entras como
    // "Hans" (la misma persona), sin crear un duplicado. addPerson NUNCA borra
    // a nadie: si no se puede leer la lista compartida, aborta en lugar de
    // sobrescribirla con una lista vacía (eso fue lo que borró los nombres).
    // Guardamos la lista previa para saber si es una cuenta NUEVA: si el nombre
    // no estaba antes en la cuenta, los demás deben enterarse al instante (push)
    // además del aviso que ya llega por el sondeo.
    const prevPeople = people.slice();
    let name = typed;
    try {
      const list = await withRetry(() => storage.addPerson(typed));
      const canon = (list || []).find((n) => n.toLowerCase() === typed.toLowerCase());
      if (canon) name = canon;
      setPeople(list);
      // Mi propia entrada ya está en pantalla: no debe anunciarse como ajena.
      lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), people: list };
      // Cuenta nueva (nombre que no estaba en la cuenta): aviso push inmediato
      // a los demás dispositivos, igual que cuando se anota un movimiento.
      if (!prevPeople.some((p) => p.toLowerCase() === name.toLowerCase())) {
        notifyOthers(name, name + " se unió a la cuenta", "");
      }
    } catch (e) {
      if (e && e.message === "cuenta llena") {
        setError("Esta cuenta ya tiene " + MAX_PEOPLE + " personas y no se pueden añadir más.");
        setSaving(false);
        return;
      }
      // Fallo transitorio: seguimos con el nombre escrito; se reintentará.
    }

    // Paso 3: guardar mi nombre personal (con reintento)
    try {
      const result = await withRetry(() => storage.set("my-name", name, false));
      if (!result) throw new Error("sin resultado");
    } catch (e) {
      setError("No se pudo guardar tu nombre. Revisa tu conexión e inténtalo de nuevo.");
      setSaving(false);
      return;
    }

    // Paso 4: asegurar mi entrada en la lista compartida (si el paso 2 falló).
    try {
      const list = await withRetry(() => storage.addPerson(name));
      setPeople(list);
      // Mi propia entrada ya está en pantalla: no debe anunciarse como ajena.
      lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), people: list };
      // Si esta persona no estaba en la lista anterior de ESTE dispositivo,
      // avisamos por push (el paso 2 pudo fallar; aquí se cubre el caso).
      if (!prevPeople.some((p) => p.toLowerCase() === name.toLowerCase())) {
        notifyOthers(name, name + " se unió a la cuenta", "");
      }
      // Si este dispositivo tenía otro nombre que ya no está en la lista
      // (alguien lo cambió mientras estaba apagado), entramos como la misma
      // persona: migramos sus movimientos al nombre actual.
      const oldMyName = myNameRef.current;
      if (oldMyName && oldMyName !== name && !list.includes(oldMyName)) {
        // Entramos como la misma persona y mostramos el aviso para CONFIRMAR
        // la migración de movimientos (antes se migraba sin preguntar).
        setPendingRename({ oldName: oldMyName, newName: name });
      }
      setMyName(name);
      setPerson(name);
      setPhase("app");
    } catch (e) {
      if (e && e.message === "cuenta llena") {
        // La cuenta está llena: no entramos en la app (mi nombre no está en la lista).
        setError("Esta cuenta ya tiene " + MAX_PEOPLE + " personas y no se pueden añadir más.");
      } else {
        // Fallo transitorio: puede que ya esté en la lista, entramos y avisamos.
        setError("Tu nombre se guardó, pero no se pudo sincronizar la lista compartida. Pulsa el botón de actualizar dentro de la app.");
        setMyName(name);
        setPerson(name);
        setPhase("app");
      }
    }

    setSaving(false);
  }

  // ---- Ajustes: cambiar mi contraseña (pedir la actual + nueva + repetir) ----
  async function changeMyPassword() {
    const next = newPass.trim();
    if (!next || next.length < 8) {
      setError("La contraseña nueva debe tener al menos 8 caracteres.");
      return;
    }
    if (next !== newPass2) {
      setError("Las contraseñas no coinciden.");
      return;
    }
    if (!currentPass) {
      setError("Escribe tu contraseña actual para confirmar el cambio.");
      return;
    }
    setSaving(true);
    setError("");
    try {
      // Pedimos la contraseña ACTUAL antes de permitir el cambio: sin ella, un
      // teléfono desbloqueado en sesión bastaría para cambiar la contraseña.
      const me = await storage.getUser(myName);
      if (!me || !(await checkPassword(currentPass, me.salt, me.hash))) {
        setError("La contraseña actual no es correcta.");
        setSaving(false);
        return;
      }
      const salt = randomSalt();
      const hash = await hashPassword(next, salt);
      const ok = await storage.saveUser(myName, salt, hash);
      if (!ok) throw new Error("no se pudo guardar");
      setNewPass("");
      setNewPass2("");
      setCurrentPass("");
      showNotice(["Contraseña actualizada."], true);
    } catch (e) {
      setError("No se pudo cambiar la contraseña. Revisa tu conexión.");
    } finally {
      setSaving(false);
    }
  }

  // ---- Ajustes: salir de la cuenta en este dispositivo ----
  function logout() {
    // Al salir se retira la suscripción push de este dispositivo: si la cuenta
    // sigue usando push, no deben llegarle notificaciones a quien ya salió.
    (async () => {
      try {
        const reg = await navigator.serviceWorker.getRegistration();
        if (reg) {
          const sub = await reg.pushManager.getSubscription();
          if (sub) {
            await deletePushSubscription(sub);
            await sub.unsubscribe();
          }
        }
      } catch (e) {
        // sin push o sin permiso: no pasa nada
      }
    })();
    try {
      localStorage.removeItem("cc:my-name");
    } catch (e) {
      // sin localStorage: no pasa nada
    }
    setMyName("");
    setPerson("");
    setNameDraft("");
    setPassDraft("");
    setUserExists(null);
    setShowSettings(false);
    setPhase("onboarding");
  }


  // ---- añadir o editar transacción ----
  async function addTransaction(e) {
    e.preventDefault();
    setError("");

    const value = parseFloat(amount.replace(",", "."));
    const isCardPayment = formType === "pago_tarjeta";
    const isPrestamo = formType === "prestamo";

    const defaultCard = (settings.cards || []).find((c) => c.id === formCardId) || (settings.cards || [])[0];
    const finalDesc = desc.trim() || (
      isCardPayment
        ? `Abono a ${defaultCard ? defaultCard.name : "tarjeta"}`
        : isPrestamo
        ? `Préstamo: ${person} prestó a ${formBorrower || "otro"}`
        : category.trim()
        ? category.trim()
        : ""
    );

    if (!finalDesc) {
      setError("Falta la descripción o categoría.");
      return;
    }
    if (!amount.trim() || isNaN(value) || value <= 0) {
      setError("El importe no es válido. Escribe un número mayor que 0 (ej. 25,50).");
      return;
    }
    if (!person) {
      setError("Falta seleccionar quién lo anota.");
      return;
    }
    if (isPrestamo) {
      if (!formBorrower || formBorrower === person) {
        setError("Selecciona a quién se le prestó el dinero (debe ser una persona distinta).");
        return;
      }
    }
    if (isCardPayment && !formCardId && (settings.cards || []).length > 0) {
      setError("Selecciona a qué tarjeta deseas abonar.");
      return;
    }
    // Moneda del movimiento: la base (S/) por defecto. Si es otra, hay que
    // indicar a cuánto equivale 1 unidad en la moneda base (tipo de cambio).
    const baseCur = settings.currency || "S/";
    const cur = formCurrency.trim() || baseCur;
    const rateVal = parseFloat(String(formRate).replace(",", "."));
    if (cur !== baseCur && !(isFinite(rateVal) && rateVal > 0)) {
      setError("Falta el tipo de cambio: a cuánto equivale 1 " + cur + " en " + baseCur + " (ej. 3,70).");
      return;
    }

    const isCuotaPayment = formType === "pago_cuota" || !!installmentPayingId;
    const finalInstId = (isCuotaPayment ? (formInstallmentId || installmentPayingId || undefined) : undefined) || (editingTx ? editingTx.installmentId : undefined);

    const editing = editingTx;

    let finalCashbackAmount = undefined;
    let finalCashbackPercent = undefined;
    if (formType === "gasto" && formPaymentMethod === "tarjeta" && formCardId && formCashbackEnabled) {
      const targetCard = (settings.cards || []).find((c) => c.id === formCardId);
      if (targetCard) {
        const pct = formCashbackPercent !== "" && isFinite(Number(formCashbackPercent)) && Number(formCashbackPercent) >= 0
          ? Number(formCashbackPercent)
          : (targetCard.cashbackPercent != null ? Number(targetCard.cashbackPercent) : 1.5);
        if (pct > 0) {
          finalCashbackPercent = pct;
          finalCashbackAmount = Math.round((value * (pct / 100)) * 100) / 100;
        }
      }
    }

    const entry = {
      // En modo edición reutilizamos el MISMO id: el upsert por id actualiza
      // el movimiento sin duplicarlo, y queda un historial deshacible.
      id: editing ? editing.id : uid(),
      type: formType === "pago_cuota" ? "gasto" : formType,
      amount: value,
      description: finalDesc,
      category: category.trim() || undefined,
      note: note.trim() || undefined,
      photo: photo || undefined,
      person,
      recipient: isPrestamo ? (formBorrower || undefined) : undefined,
      isInternal: isPrestamo ? true : undefined,
      date,
      currency: cur,
      rate: cur !== baseCur && isFinite(rateVal) && rateVal > 0 ? rateVal : undefined,
      paymentMethod: isCardPayment ? (formPaymentMethod || "efectivo") : formPaymentMethod,
      cardId: (formPaymentMethod === "tarjeta" || isCardPayment) ? (formCardId || undefined) : undefined,
      installmentId: finalInstId,
      cashbackAmount: finalCashbackAmount,
      cashbackPercent: finalCashbackPercent,
    };

    setSaving(true);
    setError("");
    try {
      // Con Supabase o el backend la escritura es atómica (upsert por id);
      // en modo local cae a read-modify-write con reintentos.
      const updated = await withRetry(() => storage.addTransaction(entry));
      setTransactions(updated);
      // Mi propia acción ya está en pantalla: no debe anunciarse como ajena.
      lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updated };
      const undoEid = uid();
      if (editing) {
        // El historial guarda el estado ANTERIOR para poder deshacer la edición.
        pushHistory({ label: "Editar " + editing.description, op: "editTx", tx: editing, eid: undoEid });
      } else {
        pushHistory({ label: "Anotar " + entry.description, op: "addTx", id: entry.id, eid: undoEid });
      }
      // Avisa a los demás dispositivos con una notificación push
      const actor = myNameRef.current || person || "Alguien";
      let noticeText = "";
      if (isCardPayment) {
        noticeText = actor === person
          ? `${actor} abonó a la tarjeta: ${entry.description} (${fmtMoney(entry.amount, txCurrency(entry, baseCur))})`
          : `${actor} abonó a la tarjeta de ${person}: ${entry.description} (${fmtMoney(entry.amount, txCurrency(entry, baseCur))})`;
      } else if (isPrestamo) {
        noticeText = actor === person
          ? `${actor} prestó ${fmtMoney(entry.amount, txCurrency(entry, baseCur))} a ${formBorrower || "alguien"}`
          : `${actor} registró un préstamo de ${person} a ${formBorrower || "alguien"} (${fmtMoney(entry.amount, txCurrency(entry, baseCur))})`;
      } else if (editing) {
        noticeText = actor === (editing.person || person)
          ? `${actor} editó: ${entry.description} (${entry.type === "gasto" ? "−" : "+"}${fmtMoney(entry.amount, txCurrency(entry, baseCur))})`
          : `${actor} editó el movimiento de ${editing.person || person}: ${entry.description}`;
      } else {
        noticeText = actor === person
          ? `${actor} anotó: ${entry.description} (${entry.type === "gasto" ? "−" : "+"}${fmtMoney(entry.amount, txCurrency(entry, baseCur))})`
          : `${actor} anotó para ${person}: ${entry.description} (${entry.type === "gasto" ? "−" : "+"}${fmtMoney(entry.amount, txCurrency(entry, baseCur))})`;
      }
      notifyOthers(actor, noticeText, undoEid);

      // Si se activó la casilla de crear seguimiento en cuotas/préstamos
      if (isPrestamo && formCreateInstallment && !editing) {
        const instList = Array.isArray(settings.installments) ? settings.installments : [];
        const numCuotas = parseInt(formInstallmentCount, 10) || 1;
        const newInst = {
          id: uid(),
          name: finalDesc || `Préstamo de ${person}`,
          person: formBorrower, // Deudor (quien debe pagar)
          recipient: person, // Prestamista (a quien se le debe)
          isInternal: true,
          amount: numCuotas > 1 ? Math.round((value / numCuotas) * 100) / 100 : value,
          totalAmount: value,
          totalInstallments: numCuotas > 1 ? numCuotas : 1,
          paidInstallments: 0,
          currency: cur,
          dueDay: new Date(date).getDate() || 15,
          targetDueMonth: "auto",
          color: "gold",
        };
        const nextInstallments = [...instList, newInst];
        const nextSettings = { ...settings, installments: nextInstallments };
        try {
          await storage.set("settings", JSON.stringify(nextSettings), true);
          setSettings(nextSettings);
          lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), settings: normalizeSettings(nextSettings) };
        } catch (e) {}
      }

      if (finalInstId && !editing) {
        const instList = Array.isArray(settings.installments) ? settings.installments : [];
        const targetInst = instList.find((i) => i.id === finalInstId);
        if (targetInst) {
          const nextPaid = Math.min(targetInst.totalInstallments || 9999, (targetInst.paidInstallments || 0) + 1);
          const nextInstallments = instList.map((i) => i.id === finalInstId ? { ...i, paidInstallments: nextPaid } : i);
          const nextSettings = { ...settings, installments: nextInstallments };
          try {
            await storage.set("settings", JSON.stringify(nextSettings), true);
            setSettings(nextSettings);
            lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), settings: normalizeSettings(nextSettings) };
          } catch (e) {}
        }
      }
      if (formLoanId && !editing) {
        const origTx = transactions.find((t) => t.id === formLoanId);
        if (origTx) {
          const noteWithSettled = (origTx.note ? origTx.note + " " : "") + "[saldado]";
          const settledTx = { ...origTx, isSettled: true, note: noteWithSettled };
          try { await storage.addTransaction(settledTx); } catch (e) {}
        }
      }
      setInstallmentPayingId(null);
      setFormInstallmentId("");
      setFormLoanId("");
      setFormCashbackEnabled(true);
      setFormCashbackPercent("");
      setFormCashbackCustom(false);

      setAmount("");
      setDesc("");
      setCategory("");
      setNote("");
      setPhoto(null);
      setFormCurrency(settings.currency || "S/");
      setFormRate("");
      setFormPaymentMethod("efectivo");
      setFormCardId("");
      setFormBorrower("");
      setFormCreateInstallment(false);
      setFormInstallmentCount("1");
      setEditingTx(null);
      setPerson(myNameRef.current || (people[0] || ""));
      setShowForm(false);
      // El personaje reacciona: alegría al entrar dinero o saldar deuda, pena al gastar.
      reactMood(entry.type === "gasto" ? "sad" : "happy");
    } catch (e) {
      setError(
        editing
          ? "No se pudo guardar el cambio. Revisa tu conexión e inténtalo de nuevo."
          : "No se pudo anotar el movimiento. Revisa tu conexión e inténtalo de nuevo."
      );
    } finally {
      setSaving(false);
    }
  }

  // Salda un préstamo directo o cuota entre miembros y lo retira de deudas pendientes
  async function saldarPeerDebt(debt) {
    if (!debt) return;
    setSaving(true);
    setError("");
    const baseCur = settings.currency || "S/";
    const cur = debt.currency || baseCur;
    const amountToSettle = Number(debt.remainingAmount) || Number(debt.amount) || 0;
    const undoEid = uid();

    try {
      let nextSettings = settings;
      if (debt.sourceType === "cuota" && debt.instId) {
        const instList = Array.isArray(settings.installments) ? settings.installments : [];
        const targetInst = instList.find((i) => i.id === debt.instId);
        if (targetInst) {
          const nextPaid = targetInst.totalInstallments || (targetInst.paidInstallments || 0) + 1;
          const nextInstallments = instList.map((i) =>
            i.id === debt.instId ? { ...i, paidInstallments: nextPaid } : i
          );
          nextSettings = { ...settings, installments: nextInstallments };
          await storage.set("settings", JSON.stringify(nextSettings), true);
          setSettings(nextSettings);
        }

        const payEntry = {
          id: uid(),
          type: "gasto",
          amount: amountToSettle,
          description: `Saldado préstamo de ${debt.lender} (${debt.name})`,
          person: debt.borrower,
          recipient: debt.lender,
          date: todayISO(),
          currency: cur,
          paymentMethod: "prestamo_pago",
          installmentId: debt.instId,
        };
        const updatedTxs = await withRetry(() => storage.addTransaction(payEntry));
        setTransactions(updatedTxs);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updatedTxs, settings: normalizeSettings(nextSettings) };
        pushHistory({ label: `Saldar préstamo a ${debt.lender}`, op: "addTx", id: payEntry.id, eid: undoEid });
        const actor = myNameRef.current || debt.borrower || "Alguien";
        notifyOthers(
          actor,
          actor === debt.borrower
            ? `${actor} saldó el préstamo de ${fmtMoney(amountToSettle, cur)} a ${debt.lender}`
            : `${actor} saldó el préstamo entre ${debt.borrower} y ${debt.lender} (${fmtMoney(amountToSettle, cur)})`,
          undoEid
        );
      } else if (debt.sourceType === "movimiento" && debt.txId) {
        const origTx = transactions.find((t) => t.id === debt.txId);
        if (origTx) {
          const noteWithSettled = (origTx.note ? origTx.note + " " : "") + "[saldado]";
          const settledTx = {
            ...origTx,
            isSettled: true,
            note: noteWithSettled,
          };
          await withRetry(() => storage.addTransaction(settledTx));
        }

        const payEntry = {
          id: uid(),
          type: "gasto",
          amount: amountToSettle,
          description: `Devolución de préstamo a ${debt.lender} (${debt.name})`,
          person: debt.borrower,
          recipient: debt.lender,
          date: todayISO(),
          currency: cur,
          paymentMethod: "prestamo_pago",
          loanId: debt.txId,
        };
        const finalTxs = await withRetry(() => storage.addTransaction(payEntry));
        setTransactions(finalTxs);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: finalTxs };
        pushHistory({ label: `Saldar préstamo a ${debt.lender}`, op: "addTx", id: payEntry.id, eid: undoEid });
        const actor = myNameRef.current || debt.borrower || "Alguien";
        notifyOthers(
          actor,
          actor === debt.borrower
            ? `${actor} devolvió el préstamo de ${fmtMoney(amountToSettle, cur)} a ${debt.lender}`
            : `${actor} registró la devolución del préstamo de ${debt.borrower} a ${debt.lender} (${fmtMoney(amountToSettle, cur)})`,
          undoEid
        );
      }
      setPendingSettleDebt(null);
      reactMood("happy");
    } catch (err) {
      setError("No se pudo saldar el préstamo. Inténtalo de nuevo.");
    } finally {
      setSaving(false);
    }
  }

  // Canjea cashback acumulado aplicándolo a la deuda de la tarjeta o como ingreso a la cuenta
  async function redeemCashback(card, amount, destination) {
    if (!card || !amount || Number(amount) <= 0) return;
    setSaving(true);
    setError("");
    const cur = card.currency || settings.currency || "S/";
    const amtNum = Math.round(Number(amount) * 100) / 100;
    const undoEid = uid();

    try {
      const actor = myNameRef.current || card.person || "Alguien";
      if (destination === "tarjeta") {
        const payEntry = {
          id: uid(),
          type: "pago_tarjeta",
          amount: amtNum,
          description: `Canje de Cashback · ${card.name}`,
          person: card.person || myNameRef.current || (people[0] || "Alguien"),
          date: todayISO(),
          currency: cur,
          paymentMethod: "cashback",
          cardId: card.id,
          note: "[canje_cashback]",
        };
        const updatedTxs = await withRetry(() => storage.addTransaction(payEntry));
        setTransactions(updatedTxs);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updatedTxs };
        pushHistory({ label: `Canjear cashback en ${card.name}`, op: "addTx", id: payEntry.id, eid: undoEid });
        notifyOthers(
          actor,
          actor === card.person
            ? `${actor} canjeó ${fmtMoney(amtNum, cur)} de cashback en ${card.name}`
            : `${actor} canjeó ${fmtMoney(amtNum, cur)} de cashback en la tarjeta de ${card.person}`,
          undoEid
        );
      } else {
        const incEntry = {
          id: uid(),
          type: "ingreso",
          amount: amtNum,
          description: `Recompensa Cashback · ${card.name}`,
          person: card.person || myNameRef.current || (people[0] || "Alguien"),
          date: todayISO(),
          currency: cur,
          paymentMethod: "cashback",
          cardId: card.id,
          note: "[canje_cashback]",
        };
        const updatedTxs = await withRetry(() => storage.addTransaction(incEntry));
        setTransactions(updatedTxs);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updatedTxs };
        pushHistory({ label: `Cobrar cashback de ${card.name}`, op: "addTx", id: incEntry.id, eid: undoEid });
        notifyOthers(
          actor,
          actor === card.person
            ? `${actor} cobró ${fmtMoney(amtNum, cur)} de cashback de ${card.name}`
            : `${actor} cobró ${fmtMoney(amtNum, cur)} de cashback de la tarjeta de ${card.person}`,
          undoEid
        );
      }
      setPendingCashbackRedeem(null);
      reactMood("happy");
    } catch (e) {
      setError("No se pudo procesar el canje de cashback. Inténtalo de nuevo.");
    } finally {
      setSaving(false);
    }
  }

  // Guarda rápidamente las reglas de cashback de una tarjeta (activar, desactivar o cambiar %)
  async function saveQuickCashback(cardId, hasCb, pct, excludeServ) {
    const list = Array.isArray(settings.cards) ? settings.cards : [];
    const target = list.find((c) => c.id === cardId);
    if (!target) return;
    const numPct = hasCb ? (Number(pct) > 0 ? Number(pct) : 1.5) : undefined;
    const updatedCard = {
      ...target,
      hasCashback: !!hasCb,
      cashbackPercent: numPct,
      excludeServices: excludeServ !== false,
    };
    const nextCards = list.map((c) => (c.id === cardId ? updatedCard : c));
    const nextSettings = { ...settings, cards: nextCards };
    setSettings(nextSettings);
    lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), settings: normalizeSettings(nextSettings) };
    setQuickCashbackCard(null);
    try {
      await withRetry(() => storage.set("settings", JSON.stringify(nextSettings), true));
    } catch (e) {
      setError("No se pudo guardar la regla de cashback en la nube. Se guardó localmente.");
    }
  }

  async function deleteTransaction(id) {
    const tx = transactions.find((t) => t.id === id);
    setSaving(true);
    setError("");
    try {
      const updated = await withRetry(() => storage.deleteTransaction(id));
      setTransactions(updated);
      // Mi propia acción ya está en pantalla: no debe anunciarse como ajena.
      lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updated };
      if (tx) {
        lastDeletedTxRef.current = tx;
        const undoEid = uid();
        const actor = myNameRef.current || "Alguien";
        pushHistory({ label: "Borrar " + tx.description, op: "delTx", tx, eid: undoEid });
        const noticeText = actor === tx.person
          ? `${actor} borró: ${tx.description}`
          : `${actor} borró el movimiento de ${tx.person}: ${tx.description}`;
        notifyOthers(actor, noticeText, undoEid);
        // Borrar un gasto alegra (el saldo sube); borrar un ingreso entristece.
        reactMood(tx.type === "gasto" ? "happy" : "sad");
      }
    } catch (e) {
      setError("No se pudo borrar. Inténtalo de nuevo.");
    } finally {
      setSaving(false);
    }
  }

  // Abre el visor con la foto del recibo. La imagen se descarga bajo demanda
  // (getTxPhoto) porque el sondeo ya no trae las fotos por rendimiento.
  async function openPhoto(tx) {
    if (!tx) return;
    if (tx.photo) {
      setViewPhoto(tx.photo);
      return;
    }
    try {
      const url = await storage.getTxPhoto(tx.id);
      setViewPhoto(url || null);
    } catch (e) {
      setError("No se pudo cargar la foto del recibo. Revisa tu conexión.");
    }
  }

  // Abre el formulario en modo edición con los datos de un movimiento existente.
  function openEdit(tx) {
    setEditingTx(tx);
    setFormType(tx.type || "gasto");
    setAmount(String(tx.amount != null ? tx.amount : "").replace(".", ","));
    setDesc(tx.description || "");
    setCategory(tx.category || "");
    setNote(tx.note || "");
    setPhoto(tx.photo || null);
    setPerson(tx.person || "");
    setFormBorrower(tx.recipient || "");
    setDate(tx.date || todayISO());
    setFormPaymentMethod(tx.paymentMethod || (tx.cardId ? "tarjeta" : "efectivo"));
    setFormCardId(tx.cardId || "");
    setFormCurrency(tx.currency || settings.currency || "S/");
    setFormRate(tx.rate != null ? String(tx.rate).replace(".", ",") : "");
    setFormCashbackEnabled(tx.cashbackAmount != null ? Number(tx.cashbackAmount) > 0 : true);
    setFormCashbackPercent(tx.cashbackPercent != null ? String(tx.cashbackPercent) : "");
    setFormCashbackCustom(tx.cashbackPercent != null);
    setShowForm(true);
    setError("");
    // El sondeo ya no trae las fotos (solo hasPhoto): si el movimiento tiene
    // recibo, se carga bajo demanda para que el formulario lo muestre y, sobre
    // todo, para que al guardar la edición no se borre la foto por error
    // (photo venía undefined del estado ligero).
    if (!tx.photo && tx.hasPhoto) {
      storage
        .getTxPhoto(tx.id)
        .then((url) => {
          if (url) setPhoto(url);
        })
        .catch(() => {});
    }
  }

  // El usuario eligió una imagen: la comprimimos y la dejamos lista para
  // guardarla junto al movimiento (o quitamos la foto al editar).
  async function onPickPhoto(e) {
    const file = e.target.files && e.target.files[0];
    e.target.value = ""; // permite volver a elegir el mismo archivo
    if (!file) return;
    setPhotoBusy(true);
    setError("");
    try {
      const dataUrl = await fileToCompressedDataUrl(file);
      setPhoto(dataUrl);
    } catch (err) {
      setError((err && err.message) || "No se pudo cargar la foto.");
    } finally {
      setPhotoBusy(false);
    }
  }

  // Elige la moneda del movimiento y rellena el tipo de cambio automáticamente
  // cuando es una moneda conocida distinta de la base (1 $ = ? S/).
  function pickCurrency(next) {
    const base = settings.currency || "S/";
    if (next !== formCurrency) setFormRate("");
    setFormCurrency(next);
    const fromIso = toIsoCurrency(next);
    const toIso = toIsoCurrency(base);
    if (!next || next === base || !fromIso || !toIso || fromIso === toIso) {
      rateSeqRef.current++; // ignora respuestas en vuelo de otra moneda
      setRateLoading(false);
      return;
    }
    const seq = ++rateSeqRef.current;
    setRateLoading(true);
    fetchRate(next, base)
      .then((r) => {
        if (seq === rateSeqRef.current && r != null && isFinite(r) && r > 0) {
          setFormRate(String(Math.round(r * 10000) / 10000).replace(".", ","));
        }
      })
      .catch(() => {
        // Sin conexión o sin datos: el tipo de cambio se escribe a mano.
      })
      .finally(() => {
        if (seq === rateSeqRef.current) setRateLoading(false);
      });
  }

  // Confirma la migración de movimientos del cambio de nombre pendiente.
  // Si falla (red), el aviso se queda visible para poder reintentarlo.
  async function confirmRename() {
    if (!pendingRename) return;
    const { oldName, newName } = pendingRename;
    setSaving(true);
    setError("");
    try {
      const res = await withRetry(() => storage.renamePerson(oldName, newName));
      if (!res) throw new Error("fallo");
      if (res.changed && res.transactions) {
        setTransactions(res.transactions);
        // Mi propia migración ya está en pantalla: no debe anunciarse como ajena.
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: res.transactions };
      }
      pushHistory({
        label: "Cambio de nombre de " + oldName + " a " + newName,
        op: "rename",
        from: newName,
        to: oldName,
      });
      setPendingRename(null);
    } catch (e) {
      setError("No se pudieron migrar los movimientos. Revisa tu conexión e inténtalo de nuevo.");
    } finally {
      setSaving(false);
    }
  }

  // Añade una acción al historial COMPARTIDO (hasta 30 en la nube, 10 en
  // pantalla) para que cualquiera pueda deshacerla. Cada entrada lleva autor
  // (by) y marca de tiempo (at); se fusiona con lo que haya en la nube para
  // no pisar entradas de otros. Con reintento: si dos personas empujan a la
  // vez, la segunda re-lee y re-fusiona en vez de sobreescribir (reduce el
  // riesgo de perder una entrada por last-write-wins).
  async function pushHistory(entry) {
    // Las fotos (base64 grandes) NO se guardan en el historial compartido: cada
    // sondeo las descargaría entera. Al deshacer se restaura el movimiento sin
    // la foto del recibo.
    const clean = entry && entry.tx ? { ...entry, tx: { ...entry.tx, photo: undefined } } : entry;
    const full = normalizeHistoryEntry(clean, myNameRef.current);
    try {
      await withRetry(async () => {
        const rh = await storage.get("history", true);
        const base = rh ? safeParse(rh.value, []) : [];
        const merged = mergeHistory([full, ...(Array.isArray(base) ? base : [])], 30);
        setHistory(mergeHistory(merged, 10));
        await storage.set("history", JSON.stringify(merged), true);
      });
    } catch (e) {
      // Si la nube no está disponible, al menos lo dejamos en pantalla y lo
      // encolamos para subirlo cuando se recupere. flushPendingWrites lo
      // reconciliará por eid: si la entrada ya está en la nube (respuesta
      // perdida por timeout), el merge la deja igual y el pendiente se limpia
      // sin quedarse clavado en la cola local.
      setHistory((prev) => mergeHistory([full, ...prev]));
      if (shouldQueuePending()) enqueuePendingWrite({ op: "history", entry: full });
    }
  }

  // Pide confirmación antes de deshacer una acción del historial compartido.
  // Recibe la entrada pulsada en el panel; se re-lee el historial fresco de la
  // nube para mostrar la versión actual y no ofrecer deshacer algo ya deshecho.
  async function askUndo(entry) {
    setError("");
    if (!entry) return;
    try {
      const rh = await storage.get("history", true);
      const parsed = rh ? safeParse(rh.value, []) : [];
      const fresh = mergeHistory(parsed);
      const found = fresh.find((e) => e.eid === entry.eid);
      if (!found) {
        setHistory(fresh);
        showNotice([`«${entry.label}» ya no está en el historial (alguien la deshizo).`], true);
        return;
      }
      setPendingUndo(found);
    } catch (e) {
      // Si no se puede leer la nube, usamos la entrada que hay en pantalla;
      // confirmUndo volverá a comprobar antes de tocar nada.
      setPendingUndo(entry);
    }
  }

  // Copia el detalle de una acción del historial (quién, qué y cuándo exacto)
  // al portapapeles. Usa la API moderna y, si no existe (p. ej. navegadores
  // viejos o contexto no seguro), cae a un textarea oculto con execCommand.
  async function copyHistoryDetail(entry) {
    if (!entry) return;
    const full = dayLabel(entry.at) + " " + fmtClock(entry.at);
    const text = (entry.by || "Alguien") + ": " + (entry.label || "") + " — " + full;
    const done = () => {
      setCopiedDetail(true);
      setTimeout(() => setCopiedDetail(false), 2000);
    };
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(text);
        done();
        return;
      }
    } catch (e) {
      // caemos al fallback
    }
    // fallback a textarea oculto: si execCommand lanza, el textarea se limpia
    // igualmente (finally) para no dejar basura en el DOM.
    const ta = document.createElement("textarea");
    ta.value = text;
    ta.style.position = "fixed";
    ta.style.opacity = "0";
    document.body.appendChild(ta);
    try {
      ta.select();
      document.execCommand("copy");
      done();
    } catch (e) {
      showNotice(["No se pudo copiar el detalle en este navegador."], true);
    } finally {
      document.body.removeChild(ta);
    }
  }

  // Ejecuta el deshacer confirmado. Re-lee el historial fresco: se deshace
  // SOLO si la entrada sigue estando en el historial (evita deshacer algo que
  // otra persona ya deshizo mientras se confirmaba). Vale para CUALQUIER
  // entrada del panel, no solo la última.
  async function confirmUndo() {
    const entry = pendingUndo;
    if (!entry) return;
    setPendingUndo(null);
    setSaving(true);
    setError("");
    try {
      const rh = await storage.get("history", true);
      const parsed = rh ? safeParse(rh.value, []) : [];
      const fresh = mergeHistory(parsed);
      if (!fresh.some((e) => e.eid === entry.eid)) {
        // Conflicto: la entrada que se quería deshacer ya no está (otra persona
        // la deshizo o el historial se reinició mientras se confirmaba).
        setHistory(fresh);
        showNotice([`«${entry.label}» ya no está en el historial (alguien la deshizo). No se deshizo nada.`], true);
        return;
      }
      // 1) revertir la acción
      if (entry.op === "addTx") {
        const updated = await withRetry(() => storage.deleteTransaction(entry.id));
        setTransactions(updated);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updated };
        // El personaje reacciona al deshacer según cómo cambió el saldo: quitar
        // un gasto alegra (el saldo sube) y quitar un ingreso entristece.
        const removedTx = transactions.find((t) => t.id === entry.id);
        const delta = removedTx
          ? removedTx.type === "gasto"
            ? txToBase(removedTx, settings.currency)
            : -txToBase(removedTx, settings.currency)
          : 0;
        if (delta !== 0) reactMood(delta > 0 ? "happy" : "sad");
      } else if (entry.op === "delTx" || entry.op === "editTx") {
        // Deshacer un borrado o una edición: restauramos el movimiento (upsert por id).
        const updated = await withRetry(() => storage.addTransaction(entry.tx));
        setTransactions(updated);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updated };
        // Si era un BORRADO, dejamos constancia local de que se restauró: si la
        // restauración no llegó a la nube (sin conexión), la recuperación del
        // arranque debe re-subirlo en vez de tratarlo como borrado (su entrada
        // delTx sigue en el historial compartido de la nube). addTransaction ya
        // limpió el tombstone local (cc:deletedTx) en las tres ramas.
        if (entry.op === "delTx" && entry.tx && entry.tx.id) markTxRestored(entry.tx.id);
        // El personaje reacciona al deshacer según el cambio REAL de saldo que
        // provoca (calculado desde la entrada, sin depender del estado en
        // memoria): restaurar un gasto entristece (el saldo baja) y restaurar
        // un ingreso alegra. En una edición, el delta es la diferencia entre la
        // versión anterior (que vuelve) y la actual.
        const baseCur = settings.currency || "S/";
        let delta = 0;
        if (entry.op === "delTx") {
          delta = signedTx(entry.tx, baseCur); // el movimiento restaurado vuelve a la cuenta
        } else if (entry.op === "editTx") {
          const curTx = transactions.find((t) => t.id === entry.tx.id);
          delta = signedTx(entry.tx, baseCur) - signedTx(curTx, baseCur);
        }
        if (delta !== 0) reactMood(delta > 0 ? "happy" : "sad");
      } else if (entry.op === "assignTx") {
        // Deshacer una ASIGNACIÓN de movimientos huérfanos: solo se revierte el
        // renombrado, sin tocar la lista de personas (a diferencia de "rename",
        // que re-añade y borra nombres de la lista).
        const res = await withRetry(() => storage.renamePerson(entry.from, entry.to));
        if (!res) throw new Error("fallo");
        if (res.changed && res.transactions) {
          setTransactions(res.transactions);
          lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: res.transactions };
        }
      } else if (entry.op === "rename") {
        const res = await withRetry(() => storage.renamePerson(entry.from, entry.to));
        if (!res) throw new Error("fallo");
        if (res.changed && res.transactions) setTransactions(res.transactions);
        // Vuelvo a añadir el nombre antiguo a la lista si ya no estaba (sin borrar a nadie).
        try {
          await withRetry(() => storage.addPerson(entry.to));
        } catch (e) {
          // Si falla (p. ej. cuenta llena), la identidad y las transacciones ya volvieron.
        }
        // Si la migración deshecha era la de MI nombre, recupero mi identidad anterior.
        if (myNameRef.current === entry.from) {
          setMyName(entry.to);
          setPerson(entry.to);
          try {
            await storage.set("my-name", entry.to, false);
          } catch (e) {
            // No pasa nada: la próxima vez se reintenta.
          }
        }
        // Refresco la lista: si el nombre nuevo ya no tiene movimientos, lo quito
        // para no dejar dos personas (borrado dirigido, sin tocar a los demás).
        try {
          const stNow = await storage.getState();
          let finalPeople = stNow.people;
          if (!stNow.transactions.some((t) => t.person === entry.from)) {
            const peopleAfter = await storage.deletePerson(entry.from);
            if (Array.isArray(peopleAfter)) finalPeople = peopleAfter;
          }
          setPeople(finalPeople);
          setSettings(normalizeSettings(stNow.settings));
          // Mi propio deshacer ya está en pantalla: no debe anunciarse como ajeno.
          lastShownRef.current = { people: finalPeople, transactions: stNow.transactions, settings: normalizeSettings(stNow.settings) };
        } catch (e) {
          // Si falla el refresco, el siguiente sondeo (5 s) actualiza la lista.
        }
      }
      // 2) quitar la entrada deshecha del historial compartido
      const rest = fresh.filter((e) => e.eid !== entry.eid);
      setHistory(rest);
      await storage.set("history", JSON.stringify(rest), true);
    } catch (e) {
      setError("No se pudo deshacer. Revisa tu conexión e inténtalo de nuevo.");
    } finally {
      setSaving(false);
    }
  }

  // Cambia de espacio financiero activo y recarga el estado
  async function handleSwitchSpace(targetCodeOrId) {
    if (!targetCodeOrId) return;
    suppressNoticesUntil = Date.now() + 15000;
    setSaving(true);
    setNotice(null);
    lastShownRef.current = null;
    setSpaceFeedback({ type: "info", text: "Cargando espacio..." });
    try {
      const acc = await storage.findAccount(targetCodeOrId);
      if (!acc) {
        setSpaceFeedback({ type: "error", text: "No se encontró ningún espacio con ese código o ID." });
        setSaving(false);
        return;
      }
      setActiveAccountId(acc.id);
      setActiveAccountCode(acc.code || acc.id);
      saveKnownAccount(acc);
      setShowSpaceModal(false);
      setSpaceFeedback(null);
      // Recarga el estado del nuevo espacio con flag de cambio de espacio
      await refreshShared(true, true);
      setSpaceFeedback({ type: "success", text: `¡Conectado a ${acc.name || "la cuenta"}!` });
      setTimeout(() => setSpaceFeedback(null), 3000);
    } catch (err) {
      setSpaceFeedback({ type: "error", text: "No se pudo cambiar de espacio. Revisa tu conexión." });
    } finally {
      setSaving(false);
    }
  }

  // Genera una invitación con límites personalizados (1 persona, límite de personas o tiempo)
  async function handleGenerateCustomInvite() {
    const invId = "inv_" + uid();
    const invCode = "INV-" + uid().slice(0, 5).toUpperCase();
    let expiresAt = null;
    if (inviteLimitType === "time") {
      expiresAt = Date.now() + (Number(inviteExpireHours) || 24) * 60 * 60 * 1000;
    } else if (inviteLimitType === "single" || inviteLimitType === "count") {
      // Expiración por defecto 7 días para invitaciones contadas
      expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000;
    }

    const maxUses = inviteLimitType === "single" ? 1 : inviteLimitType === "count" ? Number(inviteMaxCount) || 2 : null;

    const newInv = {
      id: invId,
      code: invCode,
      type: inviteLimitType,
      maxUses,
      usedCount: 0,
      usedBy: [],
      expiresAt,
      createdAt: Date.now(),
      createdBy: myName || "Administrador",
      active: true,
    };

    const nextInvs = [newInv, ...(Array.isArray(settings.invitations) ? settings.invitations : [])];
    const updatedSettings = { ...settings, invitations: nextInvs };
    setSettings(updatedSettings);
    setCurrentInviteToken(newInv);
    try {
      await storage.set("settings", JSON.stringify(updatedSettings), true);
    } catch (e) {}
  }

  async function handleRevokeInvite(invId) {
    const nextInvs = (settings.invitations || []).map(i => i.id === invId ? { ...i, active: false } : i);
    const updatedSettings = { ...settings, invitations: nextInvs };
    setSettings(updatedSettings);
    if (currentInviteToken && currentInviteToken.id === invId) setCurrentInviteToken(null);
    try {
      await storage.set("settings", JSON.stringify(updatedSettings), true);
    } catch (e) {}
  }

  // Crea un nuevo espacio independiente
  async function handleCreateSpace() {
    const name = (newSpaceName || "").trim();
    if (!name) {
      setSpaceFeedback({ type: "error", text: "Escribe un nombre para el nuevo espacio." });
      return;
    }
    setSaving(true);
    setSpaceFeedback({ type: "info", text: "Creando espacio..." });
    try {
      const newAcc = await storage.createAccount(name);
      setActiveAccountId(newAcc.id);
      setActiveAccountCode(newAcc.code);
      saveKnownAccount(newAcc);
      setNewSpaceName("");
      setShowSpaceModal(false);
      setSpaceFeedback(null);
      await refreshShared(true);
      setShowInviteModal(true); // Mostrar modal de invitación del nuevo espacio
    } catch (err) {
      setSpaceFeedback({ type: "error", text: "No se pudo crear el espacio. Inténtalo de nuevo." });
    } finally {
      setSaving(false);
    }
  }

  // Se une a un espacio con código
  async function handleJoinSpace() {
    const code = (joinCodeDraft || "").trim().toUpperCase();
    if (!code) {
      setSpaceFeedback({ type: "error", text: "Ingresa el código de invitación (ej. FAM-892)." });
      return;
    }
    await handleSwitchSpace(code);
  }

  async function openSettings() {
    // Refrescamos la lista compartida ANTES de abrir el editor de personas:
    // así nunca guardamos una lista vacía (que borraría a los demás) si la
    // carga inicial falló o quedó desactualizada.
    setShowSettings(true);
    setError("");
    let localKey = "";
    try { localKey = localStorage.getItem("cc:local_gemini_api_key") || ""; } catch (e) {}
    try {
      const st = await storage.getState();
      setPeople((prev) => (sameJson(prev, st.people) ? prev : st.people));
      // Este refresco ya muestra la lista compartida: no debe re-anunciarse
      // como novedad en el siguiente sondeo.
      lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), people: st.people };
      const list = st.people && st.people.length ? st.people : [myName || ""];
      setEditPeople(list.slice());
      setEditSplitEnabled(st.settings.splitMode !== "sin");
      setEditCurrency(st.settings.currency || "S/");
      const loadedCats = Array.isArray(st.settings.categories) && st.settings.categories.length > 0 ? st.settings.categories : DEFAULT_CATEGORIES;
      setEditCategoryList(loadedCats.slice());
      setEditCategories(loadedCats.join(", "));
      setNewCategoryDraft("");
      setEditCards(Array.isArray(st.settings.cards) ? st.settings.cards.slice() : []);
      setEditInstallments(Array.isArray(st.settings.installments) ? st.settings.installments.slice() : []);
      setEditAccountName(st.settings.accountName || "Cuenta compartida");
      setEditMascot(st.settings.mascot || "coin");
      if (localKey) {
        setEditGeminiApiKey(localKey);
        setEditGeminiIsPrivate(true);
      } else {
        setEditGeminiApiKey(st.settings.geminiApiKey || "");
        setEditGeminiIsPrivate(false);
      }
      setShowEditGeminiKeyField(false);
    } catch (e) {
      // Si no se pudo leer, abrimos con lo que tengamos en memoria.
      setEditPeople(people.length ? people.slice() : [myName || ""]);
      setEditSplitEnabled(settings.splitMode !== "sin");
      setEditCurrency(settings.currency);
      const curCats = Array.isArray(settings.categories) && settings.categories.length > 0 ? settings.categories : DEFAULT_CATEGORIES;
      setEditCategoryList(curCats.slice());
      setEditCategories(curCats.join(", "));
      setNewCategoryDraft("");
      setEditCards(Array.isArray(settings.cards) ? settings.cards.slice() : []);
      setEditInstallments(Array.isArray(settings.installments) ? settings.installments.slice() : []);
      setEditAccountName(settings.accountName || "Cuenta compartida");
      setEditMascot(settings.mascot || "coin");
      if (localKey) {
        setEditGeminiApiKey(localKey);
        setEditGeminiIsPrivate(true);
      } else {
        setEditGeminiApiKey(settings.geminiApiKey || "");
        setEditGeminiIsPrivate(false);
      }
      setShowEditGeminiKeyField(false);
    }
  }

  // Botón "Revisar duplicados y huérfanos" de Ajustes: cierra Ajustes, abre
  // AMBOS paneles (duplicados y movimientos huérfanos) con TODOS los grupos
  // (borra las omisiones de este dispositivo para que no queden ocultos) y
  // baja hasta el primer aviso para revisarlos.
  function openDupReview() {
    setShowSettings(false);
    try {
      localStorage.removeItem("cc:dupDismissed");
    } catch (e) {
      /* sin localStorage: no pasa nada */
    }
    try {
      localStorage.removeItem("cc:orphanDismissed");
    } catch (e) {
      /* sin localStorage: no pasa nada */
    }
    setDupDismissed([]);
    setOrphanDismissed([]);
    setShowDupPanel(true);
    setShowOrphanPanel(true);
    // Los avisos se pintan al re-renderizar: bajamos tras el render (dos
    // frames por si el panel tarda en montarse). El de duplicados está justo
    // encima del de huérfanos; si no hay duplicados, se baja al de huérfanos.
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        const el = document.getElementById("cc-dup-banner") || document.getElementById("cc-orphan-banner");
        if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
      });
    });
  }

  async function saveSettings() {
    // Sin duplicados: evita personas repetidas en la lista (y que un duplicado
    // accidental se confunda con un cambio de nombre al detectar renames).
    const cleanedPeople = [...new Set(editPeople.map((p) => p.trim()).filter(Boolean))];
    if (cleanedPeople.length === 0) {
      setError("Debe haber al menos una persona.");
      return;
    }
    if (cleanedPeople.length > MAX_PEOPLE) {
      setError("Como máximo pueden ser " + MAX_PEOPLE + " personas.");
      return;
    }
    const splitMode = editSplitEnabled ? "igual" : "sin";

    if (editGeminiIsPrivate) {
      try {
        if (editGeminiApiKey.trim()) {
          localStorage.setItem("cc:local_gemini_api_key", editGeminiApiKey.trim());
        } else {
          localStorage.removeItem("cc:local_gemini_api_key");
        }
      } catch (e) {}
    } else {
      try { localStorage.removeItem("cc:local_gemini_api_key"); } catch (e) {}
    }

    const finalCats = Array.isArray(editCategoryList) && editCategoryList.length > 0
      ? editCategoryList
      : (editCategories.split(",").map((c) => c.trim()).filter(Boolean));

    const nextSettings = {
      splitEnabled: editSplitEnabled,
      currency: editCurrency.trim() || "S/",
      splitMode,
      categories: finalCats.length > 0 ? finalCats : DEFAULT_CATEGORIES,
      cards: Array.isArray(editCards) ? editCards : [],
      installments: Array.isArray(editInstallments) ? editInstallments : [],
      accountName: editAccountName.trim() || "Cuenta compartida",
      mascot: MASCOT_KINDS.some((k) => k.id === editMascot) ? editMascot : "coin",
      geminiApiKey: !editGeminiIsPrivate ? (editGeminiApiKey.trim() || undefined) : undefined,
    };
    // Cambio de nombre: si MI nombre desaparece de la lista y entra exactamente
    // uno nuevo, soy yo renombrándome. Actualizo mi identidad en este
    // dispositivo y muestro el aviso para CONFIRMAR la migración de movimientos.
    const rename = detectRename(people, cleanedPeople);
    const isSelfRename = rename && rename.oldName === myName;
    setSaving(true);
    setError("");
    try {
      await withRetry(async () => {
        if (isSelfRename) {
          await storage.set("my-name", rename.newName, false);
          // La contraseña sigue a la persona al cambiar el nombre: movemos el
          // registro de usuario del nombre antiguo al nuevo.
          try {
            const u = await storage.getUser(rename.oldName);
            if (u) {
              await storage.saveUser(rename.newName, u.salt, u.hash);
              await storage.removeUser(rename.oldName);
            }
          } catch (e) {
            // No pasa nada: el usuario podrá poner su contraseña al entrar.
          }
        }
        await Promise.all([
          storage.set("people", JSON.stringify(cleanedPeople), true),
          storage.set("settings", JSON.stringify(nextSettings), true),
        ]);
        // Bajas dirigidas: cada persona que se quitó de la lista se elimina de la
        // nube UNA A UNA (removePerson), nunca en masa, para no borrar a nadie
        // que entrara a la vez desde otro dispositivo.
        const removed = people.filter((n) => !cleanedPeople.includes(n));
        for (const n of removed) {
          try {
            await storage.removePerson(n);
          } catch (e) {
            // Se reintentará en el siguiente sondeo de la lista.
          }
        }
      });
      setPeople(cleanedPeople);
      setSettings(nextSettings);
      // Mis propios ajustes ya están en pantalla: no deben anunciarse como ajenos.
      lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), people: cleanedPeople, settings: nextSettings };
      if (isSelfRename) {
        setMyName(rename.newName);
        setPerson(rename.newName);
        setPendingRename({ oldName: rename.oldName, newName: rename.newName });
      }
      setShowSettings(false);
    } catch (e) {
      setError("No se pudieron guardar los ajustes. Inténtalo de nuevo.");
    } finally {
      setSaving(false);
    }
  }

  // ---- Tarjetas de crédito y deuda por individuo ----
  const cardStats = useMemo(() => {
    const base = settings.currency || "S/";
    const cardsList = Array.isArray(settings.cards) ? settings.cards : [];

    const stats = cardsList.map((card) => {
      const cardCurr = card.currency || base;
      const cardCharges = transactions.filter((t) => t.cardId === card.id && t.type === "gasto");
      const cardPayments = transactions.filter((t) => t.cardId === card.id && t.type === "pago_tarjeta");
      const totalCharges = cardCharges.reduce((s, t) => {
        const cur = t.currency || base;
        if (cur === cardCurr) return s + (Number(t.amount) || 0);
        return s + txToBase(t, cardCurr);
      }, 0);
      const totalPayments = cardPayments.reduce((s, t) => {
        const cur = t.currency || base;
        if (cur === cardCurr) return s + (Number(t.amount) || 0);
        return s + txToBase(t, cardCurr);
      }, 0);
      const isDebit = card.cardType === "debito";
      const currentDebt = isDebit ? 0 : Math.max(0, totalCharges - totalPayments);
      const cycleInfo = isDebit ? null : getCardCycleInfo(card);
      const available = !isDebit && card.creditLimit ? Math.max(0, card.creditLimit - currentDebt) : null;

      // ---- Gastos de este mes con la tarjeta (especialmente útil para débito) ----
      const nowD = new Date();
      const mesKey = nowD.getFullYear() + "-" + String(nowD.getMonth() + 1).padStart(2, "0");
      const currentMonthSpent = cardCharges
        .filter((t) => String(t.date || "").startsWith(mesKey))
        .reduce((s, t) => {
          const cur = t.currency || base;
          if (cur === cardCurr) return s + (Number(t.amount) || 0);
          return s + txToBase(t, cardCurr);
        }, 0);

      // ---- Cashback de la tarjeta ----
      const totalCashbackAccumulated = cardCharges.reduce((s, t) => s + (Number(t.cashbackAmount) || 0), 0);
      const currentMonthCashback = cardCharges
        .filter((t) => String(t.date || "").startsWith(mesKey))
        .reduce((s, t) => s + (Number(t.cashbackAmount) || 0), 0);
      const redeemedCashback = transactions
        .filter((t) => t.cardId === card.id && (t.paymentMethod === "cashback" || (t.note && t.note.includes("[canje_cashback]"))))
        .reduce((s, t) => s + (Number(t.amount) || 0), 0);
      const availableCashback = Math.max(0, Math.round((totalCashbackAccumulated - redeemedCashback) * 100) / 100);
      const hasCashback = card.hasCashback === true || (card.cashbackPercent != null && Number(card.cashbackPercent) > 0) || totalCashbackAccumulated > 0;

      return {
        card,
        isDebit,
        currency: cardCurr,
        totalCharges,
        totalPayments,
        currentDebt,
        currentMonthSpent,
        cycleInfo,
        available,
        hasCashback,
        totalCashbackAccumulated,
        currentMonthCashback,
        redeemedCashback,
        availableCashback,
      };
    });

    // Agrupado por persona
    const byPersonMap = new Map();
    (people || []).forEach((p) => byPersonMap.set(p, []));
    stats.forEach((st) => {
      const p = st.card.person || "Sin titular";
      if (!byPersonMap.has(p)) byPersonMap.set(p, []);
      byPersonMap.get(p).push(st);
    });

    const totalDeuda = stats.reduce((s, st) => {
      if (st.currency === base) return s + st.currentDebt;
      return s + txToBase({ amount: st.currentDebt, currency: st.currency }, base);
    }, 0);

    const totalCashbackAccumulatedAll = stats.reduce((s, st) => {
      if (st.currency === base) return s + st.totalCashbackAccumulated;
      return s + txToBase({ amount: st.totalCashbackAccumulated, currency: st.currency }, base);
    }, 0);

    const totalCashbackAvailableAll = stats.reduce((s, st) => {
      if (st.currency === base) return s + st.availableCashback;
      return s + txToBase({ amount: st.availableCashback, currency: st.currency }, base);
    }, 0);

    const hasAnyCashback = stats.some((s) => s.hasCashback || s.totalCashbackAccumulated > 0);

    return {
      list: stats,
      byPerson: Array.from(byPersonMap.entries()).map(([pName, cards]) => ({
        person: pName,
        cards,
        totalDebt: cards.reduce((s, c) => {
          if (c.currency === base) return s + c.currentDebt;
          return s + txToBase({ amount: c.currentDebt, currency: c.currency }, base);
        }, 0),
        totalCashbackAvailable: cards.reduce((s, c) => {
          if (c.currency === base) return s + c.availableCashback;
          return s + txToBase({ amount: c.availableCashback, currency: c.currency }, base);
        }, 0),
      })),
      totalDeuda,
      totalCashbackAccumulatedAll,
      totalCashbackAvailableAll,
      hasAnyCashback,
    };
  }, [settings.cards, transactions, people, settings.currency]);

  // ---- Cuotas y Préstamos ----
  const installmentStats = useMemo(() => {
    const base = settings.currency || "S/";
    const list = Array.isArray(settings.installments) ? settings.installments : [];

    const stats = list.map((inst) => {
      const instCurr = inst.currency || base;
      const cycleInfo = getInstallmentCycleInfo(inst);
      const total = inst.totalInstallments || 0;
      const paid = inst.paidInstallments || 0;
      const totalDebt = inst.totalAmount && inst.totalAmount > 0
        ? inst.totalAmount
        : (total > 0 && (inst.amount || 0) > 0 ? total * inst.amount : (inst.amount || 0));
      const paidAmount = total > 0 && totalDebt > 0
        ? Math.min(totalDebt, (inst.amount > 0 ? inst.amount * paid : (totalDebt / total) * paid))
        : (inst.amount || 0) * paid;
      const remainingAmount = Math.max(0, totalDebt - paidAmount);
      const remainingInstallments = total > 0 ? Math.max(0, total - paid) : null;
      const progressPercent = totalDebt > 0
        ? Math.min(100, Math.round((paidAmount / totalDebt) * 100))
        : (total > 0 ? Math.min(100, Math.round((paid / total) * 100)) : 0);
      const isCompleted = (total > 0 && paid >= total) || (totalDebt > 0 && remainingAmount <= 0 && paid > 0);

      return {
        inst,
        currency: instCurr,
        cycleInfo,
        total,
        paid,
        totalDebt,
        paidAmount,
        remainingInstallments,
        progressPercent,
        remainingAmount,
        isCompleted,
      };
    });

    const activeList = stats.filter((s) => !s.isCompleted);
    const completedList = stats.filter((s) => s.isCompleted);

    // Agrupado por persona
    const byPersonMap = new Map();
    (people || []).forEach((p) => byPersonMap.set(p, []));
    stats.forEach((st) => {
      const p = (st.inst.person || "").trim() || "Sin titular";
      if (!byPersonMap.has(p)) byPersonMap.set(p, []);
      byPersonMap.get(p).push(st);
    });

    return {
      list: stats,
      activeList,
      completedList,
      byPerson: Array.from(byPersonMap.entries()).map(([person, items]) => {
        const activeItems = items.filter((i) => !i.isCompleted);
        return {
          person,
          items,
          activeItems,
          totalMonthly: activeItems.reduce((s, i) => {
            if (i.currency === base) return s + (i.inst.amount || 0);
            return s + txToBase({ amount: i.inst.amount, currency: i.currency }, base);
          }, 0),
          totalRemainingDebt: activeItems.reduce((s, i) => {
            const amt = Number(i.remainingAmount) || 0;
            if (i.currency === base) return s + amt;
            return s + txToBase({ amount: amt, currency: i.currency }, base);
          }, 0),
        };
      }),
      totalMonthlyAll: activeList.reduce((s, i) => {
        if (i.currency === base) return s + (i.inst.amount || 0);
        return s + txToBase({ amount: i.inst.amount, currency: i.currency }, base);
      }, 0),
      totalRemainingDebtAll: activeList.reduce((s, i) => {
        const amt = Number(i.remainingAmount) || 0;
        if (i.currency === base) return s + amt;
        return s + txToBase({ amount: amt, currency: i.currency }, base);
      }, 0),
    };
  }, [settings.installments, settings.currency, people]);

  // ---- Préstamos y Deudas entre nosotros (deudas directas entre miembros) ----
  const peerDebts = useMemo(() => {
    const base = settings.currency || "S/";
    const list = [];
    const completedList = [];

    // 1. Préstamos en cuotas entre miembros (isInternal o recipient que es miembro)
    (installmentStats.list || []).forEach((st) => {
      const inst = st.inst;
      const borrower = (inst.person || "").trim();
      const lender = (inst.recipient || "").trim();
      const isMemberLender = people.some((p) => String(p || "").toLowerCase() === String(lender || "").toLowerCase());
      if (borrower && lender && (inst.isInternal || isMemberLender) && String(borrower).toLowerCase() !== String(lender).toLowerCase()) {
        const item = {
          id: "inst-" + inst.id,
          sourceType: "cuota",
          instId: inst.id,
          name: inst.name,
          borrower,
          lender,
          amount: inst.amount,
          remainingAmount: st.remainingAmount,
          totalDebt: st.totalDebt,
          paidAmount: st.paidAmount,
          currency: st.currency,
          totalInstallments: st.total,
          paidInstallments: st.paid,
          cycleInfo: st.cycleInfo,
          notes: inst.notes,
          color: inst.color,
          isCompleted: st.isCompleted,
        };
        if (st.isCompleted) {
          completedList.push(item);
        } else {
          list.push(item);
        }
      }
    });

    // 2. Transacciones directas de préstamo entre nosotros que no tienen installment vinculado
    const directLoans = transactions.filter((t) =>
      (t.type === "prestamo" || t.paymentMethod === "prestamo" || (t.recipient && t.recipient !== t.person) || (t.description && /prestó a/i.test(t.description))) &&
      !t.installmentId
    );

    // Devoluciones y pagos de préstamos directos
    const repayments = transactions.filter((t) =>
      t.type === "pago_prestamo" ||
      t.paymentMethod === "prestamo_pago" ||
      (t.loanId && (t.type === "gasto" || t.type === "pago_prestamo" || t.type === "ingreso")) ||
      (t.description && /Devolución de préstamo|Saldó préstamo|Saldado préstamo/i.test(t.description))
    );

    directLoans.forEach((t) => {
      let borrower = (t.recipient || "").trim();
      let lender = (t.person || "").trim();
      if (!borrower && t.description && /prestó a\s+([^·\(\)]+)/i.test(t.description)) {
        const match = t.description.match(/prestó a\s+([^·\(\)]+)/i);
        if (match && match[1]) borrower = match[1].trim();
      }
      if (borrower && lender && borrower.toLowerCase() !== lender.toLowerCase()) {
        const cur = t.currency || base;
        const amt = Number(t.amount) || 0;
        
        // Comprobar si está marcado como saldado
        const isExplicitSettled = t.isSettled === true || (t.note && /\[saldado\]/i.test(t.note));

        // Sumar abonos/devoluciones asociados a este préstamo
        const directRepayments = repayments.filter((r) => {
          if (r.loanId && r.loanId === t.id) return true;
          if (r.description && r.description.includes(lender) && (r.person === borrower || r.description.includes(borrower))) return true;
          return false;
        });

        const paidAmount = isExplicitSettled ? amt : directRepayments.reduce((s, r) => s + (Number(r.amount) || 0), 0);
        const remainingAmount = isExplicitSettled ? 0 : Math.max(0, amt - paidAmount);
        const isCompleted = isExplicitSettled || remainingAmount <= 0;

        const item = {
          id: "tx-" + t.id,
          sourceType: "movimiento",
          txId: t.id,
          name: t.description || "Préstamo directo",
          borrower,
          lender,
          amount: amt,
          remainingAmount,
          totalDebt: amt,
          paidAmount,
          currency: cur,
          date: t.date,
          note: t.note,
          isCompleted,
        };

        if (isCompleted) {
          completedList.push(item);
        } else {
          list.push(item);
        }
      }
    });

    const totalPeerDebt = list.reduce((s, d) => {
      const amt = Number(d.remainingAmount) || 0;
      if (d.currency === base) return s + amt;
      return s + txToBase({ amount: amt, currency: d.currency }, base);
    }, 0);

    return {
      list,
      completedList,
      totalPeerDebt,
      hasDebts: list.length > 0,
    };
  }, [installmentStats, transactions, people, settings.currency]);

  // Helper para identificar préstamos entre personas y sus devoluciones
  const isPeerLoanTx = (t) => t && (
    t.type === "prestamo" ||
    t.paymentMethod === "prestamo" ||
    t.paymentMethod === "prestamo_pago" ||
    (t.recipient && t.recipient !== t.person) ||
    (t.description && /prestó a|Devolución de préstamo|Saldó préstamo|Saldado préstamo/i.test(t.description))
  );

  // ---- cálculos ----
  const totals = useMemo(() => {
    const base = settings.currency || "S/";
    const val = (t) => txToBase(t, base);

    // Ingresos totales
    const totalIngresos = transactions.filter((t) => t.type === "ingreso").reduce((s, t) => s + val(t), 0);

    // Gastos pagados en efectivo / cuenta / transferencia (dinero líquido que ya salió del grupo)
    // EXCLUYE préstamos entre miembros para no inflar los gastos del grupo ni penalizar a quien prestó
    const gastosEfectivo = transactions
      .filter((t) => t.type === "gasto" && t.paymentMethod !== "tarjeta" && !t.cardId && !isPeerLoanTx(t))
      .reduce((s, t) => s + val(t), 0);

    // Gastos cargados a tarjeta de crédito (deuda que no resta efectivo de inmediato)
    const gastosTarjeta = transactions
      .filter((t) => t.type === "gasto" && (t.paymentMethod === "tarjeta" || !!t.cardId) && !isPeerLoanTx(t))
      .reduce((s, t) => s + val(t), 0);

    // Todos los gastos reales registrados en la cuenta (efectivo + tarjeta)
    const totalGastos = gastosEfectivo + gastosTarjeta;

    // Pagos o abonos realizados a tarjetas de crédito que salieron de efectivo / cuentas bancarias
    const totalPagosTarjetaEfectivo = transactions
      .filter((t) => t.type === "pago_tarjeta" && t.paymentMethod !== "cashback" && !t.note?.includes("[canje_cashback]"))
      .reduce((s, t) => s + val(t), 0);

    // Pagos o abonos totales a tarjetas de crédito (incluye canjes de cashback)
    const totalPagosTarjeta = transactions
      .filter((t) => t.type === "pago_tarjeta")
      .reduce((s, t) => s + val(t), 0);

    // Saldo disponible real en efectivo / cuentas ("dinero normal"):
    // Entra: ingresos
    // Sale: gastos en efectivo y abonos/pagos de tarjeta con dinero real
    const saldoEfectivo = totalIngresos - gastosEfectivo - totalPagosTarjetaEfectivo;

    // Deuda neta actual en tarjetas (cargos con tarjeta menos abonos totales)
    const deudaTarjetas = Math.max(0, gastosTarjeta - totalPagosTarjeta);

    // Deuda pendiente en cuotas y préstamos activos
    const deudaCuotas = installmentStats.totalRemainingDebtAll || 0;

    // Deuda general total (tarjetas + cuotas)
    const deudaTotal = deudaTarjetas + deudaCuotas;

    // Balance neto / Patrimonio total: (Efectivo disponible) - (Deuda general total)
    const balanceNeto = saldoEfectivo - deudaTotal;

    const porPersonaGasto = {};
    people.forEach((p) => (porPersonaGasto[p] = 0));
    transactions
      .filter((t) => t.type === "gasto" && !isPeerLoanTx(t))
      .forEach((t) => {
        porPersonaGasto[t.person] = (porPersonaGasto[t.person] || 0) + val(t);
      });

    return {
      totalGastos,
      gastosEfectivo,
      gastosTarjeta,
      totalIngresos,
      totalPagosTarjeta,
      saldoEfectivo,
      deudaTarjetas,
      deudaCuotas,
      deudaTotal,
      balanceNeto,
      porPersonaGasto,
    };
  }, [transactions, people, settings.currency, installmentStats]);

  // Ánimo del personaje en reposo: el saldo decide entre CINCO niveles
  // (muy contento / contento / neutro / triste / abatido). Los extremos usan
  // un umbral fijo de ±100 en la moneda base (simplificación juguetona). El
  // burst se lo lleva al extremo dentro del componente Mascot (joy al
  // alegrarse, miserable al entristecerse).
  const mascotMood =
    totals.balanceNeto >= 100 ? "joy"
    : totals.balanceNeto > 0.005 ? "happy"
    : totals.balanceNeto < -100 ? "miserable"
    : totals.balanceNeto < -0.005 ? "sad"
    : "neutral";

  // Desglose por moneda: lo gastado/ingresado en cada moneda SIN convertir
  // (antes de pasarlo a la moneda base). Útil cuando hay gastos en $, €, etc.
  const byCurrency = useMemo(() => {
    const base = settings.currency || "S/";
    const map = new Map();
    transactions.forEach((t) => {
      const cur = txCurrency(t, base);
      const amount = Number(t && t.amount) || 0;
      if (!map.has(cur)) map.set(cur, { currency: cur, ingresos: 0, gastos: 0 });
      const row = map.get(cur);
      if (t.type === "ingreso") row.ingresos += amount;
      else if (!isPeerLoanTx(t)) row.gastos += amount;
    });
    return Array.from(map.values()).sort((a, b) => b.gastos + b.ingresos - (a.gastos + a.ingresos));
  }, [transactions, settings.currency]);

  // Desglose por persona: cada usuario con sus ingresos, gastos y saldo neto.
  // El saldo principal refleja el dinero en efectivo disponible de cada uno.
  const porPersona = useMemo(() => {
    const map = new Map();
    // Solo las personas de la lista crean filas. Los movimientos huérfanos
    // (persona que ya no está en la lista) se muestran únicamente en la fila
    // "Sin persona asignada", para no duplicarlos en el desglose.
    const inPeople = new Set((people || []).map((p) => String(p || "").trim().toLowerCase()));
    const nombres = [...(people || [])];
    nombres.forEach((n) =>
      map.set(n, {
        ingresos: 0,
        gastosEfectivo: 0,
        gastosTarjeta: 0,
        pagosTarjeta: 0,
        monedas: new Map(),
      })
    );
    const base = settings.currency || "S/";
    transactions.forEach((t) => {
      const rawPerson = t.person ? String(t.person).trim() : "";
      // Huérfano: tiene nombre pero no está en la lista → solo en la fila especial
      if (rawPerson && !inPeople.has(rawPerson.toLowerCase())) return;
      const nombre = rawPerson || "Alguien";
      if (!map.has(nombre))
        map.set(nombre, {
          ingresos: 0,
          gastosEfectivo: 0,
          gastosTarjeta: 0,
          pagosTarjeta: 0,
          monedas: new Map(),
        });
      const fila = map.get(nombre);
      const v = txToBase(t, base);
      const amount = Number(t && t.amount) || 0;
      const cur = txCurrency(t, base);
      // Desglose por moneda (sin convertir): lo que pagó/cobró en cada moneda.
      if (!fila.monedas.has(cur)) fila.monedas.set(cur, { currency: cur, ingresos: 0, gastos: 0 });
      const mon = fila.monedas.get(cur);
      if (t.type === "ingreso") {
        fila.ingresos += v;
        mon.ingresos += amount;
      } else if (t.type === "gasto" && !isPeerLoanTx(t)) {
        if (t.paymentMethod === "tarjeta" || t.cardId) {
          fila.gastosTarjeta += v;
        } else {
          fila.gastosEfectivo += v;
        }
        mon.gastos += amount;
      } else if (t.type === "pago_tarjeta") {
        fila.pagosTarjeta += v;
      }
    });
    return Array.from(map.entries()).map(([nombre, fila]) => {
      const totalGastos = fila.gastosEfectivo + fila.gastosTarjeta;
      // Saldo disponible real en efectivo (ingresos menos gastos en efectivo y abonos a tarjetas)
      const saldoEfectivo = fila.ingresos - fila.gastosEfectivo - fila.pagosTarjeta;
      const deudaTarjeta = Math.max(0, fila.gastosTarjeta - fila.pagosTarjeta);
      const cuotasGroup = installmentStats.byPerson.find((g) => String(g.person || "").toLowerCase() === String(nombre || "").toLowerCase());
      const deudaCuotas = cuotasGroup ? cuotasGroup.totalRemainingDebt : 0;
      const deudaTotal = deudaTarjeta + deudaCuotas;
      return {
        nombre,
        ingresos: fila.ingresos,
        gastos: totalGastos,
        gastosEfectivo: fila.gastosEfectivo,
        gastosTarjeta: fila.gastosTarjeta,
        pagosTarjeta: fila.pagosTarjeta,
        deudaTarjeta,
        deudaCuotas,
        deudaTotal,
        saldo: saldoEfectivo, // Saldo en efectivo disponible
        saldoNeto: saldoEfectivo - deudaTotal,
        monedas: Array.from(fila.monedas.values()).sort(
          (a, b) => b.gastos + b.ingresos - (a.gastos + a.ingresos)
        ),
      };
    });
  }, [transactions, people, settings.currency, installmentStats]);

  // Totales de los movimientos HUÉRFANOS (su persona ya no está en la lista
  // de la cuenta): se muestran en el desglose "Por persona" como una fila
  // aparte "Sin persona asignada", con el mismo desglose por moneda que el
  // resto de filas. El total general de arriba ya los incluye.
  const porPersonaHuérfanos = useMemo(() => {
    const inPeople = new Set((people || []).map((p) => String(p || "").trim().toLowerCase()));
    const base = settings.currency || "S/";
    const agg = {
      ingresos: 0,
      gastosEfectivo: 0,
      gastosTarjeta: 0,
      pagosTarjeta: 0,
      monedas: new Map(),
      count: 0,
    };
    (Array.isArray(transactions) ? transactions : []).forEach((t) => {
      if (!t || t.id == null) return;
      const nombre = String(t.person || "").trim();
      if (!nombre || inPeople.has(nombre.toLowerCase())) return;
      agg.count += 1;
      const v = txToBase(t, base);
      const amount = Number(t && t.amount) || 0;
      const cur = txCurrency(t, base);
      if (!agg.monedas.has(cur)) agg.monedas.set(cur, { currency: cur, ingresos: 0, gastos: 0 });
      const mon = agg.monedas.get(cur);
      if (t.type === "ingreso") {
        agg.ingresos += v;
        mon.ingresos += amount;
      } else if (t.type === "gasto") {
        if (t.paymentMethod === "tarjeta" || t.cardId) {
          agg.gastosTarjeta += v;
        } else {
          agg.gastosEfectivo += v;
        }
        mon.gastos += amount;
      } else if (t.type === "pago_tarjeta") {
        agg.pagosTarjeta += v;
      }
    });
    const totalGastos = agg.gastosEfectivo + agg.gastosTarjeta;
    const saldoEfectivo = agg.ingresos - agg.gastosEfectivo - agg.pagosTarjeta;
    const total = agg.ingresos + totalGastos;
    return {
      ingresos: agg.ingresos,
      gastos: totalGastos,
      gastosEfectivo: agg.gastosEfectivo,
      gastosTarjeta: agg.gastosTarjeta,
      pagosTarjeta: agg.pagosTarjeta,
      saldo: saldoEfectivo,
      count: agg.count,
      monedas: Array.from(agg.monedas.values()).sort((a, b) => b.gastos + b.ingresos - (a.gastos + a.ingresos)),
      total,
    };
  }, [transactions, people, settings.currency]);

  // Reparto a partes iguales entre N personas: quién debe a quién.
  const settleUp = useMemo(() => {
    if (settings.splitMode === "sin" || !settings.splitEnabled || settings.splitEnabled === false || people.length < 2) return null;
    const n = people.length;
    const totalGastos = totals.totalGastos;
    if (totalGastos < 0.005) return [{ text: "Cuentas saldadas", neutral: true }];
    const share = totalGastos / n;
    // saldo de cada persona: lo que pagó menos su parte
    const balances = people.map((p) => ({ p, bal: (totals.porPersonaGasto[p] || 0) - share }));
    const debts = balances.filter((b) => b.bal < -0.005);
    const credits = balances.filter((b) => b.bal > 0.005);
    const seals = [];
    let i = 0;
    let j = 0;
    while (i < debts.length && j < credits.length) {
      const amt = Math.min(-debts[i].bal, credits[j].bal);
      if (amt >= 0.005) {
        seals.push({ text: `${debts[i].p} debe ${fmtMoney(amt, settings.currency)} a ${credits[j].p}`, neutral: false });
      }
      debts[i].bal += amt;
      credits[j].bal -= amt;
      if (debts[i].bal >= -0.005) i++;
      if (credits[j].bal <= 0.005) j++;
    }
    if (!seals.length) return [{ text: "Cuentas saldadas", neutral: true }];
    return seals;
  }, [totals, people, settings.splitMode, settings.splitEnabled, settings.currency]);

  const grouped = useMemo(() => {
    const map = {};
    transactions.forEach((t) => {
      if (!map[t.date]) map[t.date] = [];
      map[t.date].push(t);
    });
    return Object.entries(map).sort((a, b) => (a[0] < b[0] ? 1 : -1));
  }, [transactions]);

  // Resumen del mes seleccionado: ingresos, gastos, saldo del mes y desglose
  // por persona SOLO de ese mes (todo convertido a la moneda base).
  const monthSummary = useMemo(() => {
    const base = settings.currency || "S/";
    const val = (t) => txToBase(t, base);
    const inMonth = transactions.filter((t) => (t.date || "").startsWith(summaryMonth));
    const ingresos = inMonth.filter((t) => t.type === "ingreso").reduce((s, t) => s + val(t), 0);
    const gastosEfectivo = inMonth
      .filter((t) => t.type === "gasto" && t.paymentMethod !== "tarjeta" && !t.cardId)
      .reduce((s, t) => s + val(t), 0);
    const gastosTarjeta = inMonth
      .filter((t) => t.type === "gasto" && (t.paymentMethod === "tarjeta" || !!t.cardId))
      .reduce((s, t) => s + val(t), 0);
    const pagosTarjeta = inMonth
      .filter((t) => t.type === "pago_tarjeta")
      .reduce((s, t) => s + val(t), 0);
    const totalGastos = gastosEfectivo + gastosTarjeta;
    // Flujo neto de efectivo en el mes: dinero líquido que entró menos dinero líquido que salió
    const netoEfectivo = ingresos - gastosEfectivo - pagosTarjeta;

    const porPersona = {};
    inMonth.forEach((t) => {
      const n = t.person || "Alguien";
      if (!porPersona[n])
        porPersona[n] = {
          ingresos: 0,
          gastos: 0,
          gastosEfectivo: 0,
          gastosTarjeta: 0,
          pagosTarjeta: 0,
          saldoEfectivo: 0,
        };
      if (t.type === "ingreso") {
        porPersona[n].ingresos += val(t);
      } else if (t.type === "gasto") {
        if (t.paymentMethod === "tarjeta" || t.cardId) {
          porPersona[n].gastosTarjeta += val(t);
        } else {
          porPersona[n].gastosEfectivo += val(t);
        }
        porPersona[n].gastos += val(t);
      } else if (t.type === "pago_tarjeta") {
        porPersona[n].pagosTarjeta += val(t);
      }
    });

    Object.values(porPersona).forEach((p) => {
      p.saldoEfectivo = p.ingresos - p.gastosEfectivo - p.pagosTarjeta;
    });

    return {
      ingresos,
      gastos: totalGastos,
      gastosEfectivo,
      gastosTarjeta,
      pagosTarjeta,
      neto: netoEfectivo,
      porPersona: Object.entries(porPersona),
    };
  }, [transactions, summaryMonth, settings.currency]);

  // ---- Duplicados y huérfanos: cada detección se calcula UNA sola vez ----
  // Grupos de duplicados completos (sin filtrar omitidos): base del contador
  // de Ajustes y de la exclusión en los huérfanos.
  const dupAll = useMemo(() => findDuplicates(transactions), [transactions]);
  // ids de TODOS los duplicados (aunque su grupo esté omitido): así el badge y
  // el panel de huérfanos excluyen exactamente lo mismo (antes podían diferir).
  const dupAllIds = useMemo(() => {
    const s = new Set();
    dupAll.forEach((g) => g.items.forEach((t) => s.add(String(t.id))));
    return s;
  }, [dupAll]);
  // Grupos de movimientos huérfanos completos (sin filtros).
  const orphanAll = useMemo(() => findOrphans(transactions, people), [transactions, people]);

  // Movimientos duplicados o fantasma detectados: misma descripción, importe,
  // fecha y moneda con distinto id (suelen ser copias re-subidas antes del
  // arreglo de sincronización, a veces con el nombre antiguo). Se prefiere
  // conservar el que pertenece a una persona de la lista actual.
  const dupGroups = useMemo(() => {
    const dismissed = new Set(Array.isArray(dupDismissed) ? dupDismissed : []);
    const inPeople = new Set((people || []).map((p) => String(p).trim().toLowerCase()));
    return dupAll
      .filter((g) => !dismissed.has(g.sig))
      .map((g) => {
        const ranked = g.items.slice().sort((a, b) => {
          const aIn = inPeople.has(String(a.person || "").trim().toLowerCase()) ? 0 : 1;
          const bIn = inPeople.has(String(b.person || "").trim().toLowerCase()) ? 0 : 1;
          if (aIn !== bIn) return aIn - bIn;
          return String(a.id).localeCompare(String(b.id));
        });
        return { ...g, items: ranked, keep: ranked[0], dupes: ranked.slice(1) };
      });
  }, [dupAll, dupDismissed, people]);

  // Número total de copias sobrantes detectadas (para el aviso).
  const dupCount = dupGroups.reduce((s, g) => s + g.dupes.length, 0);

  // Número total de copias sobrantes en la NUBE, SIN filtrar las omitidas en
  // este dispositivo: es el contador del botón "Revisar duplicados" de Ajustes
  // (muestra cuántas hay aunque ya las hayas ocultado en el aviso).
  const dupTotalCount = dupAll.reduce((s, g) => s + Math.max(0, g.items.length - 1), 0);

  // Movimientos huérfanos: su persona ya no está en la lista de la cuenta.
  // Se excluyen TODOS los que se gestionan como duplicados (aunque su grupo de
  // duplicados esté omitido: antes solo se excluían los visibles, y el contador
  // del botón podía no coincidir con lo que mostraba el panel) y los omitidos
  // en este dispositivo.
  const orphanGroups = useMemo(() => {
    const dismissed = new Set(Array.isArray(orphanDismissed) ? orphanDismissed : []);
    return orphanAll
      .map((g) => ({ ...g, items: g.items.filter((t) => !dupAllIds.has(String(t.id))) }))
      .filter((g) => g.items.length > 0 && !dismissed.has(g.person.toLowerCase()));
  }, [orphanAll, dupAllIds, orphanDismissed]);
  const orphanCount = orphanGroups.reduce((s, g) => s + g.items.length, 0);

  // Número total de movimientos huérfanos en la nube, con la MISMA exclusión
  // que el panel (dupAllIds): completa el contador del botón unificado.
  const orphanTotalCount = orphanAll.reduce(
    (s, g) => s + g.items.filter((t) => !dupAllIds.has(String(t.id))).length,
    0
  );

  // Nombre por defecto para migrar un grupo huérfano: mi nombre si está en la
  // lista actual; si no, la primera persona de la lista.
  function orphanDefaultTarget(key) {
    const my = String(myName || "").trim().toLowerCase();
    const list = (people || []).map((p) => String(p).trim()).filter(Boolean);
    if (list.some((p) => p.toLowerCase() === my)) return list.find((p) => p.toLowerCase() === my);
    return list[0] || "";
  }

  // Marca un grupo huérfano como revisado para que no vuelva a avisar.
  function omitOrphanGroup(key) {
    setOrphanDismissed((prev) => {
      const next = [...new Set([...(Array.isArray(prev) ? prev : []), key])];
      try {
        localStorage.setItem("cc:orphanDismissed", JSON.stringify(next));
      } catch (e) {}
      return next;
    });
  }

  // Migra los movimientos de un grupo huérfano al nombre elegido. Usa el mismo
  // formato de historial que el cambio de nombre (op: "rename") para que quede
  // deshacible desde el panel de historial.
  async function migrateOrphans(group, target) {
    if (!group || !target) return;
    setOrphanBusy(true);
    setError("");
    try {
      const res = await withRetry(() => storage.renamePerson(group.person, target));
      if (!res) throw new Error("fallo");
      if (res.changed && res.transactions) {
        setTransactions(res.transactions);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: res.transactions };
      }
      // Op propia "assignTx" (no "rename"): su deshacer solo revierte el
      // renombrado sin tocar la lista de personas. Mismo formato que el rename
      // (from/to invertidos): el deshacer ejecuta renamePerson(entry.from,
      // entry.to), que revierte la asignación.
      pushHistory({
        label: "Asignar movimientos de " + group.person + " a " + target,
        op: "assignTx",
        from: target,
        to: group.person,
      });
      omitOrphanGroup(group.person.toLowerCase());
      setOrphanConfirm(null);
    } catch (e) {
      setError("No se pudieron asignar los movimientos. Revisa tu conexión e inténtalo de nuevo.");
    } finally {
      setOrphanBusy(false);
    }
  }

  // Borra todos los movimientos de un grupo huérfano (cada uno con su entrada
  // de deshacer). Solo se omite el grupo si TODOS se borraron.
  async function deleteOrphans(group) {
    setOrphanBusy(true);
    setError("");
    try {
      let ok = true;
      for (const t of group.items) {
        try {
          await deleteTransaction(t.id);
        } catch (e) {
          ok = false;
        }
      }
      if (ok) omitOrphanGroup(group.person.toLowerCase());
      setOrphanConfirm(null);
    } finally {
      setOrphanBusy(false);
    }
  }

  // Marca una firma como revisada para que no vuelva a avisar en este dispositivo.
  function omitDupGroup(sig) {
    setDupDismissed((prev) => {
      const next = [...new Set([...(Array.isArray(prev) ? prev : []), sig])];
      try {
        localStorage.setItem("cc:dupDismissed", JSON.stringify(next));
      } catch (e) {}
      return next;
    });
  }

  // Borra las copias sobrantes reutilizando el borrado normal: queda en el
  // historial compartido (deshacible) y se avisa a los demás dispositivos.
  // Al terminar, las firmas tratadas dejan de mostrarse en este dispositivo.
  async function cleanDuplicates(groups) {
    setDupBusy(true);
    setError("");
    try {
      const handled = [];
      for (const g of groups) {
        let ok = true;
        for (const d of g.dupes) {
          try {
            await deleteTransaction(d.id);
          } catch (e) {
            ok = false;
            // Se sigue con el siguiente: un fallo no detiene la limpieza.
          }
        }
        // Solo se omite el grupo si TODAS sus copias se borraron de verdad:
        // si alguna falla, sigue visible para poder reintentarlo.
        if (ok) handled.push(g.sig);
      }
      setDupDismissed((prev) => {
        const next = [...new Set([...(Array.isArray(prev) ? prev : []), ...handled])];
        try {
          localStorage.setItem("cc:dupDismissed", JSON.stringify(next));
        } catch (e) {}
        return next;
      });
      // El panel solo se cierra si al menos un grupo se limpió: si todo
      // falló, queda abierto para poder reintentar.
      if (handled.length > 0) setShowDupPanel(false);
    } finally {
      setDupBusy(false);
    }
  }

  // Copias de un grupo de duplicados cuyo person ya NO está en la lista actual
  // (fantasmas con el nombre antiguo): candidatas a consolidarse (migrarse al
  // nombre actual) en vez de borrarse, para no perder el registro.
  function dupGhostItemsOf(g) {
    const inList = new Set((people || []).map((p) => String(p || "").trim().toLowerCase()));
    return (g && Array.isArray(g.items) ? g.items : []).filter((t) => {
      const p = String(t.person || "").trim().toLowerCase();
      return p && !inList.has(p);
    });
  }

  // Nombre por defecto para consolidar un grupo: la persona conservada (keep)
  // si está en la lista; si no, mi nombre si está; si no, la primera de la lista.
  function dupDefaultTarget(g) {
    const inList = (people || []).map((p) => String(p).trim().toLowerCase());
    const keep = g && g.keep;
    if (keep && inList.includes(String(keep.person || "").trim().toLowerCase())) return keep.person;
    const my = String(myName || "").trim().toLowerCase();
    if (inList.includes(my)) return (people || []).find((p) => String(p).trim().toLowerCase() === my);
    return (people || [])[0] || "";
  }

  // Migra las copias fantasma de un grupo de duplicados al nombre elegido en
  // lugar de borrarlas: cada copia se actualiza (upsert por id) y queda en el
  // historial como edición deshacible. No borra nada ni omite el grupo: tras
  // consolidar, el usuario decide si deja una sola copia o las conserva todas.
  async function consolidateDupGhost(group, target) {
    if (!group || !target) return;
    setDupBusy(true);
    setError("");
    try {
      const ghosts = dupGhostItemsOf(group);
      let updated = null;
      for (const gh of ghosts) {
        const undoEid = uid();
        updated = await withRetry(() => storage.addTransaction({ ...gh, person: target }));
        pushHistory({ label: "Consolidar " + (gh.description || "movimiento") + " a " + target, op: "editTx", tx: gh, eid: undoEid });
        notifyOthers(gh.person || "Alguien", (gh.person || "Alguien") + " consolidó: " + (gh.description || "movimiento") + " a " + target, undoEid);
      }
      if (updated) {
        setTransactions(updated);
        lastShownRef.current = { ...(lastShownRef.current || { people: [], transactions: [], settings: normalizeSettings({}) }), transactions: updated };
      }
      setDupGhostConfirm(null);
    } catch (e) {
      setError("No se pudieron consolidar las copias. Revisa tu conexión e inténtalo de nuevo.");
    } finally {
      setDupBusy(false);
    }
  }

  function shiftMonth(delta) {
    setSummaryMonth((m) => {
      const [y, mm] = m.split("-").map(Number);
      const d = new Date(y, mm - 1 + delta, 1);
      return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0");
    });
  }
  function monthLabel(ym) {
    if (!ym) return "";
    const [y, m] = ym.split("-").map(Number);
    const meses = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];
    return meses[(m - 1 + 12) % 12] + " " + y;
  }

  // ---- render ----
  if (phase === "loading") {
    return (
      <div style={{ background: C.bg, minHeight: "100vh" }} className="flex items-center justify-center">
        {FONTS_LINKS}
        <div style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-sm">
          Cargando cuenta…
        </div>
      </div>
    );
  }

  if (phase === "onboarding") {
    const isInvitedMode = onboardingMode === "invite";
    const isJoinCodeMode = onboardingMode === "join_code";
    const isCreateMode = onboardingMode === "create";

    return (
      <div
        style={{
          background: C.bg,
          backgroundImage: darkMode
            ? "radial-gradient(800px 500px at 50% -5%, rgba(15,81,50,0.10), transparent 62%), radial-gradient(700px 450px at 105% 105%, rgba(180,134,58,0.07), transparent 60%)"
            : "radial-gradient(900px 550px at 50% -6%, rgba(15,81,50,0.13), transparent 62%), radial-gradient(750px 480px at 106% 108%, rgba(180,134,58,0.09), transparent 60%)",
          minHeight: "100vh",
          color: C.ink,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          padding: "1.5rem",
        }}
      >
        {FONTS_LINKS}
        <div className="w-full max-w-sm cc-rise">
          <div
            className="cc-card-hover"
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.5rem",
              padding: "1.85rem 1.65rem",
              boxShadow: "0 24px 50px -18px rgb(2 6 23 / 0.30), 0 1px 3px rgb(2 6 23 / 0.08)",
              border: `1px solid ${C.line}`,
            }}
          >
            <div style={{ display: "flex", justifyContent: "center", marginBottom: "1rem" }}>
              <div
                style={{
                  width: "3.4rem",
                  height: "3.4rem",
                  borderRadius: "1.1rem",
                  background: BRAND_GRADIENT,
                  color: "#FFFFFF",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  boxShadow: "0 12px 24px -10px rgba(15,81,50,0.55)",
                }}
              >
                <Receipt size={24} strokeWidth={2} />
              </div>
            </div>

            <h1
              className="cc-display"
              style={{
                letterSpacing: "0.01em",
                fontSize: "1.45rem",
                fontWeight: 700,
                textAlign: "center",
                margin: "0 0 0.35rem 0",
              }}
            >
              Cuenta compartida
            </h1>

            {/* Pestañas selectoras si no viene con enlace de invitación directo */}
            {!isInvitedMode ? (
              <div style={{ display: "flex", background: C.field, padding: "0.25rem", borderRadius: "0.75rem", gap: "0.25rem", marginBottom: "1.25rem" }}>
                <button
                  type="button"
                  onClick={() => { setOnboardingMode("create"); setError(""); }}
                  style={{
                    flex: 1,
                    background: isCreateMode ? C.paper : "transparent",
                    color: isCreateMode ? C.ink : C.inkSoft,
                    border: isCreateMode ? `1px solid ${C.line}` : "none",
                    borderRadius: "0.6rem",
                    padding: "0.45rem 0.3rem",
                    fontSize: "0.75rem",
                    fontWeight: isCreateMode ? 700 : 500,
                    cursor: "pointer",
                    textAlign: "center",
                  }}
                >
                  🌱 Crear mi cuenta
                </button>
                <button
                  type="button"
                  onClick={() => { setOnboardingMode("join_code"); setError(""); }}
                  style={{
                    flex: 1,
                    background: isJoinCodeMode ? C.paper : "transparent",
                    color: isJoinCodeMode ? C.ink : C.inkSoft,
                    border: isJoinCodeMode ? `1px solid ${C.line}` : "none",
                    borderRadius: "0.6rem",
                    padding: "0.45rem 0.3rem",
                    fontSize: "0.75rem",
                    fontWeight: isJoinCodeMode ? 700 : 500,
                    cursor: "pointer",
                    textAlign: "center",
                  }}
                >
                  🔑 Tengo invitación
                </button>
              </div>
            ) : (
              /* Banner si vino con enlace de invitación */
              <div style={{ background: darkMode ? "rgba(16,185,129,0.12)" : "#ECFDF5", border: `1.5px solid ${C.green}`, borderRadius: "0.85rem", padding: "0.65rem 0.85rem", margin: "0 0 1.25rem 0", textAlign: "center" }}>
                <div style={{ fontSize: "0.7rem", fontWeight: 700, color: darkMode ? "#6EE7B7" : "#047857", textTransform: "uppercase", marginBottom: "0.15rem" }}>
                  👋 Te invitaron al espacio:
                </div>
                <div style={{ fontSize: "0.95rem", fontWeight: 800, color: C.ink, display: "flex", alignItems: "center", justifyContent: "center", gap: "0.35rem" }}>
                  <span>{settings.accountName || "Cuenta compartida"}</span>
                  <span style={{ fontSize: "0.68rem", background: C.paper, padding: "0.1rem 0.45rem", borderRadius: "6px", border: `1px solid ${C.line}`, color: C.green, fontFamily: "monospace" }}>
                    #{activeAccountCode || "DEFAULT"}
                  </span>
                </div>
                <button
                  type="button"
                  onClick={() => { setOnboardingMode("create"); setError(""); }}
                  style={{ background: "none", border: "none", color: C.inkSoft, fontSize: "0.68rem", textDecoration: "underline", cursor: "pointer", marginTop: "0.35rem" }}
                >
                  ¿Quieres crear un espacio propio independiente?
                </button>
              </div>
            )}

            {/* Si está en modo crear nuevo espacio: campo para el nombre del espacio */}
            {isCreateMode && (
              <div style={{ marginBottom: "0.85rem" }}>
                <label
                  style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                  className="text-xs uppercase tracking-widest block mb-1.5"
                >
                  Nombre de tu nuevo espacio
                </label>
                <input
                  value={onboardingSpaceName}
                  onChange={(e) => setOnboardingSpaceName(e.target.value)}
                  placeholder="Ej. Mis Gastos, Familia, Casa..."
                  maxLength={32}
                  style={{
                    fontFamily: "'Inter', sans-serif",
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.75rem",
                    color: C.ink,
                    padding: "0.65rem 0.85rem",
                    fontSize: "0.85rem",
                    width: "100%",
                    boxSizing: "border-box",
                  }}
                  className="outline-none"
                />
              </div>
            )}

            {/* Si está en modo ingresar código de invitación */}
            {isJoinCodeMode && (
              <div style={{ marginBottom: "0.85rem" }}>
                <label
                  style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                  className="text-xs uppercase tracking-widest block mb-1.5"
                >
                  Código de invitación
                </label>
                <div style={{ display: "flex", gap: "0.4rem" }}>
                  <input
                    value={joinCodeDraft}
                    onChange={(e) => setJoinCodeDraft(e.target.value.toUpperCase())}
                    placeholder="Ej. FAM-892"
                    maxLength={16}
                    style={{
                      fontFamily: "monospace",
                      background: C.field,
                      border: `1.5px solid ${C.line}`,
                      borderRadius: "0.75rem",
                      color: C.ink,
                      padding: "0.65rem 0.85rem",
                      fontSize: "0.95rem",
                      fontWeight: 800,
                      letterSpacing: "0.05em",
                      flex: 1,
                      minWidth: 0,
                    }}
                    className="outline-none"
                  />
                  <button
                    type="button"
                    onClick={async () => {
                      if (!joinCodeDraft.trim()) return;
                      await handleSwitchSpace(joinCodeDraft.trim());
                      setOnboardingMode("invite");
                    }}
                    style={{
                      background: BRAND_GRADIENT,
                      color: "#FFFFFF",
                      border: "none",
                      borderRadius: "0.75rem",
                      padding: "0 0.85rem",
                      fontSize: "0.75rem",
                      fontWeight: 700,
                      cursor: "pointer",
                      whiteSpace: "nowrap",
                    }}
                    className="cc-btn-press"
                  >
                    Verificar
                  </button>
                </div>
              </div>
            )}

            {/* Campos de Nombre de usuario y Contraseña */}
            <div style={{ marginBottom: "0.85rem" }}>
              <label
                style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                className="text-xs uppercase tracking-widest block mb-1.5"
              >
                Tu nombre
              </label>
              <input
                value={nameDraft}
                onChange={(e) => setNameDraft(e.target.value)}
                placeholder="¿Cómo te llamas?"
                maxLength={24}
                style={{
                  fontFamily: "'Inter', sans-serif",
                  background: C.field,
                  border: `1px solid ${C.line}`,
                  borderRadius: "0.75rem",
                  color: C.ink,
                  padding: "0.65rem 0.85rem",
                  fontSize: "0.85rem",
                  width: "100%",
                  boxSizing: "border-box",
                }}
                className="outline-none"
              />
            </div>

            <div style={{ marginBottom: "1rem" }}>
              <label
                style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                className="text-xs uppercase tracking-widest block mb-1.5"
              >
                Contraseña
              </label>
              <div style={{ position: "relative", width: "100%" }}>
                <input
                  type={showPassDraft ? "text" : "password"}
                  value={passDraft}
                  onChange={(e) => setPassDraft(e.target.value)}
                  placeholder="Mínimo 8 caracteres"
                  maxLength={64}
                  autoComplete={userExists ? "current-password" : "new-password"}
                  style={{
                    fontFamily: "'Inter', sans-serif",
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.75rem",
                    color: C.ink,
                    padding: "0.65rem 2.4rem 0.65rem 0.85rem",
                    fontSize: "0.85rem",
                    width: "100%",
                    boxSizing: "border-box",
                  }}
                  className="outline-none"
                  onKeyDown={(e) => e.key === "Enter" && joinAccount()}
                />
                <button
                  type="button"
                  onClick={() => setShowPassDraft(!showPassDraft)}
                  title={showPassDraft ? "Ocultar contraseña" : "Ver contraseña"}
                  style={{
                    position: "absolute",
                    right: "0.65rem",
                    top: "50%",
                    transform: "translateY(-50%)",
                    background: "none",
                    border: "none",
                    color: C.inkSoft,
                    cursor: "pointer",
                    padding: "0.25rem",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                  }}
                  className="hover:opacity-75 transition-opacity"
                >
                  {showPassDraft ? <EyeOff size={16} /> : <Eye size={16} />}
                </button>
              </div>
            </div>

            {userExists === true && (
              <p
                style={{
                  fontFamily: "'Inter', sans-serif",
                  color: "#047857",
                  backgroundColor: darkMode ? "#064E3B" : "#D1FAE5",
                  border: `1px solid ${darkMode ? "#047857" : "#A7F3D0"}`,
                  borderRadius: "0.6rem",
                  padding: "0.5rem 0.7rem",
                }}
                className="text-xs mb-3"
              >
                Ese nombre ya tiene cuenta en este espacio: escribe su contraseña para entrar.
              </p>
            )}

            <button
              type="button"
              onClick={() => joinAccount()}
              disabled={saving || !nameDraft.trim() || !passDraft}
              style={{
                fontFamily: "'Inter', sans-serif",
                background: BRAND_GRADIENT,
                color: "#FFFFFF",
                borderRadius: "0.85rem",
                boxShadow: "0 10px 22px -10px rgba(15,81,50,0.6)",
                fontWeight: 700,
                width: "100%",
                padding: "0.8rem 0",
                fontSize: "0.85rem",
                cursor: "pointer",
                border: "none",
              }}
              className="cc-btn-press hover:brightness-105 uppercase tracking-widest disabled:opacity-40 disabled:shadow-none"
            >
              {saving ? "Procesando…" : isCreateMode ? "Crear mi espacio y entrar" : userExists ? "Entrar al espacio" : "Unirme a este espacio"}
            </button>

            {error && (
              <p
                style={{
                  fontFamily: "'Inter', sans-serif",
                  color: C.red,
                  backgroundColor: darkMode ? "#450A0A" : "#FEF2F2",
                  border: `1px solid ${darkMode ? "#7F1D1D" : "#FECACA"}`,
                  borderRadius: "0.6rem",
                  padding: "0.5rem 0.7rem",
                }}
                className="text-xs mt-3 text-center cc-pop"
              >
                {error}
              </p>
            )}

            <button
              type="button"
              onClick={() => {
                setForgotOpen(!forgotOpen);
                setForgotConfirm(null);
                setForgotDone("");
                setError("");
              }}
              style={{
                fontFamily: "'Inter', sans-serif",
                color: C.inkSoft,
                background: "none",
                border: "none",
                cursor: "pointer",
                fontSize: "0.76rem",
                marginTop: "0.85rem",
                textDecoration: "underline",
              }}
              className="w-full text-center hover:opacity-70"
            >
              ¿Olvidaste tu contraseña?
            </button>

            {forgotOpen && (
              <div
                style={{
                  marginTop: "0.75rem",
                  border: `1px solid ${C.line}`,
                  borderRadius: "0.85rem",
                  padding: "0.85rem",
                  backgroundColor: C.field,
                }}
                className="cc-pop"
              >
                {forgotDone ? (
                  <p
                    style={{
                      fontFamily: "'Inter', sans-serif",
                      color: "#047857",
                      backgroundColor: darkMode ? "#064E3B" : "#D1FAE5",
                      border: `1px solid ${darkMode ? "#047857" : "#A7F3D0"}`,
                      borderRadius: "0.6rem",
                      padding: "0.5rem 0.7rem",
                    }}
                    className="text-xs cc-pop"
                  >
                    {forgotDone}
                  </p>
                ) : (
                  <>
                    <p style={{ fontFamily: "'Inter', sans-serif", color: C.ink }} className="text-xs font-semibold mb-1">
                      Restablecer contraseña de otra persona
                    </p>
                    <p style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-[11px] leading-relaxed mb-3">
                      Esta cuenta no tiene email: cualquier persona con acceso puede restablecer la contraseña.
                    </p>

                    <label
                      style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                      className="text-[10px] uppercase tracking-widest block mb-1"
                    >
                      Tu nombre
                    </label>
                    <input
                      value={forgotName}
                      onChange={(e) => {
                        setForgotName(e.target.value);
                        setForgotConfirm(null);
                        setError("");
                      }}
                      placeholder="Nombre exacto"
                      list="olvido-personas"
                      maxLength={24}
                      style={{
                        fontFamily: "'Inter', sans-serif",
                        background: C.paper,
                        border: `1px solid ${C.line}`,
                        borderRadius: "0.6rem",
                        color: C.ink,
                        padding: "0.55rem 0.7rem",
                        width: "100%",
                        boxSizing: "border-box",
                      }}
                      className="text-sm outline-none mb-3"
                    />
                    <datalist id="olvido-personas">
                      {/* Privacidad: no expone la lista pública en el onboarding */}
                    </datalist>

                    {!forgotConfirm ? (
                      <>
                        <label
                          style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                          className="text-[10px] uppercase tracking-widest block mb-1"
                        >
                          Contraseña nueva
                        </label>
                        <div style={{ position: "relative", width: "100%", marginBottom: "0.75rem" }}>
                          <input
                            type={showForgotPass ? "text" : "password"}
                            value={forgotPass}
                            onChange={(e) => setForgotPass(e.target.value)}
                            placeholder="Mínimo 8 caracteres"
                            maxLength={64}
                            autoComplete="new-password"
                            style={{
                              fontFamily: "'Inter', sans-serif",
                              background: C.paper,
                              border: `1px solid ${C.line}`,
                              borderRadius: "0.6rem",
                              color: C.ink,
                              padding: "0.55rem 2.2rem 0.55rem 0.7rem",
                              width: "100%",
                              boxSizing: "border-box",
                            }}
                            className="text-sm outline-none"
                            onKeyDown={(e) => e.key === "Enter" && resetPassword()}
                          />
                          <button
                            type="button"
                            onClick={() => setShowForgotPass(!showForgotPass)}
                            title={showForgotPass ? "Ocultar contraseña" : "Ver contraseña"}
                            style={{
                              position: "absolute",
                              right: "0.55rem",
                              top: "50%",
                              transform: "translateY(-50%)",
                              background: "none",
                              border: "none",
                              color: C.inkSoft,
                              cursor: "pointer",
                              padding: "0.2rem",
                              display: "flex",
                              alignItems: "center",
                              justifyContent: "center",
                            }}
                          >
                            {showForgotPass ? <EyeOff size={15} /> : <Eye size={15} />}
                          </button>
                        </div>
                        <button
                          type="button"
                          onClick={resetPassword}
                          disabled={saving || !forgotName.trim() || !forgotPass}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.ink,
                            color: C.paper,
                            borderRadius: "0.6rem",
                            fontWeight: 600,
                          }}
                          className="cc-btn-press w-full py-2 text-xs uppercase tracking-widest disabled:opacity-40"
                        >
                          Comprobar y restablecer
                        </button>
                      </>
                    ) : (
                      <>
                        <p style={{ fontFamily: "'Inter', sans-serif", color: C.ink }} className="text-xs mb-2">
                          ¿Restablecer la contraseña de <strong style={{ fontWeight: 700 }}>{forgotConfirm.name}</strong>?
                        </p>
                        <div className="flex gap-2">
                          <button
                            type="button"
                            onClick={resetPassword}
                            disabled={saving}
                            style={{ fontFamily: "'Inter', sans-serif", background: C.ink, color: C.paper, borderRadius: "0.6rem" }}
                            className="flex-1 py-2 text-xs uppercase tracking-widest disabled:opacity-40"
                          >
                            {saving ? "Guardando…" : "Sí, restablecer"}
                          </button>
                          <button
                            type="button"
                            onClick={() => {
                              setForgotConfirm(null);
                              setForgotPass("");
                              setError("");
                            }}
                            style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }}
                            className="flex-1 py-2 text-xs uppercase tracking-widest"
                          >
                            Cancelar
                          </button>
                        </div>
                      </>
                    )}
                  </>
                )}
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  return (
    <div
      style={{
        background: C.bg,
        backgroundImage: darkMode
          ? "radial-gradient(1000px 520px at 50% -6%, rgba(15,81,50,0.08), transparent 62%), radial-gradient(750px 450px at 105% 108%, rgba(180,134,58,0.06), transparent 60%)"
          : "radial-gradient(1100px 560px at 50% -8%, rgba(15,81,50,0.09), transparent 62%), radial-gradient(800px 480px at 106% 108%, rgba(180,134,58,0.07), transparent 60%)",
        minHeight: "100vh",
        color: C.ink,
      }}
      className="pb-16"
    >
      {FONTS_LINKS}
      <style>{`
        .dotted-row { position: relative; }
        .dotted-row .leader {
          flex: 1;
          border-bottom: 1px dotted ${C.line};
          margin: 0 6px;
          height: 1em;
        }
        input:focus, select:focus { outline: none; }
      `}</style>

      {/* Header */}
      <div className="max-w-[480px] mx-auto px-5 pt-8 pb-4 cc-rise">
        {/* Fila 1: Título completo de la cuenta + Acciones principales */}
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "0.75rem", marginBottom: "0.75rem" }}>
          <div style={{ display: "flex", alignItems: "center", gap: "0.6rem", minWidth: 0, flex: 1 }}>
            <div
              style={{
                width: "2.3rem",
                height: "2.3rem",
                borderRadius: "0.75rem",
                background: BRAND_GRADIENT,
                color: "#FFFFFF",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                boxShadow: "0 6px 16px -4px rgba(15,81,50,0.5)",
                flexShrink: 0,
              }}
            >
              <Receipt size={17} strokeWidth={2.2} />
            </div>
            <div
              onClick={() => {
                setSpaceModalTab("switch");
                setSpaceFeedback(null);
                setShowSpaceModal(true);
              }}
              role="button"
              tabIndex={0}
              title="Toca para cambiar de espacio o crear uno nuevo"
              style={{ minWidth: 0, flex: 1, cursor: "pointer" }}
              className="group"
            >
              <div style={{ display: "flex", alignItems: "center", gap: "0.35rem" }}>
                <h1
                  className="cc-display"
                  style={{
                    margin: 0,
                    fontSize: "1.15rem",
                    fontWeight: 700,
                    letterSpacing: "0.01em",
                    color: C.ink,
                    lineHeight: 1.2,
                    whiteSpace: "nowrap",
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                  }}
                >
                  {(settings.accountName || "Cuenta compartida").toUpperCase()}
                </h1>
                <ChevronDown size={14} color={C.inkSoft} className="group-hover:translate-y-0.5 transition-transform" />
              </div>
              <p style={{ margin: 0, fontSize: "0.72rem", color: C.inkSoft, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", display: "flex", alignItems: "center", gap: "0.3rem" }}>
                <span style={{ fontWeight: 700, color: C.green, background: C.field, padding: "0.05rem 0.35rem", borderRadius: "4px" }}>
                  #{activeAccountCode || "DEFAULT"}
                </span>
                <span>·</span>
                <span>{people.length > 0 ? people.join(" · ") : myName}</span>
                {getActiveAccountId() !== "default" && (
                  <button
                    type="button"
                    onClick={(e) => {
                      e.stopPropagation();
                      handleSwitchSpace("default");
                    }}
                    title="Volver a la cuenta principal con tus datos"
                    style={{
                      background: BRAND_GRADIENT,
                      color: "#FFFFFF",
                      border: "none",
                      borderRadius: "0.45rem",
                      padding: "0.1rem 0.45rem",
                      fontSize: "0.65rem",
                      fontWeight: 700,
                      cursor: "pointer",
                      marginLeft: "0.3rem",
                    }}
                    className="cc-btn-press"
                  >
                    🏠 Ir a Cuenta Principal
                  </button>
                )}
              </p>
            </div>
          </div>

          {/* Acciones principales de la esquina derecha: Refrescar y Ajustes */}
          <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", flexShrink: 0 }}>
            <button
              type="button"
              onClick={() => {
                refreshShared(false);
                schedulePendingFlush(0); // fuerza la subida de pendientes YA
              }}
              aria-label={
                pendingWrites > 0
                  ? "Sincronizando: " + pendingWrites + (pendingWrites === 1 ? " cambio" : " cambios") + " sin subir a la nube — toca para forzar la subida"
                  : "Actualizar"
              }
              title={
                pendingWrites > 0
                  ? pendingWrites + (pendingWrites === 1 ? " cambio" : " cambios") + " sin subir a la nube — se está subiendo; toca para forzar ahora"
                  : "Actualizar"
              }
              style={{
                background: C.field,
                border: `1px solid ${pendingWrites > 0 ? (darkMode ? "#92400E" : "#F59E0B") : C.line}`,
                borderRadius: "0.75rem",
                padding: pendingWrites > 0 ? "0.4rem 0.65rem" : "0.4rem",
                minWidth: "2.2rem",
                height: "2.2rem",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                gap: "0.35rem",
                cursor: "pointer",
                color: pendingWrites > 0 ? (darkMode ? "#FBBF24" : "#D97706") : C.inkSoft,
                boxShadow: "0 2px 6px -2px rgb(2 6 23 / 0.08)",
              }}
              className="cc-btn-press"
            >
              <RefreshCw
                size={14}
                color={pendingWrites > 0 ? (darkMode ? "#FBBF24" : "#D97706") : C.inkSoft}
                style={pendingWrites > 0 ? { animation: "cc-spin 1s linear infinite" } : undefined}
                className="transition-transform"
              />
              {pendingWrites > 0 && (
                <span
                  style={{
                    fontSize: "0.68rem",
                    fontWeight: 700,
                    color: darkMode ? "#FDE68A" : "#92400E",
                    whiteSpace: "nowrap",
                  }}
                >
                  Sincronizando…
                </span>
              )}
              {pendingWrites > 0 && (
                <span
                  aria-hidden="true"
                  style={{
                    background: darkMode ? "#92400E" : "#F59E0B",
                    color: "#FFF",
                    borderRadius: "9999px",
                    padding: "0.05rem 0.35rem",
                    fontSize: "0.6rem",
                    fontWeight: 700,
                    lineHeight: 1.3,
                    minWidth: "1.1rem",
                    textAlign: "center",
                  }}
                >
                  {pendingWrites > 50 ? "50+" : pendingWrites}
                </span>
              )}
            </button>
          </div>
        </div>

        {/* Fila 2: Barra de estado móvil y controles rápidos */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
            gap: "0.4rem",
            marginBottom: "1rem",
            flexWrap: "nowrap",
            overflowX: "auto",
            paddingBottom: "0.2rem",
            scrollbarWidth: "none",
          }}
          className="no-scrollbar"
        >
          {/* Chip de usuario activo */}
          <div
            style={{
              display: "inline-flex",
              alignItems: "center",
              gap: "0.35rem",
              fontSize: "0.74rem",
              color: C.ink,
              backgroundColor: C.field,
              padding: "0.25rem 0.55rem 0.25rem 0.3rem",
              borderRadius: "9999px",
              border: `1px solid ${C.line}`,
              flexShrink: 0,
            }}
          >
            <span
              style={{
                width: "1.25rem",
                height: "1.25rem",
                borderRadius: "50%",
                backgroundColor: personColor(myName || person),
                color: "#FFFFFF",
                display: "inline-flex",
                alignItems: "center",
                justifyContent: "center",
                fontSize: "0.65rem",
                fontWeight: 800,
                flexShrink: 0,
              }}
            >
              {(myName || person || "?").trim().charAt(0).toUpperCase()}
            </span>
            <span style={{ fontWeight: 600, maxWidth: "100px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {myName || person}
            </span>
          </div>

                    {/* Chip de Invitar personas */}
          <button
            type="button"
            onClick={() => setShowInviteModal(true)}
            aria-label="Invitar personas a este espacio"
            title="Invitar personas"
            style={{
              display: "inline-flex",
              alignItems: "center",
              gap: "0.3rem",
              fontSize: "0.7rem",
              fontWeight: 700,
              color: "#FFFFFF",
              background: BRAND_GRADIENT,
              border: "none",
              borderRadius: "9999px",
              padding: "0.3rem 0.65rem",
              whiteSpace: "nowrap",
              cursor: "pointer",
              boxShadow: "0 2px 8px rgba(15,81,50,0.3)",
              flexShrink: 0,
            }}
            className="cc-btn-press"
          >
            <Users size={12} />
            <span>🔗 Invitar</span>
          </button>

          {/* Chips de la derecha: Ajustes, Novedades, Modo, Estado en vivo */}
          <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", flexShrink: 0 }}>
            <button
              type="button"
              onClick={openSettings}
              aria-label="Abrir ajustes"
              title="Ajustes"
              style={{
                display: "inline-flex",
                alignItems: "center",
                gap: "0.25rem",
                fontSize: "0.68rem",
                fontWeight: 600,
                color: C.ink,
                backgroundColor: C.field,
                border: `1px solid ${C.line}`,
                borderRadius: "9999px",
                padding: "0.28rem 0.55rem",
                whiteSpace: "nowrap",
                cursor: "pointer",
              }}
              className="cc-btn-press hover:opacity-90"
            >
              <Settings size={11} />
              <span>Ajustes</span>
            </button>

            <button
              type="button"
              onClick={openChangelog}
              title="Ver últimas novedades y cambios"
              style={{
                display: "inline-flex",
                alignItems: "center",
                gap: "0.25rem",
                fontSize: "0.68rem",
                fontWeight: 600,
                color: C.ink,
                backgroundColor: C.field,
                border: `1px solid ${hasUnseenChangelog ? (darkMode ? "#059669" : "#10B981") : C.line}`,
                borderRadius: "9999px",
                padding: "0.28rem 0.55rem",
                whiteSpace: "nowrap",
                cursor: "pointer",
              }}
              className="cc-btn-press hover:opacity-90"
            >
              <Sparkles size={11} color={C.green} />
              <span>Novedades</span>
              <span
                style={{
                  background: hasUnseenChangelog ? C.green : (darkMode ? "#334155" : "#E2E8F0"),
                  color: hasUnseenChangelog ? "#FFFFFF" : C.inkSoft,
                  borderRadius: "9999px",
                  padding: "0.02rem 0.35rem",
                  fontSize: "0.6rem",
                  fontWeight: 700,
                }}
              >
                {APP_CHANGELOG[0]?.version || "v25"}
              </span>
            </button>

            <button
              type="button"
              onClick={() => setMinimalMode(!minimalMode)}
              title={minimalMode ? "Cambiar a modo completo" : "Cambiar a modo minimalista"}
              style={{
                display: "inline-flex",
                alignItems: "center",
                gap: "0.2rem",
                fontSize: "0.68rem",
                fontWeight: 700,
                color: minimalMode ? (darkMode ? "#6EE7B7" : "#047857") : C.inkSoft,
                backgroundColor: minimalMode ? (darkMode ? "#064E3B" : "#ECFDF5") : C.field,
                border: `1px solid ${minimalMode ? (darkMode ? "#059669" : "#A7F3D0") : C.line}`,
                borderRadius: "9999px",
                padding: "0.28rem 0.55rem",
                whiteSpace: "nowrap",
                cursor: "pointer",
              }}
              className="cc-btn-press hover:opacity-90"
            >
              <span>{minimalMode ? "🍃 Minimal" : "✨ Completo"}</span>
            </button>

            <span
              role="status"
              aria-live="polite"
              title={
                rtStatus === "connected"
                  ? "En vivo: los cambios de otras personas llegan al instante"
                  : rtStatus === "connecting"
                  ? "Conectando al canal en vivo…"
                  : rtStatus === "reconnecting"
                  ? "El canal en vivo se cayó y se está reconectando; mientras tanto se usa el sondeo de seguridad"
                  : "Sin canal en vivo: la app usa el sondeo de seguridad cada pocos segundos"
              }
              style={{
                display: "inline-flex",
                alignItems: "center",
                gap: "0.25rem",
                fontSize: "0.68rem",
                fontWeight: 600,
                color: C.inkSoft,
                backgroundColor: C.field,
                border: `1px solid ${C.line}`,
                borderRadius: "9999px",
                padding: "0.28rem 0.5rem",
                whiteSpace: "nowrap",
                userSelect: "none",
              }}
            >
              <span
                style={{
                  width: "0.45rem",
                  height: "0.45rem",
                  borderRadius: "50%",
                  background:
                    rtStatus === "connected"
                      ? C.green
                      : rtStatus === "polling"
                      ? C.inkSoft
                      : "#F59E0B",
                  boxShadow:
                    rtStatus === "connected" ? "0 0 0 2px rgba(15,81,50,0.25)" : "none",
                  animation:
                    rtStatus === "connecting" || rtStatus === "reconnecting"
                      ? "cc-pulse 1.4s ease-in-out infinite"
                      : "none",
                }}
              />
              <span>{rtStatus === "connected" ? "En vivo" : rtStatus === "connecting" ? "Conectando…" : rtStatus === "reconnecting" ? "Reconectando…" : "Sondeo"}</span>
            </span>
          </div>
        </div>
        {HAS_SUPABASE && syncSource === "local" && (
          <div
            role="status"
            style={{
              display: "flex",
              alignItems: "center",
              gap: "0.4rem",
              marginBottom: "0.5rem",
              fontSize: "0.75rem",
              color: "#92400E",
              backgroundColor: "#FEF3C7",
              padding: "0.4rem 0.7rem",
              borderRadius: "8px",
              width: "fit-content",
            }}
          >
            <RefreshCw size={12} />
            <span>Sin conexión con la nube: guardando solo en este dispositivo</span>
          </div>
        )}
        {dbOutdated && HAS_SUPABASE && (
          <div
            role="alert"
            style={{
              display: "flex",
              alignItems: "flex-start",
              gap: "0.4rem",
              marginBottom: "0.5rem",
              fontSize: "0.75rem",
              color: "#7F1D1D",
              backgroundColor: "#FEE2E2",
              border: "1px solid #FECACA",
              padding: "0.5rem 0.7rem",
              borderRadius: "8px",
              lineHeight: 1.45,
            }}
          >
            <span>
              <strong>La base de datos no está actualizada.</strong>{" "}
              Vuelve a ejecutar el <strong>schema.sql</strong> en Supabase (SQL Editor → New query → pega todo → Run, no borra datos). Eso también cambia la moneda a S/ automáticamente. Hasta entonces, los movimientos solo se guardan en este dispositivo.
            </span>
          </div>
        )}



        {installEvt && (
          <div
            role="status"
            style={{
              display: "flex",
              alignItems: "center",
              gap: "0.75rem",
              marginBottom: "1rem",
              fontSize: "0.85rem",
              color: "#FFFFFF",
              backgroundColor: C.green,
              padding: "0.6rem 0.85rem",
              borderRadius: "0.75rem",
            }}
          >
            <span style={{ flex: 1, fontSize: "0.78rem", lineHeight: 1.35 }}>
              Instala la app para abrirla como una aplicación de verdad (el modo sin conexión solo funciona cuando estás tú solo; en cuentas compartidas con otras personas se requiere internet para sincronizar en tiempo real).
            </span>
            <button
              type="button"
              onClick={handleInstall}
              style={{
                background: "#FFFFFF",
                color: C.green,
                border: "none",
                borderRadius: "0.5rem",
                padding: "0.4rem 0.8rem",
                fontWeight: 600,
                cursor: "pointer",
              }}
            >
              Instalar
            </button>
            <button
              type="button"
              onClick={() => setInstallEvt(null)}
              aria-label="Cerrar aviso"
              style={{ background: "none", border: "none", color: "#FFFFFF", cursor: "pointer", fontSize: "1rem" }}
            >
              ✕
            </button>
          </div>
        )}
        {showInstallTip && (
          <div
            role="status"
            style={{
              display: "flex",
              alignItems: "center",
              gap: "0.75rem",
              marginBottom: "1rem",
              fontSize: "0.85rem",
              color: "#9A3412",
              backgroundColor: "#FFF7ED",
              border: "1px solid #FED7AA",
              padding: "0.6rem 0.85rem",
              borderRadius: "0.75rem",
            }}
          >
            <span style={{ flex: 1 }}>
              En Safari: toca <strong>Compartir</strong> → <strong>Añadir a pantalla de inicio</strong> para instalar la app.
            </span>
            <button
              type="button"
              onClick={dismissInstallTip}
              aria-label="Cerrar aviso"
              style={{ background: "none", border: "none", color: "#9A3412", cursor: "pointer", fontSize: "1rem" }}
            >
              ✕
            </button>
          </div>
        )}
      </div>

      {error && (
        <div className="max-w-[480px] mx-auto px-5">
          <p style={{ fontFamily: "'Inter', sans-serif", color: C.red }} className="text-xs mb-4">
            {error}
          </p>
        </div>
      )}

      {/* Toast flotante de novedades: siempre visible aunque hagas scroll en móvil o PC */}
      {notice && (
        <div
          key={notice.at}
          role="status"
          aria-live="polite"
          style={{
            position: "fixed",
            top: "1rem",
            left: "50%",
            transform: "translateX(-50%)",
            zIndex: 99999,
            width: "calc(100% - 2rem)",
            maxWidth: "480px",
            pointerEvents: "auto",
            animation: "cc-panel-in 0.35s cubic-bezier(.16,1,.3,1)",
          }}
        >
          <div
            style={{
              border: `1.5px solid ${darkMode ? "#059669" : "#10B981"}`,
              borderLeft: `5px solid ${C.green}`,
              background: darkMode ? "#064E3B" : "#ECFDF5",
              borderRadius: "1rem",
              boxShadow: "0 20px 40px -10px rgba(0,0,0,0.4), 0 0 0 1px rgba(0,0,0,0.1)",
              padding: "0.85rem 1rem",
              display: "flex",
              alignItems: "flex-start",
              justifyContent: "space-between",
              gap: "0.75rem",
              backdropFilter: "blur(12px)",
            }}
            className="cc-pop"
          >
            <div style={{ flexShrink: 0, marginTop: "0.15rem" }}>
              <div style={{ width: "2rem", height: "2rem", borderRadius: "50%", background: C.green, display: "flex", alignItems: "center", justifyContent: "center", color: "#FFFFFF" }}>
                <BellRing size={15} style={{ animation: "cc-spin 2s ease-in-out infinite" }} />
              </div>
            </div>

            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", marginBottom: "0.25rem" }}>
                <span style={{ fontSize: "0.68rem", fontWeight: 800, textTransform: "uppercase", letterSpacing: "0.05em", color: darkMode ? "#6EE7B7" : "#047857" }}>
                  🔔 Cambio en la cuenta
                </span>
                <span style={{ fontSize: "0.65rem", color: darkMode ? "#A7F3D0" : C.inkSoft, fontWeight: 500 }}>
                  Ahora mismo
                </span>
              </div>
              {notice.lines.map((l, i) => (
                <p key={i} style={{ margin: "0.15rem 0", fontSize: "0.82rem", fontWeight: 600, color: darkMode ? "#F1F0E8" : C.ink, lineHeight: 1.4 }}>
                  {l}
                </p>
              ))}

              {notice.txId && (
                <button
                  type="button"
                  onClick={() => {
                    requestTxFocus(notice.txId);
                    setNotice(null);
                  }}
                  style={{
                    marginTop: "0.45rem",
                    background: C.green,
                    color: "#FFFFFF",
                    border: "none",
                    borderRadius: "0.5rem",
                    padding: "0.25rem 0.65rem",
                    fontSize: "0.72rem",
                    fontWeight: 700,
                    cursor: "pointer",
                    display: "inline-flex",
                    alignItems: "center",
                    gap: "0.3rem",
                  }}
                  className="cc-btn-press"
                >
                  <span>Ver movimiento</span>
                  <ArrowUpRight size={12} />
                </button>
              )}
            </div>

            <button
              onClick={() => {
                setNotice(null);
                if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current);
              }}
              aria-label="Cerrar aviso"
              style={{
                background: "rgba(0,0,0,0.06)",
                border: "none",
                borderRadius: "50%",
                width: "1.6rem",
                height: "1.6rem",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                color: darkMode ? "#F1F0E8" : C.inkSoft,
                cursor: "pointer",
                flexShrink: 0,
              }}
              className="cc-btn-press"
            >
              <X size={14} />
            </button>
          </div>
        </div>
      )}

      {/* Aviso de cambio de nombre: se pide confirmación antes de migrar. Entra
          deslizándose desde abajo (cc-panel-in), igual que los demás paneles. */}
      {pendingRename && (
        <div className="max-w-[480px] mx-auto px-5 mb-4">
          <div style={{ border: `1px solid ${C.red}`, background: C.paper, borderRadius: "1.4rem", boxShadow: C.cardShadow }} className="p-4 cc-panel-in">
            <PanelHeader
              icon={<RefreshCw size={16} />}
              title="Nombre cambiado"
              subtitle={`Detectamos que cambiaste tu nombre de ${pendingRename.oldName} a ${pendingRename.newName}.`}
              onAction={() => setPendingRename(null)}
              actionLabel="Descartar aviso"
              actionTitle="Descartar"
              actionIcon={<X size={14} />}
              style={{ marginBottom: "0.9rem", gap: "0.6rem" }}
            />
            <p style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-xs mb-4">
              ¿Migro tus movimientos (los que están a nombre de {pendingRename.oldName}) a {pendingRename.newName}?
            </p>
            <div className="flex gap-2">
              <button
                onClick={confirmRename}
                disabled={saving}
                style={{ fontFamily: "'Inter', sans-serif", background: C.ink, color: C.paper }}
                className="flex-1 py-2 text-xs uppercase tracking-widest disabled:opacity-40"
              >
                {saving ? "Migrando…" : "Sí, migrar"}
              </button>
              <button
                onClick={() => setPendingRename(null)}
                style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }}
                className="flex-1 py-2 text-xs uppercase tracking-widest"
              >
                No, dejarlo
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Historial de acciones: panel desplegable para deshacer CUALQUIER acción */}
      {history.length > 0 && (
        <div className="max-w-[480px] mx-auto px-5 mb-4">
          <div style={{ border: `1px solid ${C.line}`, background: C.paper, borderRadius: "1.4rem", overflow: "hidden", boxShadow: C.cardShadow }}>
            <PanelHeader
              icon={<History size={16} />}
              title={<>
                Historial de acciones{" "}
                <span style={{ fontWeight: 600, color: C.inkSoft }}>({history.length})</span>
              </>}
              subtitle={historyOpen
                ? "Registro de cambios con opción de deshacer"
                : "Última acción: " + (history[0].by ? history[0].by + ": " : "") + history[0].label}
              onClick={() => setHistoryOpen(!historyOpen)}
              onClickLabel={historyOpen ? "Ocultar historial de acciones" : "Ver historial de acciones"}
              expanded={historyOpen}
              onAction={() => setHistoryOpen(!historyOpen)}
              actionLabel={historyOpen ? "Ocultar historial" : "Abrir historial"}
              actionTitle={historyOpen ? "Ocultar" : "Abrir"}
              actionIcon={historyOpen ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
              actionBorder
              className="p-3.5"
              titleStyle={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
              subtitleStyle={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
            />
            {/* La lista entra deslizándose desde abajo (misma animación que
                el panel de Ajustes), en escritorio y móvil. */}
            {historyOpen && (
              <div style={{ borderTop: `1px solid ${C.line}` }} className="cc-panel-in">
                {history.map((e, i) => {
                  // Separador de día: cambia la etiqueta cuando la acción pasa
                  // a otro día (el historial ya viene ordenado de más reciente
                  // a más antiguo). Las acciones sin fecha válida van juntas.
                  const showDay = i === 0 || dayKey(e.at) !== dayKey(history[i - 1].at);
                  // Reacción del personaje a esta acción: cara de la miniatura
                  // + texto del tooltip que la explica.
                  const reacc = historyReaction(e, transactions, settings.currency);
                  return (
                    <div key={e.eid}>
                      {showDay && (
                        <div
                          style={{
                            padding: "0.45rem 1rem 0.25rem",
                            fontSize: "0.65rem",
                            fontWeight: 700,
                            letterSpacing: "0.06em",
                            textTransform: "uppercase",
                            color: C.inkSoft,
                            backgroundColor: C.field,
                            borderTop: i > 0 ? `1px solid ${C.lineSoft}` : "none",
                            display: "flex",
                            alignItems: "center",
                            gap: "0.4rem",
                          }}
                        >
                          <Calendar size={11} color={C.inkSoft} />
                          <span>{dayLabel(e.at)}</span>
                        </div>
                      )}
                      <div
                        style={{
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "space-between",
                          gap: "0.6rem",
                          padding: "0.6rem 0.95rem",
                          borderTop: i > 0 && !showDay ? `1px solid ${C.lineSoft}` : "none",
                          backgroundColor: "transparent",
                          transition: "background 0.15s ease",
                        }}
                        className="hover:bg-black/[0.02] dark:hover:bg-white/[0.02]"
                      >
                        <div className="min-w-0 flex items-center gap-2.5" style={{ flex: 1 }}>
                          {/* El personaje en miniatura con su reacción */}
                          <span
                            className="cc-mascot-wrap"
                            style={{
                              display: "inline-block",
                              flexShrink: 0,
                              animation: MASCOT_GREET + i * 0.06 + "s both",
                            }}
                          >
                            <div
                              style={{
                                width: isDesktop ? "2.3rem" : "2rem",
                                height: isDesktop ? "2.3rem" : "2rem",
                                borderRadius: "0.65rem",
                                background: C.field,
                                border: `1px solid ${C.lineSoft}`,
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                              }}
                            >
                              <Mascot
                                kind={settings.mascot || "coin"}
                                mood={reacc.mood}
                                size={isDesktop ? 26 : 22}
                                animate={false}
                                effects={false}
                                halo={false}
                              />
                            </div>
                            <span className="cc-mascot-tip" aria-hidden="true">{reacc.text}</span>
                          </span>

                          <div className="min-w-0" style={{ flex: 1 }}>
                            <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", flexWrap: "wrap" }}>
                              {e.by && (
                                <span
                                  style={{
                                    display: "inline-flex",
                                    alignItems: "center",
                                    gap: "0.25rem",
                                    fontSize: "0.68rem",
                                    fontWeight: 700,
                                    color: C.ink,
                                    background: C.chipBg,
                                    padding: "0.08rem 0.4rem",
                                    borderRadius: "999px",
                                    border: `1px solid ${C.lineSoft}`,
                                  }}
                                >
                                  <span style={{ width: "0.45rem", height: "0.45rem", borderRadius: "50%", background: personColor(e.by) }} />
                                  {e.by}
                                </span>
                              )}
                              <span
                                style={{
                                  fontFamily: "'Inter', sans-serif",
                                  fontSize: "0.78rem",
                                  fontWeight: 600,
                                  color: C.ink,
                                  overflow: "hidden",
                                  textOverflow: "ellipsis",
                                  whiteSpace: "nowrap",
                                  maxWidth: "100%",
                                }}
                              >
                                {e.label}
                              </span>
                            </div>

                            <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", marginTop: "0.15rem", fontSize: "0.68rem", color: C.inkSoft }}>
                              <Clock size={10} color={C.inkSoft} />
                              <span>{timeAgo(e.at, nowTick)}</span>
                              {fmtClock(e.at) ? (
                                <button
                                  type="button"
                                  onClick={() => {
                                    setCopiedDetail(false);
                                    setPendingUndo(null);
                                    setHistoryDetail(e);
                                  }}
                                  title="Ver detalle y copiar"
                                  style={{
                                    fontFamily: "'Inter', sans-serif",
                                    background: "none",
                                    border: "none",
                                    padding: 0,
                                    margin: 0,
                                    color: C.inkSoft,
                                    textDecoration: "underline dotted",
                                    textUnderlineOffset: "2px",
                                    cursor: "pointer",
                                    fontSize: "0.68rem",
                                  }}
                                  className="cc-btn-press"
                                >
                                  {" · " + fmtClock(e.at)}
                                </button>
                              ) : ""}
                            </div>
                          </div>
                        </div>

                        <button
                          type="button"
                          onClick={() => askUndo(e)}
                          disabled={saving}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            color: C.ink,
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.55rem",
                            padding: "0.28rem 0.55rem",
                            fontSize: "0.68rem",
                            fontWeight: 700,
                            cursor: "pointer",
                            display: "inline-flex",
                            alignItems: "center",
                            gap: "0.25rem",
                            flexShrink: 0,
                            boxShadow: "0 1px 3px rgba(0,0,0,0.04)",
                          }}
                          className="cc-btn-press hover:bg-black hover:text-white transition-colors disabled:opacity-40"
                          title="Deshacer esta acción"
                        >
                          <Undo2 size={11} />
                          <span>Deshacer</span>
                        </button>
                      </div>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        </div>
      )}

      {/* Confirmación antes de deshacer (evita conflictos en el historial
          compartido). Entra deslizándose desde abajo (cc-panel-in), igual que
          los demás paneles. */}
      {pendingUndo && (
        <div className="max-w-[480px] mx-auto px-5 mb-4">
          <div style={{ border: `1px solid ${C.line}`, background: C.paper, borderRadius: "1.4rem", boxShadow: C.cardShadow }} className="p-4 cc-panel-in">
            <PanelHeader
              icon={<Undo2 size={16} />}
              title={`¿Deshacer «${pendingUndo.label}»${pendingUndo.by ? " (" + pendingUndo.by + ")" : ""}?`}
              subtitle="Esto revierte esta acción en toda la cuenta. Si otra persona ya la deshizo mientras tanto, no se deshará nada."
              onAction={() => setPendingUndo(null)}
              actionLabel="Cancelar deshacer"
              actionTitle="Cancelar"
              actionIcon={<X size={14} />}
              style={{ marginBottom: "0.9rem", gap: "0.6rem" }}
            />
            <div className="flex gap-2">
              <button
                onClick={confirmUndo}
                disabled={saving}
                style={{ fontFamily: "'Inter', sans-serif", background: C.ink, color: C.paper }}
                className="flex-1 py-2 text-xs uppercase tracking-widest disabled:opacity-40"
              >
                {saving ? "Deshaciendo…" : "Sí, deshacer"}
              </button>
              <button
                onClick={() => setPendingUndo(null)}
                style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }}
                className="flex-1 py-2 text-xs uppercase tracking-widest"
              >
                No
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Detalle de una acción del historial (al pulsar su hora exacta) */}
      {historyDetail && (
        <div className="max-w-[480px] mx-auto px-5 mb-4">
          <div
            style={{ border: `1px solid ${C.line}`, background: C.paper, borderRadius: "1.4rem", boxShadow: C.cardShadow }}
            className="p-4 cc-panel-in"
          >
            <PanelHeader
              icon={<Receipt size={16} />}
              title="Detalle de la acción"
              subtitle="Quién, qué y cuándo"
              onAction={() => setHistoryDetail(null)}
              actionLabel="Cerrar detalle"
              actionTitle="Cerrar"
              actionIcon={<X size={14} />}
              style={{ marginBottom: "0.9rem", gap: "0.6rem" }}
            />
            <div className="flex flex-col gap-1.5 text-xs">
              <div className="flex gap-2">
                <span style={{ color: C.inkSoft, flexShrink: 0, width: "3.2rem" }}>Quién</span>
                <span style={{ color: C.ink, wordBreak: "break-word" }}>{historyDetail.by || "Alguien"}</span>
              </div>
              <div className="flex gap-2">
                <span style={{ color: C.inkSoft, flexShrink: 0, width: "3.2rem" }}>Qué</span>
                <span style={{ color: C.ink, wordBreak: "break-word" }}>{historyDetail.label}</span>
              </div>
              <div className="flex gap-2">
                <span style={{ color: C.inkSoft, flexShrink: 0, width: "3.2rem" }}>Cuándo</span>
                <span style={{ color: C.ink }}>{dayLabel(historyDetail.at)} {fmtClock(historyDetail.at)}</span>
              </div>
            </div>
            <button
              type="button"
              onClick={() => copyHistoryDetail(historyDetail)}
              style={{ fontFamily: "'Inter', sans-serif", background: C.ink, color: C.paper }}
              className="mt-4 w-full py-2 text-xs uppercase tracking-widest cc-btn-press"
            >
              {copiedDetail ? "¡Copiado!" : "Copiar detalle"}
            </button>
          </div>
        </div>
      )}

      {/* Visor de la foto del recibo (lightbox): el fondo funde su oscuridad
          (cc-fade-in, igual que el fondo del modal) y la cabecera + la foto
          entran subiendo un tramo (cc-panel-in, la misma animación que los
          demás paneles). */}
      {viewPhoto && (
        <div
          onClick={() => setViewPhoto(null)}
          className="cc-fade-in"
          style={{
            position: "fixed",
            inset: 0,
            background: "rgba(15, 23, 42, 0.92)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 70,
            padding: "1rem",
            cursor: "zoom-out",
          }}
        >
          {/* Cabecera del lightbox: PanelHeader en tono claro sobre el fondo oscuro */}
          <div
            onClick={(ev) => ev.stopPropagation()}
            className="cc-panel-in"
            style={{ position: "fixed", top: "1rem", left: 0, right: 0, maxWidth: "480px", margin: "0 auto", padding: "0 1rem" }}
          >
            <PanelHeader
              icon={<Camera size={16} />}
              title="Foto del recibo"
              subtitle="Toca la foto para cerrar"
              titleStyle={{ color: "#FFFFFF" }}
              subtitleColor="rgba(255,255,255,0.85)"
              onAction={() => setViewPhoto(null)}
              actionLabel="Cerrar foto"
              actionTitle="Cerrar"
              actionIcon={<X size={14} />}
              actionBg="rgba(255,255,255,0.15)"
              actionColor="#FFFFFF"
            />
          </div>
          <img
            src={viewPhoto}
            alt="Recibo ampliado"
            className="cc-panel-in"
            style={{ maxWidth: "100%", maxHeight: "100%", borderRadius: "0.75rem", boxShadow: "0 20px 60px rgb(0 0 0 / 0.5)" }}
          />
        </div>
      )}

      {/* Modal de Ajustes: centrado y flotante a pantalla completa */}
      
      {/* ===== MODAL FLOTANTE DE TRANSFERENCIA DE DATOS PROPIOS (position: fixed) ===== */}
      {showTransferDataModal && (() => {
        const myTx = (transactions || []).filter(t => t.person === person || t.recipient === person);
        const myCards = (settings.cards || []).filter(c => c.person === person);
        const myInstallments = (settings.installments || []).filter(i => i.person === person);
        const knownTargetSpaces = getKnownAccounts().filter(a => a && (a.id !== getActiveAccountId() && a.code !== activeAccountCode));

        const handleExecuteTransfer = async () => {
          if (!transferTargetSpace || !transferTargetSpace.trim()) {
            alert("Por favor selecciona o ingresa el código del espacio destino.");
            return;
          }
          const currentAcc = getActiveAccountId();
          const cleanTarget = transferTargetSpace.trim();
          if (cleanTarget === currentAcc || (currentAcc === "default" && cleanTarget.toUpperCase() === "DEFAULT")) {
            alert("⚠️ El espacio de destino debe ser diferente a la cuenta actual donde te encuentras.");
            return;
          }
          suppressNoticesUntil = Date.now() + 25000;
          setTransferLoading(true);
          setTransferProgressText("Preparando datos...");
          try {
            const cleanTarget = transferTargetSpace.trim();
            let targetAcc = getKnownAccounts().find(a => a && (a.id === cleanTarget || a.code === cleanTarget));
            if (!targetAcc) {
              targetAcc = await storage.findAccount(cleanTarget);
            }
            const targetAccId = targetAcc ? targetAcc.id : cleanTarget;
            const targetAccName = targetAcc ? targetAcc.name : cleanTarget;

            let copiedTxCount = 0;
            let copiedCardsCount = 0;
            let copiedInstCount = 0;

            // 1. Transferir Movimientos en Batch Ultra Rápido (1 solo request)
            if (transferIncludeTx && myTx.length > 0) {
              setTransferProgressText(`Transfiriendo ${myTx.length} movimientos a ${targetAccName}...`);
              const batchRows = myTx.map(t => {
                const newId = transferMode === "move" ? t.id : "tx_" + uid();
                suppressedTxIdSet.add(String(t.id));
                suppressedTxIdSet.add(String(newId));
                const clonedTx = {
                  ...t,
                  id: newId,
                  accountId: targetAccId,
                };
                return txToSupabaseRow(clonedTx, targetAccId);
              });
              // Limpieza automática del Set después de 60 segundos
              setTimeout(() => {
                myTx.forEach(t => suppressedTxIdSet.delete(String(t.id)));
                batchRows.forEach(r => suppressedTxIdSet.delete(String(r.id)));
              }, 60000);

              if (supabaseOk()) {
                try {
                  await supabaseFetch("transactions?on_conflict=id", {
                    method: "POST",
                    headers: { Prefer: "resolution=merge-duplicates" },
                    body: JSON.stringify(batchRows),
                  });
                } catch (e) {}
              }

              if (transferMode === "move") {
                for (const t of myTx) {
                  await storage.deleteTransaction(t.id).catch(() => {});
                }
              }
              copiedTxCount = myTx.length;
            }

            // 1.5. Asegurar que la persona esté registrada en el espacio de destino
            if (person && String(person).trim()) {
              const pClean = String(person).trim();
              if (supabaseOk()) {
                try {
                  await supabaseFetch("people?on_conflict=name,account_id", {
                    method: "POST",
                    headers: { Prefer: "resolution=merge-duplicates" },
                    body: JSON.stringify({ name: pClean, account_id: targetAccId }),
                  }).catch(async () => {
                    await supabaseFetch("people?on_conflict=name", {
                      method: "POST",
                      headers: { Prefer: "resolution=merge-duplicates" },
                      body: JSON.stringify({ name: pClean }),
                    }).catch(() => {});
                  });
                } catch (e) {}
              }
            }

            // 2. Transferir Tarjetas y Cuotas
            if (transferIncludeCards || transferIncludeInstallments) {
              setTransferProgressText("Sincronizando tarjetas y cuotas...");
              let targetSettings = null;
              if (supabaseOk()) {
                try {
                  const sRows = await supabaseFetch(`settings?account_id=eq.${encodeURIComponent(targetAccId)}&select=*`);
                  if (Array.isArray(sRows) && sRows.length > 0) targetSettings = sRows[0];
                } catch (e) {}
              }
              if (!targetSettings && targetAccId === "default") {
                try {
                  const sRows = await supabaseFetch("settings?select=*");
                  if (Array.isArray(sRows) && sRows.length > 0) targetSettings = sRows[0];
                } catch (e) {}
              }
              const baseCards = targetSettings && Array.isArray(targetSettings.cards) ? targetSettings.cards : [];
              const baseInst = targetSettings && Array.isArray(targetSettings.installments) ? targetSettings.installments : [];

              const newCards = transferIncludeCards
                ? [...baseCards.filter(c => !myCards.some(mc => mc.name.toLowerCase() === c.name.toLowerCase())), ...myCards]
                : baseCards;
              const newInst = transferIncludeInstallments
                ? [...baseInst.filter(i => !myInstallments.some(mi => mi.name.toLowerCase() === i.name.toLowerCase())), ...myInstallments]
                : baseInst;

              copiedCardsCount = transferIncludeCards ? myCards.length : 0;
              copiedInstCount = transferIncludeInstallments ? myInstallments.length : 0;

              if (supabaseOk()) {
                try {
                  await supabaseFetch("settings?on_conflict=account_id", {
                    method: "POST",
                    headers: { Prefer: "resolution=merge-duplicates" },
                    body: JSON.stringify({
                      id: 1,
                      account_id: targetAccId,
                      cards: newCards,
                      installments: newInst,
                      account_name: targetAccName || "Cuenta compartida",
                    }),
                  });
                } catch (e) {}
              }
            }

            setTransferLoading(false);
            setTransferProgressText("");
            setTransferStatus({
              type: "success",
              text: `¡Listo! Se ${transferMode === "move" ? "movieron" : "copiaron"} ${copiedTxCount} movimientos, ${copiedCardsCount} tarjetas y ${copiedInstCount} cuotas a "${targetAccName}".`,
              targetCode: targetAcc ? (targetAcc.code || targetAcc.id) : cleanTarget,
            });

            refreshShared(true);
          } catch (err) {
            setTransferLoading(false);
            setTransferProgressText("");
            alert("Ocurrió un error al transferir los datos: " + (err.message || err));
          }
        };

        return (
          <div
            role="dialog"
            aria-modal="true"
            aria-label="Pasar mis datos a otro espacio"
            onClick={() => setShowTransferDataModal(false)}
            style={{
              position: "fixed",
              inset: 0,
              zIndex: 9999,
              backgroundColor: "rgba(0, 0, 0, 0.65)",
              backdropFilter: "blur(6px)",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              padding: "1rem",
            }}
          >
            <div
              onClick={(e) => e.stopPropagation()}
              className="cc-panel-in"
              style={{
                backgroundColor: C.paper,
                borderRadius: "1.4rem",
                padding: "1.4rem",
                boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
                border: `1px solid ${C.line}`,
                maxWidth: "460px",
                width: "100%",
                boxSizing: "border-box",
                display: "flex",
                flexDirection: "column",
                gap: "0.85rem",
                maxHeight: "90vh",
                overflowY: "auto",
              }}
            >
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                  <span style={{ fontSize: "1.3rem" }}>📦</span>
                  <div>
                    <h3 style={{ margin: 0, fontSize: "1.05rem", fontWeight: 700, color: C.ink }}>
                      Pasar mis datos a otro espacio
                    </h3>
                    <span style={{ fontSize: "0.7rem", color: C.inkSoft }}>
                      Usuario actual: <strong style={{ color: C.green }}>{person}</strong>
                    </span>
                  </div>
                </div>
                <button
                  type="button"
                  onClick={() => setShowTransferDataModal(false)}
                  style={{ background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.3rem" }}
                >
                  <X size={16} />
                </button>
              </div>

              {transferStatus ? (
                <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem", padding: "0.5rem 0" }}>
                  <div
                    style={{
                      padding: "0.85rem",
                      borderRadius: "0.75rem",
                      background: darkMode ? "#064E3B" : "#ECFDF5",
                      color: darkMode ? "#6EE7B7" : "#047857",
                      border: `1px solid ${C.green}`,
                      fontSize: "0.82rem",
                      fontWeight: 600,
                    }}
                  >
                    🎉 {transferStatus.text}
                  </div>
                  <button
                    type="button"
                    onClick={() => {
                      setShowTransferDataModal(false);
                      handleSwitchSpace(transferStatus.targetCode);
                    }}
                    style={{
                      background: BRAND_GRADIENT,
                      color: "#FFF",
                      border: "none",
                      borderRadius: "0.75rem",
                      padding: "0.65rem",
                      fontSize: "0.85rem",
                      fontWeight: 800,
                      cursor: "pointer",
                    }}
                    className="cc-btn-press"
                  >
                    👉 Ir a ese espacio ahora
                  </button>
                </div>
              ) : (
                <>
                  {/* Selector de Destino */}
                  <div>
                    <label style={{ fontSize: "0.75rem", fontWeight: 700, color: C.inkSoft, display: "block", marginBottom: "0.3rem" }}>
                      Selecciona el espacio de destino:
                    </label>
                    {knownTargetSpaces.length > 0 ? (
                      <select
                        value={transferTargetSpace}
                        onChange={(e) => setTransferTargetSpace(e.target.value)}
                        style={{
                          width: "100%",
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.65rem",
                          padding: "0.55rem 0.75rem",
                          fontSize: "0.85rem",
                          color: C.ink,
                          boxSizing: "border-box",
                        }}
                      >
                        {knownTargetSpaces.map((a) => (
                          <option key={a.id || a.code} value={a.code || a.id}>
                            {a.name || "Espacio"} (#{a.code || a.id})
                          </option>
                        ))}
                      </select>
                    ) : (
                      <input
                        type="text"
                        value={transferTargetSpace}
                        onChange={(e) => setTransferTargetSpace(e.target.value)}
                        placeholder="Ingresa el código del espacio (Ej. DEFAULT o CC-XXXX)"
                        style={{
                          width: "100%",
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.65rem",
                          padding: "0.55rem 0.75rem",
                          fontSize: "0.85rem",
                          color: C.ink,
                          boxSizing: "border-box",
                        }}
                      />
                    )}
                  </div>

                  {/* Qué elementos incluir */}
                  <div style={{ background: C.field, padding: "0.75rem", borderRadius: "0.75rem", display: "flex", flexDirection: "column", gap: "0.45rem" }}>
                    <span style={{ fontSize: "0.72rem", fontWeight: 700, color: C.inkSoft, textTransform: "uppercase" }}>
                      Datos detectados de {person}:
                    </span>

                    <label style={{ display: "flex", alignItems: "center", gap: "0.45rem", fontSize: "0.8rem", color: C.ink, cursor: "pointer" }}>
                      <input
                        type="checkbox"
                        checked={transferIncludeTx}
                        onChange={(e) => setTransferIncludeTx(e.target.checked)}
                      />
                      <span>💰 {myTx.length} movimientos (gastos e ingresos propios)</span>
                    </label>

                    <label style={{ display: "flex", alignItems: "center", gap: "0.45rem", fontSize: "0.8rem", color: C.ink, cursor: "pointer" }}>
                      <input
                        type="checkbox"
                        checked={transferIncludeCards}
                        onChange={(e) => setTransferIncludeCards(e.target.checked)}
                      />
                      <span>💳 {myCards.length} tarjetas bancarias propias</span>
                    </label>

                    <label style={{ display: "flex", alignItems: "center", gap: "0.45rem", fontSize: "0.8rem", color: C.ink, cursor: "pointer" }}>
                      <input
                        type="checkbox"
                        checked={transferIncludeInstallments}
                        onChange={(e) => setTransferIncludeInstallments(e.target.checked)}
                      />
                      <span>📅 {myInstallments.length} compras en cuotas</span>
                    </label>
                  </div>

                  {/* Acción Copiar vs Mover */}
                  <div style={{ display: "flex", gap: "0.5rem" }}>
                    <button
                      type="button"
                      onClick={() => setTransferMode("copy")}
                      style={{
                        flex: 1,
                        background: transferMode === "copy" ? (darkMode ? "#064E3B" : "#ECFDF5") : C.field,
                        border: `1.5px solid ${transferMode === "copy" ? C.green : C.line}`,
                        borderRadius: "0.65rem",
                        padding: "0.5rem",
                        fontSize: "0.75rem",
                        fontWeight: 700,
                        color: transferMode === "copy" ? C.green : C.inkSoft,
                        cursor: "pointer",
                      }}
                    >
                      📋 Copiar (conservar aquí)
                    </button>
                    <button
                      type="button"
                      onClick={() => setTransferMode("move")}
                      style={{
                        flex: 1,
                        background: transferMode === "move" ? (darkMode ? "#451A03" : "#FEF3C7") : C.field,
                        border: `1.5px solid ${transferMode === "move" ? "#F59E0B" : C.line}`,
                        borderRadius: "0.65rem",
                        padding: "0.5rem",
                        fontSize: "0.75rem",
                        fontWeight: 700,
                        color: transferMode === "move" ? "#D97706" : C.inkSoft,
                        cursor: "pointer",
                      }}
                    >
                      📦 Mover (quitar de aquí)
                    </button>
                  </div>

                  {transferLoading ? (
                    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "0.4rem", padding: "0.8rem", background: C.field, borderRadius: "0.75rem" }}>
                      <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", color: C.green, fontWeight: 700, fontSize: "0.82rem" }}>
                        <RefreshCw size={15} style={{ animation: "cc-spin 0.8s linear infinite" }} />
                        <span>Transfiriendo datos...</span>
                      </div>
                      <span style={{ fontSize: "0.72rem", color: C.inkSoft }}>
                        {transferProgressText || "Por favor espera un segundo..."}
                      </span>
                    </div>
                  ) : (
                    <button
                      type="button"
                      disabled={transferLoading}
                      onClick={handleExecuteTransfer}
                      style={{
                        background: BRAND_GRADIENT,
                        color: "#FFF",
                        border: "none",
                        borderRadius: "0.75rem",
                        padding: "0.65rem",
                        fontSize: "0.85rem",
                        fontWeight: 800,
                        cursor: "pointer",
                        boxShadow: "0 4px 12px rgba(16,185,129,0.35)",
                        marginTop: "0.4rem",
                      }}
                      className="cc-btn-press"
                    >
                      🚀 Transferir mis datos ahora
                    </button>
                  )}
                </>
              )}
            </div>
          </div>
        );
      })()}

      {/* ===== MODAL FLOTANTE DE TRANSFERENCIAS MÍNIMAS (position: fixed) ===== */}
      {showMinimalTransfersModal && (() => {
        const netBalancesMap = {};
        (people || []).forEach(p => { netBalancesMap[p] = 0; });
        (peerDebts.list || []).forEach(b => {
          if (b && b.from && b.to && b.amount > 0) {
            netBalancesMap[b.from] = (netBalancesMap[b.from] || 0) - Number(b.amount);
            netBalancesMap[b.to] = (netBalancesMap[b.to] || 0) + Number(b.amount);
          }
        });
        const optimalTransfers = calculateMinimalTransfers(netBalancesMap, settings.currency);

        return (
          <div
            role="dialog"
            aria-modal="true"
            aria-label="Transferencias mínimas sugeridas"
            onClick={() => setShowMinimalTransfersModal(false)}
            style={{
              position: "fixed",
              inset: 0,
              zIndex: 9999,
              backgroundColor: "rgba(0, 0, 0, 0.65)",
              backdropFilter: "blur(6px)",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              padding: "1rem",
            }}
          >
            <div
              onClick={(e) => e.stopPropagation()}
              className="cc-panel-in"
              style={{
                backgroundColor: C.paper,
                borderRadius: "1.4rem",
                padding: "1.4rem",
                boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
                border: `1px solid ${C.line}`,
                maxWidth: "460px",
                width: "100%",
                boxSizing: "border-box",
                display: "flex",
                flexDirection: "column",
                gap: "0.85rem",
                maxHeight: "90vh",
                overflowY: "auto",
              }}
            >
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
                  <div style={{ width: "2.2rem", height: "2.2rem", borderRadius: "0.75rem", background: BRAND_GRADIENT, color: "#FFF", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "1.1rem" }}>
                    ⚡
                  </div>
                  <div>
                    <h3 style={{ margin: 0, fontSize: "1.05rem", fontWeight: 700, color: C.ink }}>
                      Transferencias Mínimas Óptimas
                    </h3>
                    <span style={{ fontSize: "0.7rem", color: C.inkSoft }}>
                      Saldar todas las deudas con la menor cantidad de pasos
                    </span>
                  </div>
                </div>
                <button
                  type="button"
                  onClick={() => setShowMinimalTransfersModal(false)}
                  style={{ background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.3rem" }}
                >
                  <X size={16} />
                </button>
              </div>

              {optimalTransfers.length === 0 ? (
                <div style={{ textAlign: "center", padding: "1.5rem 0", color: C.inkSoft }}>
                  <div style={{ fontSize: "2rem", marginBottom: "0.3rem" }}>✨</div>
                  <p style={{ margin: 0, fontWeight: 700, color: C.green }}>¡Todos están al día!</p>
                  <p style={{ margin: "0.2rem 0 0", fontSize: "0.75rem" }}>No se requiere ninguna transferencia.</p>
                </div>
              ) : (
                <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
                  <p style={{ margin: 0, fontSize: "0.75rem", color: C.inkSoft }}>
                    Con solo <strong>{optimalTransfers.length} {optimalTransfers.length === 1 ? "transferencia directa" : "transferencias directas"}</strong> se saldan todas las cuentas del grupo:
                  </p>
                  {optimalTransfers.map((t, idx) => (
                    <div
                      key={idx}
                      style={{
                        background: C.field,
                        border: `1px solid ${C.lineSoft}`,
                        borderRadius: "0.75rem",
                        padding: "0.75rem 0.9rem",
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "space-between",
                        gap: "0.5rem",
                      }}
                    >
                      <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", minWidth: 0 }}>
                        <span style={{ fontWeight: 800, color: C.ink, fontSize: "0.82rem" }}>{t.from}</span>
                        <span style={{ color: C.inkSoft, fontSize: "0.75rem" }}>→ le paga a →</span>
                        <span style={{ fontWeight: 800, color: C.green, fontSize: "0.82rem" }}>{t.to}</span>
                      </div>
                      <div style={{ fontWeight: 900, color: C.ink, fontSize: "0.95rem", whiteSpace: "nowrap" }}>
                        {fmtMoney(t.amount, t.currency)}
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          </div>
        );
      })()}

      {/* ===== MODAL FLOTANTE DE NUEVA META DE AHORRO (position: fixed) ===== */}
      {showSavingsModal && (
        <div
          role="dialog"
          aria-modal="true"
          aria-label="Nueva Meta de Ahorro"
          onClick={() => setShowSavingsModal(false)}
          style={{
            position: "fixed",
            inset: 0,
            zIndex: 9999,
            backgroundColor: "rgba(0, 0, 0, 0.65)",
            backdropFilter: "blur(6px)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            padding: "1rem",
          }}
        >
          <div
            onClick={(e) => e.stopPropagation()}
            className="cc-panel-in"
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.4rem",
              padding: "1.4rem",
              boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
              border: `1px solid ${C.line}`,
              maxWidth: "420px",
              width: "100%",
              boxSizing: "border-box",
              display: "flex",
              flexDirection: "column",
              gap: "0.85rem",
            }}
          >
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
              <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                <span style={{ fontSize: "1.3rem" }}>🎯</span>
                <h3 style={{ margin: 0, fontSize: "1.05rem", fontWeight: 700, color: C.ink }}>
                  Nueva Meta de Ahorro
                </h3>
              </div>
              <button
                type="button"
                onClick={() => setShowSavingsModal(false)}
                style={{ background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.3rem" }}
              >
                <X size={16} />
              </button>
            </div>

            <div>
              <label style={{ fontSize: "0.75rem", fontWeight: 700, color: C.inkSoft, display: "block", marginBottom: "0.25rem" }}>
                Nombre del proyecto o meta:
              </label>
              <input
                type="text"
                value={savingsDraft.name}
                onChange={(e) => setSavingsDraft({ ...savingsDraft, name: e.target.value })}
                placeholder="Ej. Vacaciones, Fondo de emergencia, Mueble"
                style={{
                  width: "100%",
                  background: C.field,
                  border: `1px solid ${C.line}`,
                  borderRadius: "0.65rem",
                  padding: "0.55rem 0.75rem",
                  fontSize: "0.85rem",
                  color: C.ink,
                  boxSizing: "border-box",
                }}
              />
            </div>

            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.6rem" }}>
              <div>
                <label style={{ fontSize: "0.75rem", fontWeight: 700, color: C.inkSoft, display: "block", marginBottom: "0.25rem" }}>
                  Monto Objetivo ({settings.currency}):
                </label>
                <input
                  type="number"
                  step="any"
                  value={savingsDraft.targetAmount}
                  onChange={(e) => setSavingsDraft({ ...savingsDraft, targetAmount: e.target.value })}
                  placeholder="0.00"
                  style={{
                    width: "100%",
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.65rem",
                    padding: "0.55rem 0.75rem",
                    fontSize: "0.85rem",
                    color: C.ink,
                    boxSizing: "border-box",
                  }}
                />
              </div>

              <div>
                <label style={{ fontSize: "0.75rem", fontWeight: 700, color: C.inkSoft, display: "block", marginBottom: "0.25rem" }}>
                  Ahorro Inicial ({settings.currency}):
                </label>
                <input
                  type="number"
                  step="any"
                  value={savingsDraft.currentAmount}
                  onChange={(e) => setSavingsDraft({ ...savingsDraft, currentAmount: e.target.value })}
                  placeholder="0.00"
                  style={{
                    width: "100%",
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.65rem",
                    padding: "0.55rem 0.75rem",
                    fontSize: "0.85rem",
                    color: C.ink,
                    boxSizing: "border-box",
                  }}
                />
              </div>
            </div>

            <div>
              <label style={{ fontSize: "0.75rem", fontWeight: 700, color: C.inkSoft, display: "block", marginBottom: "0.25rem" }}>
                Fecha objetivo (opcional):
              </label>
              <input
                type="date"
                value={savingsDraft.targetDate}
                onChange={(e) => setSavingsDraft({ ...savingsDraft, targetDate: e.target.value })}
                style={{
                  width: "100%",
                  background: C.field,
                  border: `1px solid ${C.line}`,
                  borderRadius: "0.65rem",
                  padding: "0.55rem 0.75rem",
                  fontSize: "0.85rem",
                  color: C.ink,
                  boxSizing: "border-box",
                }}
              />
            </div>

            <button
              type="button"
              onClick={() => {
                if (!savingsDraft.name.trim()) {
                  alert("Ingresa un nombre para la meta de ahorro.");
                  return;
                }
                const target = Number(savingsDraft.targetAmount);
                if (isNaN(target) || target <= 0) {
                  alert("Ingresa un monto objetivo válido.");
                  return;
                }
                const newGoal = {
                  id: "goal_" + uid(),
                  name: savingsDraft.name.trim(),
                  targetAmount: target,
                  currentAmount: Number(savingsDraft.currentAmount) || 0,
                  targetDate: savingsDraft.targetDate || undefined,
                  createdAt: new Date().toISOString(),
                };
                const updatedGoals = [...(settings.savingsGoals || []), newGoal];
                updateSettings({ savingsGoals: updatedGoals });
                setShowSavingsModal(false);
              }}
              style={{
                background: BRAND_GRADIENT,
                color: "#FFFFFF",
                border: "none",
                borderRadius: "0.75rem",
                padding: "0.65rem",
                fontSize: "0.85rem",
                fontWeight: 800,
                cursor: "pointer",
                boxShadow: "0 4px 12px rgba(16,185,129,0.4)",
                marginTop: "0.4rem",
              }}
              className="cc-btn-press"
            >
              Guardar Meta de Ahorro
            </button>
          </div>
        </div>
      )}

      {/* ===== MODAL FLOTANTE DE INVITACIÓN (position: fixed) ===== */}
      {showInviteModal && (() => {
        const isAccountAdmin = !settings.admin || (myName && String(myName).toLowerCase() === String(settings.admin).toLowerCase()) || (people && people.length <= 1);
        const canInvite = isAccountAdmin || settings.allowMemberInvites === true;

        const effectiveCode = (currentInviteToken && currentInviteToken.code) ? currentInviteToken.code : (activeAccountCode || "DEFAULT");
        const effectiveLink = `${typeof window !== "undefined" ? window.location.origin : ""}/?cuenta=${encodeURIComponent(activeAccountCode || "DEFAULT")}${currentInviteToken ? `&inv=${encodeURIComponent(currentInviteToken.id)}` : ""}`;

        return (
          <div
            role="dialog"
            aria-modal="true"
            aria-label="Invitar personas al espacio"
            onClick={() => setShowInviteModal(false)}
            style={{
              position: "fixed",
              inset: 0,
              zIndex: 9999,
              backgroundColor: "rgba(0, 0, 0, 0.65)",
              backdropFilter: "blur(6px)",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              padding: "1rem",
            }}
          >
            <div
              onClick={(e) => e.stopPropagation()}
              className="cc-panel-in"
              style={{
                backgroundColor: C.paper,
                borderRadius: "1.4rem",
                padding: "1.4rem",
                boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
                border: `1px solid ${C.line}`,
                maxWidth: "490px",
                width: "100%",
                boxSizing: "border-box",
                display: "flex",
                flexDirection: "column",
                gap: "0.85rem",
                maxHeight: "90vh",
                overflowY: "auto",
              }}
            >
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
                  <div style={{ width: "2.2rem", height: "2.2rem", borderRadius: "0.75rem", background: BRAND_GRADIENT, color: "#FFF", display: "flex", alignItems: "center", justifyContent: "center" }}>
                    <Users size={16} />
                  </div>
                  <div>
                    <h3 style={{ margin: 0, fontSize: "1.05rem", fontWeight: 700, color: C.ink }}>
                      Invitar a {settings.accountName || "este espacio"}
                    </h3>
                    <span style={{ fontSize: "0.7rem", color: C.inkSoft }}>
                      Administrador: <strong style={{ color: C.green }}>{settings.admin || (people[0] || "Tú")}</strong>
                    </span>
                  </div>
                </div>
                <button
                  type="button"
                  onClick={() => setShowInviteModal(false)}
                  style={{ background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.3rem" }}
                >
                  <X size={16} />
                </button>
              </div>

              {!canInvite ? (
                /* Bloque cuando el miembro NO tiene permiso */
                <div style={{ background: darkMode ? "#451A03" : "#FEF3C7", border: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`, borderRadius: "0.85rem", padding: "1rem", textAlign: "center" }}>
                  <div style={{ fontSize: "1.5rem", marginBottom: "0.3rem" }}>🔒</div>
                  <div style={{ fontSize: "0.85rem", fontWeight: 700, color: darkMode ? "#FDE68A" : "#92400E", marginBottom: "0.3rem" }}>
                    Permisos de Administrador Requeridos
                  </div>
                  <p style={{ margin: 0, fontSize: "0.73rem", color: darkMode ? "#FCD34D" : "#B45309", lineHeight: 1.45 }}>
                    Solo el administrador (<strong>{settings.admin || "el creador"}</strong>) puede generar enlaces de invitación para este espacio.
                  </p>
                </div>
              ) : (
                <>
                  {/* Selector de tipo de invitación */}
                  <div style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.85rem", padding: "0.75rem 0.85rem" }}>
                    <span style={{ fontSize: "0.7rem", fontWeight: 700, color: C.inkSoft, textTransform: "uppercase", display: "block", marginBottom: "0.45rem" }}>
                      1. Restricciones de la invitación:
                    </span>
                    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.35rem", marginBottom: "0.6rem" }}>
                      <button
                        type="button"
                        onClick={() => setInviteLimitType("single")}
                        style={{
                          background: inviteLimitType === "single" ? C.paper : "transparent",
                          border: `1.5px solid ${inviteLimitType === "single" ? C.green : C.line}`,
                          borderRadius: "0.6rem",
                          padding: "0.45rem 0.25rem",
                          textAlign: "center",
                          cursor: "pointer",
                          color: inviteLimitType === "single" ? C.ink : C.inkSoft,
                        }}
                      >
                        <div style={{ fontSize: "0.85rem" }}>👤</div>
                        <div style={{ fontSize: "0.68rem", fontWeight: 700 }}>1 persona</div>
                        <div style={{ fontSize: "0.6rem", color: C.inkSoft }}>1 solo uso</div>
                      </button>

                      <button
                        type="button"
                        onClick={() => setInviteLimitType("count")}
                        style={{
                          background: inviteLimitType === "count" ? C.paper : "transparent",
                          border: `1.5px solid ${inviteLimitType === "count" ? C.green : C.line}`,
                          borderRadius: "0.6rem",
                          padding: "0.45rem 0.25rem",
                          textAlign: "center",
                          cursor: "pointer",
                          color: inviteLimitType === "count" ? C.ink : C.inkSoft,
                        }}
                      >
                        <div style={{ fontSize: "0.85rem" }}>👥</div>
                        <div style={{ fontSize: "0.68rem", fontWeight: 700 }}>Por cantidad</div>
                        <div style={{ fontSize: "0.6rem", color: C.inkSoft }}>Límite personas</div>
                      </button>

                      <button
                        type="button"
                        onClick={() => setInviteLimitType("time")}
                        style={{
                          background: inviteLimitType === "time" ? C.paper : "transparent",
                          border: `1.5px solid ${inviteLimitType === "time" ? C.green : C.line}`,
                          borderRadius: "0.6rem",
                          padding: "0.45rem 0.25rem",
                          textAlign: "center",
                          cursor: "pointer",
                          color: inviteLimitType === "time" ? C.ink : C.inkSoft,
                        }}
                      >
                        <div style={{ fontSize: "0.85rem" }}>⏳</div>
                        <div style={{ fontSize: "0.68rem", fontWeight: 700 }}>Por tiempo</div>
                        <div style={{ fontSize: "0.6rem", color: C.inkSoft }}>Expira en horas</div>
                      </button>
                    </div>

                    {/* Parámetro adicional según tipo */}
                    {inviteLimitType === "count" && (
                      <div className="flex items-center justify-between gap-2 mt-2 pt-2" style={{ borderTop: `1px dashed ${C.line}` }}>
                        <span style={{ fontSize: "0.72rem", color: C.ink, fontWeight: 600 }}>Máximo de personas:</span>
                        <select
                          value={inviteMaxCount}
                          onChange={(e) => setInviteMaxCount(Number(e.target.value))}
                          style={{ background: C.paper, border: `1px solid ${C.line}`, borderRadius: "0.5rem", padding: "0.25rem 0.5rem", fontSize: "0.75rem", color: C.ink, fontWeight: 700 }}
                        >
                          <option value={2}>2 personas</option>
                          <option value={3}>3 personas</option>
                          <option value={5}>5 personas</option>
                          <option value={10}>10 personas</option>
                        </select>
                      </div>
                    )}

                    {inviteLimitType === "time" && (
                      <div className="flex items-center justify-between gap-2 mt-2 pt-2" style={{ borderTop: `1px dashed ${C.line}` }}>
                        <span style={{ fontSize: "0.72rem", color: C.ink, fontWeight: 600 }}>Tiempo de validez:</span>
                        <select
                          value={inviteExpireHours}
                          onChange={(e) => setInviteExpireHours(Number(e.target.value))}
                          style={{ background: C.paper, border: `1px solid ${C.line}`, borderRadius: "0.5rem", padding: "0.25rem 0.5rem", fontSize: "0.75rem", color: C.ink, fontWeight: 700 }}
                        >
                          <option value={1}>1 hora</option>
                          <option value={12}>12 horas</option>
                          <option value={24}>24 horas (1 día)</option>
                          <option value={72}>3 días</option>
                          <option value={168}>7 días</option>
                        </select>
                      </div>
                    )}

                    <button
                      type="button"
                      onClick={handleGenerateCustomInvite}
                      style={{
                        width: "100%",
                        marginTop: "0.5rem",
                        background: BRAND_GRADIENT,
                        color: "#FFFFFF",
                        border: "none",
                        borderRadius: "0.65rem",
                        padding: "0.55rem",
                        fontSize: "0.75rem",
                        fontWeight: 700,
                        cursor: "pointer",
                      }}
                      className="cc-btn-press"
                    >
                      ✨ Generar nuevo enlace con estas restricciones
                    </button>
                  </div>

                  {/* Tarjeta de Código Generado */}
                  <div style={{ background: C.field, border: `1.5px solid ${C.lineSoft}`, borderRadius: "0.85rem", padding: "0.75rem 0.85rem", textAlign: "center" }}>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.25rem" }}>
                      <span style={{ fontSize: "0.65rem", fontWeight: 700, color: C.inkSoft, textTransform: "uppercase" }}>
                        Código de invitación:
                      </span>
                      {currentInviteToken && (
                        <span style={{ fontSize: "0.62rem", background: C.successBg, color: C.green, fontWeight: 800, padding: "0.1rem 0.4rem", borderRadius: "999px" }}>
                          ✓ {currentInviteToken.type === "single" ? "1 solo uso" : currentInviteToken.type === "count" ? `Hasta ${currentInviteToken.maxUses} personas` : `Válido ${inviteExpireHours}h`}
                        </span>
                      )}
                    </div>
                    <div style={{ fontSize: "1.45rem", fontWeight: 900, color: C.green, letterSpacing: "0.1em", fontFamily: "monospace" }}>
                      {effectiveCode}
                    </div>
                  </div>

                  {/* Enlace directo */}
                  <div>
                    <div style={{ display: "flex", gap: "0.4rem" }}>
                      <input
                        readOnly
                        value={effectiveLink}
                        style={{
                          flex: 1,
                          minWidth: 0,
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.65rem",
                          padding: "0.5rem 0.7rem",
                          fontSize: "0.72rem",
                          color: C.ink,
                        }}
                      />
                      <button
                        type="button"
                        onClick={() => {
                          navigator.clipboard.writeText(effectiveLink);
                          setInviteCopied(true);
                          setTimeout(() => setInviteCopied(false), 2500);
                        }}
                        style={{
                          background: inviteCopied ? "#059669" : C.ink,
                          color: "#FFFFFF",
                          border: "none",
                          borderRadius: "0.65rem",
                          padding: "0.5rem 0.75rem",
                          fontSize: "0.72rem",
                          fontWeight: 700,
                          cursor: "pointer",
                          whiteSpace: "nowrap",
                        }}
                        className="cc-btn-press"
                      >
                        {inviteCopied ? "✓ Copiado" : "Copiar"}
                      </button>
                    </div>
                  </div>

                  {/* Botón compartir por WhatsApp */}
                  <a
                    href={`https://api.whatsapp.com/send?text=${encodeURIComponent(
                      `¡Hola! Te invito a unirte a nuestro espacio '${settings.accountName || "Cuenta compartida"}'. Abre este enlace para ver y anotar gastos juntos (${inviteLimitType === "single" ? "válido para 1 persona" : "enlace privado"}): ${effectiveLink}`
                    )}`}
                    target="_blank"
                    rel="noopener noreferrer"
                    style={{
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      gap: "0.45rem",
                      background: "#25D366",
                      color: "#FFFFFF",
                      borderRadius: "0.65rem",
                      padding: "0.55rem",
                      textDecoration: "none",
                      fontWeight: 700,
                      fontSize: "0.75rem",
                      boxShadow: "0 4px 12px rgba(37,211,102,0.3)",
                    }}
                    className="cc-btn-press hover:brightness-105"
                  >
                    <Share2 size={14} />
                    <span>Enviar invitación por WhatsApp</span>
                  </a>

                  {/* Permiso para que otros miembros inviten (solo visible para el admin) */}
                  {isAccountAdmin && (
                    <div className="flex items-center justify-between gap-3 pt-2" style={{ borderTop: `1px solid ${C.lineSoft}` }}>
                      <div className="min-w-0">
                        <p style={{ margin: 0, fontSize: "0.74rem", fontWeight: 700, color: C.ink }}>
                          Permitir a otros miembros invitar
                        </p>
                        <p style={{ margin: 0, fontSize: "0.65rem", color: C.inkSoft }}>
                          Si está desactivado, solo tú como administrador puedes generar enlaces.
                        </p>
                      </div>
                      <Toggle
                        on={settings.allowMemberInvites === true}
                        onToggle={async (val) => {
                          const updated = { ...settings, allowMemberInvites: Boolean(val) };
                          setSettings(updated);
                          try {
                            await storage.set("settings", JSON.stringify(updated), true);
                          } catch (e) {}
                        }}
                        label="Permitir invitar a otros miembros"
                      />
                    </div>
                  )}
                </>
              )}
            </div>
          </div>
        );
      })()}

      {/* ===== MODAL FLOTANTE DE GESTIÓN DE ESPACIOS (Crear / Unirse) ===== */}
      {showSpaceModal && (
        <div
          role="dialog"
          aria-modal="true"
          aria-label="Gestión de espacios"
          onClick={() => setShowSpaceModal(false)}
          style={{
            position: "fixed",
            inset: 0,
            zIndex: 9999,
            backgroundColor: "rgba(0, 0, 0, 0.65)",
            backdropFilter: "blur(6px)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            padding: "1rem",
          }}
        >
          <div
            onClick={(e) => e.stopPropagation()}
            className="cc-panel-in"
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.4rem",
              padding: "1.5rem",
              boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
              border: `1px solid ${C.line}`,
              maxWidth: "460px",
              width: "100%",
              boxSizing: "border-box",
              display: "flex",
              flexDirection: "column",
              gap: "0.9rem",
            }}
          >
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
              <h3 style={{ margin: 0, fontSize: "1.05rem", fontWeight: 700, color: C.ink }}>
                {spaceModalTab === "create" ? "🌱 Crear nuevo espacio" : "🔑 Unirme con código"}
              </h3>
              <button
                type="button"
                onClick={() => setShowSpaceModal(false)}
                style={{ background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.3rem" }}
              >
                <X size={16} />
              </button>
            </div>

            {/* Pestañas de selector Crear / Unirse */}
            <div style={{ display: "flex", background: C.field, padding: "0.25rem", borderRadius: "0.75rem", gap: "0.25rem", flexWrap: "wrap" }}>
              <button
                type="button"
                onClick={() => { setSpaceModalTab("switch"); setSpaceFeedback(null); }}
                style={{
                  flex: "1 1 30%",
                  background: spaceModalTab === "switch" ? C.paper : "transparent",
                  color: spaceModalTab === "switch" ? C.ink : C.inkSoft,
                  border: spaceModalTab === "switch" ? `1px solid ${C.line}` : "none",
                  borderRadius: "0.6rem",
                  padding: "0.45rem 0.2rem",
                  fontSize: "0.72rem",
                  fontWeight: spaceModalTab === "switch" ? 700 : 500,
                  cursor: "pointer",
                  textAlign: "center",
                }}
              >
                📂 Mis Espacios
              </button>
              <button
                type="button"
                onClick={() => { setSpaceModalTab("create"); setSpaceFeedback(null); }}
                style={{
                  flex: "1 1 30%",
                  background: spaceModalTab === "create" ? C.paper : "transparent",
                  color: spaceModalTab === "create" ? C.ink : C.inkSoft,
                  border: spaceModalTab === "create" ? `1px solid ${C.line}` : "none",
                  borderRadius: "0.6rem",
                  padding: "0.45rem 0.2rem",
                  fontSize: "0.72rem",
                  fontWeight: spaceModalTab === "create" ? 700 : 500,
                  cursor: "pointer",
                  textAlign: "center",
                }}
              >
                🌱 Crear Nuevo
              </button>
              <button
                type="button"
                onClick={() => { setSpaceModalTab("join"); setSpaceFeedback(null); }}
                style={{
                  flex: "1 1 30%",
                  background: spaceModalTab === "join" ? C.paper : "transparent",
                  color: spaceModalTab === "join" ? C.ink : C.inkSoft,
                  border: spaceModalTab === "join" ? `1px solid ${C.line}` : "none",
                  borderRadius: "0.6rem",
                  padding: "0.45rem 0.2rem",
                  fontSize: "0.72rem",
                  fontWeight: spaceModalTab === "join" ? 700 : 500,
                  cursor: "pointer",
                  textAlign: "center",
                }}
              >
                🔑 Unirme Código
              </button>
            </div>

            {spaceFeedback && (
              <div
                style={{
                  padding: "0.55rem 0.75rem",
                  borderRadius: "0.65rem",
                  fontSize: "0.75rem",
                  fontWeight: 600,
                  background: spaceFeedback.type === "error" ? (darkMode ? "#7F1D1D" : "#FEE2E2") : (darkMode ? "#064E3B" : "#D1FAE5"),
                  color: spaceFeedback.type === "error" ? (darkMode ? "#FCA5A5" : "#B91C1C") : (darkMode ? "#6EE7B7" : "#047857"),
                }}
              >
                {spaceFeedback.text}
              </div>
            )}

            {spaceModalTab === "switch" ? (
              <div style={{ display: "flex", flexDirection: "column", gap: "0.6rem" }}>
                <span style={{ fontSize: "0.72rem", fontWeight: 700, color: C.inkSoft, textTransform: "uppercase" }}>
                  Espacios guardados en este dispositivo:
                </span>
                <div style={{ display: "flex", flexDirection: "column", gap: "0.45rem", maxHeight: "240px", overflowY: "auto" }}>
                  {getKnownAccounts().map((acc) => {
                    const isActive = (acc.id === getActiveAccountId() || (acc.code && acc.code === activeAccountCode));
                    return (
                      <div
                        key={acc.id || acc.code}
                        onClick={() => !isActive && handleSwitchSpace(acc.code || acc.id)}
                        style={{
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "space-between",
                          background: isActive ? (darkMode ? "#064E3B" : "#ECFDF5") : C.field,
                          border: `1.5px solid ${isActive ? C.green : C.line}`,
                          borderRadius: "0.75rem",
                          padding: "0.65rem 0.85rem",
                          cursor: isActive ? "default" : "pointer",
                        }}
                        className={isActive ? "" : "cc-btn-press hover:border-emerald-500"}
                      >
                        <div>
                          <div style={{ fontWeight: 700, fontSize: "0.85rem", color: C.ink }}>
                            {acc.name || "Espacio"}
                          </div>
                          <div style={{ fontSize: "0.68rem", color: C.inkSoft, fontFamily: "monospace" }}>
                            Código: {acc.code || acc.id}
                          </div>
                        </div>
                        <div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
                          {isActive ? (
                            <span style={{ fontSize: "0.7rem", fontWeight: 800, color: C.green, background: C.successBg, padding: "0.15rem 0.5rem", borderRadius: "999px" }}>
                              ✓ Activo
                            </span>
                          ) : (
                            <button
                              type="button"
                              style={{ background: C.paper, border: `1px solid ${C.line}`, borderRadius: "0.5rem", padding: "0.3rem 0.6rem", fontSize: "0.7rem", fontWeight: 600, color: C.ink }}
                            >
                              Entrar
                            </button>
                          )}
                          <button
                            type="button"
                            title="Olvidar este espacio de este dispositivo"
                            onClick={(e) => {
                              e.stopPropagation();
                              removeKnownAccount(acc.id || acc.code);
                              setSpaceFeedback({ type: "info", text: "Espacio olvidado de este dispositivo." });
                              setTimeout(() => setSpaceFeedback(null), 2500);
                            }}
                            style={{ background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.25rem" }}
                            className="hover:text-rose-500"
                          >
                            <Trash2 size={13} />
                          </button>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            ) : spaceModalTab === "create" ? (
              <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
                <div>
                  <label style={{ fontSize: "0.72rem", fontWeight: 700, color: C.inkSoft, textTransform: "uppercase", display: "block", marginBottom: "0.3rem" }}>
                    Nombre del nuevo espacio:
                  </label>
                  <input
                    value={newSpaceName}
                    onChange={(e) => setNewSpaceName(e.target.value)}
                    placeholder="Ej. Mis Gastos Personales, Casa Roomies, Viaje..."
                    maxLength={32}
                    style={{
                      width: "100%",
                      boxSizing: "border-box",
                      background: C.field,
                      border: `1px solid ${C.line}`,
                      borderRadius: "0.75rem",
                      padding: "0.65rem 0.85rem",
                      fontSize: "0.85rem",
                      color: C.ink,
                    }}
                    onKeyDown={(e) => e.key === "Enter" && handleCreateSpace()}
                  />
                </div>
                <p style={{ margin: 0, fontSize: "0.7rem", color: C.inkSoft, lineHeight: 1.4 }}>
                  Se creará un espacio totalmente limpio e independiente en el mismo servidor con su propio código de invitación.
                </p>
                <button
                  type="button"
                  onClick={handleCreateSpace}
                  disabled={saving || !newSpaceName.trim()}
                  style={{
                    background: BRAND_GRADIENT,
                    color: "#FFFFFF",
                    border: "none",
                    borderRadius: "0.75rem",
                    padding: "0.75rem",
                    fontWeight: 700,
                    fontSize: "0.82rem",
                    cursor: "pointer",
                    boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                  }}
                  className="cc-btn-press disabled:opacity-40"
                >
                  {saving ? "Creando…" : "Crear e ingresar"}
                </button>
              </div>
            ) : (
              <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
                <div>
                  <label style={{ fontSize: "0.72rem", fontWeight: 700, color: C.inkSoft, textTransform: "uppercase", display: "block", marginBottom: "0.3rem" }}>
                    Código de invitación:
                  </label>
                  <input
                    value={joinCodeDraft}
                    onChange={(e) => setJoinCodeDraft(e.target.value.toUpperCase())}
                    placeholder="Ej. FAM-892 o DEFAULT"
                    maxLength={16}
                    style={{
                      width: "100%",
                      boxSizing: "border-box",
                      background: C.field,
                      border: `1.5px solid ${C.line}`,
                      borderRadius: "0.75rem",
                      padding: "0.65rem 0.85rem",
                      fontSize: "1rem",
                      fontWeight: 800,
                      color: C.ink,
                      letterSpacing: "0.08em",
                      fontFamily: "monospace",
                    }}
                    onKeyDown={(e) => e.key === "Enter" && handleJoinSpace()}
                  />
                </div>
                <p style={{ margin: 0, fontSize: "0.7rem", color: C.inkSoft, lineHeight: 1.4 }}>
                  Pídele el código o enlace de invitación al creador de la cuenta compartida.
                </p>
                <button
                  type="button"
                  onClick={handleJoinSpace}
                  disabled={saving || !joinCodeDraft.trim()}
                  style={{
                    background: BRAND_GRADIENT,
                    color: "#FFFFFF",
                    border: "none",
                    borderRadius: "0.75rem",
                    padding: "0.75rem",
                    fontWeight: 700,
                    fontSize: "0.82rem",
                    cursor: "pointer",
                    boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                  }}
                  className="cc-btn-press disabled:opacity-40"
                >
                  {saving ? "Verificando…" : "Unirme a este espacio"}
                </button>
              </div>
            )}
          </div>
        </div>
      )}

      {showSettings && (
        <div
          onClick={() => setShowSettings(false)}
          role="dialog"
          aria-modal="true"
          aria-label="Ajustes de la cuenta"
          className="cc-fade-in"
          style={{
            position: "fixed",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            width: "100%",
            height: "100%",
            background: "rgba(15, 23, 42, 0.72)",
            backdropFilter: "blur(5px)",
            WebkitBackdropFilter: "blur(5px)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 90,
            padding: "0.75rem",
            boxSizing: "border-box",
          }}
        >
          <div
            onClick={(e) => e.stopPropagation()}
            className="cc-panel-in"
            style={{
              backgroundColor: C.paper,
              borderRadius: isDesktop ? "1.4rem" : "1.15rem",
              padding: isDesktop ? (minimalMode ? "1.2rem 1.15rem" : "1.4rem 1.25rem") : "1rem 0.85rem",
              boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
              border: `1px solid ${C.line}`,
              maxWidth: "680px",
              width: "100%",
              maxHeight: isDesktop ? "90vh" : "94vh",
              overflowY: "auto",
              overflowX: "hidden",
              boxSizing: "border-box",
              display: "flex",
              flexDirection: "column",
            }}
          >
                {/* Cabecera del panel (nunca se comprime) */}
                <div style={{ flexShrink: 0 }}>
                  <PanelHeader
                    icon={<Settings size={16} />}
                    title="Ajustes"
                    subtitle="Configuración, tarjetas y preferencias"
                    onAction={() => setShowSettings(false)}
                    actionLabel="Cerrar ajustes"
                    actionTitle="Cerrar"
                    actionIcon={<X size={14} />}
                    style={{ marginBottom: "0.85rem", gap: "0.6rem" }}
                  />

                  {/* Selector táctil rápido en móviles */}
                  {!isDesktop && (
                    <div style={{ marginBottom: "0.75rem", display: "flex", alignItems: "center", gap: "0.5rem" }}>
                      <span style={{ fontSize: "0.72rem", color: C.inkSoft, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", flexShrink: 0 }}>
                        Sección:
                      </span>
                      <select
                        value={settingsTab}
                        onChange={(e) => setSettingsTab(e.target.value)}
                        style={{
                          flex: 1,
                          minWidth: 0,
                          background: C.field,
                          border: `1.5px solid ${C.lineSoft}`,
                          borderRadius: "0.75rem",
                          padding: "0.55rem 0.85rem",
                          fontSize: "0.85rem",
                          fontWeight: 700,
                          color: C.ink,
                          outline: "none",
                          cursor: "pointer",
                        }}
                      >
                        {(minimalMode ? [
                          { id: "cuenta", label: "👤 Cuenta y Mi Perfil" },
                          { id: "categorias", label: `🏷️ Categorías (${editCategoryList.length})` },
                          { id: "ia", label: "✨ IA & Bot" },
                          { id: "preferencias", label: "⚙️ Opciones del sistema" },
                          { id: "tarjetas", label: `💳 Tarjetas (${editCards.length})` },
                          { id: "cuotas", label: `📅 Cuotas (${editInstallments.length})` },
                        ] : [
                          { id: "cuenta", label: "👤 Cuenta y Mi Perfil" },
                          { id: "categorias", label: `🏷️ Categorías (${editCategoryList.length})` },
                          { id: "tarjetas", label: `💳 Tarjetas (${editCards.length})` },
                          { id: "cuotas", label: `📅 Cuotas (${editInstallments.length})` },
                          { id: "ia", label: "✨ IA & Bot" },
                          { id: "preferencias", label: "⚙️ Opciones del sistema" },
                        ]).map((opt) => (
                          <option key={opt.id} value={opt.id}>{opt.label}</option>
                        ))}
                      </select>
                    </div>
                  )}

                  {/* Barra de pestañas táctil: nunca se comprime en móvil */}
                  <div
                    role="tablist"
                    aria-label="Secciones de ajustes"
                    onWheel={(e) => {
                      if (!isDesktop && e.deltaY !== 0) {
                        e.currentTarget.scrollLeft += e.deltaY;
                      }
                    }}
                    style={{
                      display: "flex",
                      flexWrap: isDesktop ? "wrap" : "nowrap",
                      overflowX: isDesktop ? "visible" : "auto",
                      gap: "0.35rem",
                      background: C.field,
                      padding: "0.35rem 0.45rem",
                      borderRadius: "0.95rem",
                      marginBottom: "1.25rem",
                      border: `1px solid ${C.lineSoft}`,
                      scrollbarWidth: "none",
                      WebkitOverflowScrolling: "touch",
                      flexShrink: 0,
                      minHeight: isDesktop ? "auto" : "48px",
                      alignItems: "center",
                    }}
                    className="no-scrollbar"
                  >
                    {(minimalMode ? [
                      { id: "cuenta", label: "Cuenta y Perfil", icon: <Users size={14} /> },
                      { id: "categorias", label: "Categorías", icon: <Tag size={14} />, count: editCategoryList.length },
                      { id: "ia", label: "IA & Bot", icon: <Sparkles size={14} /> },
                      { id: "preferencias", label: "Opciones", icon: <Settings size={14} /> },
                      { id: "tarjetas", label: "Tarjetas", icon: <CreditCard size={14} />, count: editCards.length },
                      { id: "cuotas", label: "Cuotas", icon: <Calendar size={14} />, count: editInstallments.length },
                    ] : [
                      { id: "cuenta", label: "Cuenta y Perfil", icon: <Users size={14} /> },
                      { id: "categorias", label: "Categorías", icon: <Tag size={14} />, count: editCategoryList.length },
                      { id: "tarjetas", label: "Tarjetas", icon: <CreditCard size={14} />, count: editCards.length },
                      { id: "cuotas", label: "Cuotas", icon: <Calendar size={14} />, count: editInstallments.length },
                      { id: "ia", label: "IA & Bot", icon: <Sparkles size={14} /> },
                      { id: "preferencias", label: "Opciones", icon: <Settings size={14} /> },
                    ]).map((tab) => {
                      const active = settingsTab === tab.id;
                      return (
                        <button
                          key={tab.id}
                          type="button"
                          role="tab"
                          aria-selected={active}
                          onClick={() => setSettingsTab(tab.id)}
                          style={{
                            flex: isDesktop ? "1 1 auto" : "0 0 auto",
                            flexShrink: 0,
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            gap: "0.35rem",
                            padding: isDesktop ? "0.45rem 0.65rem" : "0.55rem 0.9rem",
                            minHeight: isDesktop ? "auto" : "38px",
                            borderRadius: "0.75rem",
                            border: active ? `1.5px solid ${C.line}` : "1.5px solid transparent",
                            background: active ? C.paper : "transparent",
                            color: active ? C.ink : C.inkSoft,
                            fontWeight: active ? 700 : 500,
                            fontSize: isDesktop ? "0.74rem" : "0.8rem",
                            cursor: "pointer",
                            boxShadow: active ? "0 3px 10px rgba(0,0,0,0.08)" : "none",
                            transition: "all 0.15s ease",
                            position: "relative",
                            whiteSpace: "nowrap",
                            WebkitTapHighlightColor: "transparent",
                          }}
                        >
                          {tab.icon}
                          <span>{tab.label}</span>
                          {tab.count != null && tab.count > 0 && (
                            <span
                              style={{
                                fontSize: "0.65rem",
                                background: active ? BRAND_GRADIENT : C.line,
                                color: active ? "#FFF" : C.inkSoft,
                                borderRadius: "999px",
                                padding: "0.05rem 0.4rem",
                                fontWeight: 700,
                                lineHeight: 1.2,
                              }}
                            >
                              {tab.count}
                            </span>
                          )}
                          {tab.highlight && !active && (
                            <span
                              style={{
                                position: "absolute",
                                top: "4px",
                                right: "6px",
                                width: "0.45rem",
                                height: "0.45rem",
                                borderRadius: "50%",
                                background: C.green,
                                boxShadow: `0 0 0 2px ${C.field}`,
                              }}
                            />
                          )}
                        </button>
                      );
                    })}
                  </div>
                </div>

                {/* ===== PESTAÑA 1: CUENTA Y MI PERFIL (UNIFICADA) ===== */}
                {settingsTab === "cuenta" && (
                  <div style={{ display: "flex", flexDirection: "column", gap: "0.95rem" }}>
                    {/* Tarjeta 1: Espacio Actual & Invitaciones */}
                    <div
                      style={{
                        background: C.field,
                        border: `1.5px solid ${darkMode ? "rgba(16,185,129,0.3)" : "#A7F3D0"}`,
                        borderRadius: "1rem",
                        padding: "0.95rem 1rem",
                        display: "flex",
                        flexDirection: "column",
                        gap: "0.65rem",
                      }}
                    >
                      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", flexWrap: "wrap" }}>
                        <span style={{ fontSize: "0.82rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                          <Users size={15} color={C.green} /> Espacio: <strong style={{ color: C.green }}>{settings.accountName || "Cuenta compartida"}</strong>
                        </span>
                        <span style={{ fontSize: "0.68rem", fontWeight: 800, background: darkMode ? "#064E3B" : "#ECFDF5", color: darkMode ? "#6EE7B7" : "#047857", border: `1px solid ${C.green}`, padding: "0.15rem 0.55rem", borderRadius: "999px", letterSpacing: "0.05em", fontFamily: "monospace" }}>
                          #{activeAccountCode || "DEFAULT"}
                        </span>
                      </div>

                      <p style={{ margin: 0, fontSize: "0.72rem", color: C.inkSoft, lineHeight: 1.45 }}>
                        Comparte los gastos con tu pareja, familia o amigos, o cambia a un espacio propio para tus finanzas personales.
                      </p>

                      <div style={{ display: "flex", gap: "0.45rem", flexWrap: "wrap" }}>
                        <button
                          type="button"
                          onClick={() => setShowInviteModal(true)}
                          style={{
                            flex: "1 1 auto",
                            background: BRAND_GRADIENT,
                            color: "#FFFFFF",
                            border: "none",
                            borderRadius: "0.65rem",
                            padding: "0.5rem 0.85rem",
                            fontSize: "0.75rem",
                            fontWeight: 700,
                            cursor: "pointer",
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            gap: "0.35rem",
                            boxShadow: "0 2px 8px rgba(16,185,129,0.3)",
                          }}
                          className="cc-btn-press"
                        >
                          <Users size={13} />
                          <span>🔗 Invitar a personas</span>
                        </button>

                        <button
                          type="button"
                          onClick={() => {
                            setSpaceModalTab("switch");
                            setShowSpaceModal(true);
                          }}
                          style={{
                            flex: "1 1 auto",
                            background: C.paper,
                            color: C.ink,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.65rem",
                            padding: "0.5rem 0.85rem",
                            fontSize: "0.75rem",
                            fontWeight: 600,
                            cursor: "pointer",
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            gap: "0.35rem",
                          }}
                          className="cc-btn-press"
                        >
                          <RefreshCw size={13} />
                          <span>Cambiar / Crear espacio</span>
                        </button>

                        <button
                          type="button"
                          onClick={() => {
                            setTransferStatus(null);
                            const known = getKnownAccounts().filter(a => a.id !== getActiveAccountId() && a.code !== activeAccountCode);
                            if (known.length > 0) setTransferTargetSpace(known[0].code || known[0].id);
                            setShowTransferDataModal(true);
                          }}
                          style={{
                            width: "100%",
                            background: darkMode ? "rgba(16,185,129,0.15)" : "#ECFDF5",
                            color: darkMode ? "#6EE7B7" : "#047857",
                            border: `1px dashed ${C.green}`,
                            borderRadius: "0.65rem",
                            padding: "0.5rem 0.85rem",
                            fontSize: "0.75rem",
                            fontWeight: 700,
                            cursor: "pointer",
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            gap: "0.35rem",
                          }}
                          className="cc-btn-press"
                        >
                          <span>📦 Pasar mis datos propios a otra cuenta</span>
                        </button>
                      </div>
                    </div>

                    {/* Nombre y Moneda en una sola fila compacta */}
                    <div style={{ display: "flex", gap: "0.6rem" }}>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <label style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-[10px] uppercase tracking-widest block mb-1 font-bold">
                          Nombre de la cuenta
                        </label>
                        <input
                          value={editAccountName}
                          onChange={(e) => setEditAccountName(e.target.value)}
                          placeholder="Cuenta compartida"
                          maxLength={40}
                          style={{ fontFamily: "'Inter', sans-serif", background: C.field, border: `1px solid ${C.line}`, borderRadius: "0.65rem", color: C.ink, padding: "0.48rem 0.75rem", fontSize: "0.82rem", outline: "none", width: "100%" }}
                        />
                      </div>
                      <div style={{ width: "6rem", flexShrink: 0 }}>
                        <label style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-[10px] uppercase tracking-widest block mb-1 font-bold">
                          Moneda
                        </label>
                        <select
                          value={editCurrency}
                          onChange={(e) => setEditCurrency(e.target.value)}
                          style={{ fontFamily: "'Inter', sans-serif", background: C.field, border: `1px solid ${C.line}`, borderRadius: "0.65rem", color: C.ink, padding: "0.48rem 0.5rem", fontSize: "0.82rem", outline: "none", width: "100%", fontWeight: 700 }}
                        >
                          <option value="S/">S/ (Soles)</option>
                          <option value="$">$ (USD)</option>
                          <option value="€">€ (Euros)</option>
                        </select>
                      </div>
                    </div>

                    {/* Personas de la cuenta */}
                    <div>
                      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.35rem" }}>
                        <label style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-[10px] uppercase tracking-widest font-bold">
                          Personas ({editPeople.filter((p) => p.trim()).length}/{MAX_PEOPLE})
                        </label>
                        <button
                          type="button"
                          onClick={() => setEditPeople([...editPeople, ""])}
                          style={{ fontFamily: "'Inter', sans-serif", color: C.ink, background: C.chipBg, border: `1px solid ${C.line}`, borderRadius: "999px", padding: "0.2rem 0.55rem", fontSize: "0.68rem" }}
                          className="cc-btn-press flex items-center gap-1 hover:opacity-80 transition-opacity font-semibold"
                        >
                          <Plus size={11} /> Añadir
                        </button>
                      </div>
                      <div style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>
                        {editPeople.map((p, i) => (
                          <div key={i} className="flex items-center gap-1.5">
                            <span
                              style={{
                                width: "0.55rem",
                                height: "0.55rem",
                                borderRadius: "50%",
                                background: p.trim() ? personColor(p.trim()) : C.line,
                                flexShrink: 0,
                              }}
                            />
                            <input
                              value={p}
                              onChange={(e) => {
                                const next = [...editPeople];
                                next[i] = e.target.value;
                                setEditPeople(next);
                              }}
                              placeholder={`Persona ${i + 1}`}
                              maxLength={24}
                              style={{ fontFamily: "'Inter', sans-serif", background: C.field, border: `1px solid ${C.line}`, borderRadius: "0.55rem", color: C.ink, padding: "0.38rem 0.65rem", outline: "none", fontSize: "0.8rem", flex: 1, minWidth: 0 }}
                            />
                            {editPeople.length > 1 && (
                              <button
                                type="button"
                                onClick={() => setEditPeople(editPeople.filter((_, j) => j !== i))}
                                title="Quitar persona"
                                aria-label="Quitar persona"
                                style={{ color: C.inkSoft, background: "none", border: "none", cursor: "pointer", padding: "0.2rem" }}
                              >
                                <X size={13} />
                              </button>
                            )}
                          </div>
                        ))}
                      </div>
                    </div>

                    {(dupTotalCount > 0 || orphanTotalCount > 0) && (
                      <button
                        type="button"
                        onClick={openDupReview}
                        style={{
                          fontFamily: "'Inter', sans-serif",
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "space-between",
                          gap: "0.5rem",
                          width: "100%",
                          padding: "0.45rem 0.75rem",
                          borderRadius: "0.65rem",
                          border: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`,
                          backgroundColor: darkMode ? "#451A03" : "#FEF3C7",
                          color: darkMode ? "#FDE68A" : "#92400E",
                          cursor: "pointer",
                          textAlign: "left",
                          fontSize: "0.74rem",
                        }}
                      >
                        <span className="flex items-center gap-1 font-semibold">Revisar duplicados y huérfanos</span>
                        <span
                          style={{
                            background: darkMode ? "#92400E" : "#F59E0B",
                            color: "#FFF",
                            borderRadius: "9999px",
                            padding: "0.1rem 0.45rem",
                            fontWeight: 700,
                            fontSize: "0.68rem",
                          }}
                        >
                          {orphanTotalCount > 0
                            ? dupTotalCount > 0
                              ? dupTotalCount + " · " + orphanTotalCount
                              : orphanTotalCount
                            : dupTotalCount}
                        </span>
                      </button>
                    )}



                    {/* Personajes de la cuenta en formato compacto y tamaño chico */}
                    <div>
                      <label style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-[10px] uppercase tracking-widest block mb-1.5 font-bold">
                        Personaje de la cuenta
                      </label>
                      <div style={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: "0.35rem" }}>
                        {MASCOT_KINDS.map((k) => {
                          const selected = editMascot === k.id;
                          const selSize = selected ? 32 : 26;
                          const popping = previewPop && previewPop.kind === k.id;
                          return (
                            <button
                              key={k.id}
                              type="button"
                              onClick={() => {
                                setEditMascot(k.id);
                                setPreviewPop({ kind: k.id, n: (previewPop ? previewPop.n : 0) + 1 });
                                setTimeout(() => {
                                  if (!document.hidden && soundEnabledRef.current && uiRef.current.showSettings) {
                                    playMascotSound("happy");
                                  }
                                }, 150);
                              }}
                              aria-pressed={selected}
                              title={k.label}
                              style={{
                                display: "flex",
                                flexDirection: "column",
                                alignItems: "center",
                                justifyContent: "center",
                                gap: "0.15rem",
                                padding: "0.35rem 0.15rem",
                                borderRadius: "0.65rem",
                                border: selected ? "1.5px solid " + C.green : "1px solid " + C.line,
                                background: selected ? C.successBg : C.field,
                                cursor: "pointer",
                                minHeight: "3.5rem",
                              }}
                              className="cc-btn-press"
                            >
                              <span
                                key={popping ? previewPop.n : undefined}
                                onAnimationEnd={(e) => {
                                  if ((e.animationName === "cc-mascot-dance" || e.animationName === "cc-mascot-dance-soft") && previewPop && previewPop.kind === k.id) {
                                    setPreviewPop(null);
                                  }
                                }}
                                style={popping ? { display: "inline-block", animation: MASCOT_DANCE_SOFT } : undefined}
                              >
                                <Mascot kind={k.id} mood="happy" size={selSize} halo={false} animate={!popping} />
                              </span>
                              <span
                                style={{
                                  fontSize: "0.62rem",
                                  fontWeight: selected ? 800 : 500,
                                  color: selected ? C.green : C.inkSoft,
                                  maxWidth: "100%",
                                  overflow: "hidden",
                                  textOverflow: "ellipsis",
                                }}
                              >
                                {k.label}
                              </span>
                            </button>
                          );
                        })}
                      </div>
                    </div>

                    {/* Tarjeta: Mi Perfil y Seguridad (Mi cuenta) */}
                    <div style={{ background: C.field, border: `1.5px solid ${C.lineSoft}`, borderRadius: "1rem", padding: "1rem", marginTop: "0.25rem" }}>
                      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.65rem" }}>
                        <span style={{ fontSize: "0.75rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, letterSpacing: "0.05em", display: "flex", alignItems: "center", gap: "0.35rem" }}>
                          <ShieldCheck size={14} color={C.green} /> Mi usuario en este dispositivo
                        </span>
                        <span style={{ background: C.successBg, color: C.green, border: `1px solid ${C.line}`, borderRadius: "999px", padding: "0.15rem 0.55rem", fontSize: "0.7rem", fontWeight: 700 }}>
                          ✓ Conectado como {myName}
                        </span>
                      </div>

                      <div style={{ display: "flex", alignItems: "center", gap: "0.65rem", marginBottom: "0.85rem", background: C.paper, padding: "0.65rem 0.85rem", borderRadius: "0.75rem", border: `1px solid ${C.lineSoft}` }}>
                        <div style={{ width: "2.2rem", height: "2.2rem", borderRadius: "50%", background: personColor(myName), color: "#FFF", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 800, fontSize: "0.95rem" }}>
                          {(myName || "?").trim().charAt(0).toUpperCase()}
                        </div>
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontWeight: 800, fontSize: "0.95rem", color: C.ink }}>
                            {myName}
                          </div>
                          <div style={{ fontSize: "0.68rem", color: C.inkSoft }}>
                            Tus registros y gastos se anotarán con este nombre.
                          </div>
                        </div>
                      </div>

                      {/* Actualizar contraseña */}
                      <div style={{ borderTop: `1px solid ${C.line}`, paddingTop: "0.85rem" }}>
                        <label style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-[10px] uppercase tracking-widest block mb-2 font-bold flex items-center gap-1">
                          <KeyRound size={12} />
                          Cambiar contraseña de {myName}
                        </label>
                        <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.45rem", marginBottom: "0.5rem" }}>
                          <div style={{ position: "relative", width: "100%" }}>
                            <input
                              type={showNewPass ? "text" : "password"}
                              value={newPass}
                              onChange={(e) => setNewPass(e.target.value)}
                              placeholder="Contraseña nueva (mín. 8)"
                              maxLength={64}
                              autoComplete="new-password"
                              style={{ fontFamily: "'Inter', sans-serif", background: C.paper, border: `1px solid ${C.line}`, borderRadius: "0.65rem", color: C.ink, padding: "0.5rem 2.2rem 0.5rem 0.75rem", outline: "none", fontSize: "0.78rem", width: "100%", boxSizing: "border-box" }}
                            />
                            <button
                              type="button"
                              onClick={() => setShowNewPass(!showNewPass)}
                              title={showNewPass ? "Ocultar" : "Ver"}
                              style={{ position: "absolute", right: "0.5rem", top: "50%", transform: "translateY(-50%)", background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.2rem" }}
                            >
                              {showNewPass ? <EyeOff size={14} /> : <Eye size={14} />}
                            </button>
                          </div>
                          <div style={{ position: "relative", width: "100%" }}>
                            <input
                              type={showNewPass2 ? "text" : "password"}
                              value={newPass2}
                              onChange={(e) => setNewPass2(e.target.value)}
                              placeholder="Repite la contraseña nueva"
                              maxLength={64}
                              autoComplete="new-password"
                              style={{ fontFamily: "'Inter', sans-serif", background: C.paper, border: `1px solid ${C.line}`, borderRadius: "0.65rem", color: C.ink, padding: "0.5rem 2.2rem 0.5rem 0.75rem", outline: "none", fontSize: "0.78rem", width: "100%", boxSizing: "border-box" }}
                            />
                            <button
                              type="button"
                              onClick={() => setShowNewPass2(!showNewPass2)}
                              title={showNewPass2 ? "Ocultar" : "Ver"}
                              style={{ position: "absolute", right: "0.5rem", top: "50%", transform: "translateY(-50%)", background: "none", border: "none", color: C.inkSoft, cursor: "pointer", padding: "0.2rem" }}
                            >
                              {showNewPass2 ? <EyeOff size={14} /> : <Eye size={14} />}
                            </button>
                          </div>
                        </div>
                        <button
                          type="button"
                          onClick={changeMyPassword}
                          disabled={saving || !newPass.trim()}
                          style={{ fontFamily: "'Inter', sans-serif", color: C.ink, border: `1px solid ${C.line}`, borderRadius: "0.65rem", background: C.paper }}
                          className="cc-btn-press w-full py-2 text-xs uppercase tracking-widest font-bold disabled:opacity-40 mb-3"
                        >
                          Actualizar contraseña
                        </button>
                      </div>

                      <button
                        type="button"
                        onClick={logout}
                        style={{ fontFamily: "'Inter', sans-serif", color: C.red, border: `1px solid ${C.red}`, background: C.dangerBg, borderRadius: "0.65rem" }}
                        className="cc-btn-press w-full py-2.5 text-xs uppercase tracking-widest flex items-center justify-center gap-1.5 hover:opacity-80 transition-opacity font-bold"
                      >
                        <LogOut size={13} style={{ flexShrink: 0 }} />
                        Cerrar sesión en este dispositivo
                      </button>
                    </div>
                  </div>
                )}

                {/* ===== PESTAÑA CATEGORÍAS ===== */}
                {settingsTab === "categorias" && (
                  <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
                    <div style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.95rem", padding: "1rem" }}>
                      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.6rem" }}>
                        <span style={{ fontSize: "0.82rem", color: C.ink, fontWeight: 700, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                          <Tag size={14} color={C.green} /> Categorías de la cuenta
                        </span>
                        <span style={{ background: C.successBg, color: C.green, borderRadius: "999px", padding: "0.15rem 0.55rem", fontSize: "0.68rem", fontWeight: 700 }}>
                          {editCategoryList.length} activas
                        </span>
                      </div>
                      <p style={{ fontSize: "0.72rem", color: C.inkSoft, margin: "0 0 0.85rem", lineHeight: 1.45 }}>
                        Crea, elimina o personaliza las categorías para clasificar tus gastos. Se actualizan en la nube para todos.
                      </p>

                      {/* Input para agregar nueva categoría */}
                      <div style={{ display: "flex", gap: "0.4rem", marginBottom: "1rem", width: "100%" }}>
                        <input
                          value={newCategoryDraft}
                          onChange={(e) => setNewCategoryDraft(e.target.value)}
                          onKeyDown={(e) => {
                            if (e.key === "Enter" && newCategoryDraft.trim()) {
                              e.preventDefault();
                              const clean = cleanCategoryName(newCategoryDraft.trim());
                              if (clean && !editCategoryList.some((c) => c.toLowerCase() === clean.toLowerCase())) {
                                setEditCategoryList([...editCategoryList, clean]);
                                setNewCategoryDraft("");
                              }
                            }
                          }}
                          placeholder="Nueva categoría (ej. Mascotas)..."
                          maxLength={35}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.paper,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.65rem",
                            color: C.ink,
                            padding: "0.48rem 0.65rem",
                            fontSize: "0.8rem",
                            outline: "none",
                            flex: 1,
                            minWidth: 0,
                            width: "100%",
                          }}
                        />
                        <button
                          type="button"
                          onClick={() => {
                            if (newCategoryDraft.trim()) {
                              const clean = cleanCategoryName(newCategoryDraft.trim());
                              if (clean && !editCategoryList.some((c) => c.toLowerCase() === clean.toLowerCase())) {
                                setEditCategoryList([...editCategoryList, clean]);
                                setNewCategoryDraft("");
                              }
                            }
                          }}
                          disabled={!newCategoryDraft.trim()}
                          style={{
                            background: BRAND_GRADIENT,
                            color: "#FFFFFF",
                            border: "none",
                            borderRadius: "0.65rem",
                            padding: "0.48rem 0.85rem",
                            fontSize: "0.76rem",
                            fontWeight: 700,
                            cursor: newCategoryDraft.trim() ? "pointer" : "default",
                            opacity: newCategoryDraft.trim() ? 1 : 0.4,
                            display: "inline-flex",
                            alignItems: "center",
                            gap: "0.3rem",
                          }}
                          className="cc-btn-press"
                        >
                          <Plus size={13} />
                          <span>Agregar</span>
                        </button>
                      </div>

                      {/* Lista visual de categorías con icono y botón eliminar */}
                      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(135px, 1fr))", gap: "0.45rem", maxHeight: "15rem", overflowY: "auto", padding: "0.2rem 0" }}>
                        {editCategoryList.map((cat) => {
                          const icon = getCategoryIcon(cat);
                          return (
                            <div
                              key={cat}
                              style={{
                                background: C.paper,
                                border: `1px solid ${C.line}`,
                                borderRadius: "0.65rem",
                                padding: "0.45rem 0.6rem",
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "space-between",
                                gap: "0.35rem",
                              }}
                            >
                              <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", minWidth: 0 }}>
                                <span style={{ fontSize: "0.95rem" }}>{icon}</span>
                                <span style={{ fontSize: "0.74rem", fontWeight: 600, color: C.ink, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                                  {cat}
                                </span>
                              </div>
                              <button
                                type="button"
                                onClick={() => setEditCategoryList(editCategoryList.filter((c) => c !== cat))}
                                title="Eliminar categoría"
                                style={{
                                  background: "none",
                                  border: "none",
                                  color: C.inkSoft,
                                  cursor: "pointer",
                                  padding: "0.15rem",
                                  display: "flex",
                                  alignItems: "center",
                                }}
                                className="hover:text-red-500"
                              >
                                <Trash2 size={12} />
                              </button>
                            </div>
                          );
                        })}
                      </div>

                      {/* Botón para restablecer categorías por defecto */}
                      <div style={{ marginTop: "0.85rem", display: "flex", justifyContent: "flex-end" }}>
                        <button
                          type="button"
                          onClick={() => setEditCategoryList(DEFAULT_CATEGORIES.slice())}
                          style={{
                            background: "none",
                            border: "none",
                            color: C.inkSoft,
                            fontSize: "0.72rem",
                            cursor: "pointer",
                            textDecoration: "underline",
                          }}
                        >
                          Restablecer categorías recomendadas
                        </button>
                      </div>
                    </div>
                  </div>
                )}

                {/* ===== PESTAÑA 2: TARJETAS DE CRÉDITO ===== */}
                {settingsTab === "tarjetas" && (
                  <div>
                    <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.75rem", marginBottom: "1rem" }}>
                      <div>
                        <div style={{ fontSize: "0.85rem", fontWeight: 700, color: C.ink, marginBottom: "0.2rem" }}>
                          Tarjetas de crédito registradas
                        </div>
                        <div style={{ fontSize: "0.72rem", color: C.inkSoft, lineHeight: 1.4 }}>
                          Asigna tarjetas a cada persona con fecha de corte y pago para separar la deuda del dinero en efectivo.
                        </div>
                      </div>
                      <button
                        type="button"
                        onClick={() => {
                          const availablePerson = editPeople.find((p) => p.trim()) || myName || "";
                          setCardDraft({
                            id: "",
                            name: "",
                            person: availablePerson,
                            billingCycle: "mensual",
                            cutoffDay: 20,
                            dueDay: 10,
                            creditLimit: "",
                            currency: editCurrency || "S/",
                            color: "emerald",
                          });
                          setShowCardModal(true);
                        }}
                        style={{
                          fontFamily: "'Inter', sans-serif",
                          color: "#FFFFFF",
                          background: BRAND_GRADIENT,
                          border: "none",
                          borderRadius: "0.75rem",
                          padding: "0.45rem 0.85rem",
                          fontSize: "0.75rem",
                          fontWeight: 700,
                          cursor: "pointer",
                          flexShrink: 0,
                          boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                        }}
                        className="cc-btn-press hover:opacity-90 flex items-center gap-1.5"
                      >
                        <Plus size={13} />
                        Nueva tarjeta
                      </button>
                    </div>

                    {/* Modal / Formulario para añadir o editar tarjeta */}
                    {showCardModal && (() => {
                      const previewCycle = getCardCycleInfo(cardDraft);
                      return (
                        <div
                          style={{
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "1rem",
                            padding: "1rem",
                            marginBottom: "1.25rem",
                            boxShadow: C.cardShadow,
                          }}
                        >
                          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.85rem" }}>
                            <span style={{ fontSize: "0.85rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                              {cardDraft.cardType === "debito" ? <Landmark size={14} color={C.green} /> : <CreditCard size={14} color={C.green} />}
                              {cardDraft.id ? `Editar tarjeta (${cardDraft.cardType === "debito" ? "Débito" : "Crédito"})` : "Crear nueva tarjeta"}
                            </span>
                            <button
                              type="button"
                              onClick={() => setShowCardModal(false)}
                              style={{ color: C.inkSoft, background: "none", border: "none", cursor: "pointer" }}
                            >
                              <X size={15} />
                            </button>
                          </div>

                          <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
                            {/* Selector de Tipo: Crédito vs Débito */}
                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.3rem" }}>
                                Tipo de tarjeta
                              </label>
                              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem" }}>
                                <button
                                  type="button"
                                  onClick={() => setCardDraft({ ...cardDraft, cardType: "credito" })}
                                  style={{
                                    fontFamily: "'Inter', sans-serif",
                                    padding: "0.5rem 0.6rem",
                                    borderRadius: "0.7rem",
                                    border: (cardDraft.cardType || "credito") === "credito" ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                                    background: (cardDraft.cardType || "credito") === "credito" ? C.chipBg : C.paper,
                                    color: (cardDraft.cardType || "credito") === "credito" ? C.ink : C.inkSoft,
                                    fontSize: "0.78rem",
                                    fontWeight: (cardDraft.cardType || "credito") === "credito" ? 700 : 500,
                                    cursor: "pointer",
                                    textAlign: "center",
                                    display: "flex",
                                    alignItems: "center",
                                    justifyContent: "center",
                                    gap: "0.35rem",
                                  }}
                                  className="cc-btn-press"
                                >
                                  <CreditCard size={14} color={(cardDraft.cardType || "credito") === "credito" ? C.green : C.inkSoft} />
                                  <span>💳 Crédito</span>
                                </button>
                                <button
                                  type="button"
                                  onClick={() => setCardDraft({ ...cardDraft, cardType: "debito" })}
                                  style={{
                                    fontFamily: "'Inter', sans-serif",
                                    padding: "0.5rem 0.6rem",
                                    borderRadius: "0.7rem",
                                    border: cardDraft.cardType === "debito" ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                                    background: cardDraft.cardType === "debito" ? C.chipBg : C.paper,
                                    color: cardDraft.cardType === "debito" ? C.ink : C.inkSoft,
                                    fontSize: "0.78rem",
                                    fontWeight: cardDraft.cardType === "debito" ? 700 : 500,
                                    cursor: "pointer",
                                    textAlign: "center",
                                    display: "flex",
                                    alignItems: "center",
                                    justifyContent: "center",
                                    gap: "0.35rem",
                                  }}
                                  className="cc-btn-press"
                                >
                                  <Landmark size={14} color={cardDraft.cardType === "debito" ? C.green : C.inkSoft} />
                                  <span>🏧 Débito</span>
                                </button>
                              </div>
                              {cardDraft.cardType === "debito" && (
                                <div style={{ fontSize: "0.68rem", color: C.inkSoft, marginTop: "0.35rem", padding: "0.35rem 0.6rem", background: darkMode ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)", borderRadius: "0.5rem", border: `1px solid ${C.lineSoft}` }}>
                                  💡 <strong>Tarjeta de Débito:</strong> Descuenta directamente de tu cuenta bancaria. No genera ciclo de deuda mensual ni requiere fechas de corte o pago.
                                </div>
                              )}
                            </div>

                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                Nombre de la tarjeta
                              </label>
                              <input
                                value={cardDraft.name}
                                onChange={(e) => setCardDraft({ ...cardDraft, name: e.target.value })}
                                placeholder={cardDraft.cardType === "debito" ? "Ej. BCP Débito, Interbank Débito..." : "Ej. BBVA Visa Signature, IO Tarjeta..."}
                                maxLength={30}
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                }}
                              />
                            </div>

                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                Titular de la tarjeta
                              </label>
                              <select
                                value={cardDraft.person}
                                onChange={(e) => setCardDraft({ ...cardDraft, person: e.target.value })}
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                }}
                              >
                                {editPeople.filter((p) => p.trim()).map((p) => (
                                  <option key={p} value={p}>
                                    {p}
                                  </option>
                                ))}
                              </select>
                            </div>

                            {/* Solo si es TARJETA DE CRÉDITO: Duración / Frecuencia / Corte / Pago / Límite */}
                            {cardDraft.cardType !== "debito" && (
                              <>
                                {/* Duración / Frecuencia del ciclo (Mensual 30d vs Quincenal 15d) */}
                                <div>
                                  <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.3rem" }}>
                                    Duración del ciclo / Frecuencia
                                  </label>
                                  <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem" }}>
                                    <button
                                      type="button"
                                      onClick={() => setCardDraft({ ...cardDraft, billingCycle: "mensual" })}
                                      style={{
                                        fontFamily: "'Inter', sans-serif",
                                        padding: "0.45rem 0.6rem",
                                        borderRadius: "0.65rem",
                                        border: (cardDraft.billingCycle || "mensual") === "mensual" ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                                        background: (cardDraft.billingCycle || "mensual") === "mensual" ? C.chipBg : C.paper,
                                        color: (cardDraft.billingCycle || "mensual") === "mensual" ? C.ink : C.inkSoft,
                                        fontSize: "0.78rem",
                                        fontWeight: (cardDraft.billingCycle || "mensual") === "mensual" ? 700 : 500,
                                        cursor: "pointer",
                                        textAlign: "center",
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: "0.3rem",
                                      }}
                                    >
                                      <span>🗓️ Mensual (30 días)</span>
                                    </button>
                                    <button
                                      type="button"
                                      onClick={() => setCardDraft({ ...cardDraft, billingCycle: "quincenal" })}
                                      style={{
                                        fontFamily: "'Inter', sans-serif",
                                        padding: "0.45rem 0.6rem",
                                        borderRadius: "0.65rem",
                                        border: cardDraft.billingCycle === "quincenal" ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                                        background: cardDraft.billingCycle === "quincenal" ? C.chipBg : C.paper,
                                        color: cardDraft.billingCycle === "quincenal" ? C.ink : C.inkSoft,
                                        fontSize: "0.78rem",
                                        fontWeight: cardDraft.billingCycle === "quincenal" ? 700 : 500,
                                        cursor: "pointer",
                                        textAlign: "center",
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        gap: "0.3rem",
                                      }}
                                    >
                                      <span>⚡ Quincenal (15 días)</span>
                                    </button>
                                  </div>
                                </div>

                                {/* Fechas de corte y pago con selectores de mes independientes */}
                                <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.75rem" }}>
                                  {/* Bloque Corte */}
                                  <div style={{ background: C.paper, padding: "0.75rem", borderRadius: "0.75rem", border: `1px solid ${C.lineSoft}`, display: "flex", flexDirection: "column", gap: "0.45rem" }}>
                                    <div style={{ fontSize: "0.75rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.3rem" }}>
                                      <span>✂️ Corte (facturación)</span>
                                    </div>
                                    <div>
                                      <label style={{ fontSize: "0.65rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.2rem" }}>
                                        Día de corte (1–31)
                                      </label>
                                      <input
                                        type="number"
                                        min={1}
                                        max={31}
                                        value={cardDraft.cutoffDay}
                                        onChange={(e) => setCardDraft({ ...cardDraft, cutoffDay: Number(e.target.value) || 1 })}
                                        style={{
                                          background: C.field,
                                          border: `1px solid ${C.line}`,
                                          borderRadius: "0.55rem",
                                          padding: "0.45rem 0.65rem",
                                          fontSize: "0.85rem",
                                          color: C.ink,
                                          width: "100%",
                                          outline: "none",
                                        }}
                                      />
                                    </div>
                                    <div>
                                      <label style={{ fontSize: "0.65rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.2rem" }}>
                                        Mes del próximo corte
                                      </label>
                                      <select
                                        value={cardDraft.targetCutoffMonth || "auto"}
                                        onChange={(e) => setCardDraft({ ...cardDraft, targetCutoffMonth: e.target.value })}
                                        style={{
                                          background: C.field,
                                          border: `1px solid ${C.line}`,
                                          borderRadius: "0.55rem",
                                          padding: "0.45rem 0.55rem",
                                          fontSize: "0.8rem",
                                          color: C.ink,
                                          width: "100%",
                                          outline: "none",
                                          fontWeight: 600,
                                        }}
                                      >
                                        <option value="auto">⚡ Automático (según hoy)</option>
                                        {getUpcomingMonthsList(2, 12).map((m) => (
                                          <option key={m.key} value={m.key}>
                                            🗓️ {m.label}
                                          </option>
                                        ))}
                                      </select>
                                    </div>
                                  </div>

                                  {/* Bloque Pago */}
                                  <div style={{ background: C.paper, padding: "0.75rem", borderRadius: "0.75rem", border: `1px solid ${C.lineSoft}`, display: "flex", flexDirection: "column", gap: "0.45rem" }}>
                                    <div style={{ fontSize: "0.75rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.3rem" }}>
                                      <span>⏰ Pago (vencimiento)</span>
                                    </div>
                                    <div>
                                      <label style={{ fontSize: "0.65rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.2rem" }}>
                                        Día de pago (1–31)
                                      </label>
                                      <input
                                        type="number"
                                        min={1}
                                        max={31}
                                        value={cardDraft.dueDay}
                                        onChange={(e) => setCardDraft({ ...cardDraft, dueDay: Number(e.target.value) || 1 })}
                                        style={{
                                          background: C.field,
                                          border: `1px solid ${C.line}`,
                                          borderRadius: "0.55rem",
                                          padding: "0.45rem 0.65rem",
                                          fontSize: "0.85rem",
                                          color: C.ink,
                                          width: "100%",
                                          outline: "none",
                                        }}
                                      />
                                    </div>
                                    <div>
                                      <label style={{ fontSize: "0.65rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.2rem" }}>
                                        Mes del próximo pago
                                      </label>
                                      <select
                                        value={cardDraft.targetDueMonth || "auto"}
                                        onChange={(e) => setCardDraft({ ...cardDraft, targetDueMonth: e.target.value })}
                                        style={{
                                          background: C.field,
                                          border: `1px solid ${C.line}`,
                                          borderRadius: "0.55rem",
                                          padding: "0.45rem 0.55rem",
                                          fontSize: "0.8rem",
                                          color: C.ink,
                                          width: "100%",
                                          outline: "none",
                                          fontWeight: 600,
                                        }}
                                      >
                                        <option value="auto">⚡ Automático (según hoy)</option>
                                        {getUpcomingMonthsList(2, 12).map((m) => (
                                          <option key={m.key} value={m.key}>
                                            🗓️ {m.label}
                                          </option>
                                        ))}
                                      </select>
                                    </div>
                                  </div>
                                </div>

                                {/* Previsualización en vivo de las fechas calculadas */}
                                {previewCycle && (
                                  <div
                                    style={{
                                      background: darkMode ? "rgba(16, 185, 129, 0.12)" : "#ECFDF5",
                                      border: `1px solid ${darkMode ? "rgba(16, 185, 129, 0.3)" : "#A7F3D0"}`,
                                      borderRadius: "0.65rem",
                                      padding: "0.55rem 0.75rem",
                                      fontSize: "0.74rem",
                                      color: darkMode ? "#A7F3D0" : "#065F46",
                                      display: "flex",
                                      flexDirection: "column",
                                      gap: "0.25rem",
                                    }}
                                  >
                                    <div style={{ fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                                      <span>✂️ Próximo corte: <strong>{previewCycle.cutoffFullLabel || previewCycle.cutoffLabel}</strong> ({previewCycle.cutoffRelative})</span>
                                    </div>
                                    <div style={{ fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                                      <span>⏰ Próximo pago: <strong>{previewCycle.dueFullLabel || previewCycle.dueLabel}</strong> ({previewCycle.dueRelative})</span>
                                    </div>
                                  </div>
                                )}

                                <div>
                                  <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                    Límite de crédito y moneda <span style={{ textTransform: "none", fontWeight: 400 }}>(opcional)</span>
                                  </label>
                                  <div style={{ display: "flex", gap: "0.5rem" }}>
                                    <select
                                      value={cardDraft.currency || editCurrency || "S/"}
                                      onChange={(e) => setCardDraft({ ...cardDraft, currency: e.target.value })}
                                      style={{
                                        background: C.paper,
                                        border: `1px solid ${C.line}`,
                                        borderRadius: "0.65rem",
                                        padding: "0.55rem 0.6rem",
                                        fontSize: "0.85rem",
                                        fontWeight: 700,
                                        color: C.ink,
                                        outline: "none",
                                        width: "6rem",
                                        flexShrink: 0,
                                      }}
                                    >
                                      <option value="S/">S/ (Soles)</option>
                                      <option value="$">$ (Dólares)</option>
                                      <option value="€">€ (Euros)</option>
                                    </select>
                                    <input
                                      type="number"
                                      value={cardDraft.creditLimit}
                                      onChange={(e) => setCardDraft({ ...cardDraft, creditLimit: e.target.value })}
                                      placeholder="Ej. 3500"
                                      style={{
                                        background: C.paper,
                                        border: `1px solid ${C.line}`,
                                        borderRadius: "0.65rem",
                                        padding: "0.55rem 0.75rem",
                                        fontSize: "0.85rem",
                                        color: C.ink,
                                        width: "100%",
                                        outline: "none",
                                      }}
                                    />
                                  </div>
                                </div>
                              </>
                            )}

                            {/* Si es TARJETA DE DÉBITO: selector de moneda */}
                            {cardDraft.cardType === "debito" && (
                              <div>
                                <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                  Moneda de la cuenta de débito
                                </label>
                                <select
                                  value={cardDraft.currency || editCurrency || "S/"}
                                  onChange={(e) => setCardDraft({ ...cardDraft, currency: e.target.value })}
                                  style={{
                                    background: C.paper,
                                    border: `1px solid ${C.line}`,
                                    borderRadius: "0.65rem",
                                    padding: "0.55rem 0.75rem",
                                    fontSize: "0.85rem",
                                    fontWeight: 700,
                                    color: C.ink,
                                    outline: "none",
                                    width: "100%",
                                  }}
                                >
                                  <option value="S/">S/ (Soles)</option>
                                  <option value="$">$ (Dólares)</option>
                                  <option value="€">€ (Euros)</option>
                                </select>
                              </div>
                            )}

                            {/* Sección de Programa de Cashback */}
                            <div style={{ background: C.paper, padding: "0.85rem", borderRadius: "0.85rem", border: `1px solid ${cardDraft.hasCashback ? (darkMode ? "#059669" : "#10B981") : C.lineSoft}`, display: "flex", flexDirection: "column", gap: "0.6rem" }}>
                              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                                <div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
                                  <span style={{ fontSize: "1.1rem" }}>🎁</span>
                                  <div>
                                    <div style={{ fontSize: "0.78rem", fontWeight: 700, color: C.ink }}>Programa de Cashback / Puntos</div>
                                    <div style={{ fontSize: "0.65rem", color: C.inkSoft }}>Dinero de vuelta en compras con esta tarjeta</div>
                                  </div>
                                </div>
                                <label style={{ display: "flex", alignItems: "center", gap: "0.35rem", cursor: "pointer" }}>
                                  <input
                                    type="checkbox"
                                    checked={!!cardDraft.hasCashback}
                                    onChange={(e) => setCardDraft({ ...cardDraft, hasCashback: e.target.checked })}
                                    style={{ accentColor: "#10B981", width: "1.1rem", height: "1.1rem", cursor: "pointer" }}
                                  />
                                  <span style={{ fontSize: "0.74rem", fontWeight: 700, color: cardDraft.hasCashback ? C.green : C.inkSoft }}>
                                    {cardDraft.hasCashback ? "Activado" : "Desactivado"}
                                  </span>
                                </label>
                              </div>

                              {cardDraft.hasCashback && (
                                <div style={{ display: "flex", flexDirection: "column", gap: "0.65rem", borderTop: `1px solid ${C.lineSoft}`, paddingTop: "0.55rem" }}>
                                  <div>
                                    <label style={{ fontSize: "0.66rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.3rem" }}>
                                      Porcentaje de Cashback (%)
                                    </label>
                                    <div style={{ display: "flex", gap: "0.35rem", flexWrap: "wrap", marginBottom: "0.4rem" }}>
                                      {["1", "1.5", "2", "3", "5"].map((pct) => {
                                        const isSel = String(cardDraft.cashbackPercent) === pct;
                                        return (
                                          <button
                                            type="button"
                                            key={pct}
                                            onClick={() => setCardDraft({ ...cardDraft, cashbackPercent: pct })}
                                            style={{
                                              background: isSel ? C.ink : C.field,
                                              color: isSel ? C.paper : C.ink,
                                              border: `1px solid ${isSel ? "transparent" : C.line}`,
                                              borderRadius: "0.5rem",
                                              padding: "0.25rem 0.55rem",
                                              fontSize: "0.74rem",
                                              fontWeight: isSel ? 700 : 500,
                                              cursor: "pointer",
                                            }}
                                          >
                                            {pct}%
                                          </button>
                                        );
                                      })}
                                    </div>
                                    <div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
                                      <input
                                        type="number"
                                        step="0.1"
                                        min="0"
                                        max="100"
                                        value={cardDraft.cashbackPercent != null ? cardDraft.cashbackPercent : ""}
                                        onChange={(e) => setCardDraft({ ...cardDraft, cashbackPercent: e.target.value })}
                                        placeholder="Otro %"
                                        style={{
                                          background: C.field,
                                          border: `1px solid ${C.line}`,
                                          borderRadius: "0.55rem",
                                          padding: "0.35rem 0.6rem",
                                          fontSize: "0.8rem",
                                          color: C.ink,
                                          width: "5.5rem",
                                          outline: "none",
                                          fontWeight: 700,
                                        }}
                                      />
                                      <span style={{ fontSize: "0.72rem", color: C.inkSoft }}>% de cashback por compra</span>
                                    </div>
                                  </div>

                                  {/* Regla de Cobertura de Cashback */}
                                  <div>
                                    <label style={{ fontSize: "0.66rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.35rem" }}>
                                      Regla de Cobertura y Exclusiones
                                    </label>
                                    <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.5rem" }}>
                                      <button
                                        type="button"
                                        onClick={() => setCardDraft({ ...cardDraft, excludeServices: true })}
                                        style={{
                                          fontFamily: "'Inter', sans-serif",
                                          padding: "0.55rem 0.65rem",
                                          borderRadius: "0.75rem",
                                          border: (cardDraft.excludeServices !== false) ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                                          background: (cardDraft.excludeServices !== false) ? (darkMode ? "rgba(16, 185, 129, 0.12)" : "#ECFDF5") : C.field,
                                          color: (cardDraft.excludeServices !== false) ? C.ink : C.inkSoft,
                                          fontSize: "0.75rem",
                                          fontWeight: (cardDraft.excludeServices !== false) ? 700 : 500,
                                          cursor: "pointer",
                                          textAlign: "left",
                                          display: "flex",
                                          flexDirection: "column",
                                          gap: "0.2rem",
                                        }}
                                      >
                                        <div style={{ display: "flex", alignItems: "center", gap: "0.3rem", fontWeight: 800, color: (cardDraft.excludeServices !== false) ? (darkMode ? "#6EE7B7" : "#047857") : C.ink }}>
                                          <span>🛡️ En todo menos servicios</span>
                                        </div>
                                        <div style={{ fontSize: "0.65rem", color: C.inkSoft, fontWeight: 400 }}>
                                          Acumula en compras generales y excluye luz, agua, telefonía, internet y tributos.
                                        </div>
                                      </button>

                                      <button
                                        type="button"
                                        onClick={() => setCardDraft({ ...cardDraft, excludeServices: false })}
                                        style={{
                                          fontFamily: "'Inter', sans-serif",
                                          padding: "0.55rem 0.65rem",
                                          borderRadius: "0.75rem",
                                          border: cardDraft.excludeServices === false ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                                          background: cardDraft.excludeServices === false ? (darkMode ? "rgba(16, 185, 129, 0.12)" : "#ECFDF5") : C.field,
                                          color: cardDraft.excludeServices === false ? C.ink : C.inkSoft,
                                          fontSize: "0.75rem",
                                          fontWeight: cardDraft.excludeServices === false ? 700 : 500,
                                          cursor: "pointer",
                                          textAlign: "left",
                                          display: "flex",
                                          flexDirection: "column",
                                          gap: "0.2rem",
                                        }}
                                      >
                                        <div style={{ display: "flex", alignItems: "center", gap: "0.3rem", fontWeight: 800, color: cardDraft.excludeServices === false ? (darkMode ? "#6EE7B7" : "#047857") : C.ink }}>
                                          <span>🌟 En TODO (100% compras)</span>
                                        </div>
                                        <div style={{ fontSize: "0.65rem", color: C.inkSoft, fontWeight: 400 }}>
                                          Acumula cashback en absolutamente cualquier gasto, incluidos recibos de servicios.
                                        </div>
                                      </button>
                                    </div>
                                  </div>
                                </div>
                              )}
                            </div>

                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.35rem" }}>
                                Color y estilo ({CARD_COLORS.length} disponibles)
                              </label>
                              <div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "0.45rem" }}>
                                {CARD_COLORS.map((col) => (
                                  <button
                                    type="button"
                                    key={col.id}
                                    onClick={() => setCardDraft({ ...cardDraft, color: col.id })}
                                    style={{
                                      height: "2.1rem",
                                      borderRadius: "0.6rem",
                                      background: col.gradient,
                                      border: cardDraft.color === col.id ? `2px solid ${C.ink}` : `1px solid ${col.border}`,
                                      cursor: "pointer",
                                      boxShadow: cardDraft.color === col.id ? "0 0 0 2px rgba(16,185,129,0.5)" : "none",
                                    }}
                                    title={col.label}
                                  />
                                ))}
                              </div>
                            </div>

                            <div style={{ display: "flex", gap: "0.5rem", marginTop: "0.5rem" }}>
                              <button
                                type="button"
                                onClick={() => setShowCardModal(false)}
                                style={{
                                  flex: 1,
                                  background: "transparent",
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.75rem",
                                  padding: "0.55rem 0",
                                  fontSize: "0.78rem",
                                  color: C.inkSoft,
                                  cursor: "pointer",
                                }}
                              >
                                Cancelar
                              </button>
                              <button
                                type="button"
                                onClick={() => {
                                  if (!cardDraft.name.trim()) return;
                                  const isDeb = cardDraft.cardType === "debito";
                                  const cardId = cardDraft.id || uid();
                                  const newCard = {
                                    id: cardId,
                                    name: cardDraft.name.trim(),
                                    cardType: isDeb ? "debito" : "credito",
                                    person: cardDraft.person.trim() || editPeople[0] || myName || "",
                                    billingCycle: isDeb ? undefined : (cardDraft.billingCycle === "quincenal" ? "quincenal" : "mensual"),
                                    cutoffDay: isDeb ? undefined : Math.min(31, Math.max(1, Number(cardDraft.cutoffDay) || 20)),
                                    dueDay: isDeb ? undefined : Math.min(31, Math.max(1, Number(cardDraft.dueDay) || 10)),
                                    targetCutoffMonth: !isDeb && cardDraft.targetCutoffMonth && cardDraft.targetCutoffMonth !== "auto" ? cardDraft.targetCutoffMonth : undefined,
                                    targetDueMonth: !isDeb && cardDraft.targetDueMonth && cardDraft.targetDueMonth !== "auto" ? cardDraft.targetDueMonth : undefined,
                                    creditLimit: !isDeb && cardDraft.creditLimit && Number(cardDraft.creditLimit) > 0 ? Number(cardDraft.creditLimit) : undefined,
                                    currency: cardDraft.currency || editCurrency || "S/",
                                    color: cardDraft.color || "emerald",
                                    hasCashback: !!cardDraft.hasCashback,
                                    cashbackPercent: cardDraft.hasCashback && cardDraft.cashbackPercent ? Number(cardDraft.cashbackPercent) : undefined,
                                    excludeServices: cardDraft.excludeServices !== false,
                                    minAmount: cardDraft.minAmount && Number(cardDraft.minAmount) > 0 ? Number(cardDraft.minAmount) : undefined,
                                    monthlyCap: cardDraft.monthlyCap && Number(cardDraft.monthlyCap) > 0 ? Number(cardDraft.monthlyCap) : undefined,
                                  };
                                  const exists = editCards.some((c) => c.id === cardId);
                                  if (exists) {
                                    setEditCards(editCards.map((c) => (c.id === cardId ? newCard : c)));
                                  } else {
                                    setEditCards([...editCards, newCard]);
                                  }
                                  setShowCardModal(false);
                                }}
                                style={{
                                  flex: 1.5,
                                  background: BRAND_GRADIENT,
                                  color: "#FFFFFF",
                                  border: "none",
                                  borderRadius: "0.75rem",
                                  padding: "0.55rem 0",
                                  fontSize: "0.78rem",
                                  fontWeight: 700,
                                  cursor: "pointer",
                                }}
                                className="cc-btn-press hover:opacity-90"
                              >
                                {cardDraft.id ? "Guardar cambios" : "Crear tarjeta"}
                              </button>
                            </div>
                          </div>
                        </div>
                      );
                    })()}

                    {/* Lista de tarjetas separada por titular */}
                    {(() => {
                      const editPeopleList = Array.from(
                        new Set([
                          ...editPeople.filter((p) => p.trim()),
                          ...editCards.map((c) => (c.person || "").trim()).filter(Boolean),
                        ])
                      );

                      if (editCards.length === 0) {
                        return (
                          <div
                            style={{
                              background: C.field,
                              border: `1px dashed ${C.line}`,
                              borderRadius: "1rem",
                              padding: "2rem 1.5rem",
                              textAlign: "center",
                              color: C.inkSoft,
                              fontSize: "0.82rem",
                            }}
                          >
                            <CreditCard size={28} color={C.inkSoft} style={{ margin: "0 auto 0.6rem", opacity: 0.6 }} />
                            <p style={{ margin: "0 0 0.4rem", fontWeight: 600, color: C.ink }}>No hay tarjetas registradas</p>
                            <p style={{ margin: 0, fontSize: "0.75rem" }}>
                              Pulsa en <strong>"Nueva tarjeta"</strong> para registrar tu primera tarjeta de crédito.
                            </p>
                          </div>
                        );
                      }

                      const filteredPeopleList = settingsCardFilter === "todas"
                        ? editPeopleList
                        : editPeopleList.filter((p) => p === settingsCardFilter);

                      return (
                        <div style={{ display: "flex", flexDirection: "column", gap: "0.85rem" }}>
                          {/* Filtro rápido por titular si hay más de 1 persona */}
                          {editPeopleList.length > 1 && (
                            <div style={{ display: "flex", gap: "0.35rem", flexWrap: "wrap", marginBottom: "0.25rem" }}>
                              <button
                                type="button"
                                onClick={() => setSettingsCardFilter("todas")}
                                style={{
                                  background: settingsCardFilter === "todas" ? C.ink : C.field,
                                  color: settingsCardFilter === "todas" ? C.paper : C.inkSoft,
                                  border: `1px solid ${settingsCardFilter === "todas" ? "transparent" : C.line}`,
                                  borderRadius: "999px",
                                  padding: "0.3rem 0.75rem",
                                  fontSize: "0.75rem",
                                  fontWeight: settingsCardFilter === "todas" ? 700 : 500,
                                  cursor: "pointer",
                                }}
                                className="cc-btn-press"
                              >
                                Todas ({editCards.length})
                              </button>
                              {editPeopleList.map((p) => {
                                const isSel = settingsCardFilter === p;
                                const count = editCards.filter((c) => (c.person || "").trim() === p).length;
                                const isTu = p === myName;
                                return (
                                  <button
                                    type="button"
                                    key={p}
                                    onClick={() => setSettingsCardFilter(p)}
                                    style={{
                                      background: isSel ? C.ink : C.field,
                                      color: isSel ? C.paper : C.inkSoft,
                                      border: `1px solid ${isSel ? "transparent" : C.line}`,
                                      borderRadius: "999px",
                                      padding: "0.3rem 0.75rem",
                                      fontSize: "0.75rem",
                                      fontWeight: isSel ? 700 : 500,
                                      cursor: "pointer",
                                      display: "flex",
                                      alignItems: "center",
                                      gap: "0.35rem",
                                    }}
                                    className="cc-btn-press"
                                  >
                                    <span style={{ width: "0.5rem", height: "0.5rem", borderRadius: "50%", background: personColor(p) }} />
                                    <span>{isTu ? `⭐ ${p}` : p} ({count})</span>
                                  </button>
                                );
                              })}
                            </div>
                          )}

                          {/* Secciones separadas por persona */}
                          {filteredPeopleList.map((personName) => {
                            const personCards = editCards.filter((c) => (c.person || "").trim() === personName);
                            const isTu = personName === myName;
                            return (
                              <div
                                key={personName}
                                style={{
                                  background: C.field,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "1.1rem",
                                  padding: "0.95rem 1rem",
                                }}
                              >
                                {/* Cabecera del titular */}
                                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: personCards.length > 0 ? "0.75rem" : 0, paddingBottom: personCards.length > 0 ? "0.5rem" : 0, borderBottom: personCards.length > 0 ? `1px solid ${C.lineSoft}` : "none" }}>
                                  <div style={{ display: "flex", alignItems: "center", gap: "0.55rem" }}>
                                    <div
                                      style={{
                                        width: "1.8rem",
                                        height: "1.8rem",
                                        borderRadius: "50%",
                                        backgroundColor: personColor(personName),
                                        color: "#FFFFFF",
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        fontWeight: 700,
                                        fontSize: "0.82rem",
                                        flexShrink: 0,
                                      }}
                                    >
                                      {(personName || "?").trim().charAt(0).toUpperCase()}
                                    </div>
                                    <div>
                                      <div style={{ fontSize: "0.86rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.35rem" }}>
                                        <span>{personName}</span>
                                        {isTu && <span style={{ color: C.green, fontSize: "0.72rem", fontWeight: 600 }}>(tú)</span>}
                                      </div>
                                      <div style={{ fontSize: "0.68rem", color: C.inkSoft }}>
                                        {personCards.length} {personCards.length === 1 ? "tarjeta registrada" : "tarjetas registradas"}
                                      </div>
                                    </div>
                                  </div>

                                  <button
                                    type="button"
                                    onClick={() => {
                                      setCardDraft({
                                        id: "",
                                        name: "",
                                        cardType: "credito",
                                        person: personName,
                                        cutoffDay: 20,
                                        dueDay: 10,
                                        creditLimit: "",
                                        currency: editCurrency || "S/",
                                        color: "emerald",
                                      });
                                      setShowCardModal(true);
                                    }}
                                    style={{
                                      background: C.paper,
                                      border: `1px solid ${C.line}`,
                                      borderRadius: "0.6rem",
                                      padding: "0.3rem 0.65rem",
                                      fontSize: "0.72rem",
                                      fontWeight: 600,
                                      color: C.ink,
                                      cursor: "pointer",
                                      display: "flex",
                                      alignItems: "center",
                                      gap: "0.25rem",
                                    }}
                                    className="cc-btn-press"
                                  >
                                    <Plus size={11} />
                                    <span>Añadir</span>
                                  </button>
                                </div>

                                {/* Cuadrícula de tarjetas de esta persona */}
                                {personCards.length === 0 ? (
                                  <div style={{ fontSize: "0.75rem", color: C.inkSoft, textAlign: "center", padding: "0.5rem 0" }}>
                                    {personName} no tiene tarjetas registradas todavía.
                                  </div>
                                ) : (
                                  <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.65rem" }}>
                                    {personCards.map((c) => {
                                      const pal = CARD_COLORS.find((p) => p.id === c.color) || CARD_COLORS[0];
                                      const cardCurr = c.currency || editCurrency || "S/";
                                      const isDeb = c.cardType === "debito";
                                      return (
                                        <div
                                          key={c.id}
                                          style={{
                                            background: C.paper,
                                            border: `1px solid ${C.line}`,
                                            borderRadius: "0.9rem",
                                            padding: "0.8rem",
                                            display: "flex",
                                            flexDirection: "column",
                                            justifyContent: "space-between",
                                            gap: "0.6rem",
                                          }}
                                        >
                                          <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.5rem" }}>
                                            <div style={{ display: "flex", alignItems: "center", gap: "0.6rem", minWidth: 0 }}>
                                              <div
                                                style={{
                                                  width: "2.2rem",
                                                  height: "1.5rem",
                                                  borderRadius: "0.4rem",
                                                  background: pal.gradient,
                                                  border: `1px solid ${pal.border}`,
                                                  flexShrink: 0,
                                                  boxShadow: "0 2px 6px rgba(0,0,0,0.12)",
                                                }}
                                              />
                                              <div style={{ minWidth: 0 }}>
                                                <div style={{ fontSize: "0.88rem", fontWeight: 700, color: C.ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", display: "flex", alignItems: "center", gap: "0.35rem" }}>
                                                  <span>{c.name}</span>
                                                  <span style={{ fontSize: "0.6rem", fontWeight: 800, background: isDeb ? (darkMode ? "#1E3A8A" : "#DBEAFE") : (darkMode ? "#064E3B" : "#D1FAE5"), color: isDeb ? (darkMode ? "#93C5FD" : "#1D4ED8") : (darkMode ? "#6EE7B7" : "#047857"), padding: "0.08rem 0.35rem", borderRadius: "0.35rem" }}>
                                                    {isDeb ? "🏧 Débito" : "💳 Crédito"}
                                                  </span>
                                                  {c.currency && c.currency !== (editCurrency || "S/") && (
                                                    <span style={{ fontSize: "0.62rem", background: C.chipBg, padding: "0.1rem 0.35rem", borderRadius: "0.35rem", color: C.inkSoft }}>
                                                      {c.currency}
                                                    </span>
                                                  )}
                                                </div>
                                                <div style={{ fontSize: "0.7rem", color: C.inkSoft, display: "flex", alignItems: "center", gap: "0.35rem", flexWrap: "wrap" }}>
                                                  {isDeb ? (
                                                    <span>Débito directo · Sin ciclo de deuda</span>
                                                  ) : (
                                                    <>
                                                      {c.billingCycle === "quincenal" && (
                                                        <span style={{ fontSize: "0.62rem", fontWeight: 700, background: darkMode ? "#451A03" : "#FEF3C7", color: darkMode ? "#FDE68A" : "#92400E", padding: "0.05rem 0.35rem", borderRadius: "0.35rem" }}>
                                                          ⚡ Quincenal (15d)
                                                        </span>
                                                      )}
                                                      {c.targetCutoffMonth && c.targetCutoffMonth !== "auto" && (
                                                        <span style={{ fontSize: "0.62rem", fontWeight: 700, background: darkMode ? "#064E3B" : "#D1FAE5", color: darkMode ? "#6EE7B7" : "#047857", padding: "0.05rem 0.35rem", borderRadius: "0.35rem" }}>
                                                          ✂️ {c.targetCutoffMonth}
                                                        </span>
                                                      )}
                                                      {(c.targetDueMonth || c.targetMonth) && (c.targetDueMonth || c.targetMonth) !== "auto" && (
                                                        <span style={{ fontSize: "0.62rem", fontWeight: 700, background: darkMode ? "#064E3B" : "#D1FAE5", color: darkMode ? "#6EE7B7" : "#047857", padding: "0.05rem 0.35rem", borderRadius: "0.35rem" }}>
                                                          ⏰ {c.targetDueMonth || c.targetMonth}
                                                        </span>
                                                      )}
                                                      <span>
                                                        Corte: <strong>día {c.cutoffDay}</strong> · Pago: <strong>día {c.dueDay}</strong>
                                                      </span>
                                                    </>
                                                  )}
                                                  {c.hasCashback && (
                                                    <span style={{ fontSize: "0.62rem", fontWeight: 700, background: darkMode ? "#064E3B" : "#ECFDF5", color: darkMode ? "#6EE7B7" : "#047857", border: `1px solid ${darkMode ? "#059669" : "#A7F3D0"}`, padding: "0.05rem 0.35rem", borderRadius: "0.35rem" }}>
                                                      🎁 Cashback {c.cashbackPercent || 1}% {c.excludeServices === false ? "(En todo)" : "(Menos servicios)"}
                                                    </span>
                                                  )}
                                                </div>
                                              </div>
                                            </div>

                                            <div style={{ display: "flex", alignItems: "center", gap: "0.25rem", flexShrink: 0 }}>
                                              <button
                                                type="button"
                                                onClick={() => {
                                                  setCardDraft({
                                                    id: c.id,
                                                    name: c.name,
                                                    cardType: c.cardType || "credito",
                                                    person: c.person,
                                                    billingCycle: c.billingCycle || "mensual",
                                                    cutoffDay: c.cutoffDay || 20,
                                                    dueDay: c.dueDay || 10,
                                                    targetCutoffMonth: c.targetCutoffMonth || "auto",
                                                    targetDueMonth: c.targetDueMonth || c.targetMonth || "auto",
                                                    creditLimit: c.creditLimit ? String(c.creditLimit) : "",
                                                    currency: c.currency || editCurrency || "S/",
                                                    color: c.color || "emerald",
                                                    hasCashback: c.hasCashback === true || (c.cashbackPercent != null && Number(c.cashbackPercent) > 0),
                                                    cashbackPercent: c.cashbackPercent != null ? String(c.cashbackPercent) : "1.5",
                                                    excludeServices: c.excludeServices !== false,
                                                    minAmount: c.minAmount != null ? String(c.minAmount) : "",
                                                    monthlyCap: c.monthlyCap != null ? String(c.monthlyCap) : "",
                                                  });
                                                  setShowCardModal(true);
                                                }}
                                                style={{ color: C.inkSoft, background: C.field, border: `1px solid ${C.line}`, borderRadius: "0.5rem", cursor: "pointer", padding: "0.3rem" }}
                                                title="Editar tarjeta"
                                              >
                                                <Pencil size={12} />
                                              </button>
                                              <button
                                                type="button"
                                                onClick={() => setEditCards(editCards.filter((card) => card.id !== c.id))}
                                                style={{ color: C.red, background: C.dangerBg, border: `1px solid ${C.line}`, borderRadius: "0.5rem", cursor: "pointer", padding: "0.3rem" }}
                                                title="Eliminar tarjeta"
                                              >
                                                <X size={12} />
                                              </button>
                                            </div>
                                          </div>

                                          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "0.72rem", color: C.inkSoft, background: C.field, padding: "0.35rem 0.6rem", borderRadius: "0.55rem", border: `1px solid ${C.lineSoft}` }}>
                                            <span>{pal.label}</span>
                                            {isDeb ? (
                                              <span>🏧 Cuenta Débito</span>
                                            ) : c.creditLimit ? (
                                              <span>Límite: <strong style={{ color: C.ink }}>{fmtMoney(c.creditLimit, cardCurr)}</strong></span>
                                            ) : (
                                              <span style={{ fontStyle: "italic", opacity: 0.8 }}>Sin límite fijado</span>
                                            )}
                                          </div>
                                        </div>
                                      );
                                    })}
                                  </div>
                                )}
                              </div>
                            );
                          })}
                        </div>
                      );
                    })()}
                  </div>
                )}

                {/* ===== PESTAÑA 3: CUOTAS Y PRÉSTAMOS ===== */}
                {settingsTab === "cuotas" && (
                  <div>
                    <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.75rem", marginBottom: "1rem" }}>
                      <div>
                        <div style={{ fontSize: "0.85rem", fontWeight: 700, color: C.ink, marginBottom: "0.2rem" }}>
                          Cuotas y compromisos mensuales
                        </div>
                        <div style={{ fontSize: "0.72rem", color: C.inkSoft, lineHeight: 1.4 }}>
                          Registra cuotas fijas o préstamos que pagas cada mes a una persona o entidad con fecha límite y seguimiento de cuotas.
                        </div>
                      </div>
                      <button
                        type="button"
                        onClick={() => {
                          const availablePerson = editPeople.find((p) => p.trim()) || myName || "";
                          setInstallmentDraft({
                            id: "",
                            name: "",
                            person: availablePerson,
                            recipient: "",
                            amount: "",
                            currency: editCurrency || "S/",
                            dueDay: 15,
                            totalInstallments: "",
                            paidInstallments: 0,
                            cardId: "",
                            notes: "",
                            color: "gold",
                          });
                          setShowInstallmentModal(true);
                        }}
                        style={{
                          fontFamily: "'Inter', sans-serif",
                          color: "#FFFFFF",
                          background: BRAND_GRADIENT,
                          border: "none",
                          borderRadius: "0.75rem",
                          padding: "0.45rem 0.85rem",
                          fontSize: "0.75rem",
                          fontWeight: 700,
                          cursor: "pointer",
                          flexShrink: 0,
                          boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                        }}
                        className="cc-btn-press hover:opacity-90 flex items-center gap-1.5"
                      >
                        <Plus size={13} />
                        Nueva cuota
                      </button>
                    </div>

                    {/* Modal / Formulario para crear o editar cuota */}
                    {showInstallmentModal && (
                      <div
                        style={{
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "1rem",
                          padding: "1rem",
                          marginBottom: "1.25rem",
                          boxShadow: C.cardShadow,
                        }}
                      >
                        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.85rem" }}>
                          <span style={{ fontSize: "0.85rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                            <Calendar size={14} color={C.green} />
                            {installmentDraft.id ? "Editar cuota / préstamo" : "Crear nueva cuota / compromiso"}
                          </span>
                          <button
                            type="button"
                            onClick={() => setShowInstallmentModal(false)}
                            style={{ color: C.inkSoft, background: "none", border: "none", cursor: "pointer" }}
                          >
                            <X size={15} />
                          </button>
                        </div>

                        <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
                          <div>
                            <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                              Concepto / Nombre
                            </label>
                            <input
                              value={installmentDraft.name}
                              onChange={(e) => setInstallmentDraft({ ...installmentDraft, name: e.target.value })}
                              placeholder="Ej. Préstamo moto, Deuda con Carlos, Alquiler..."
                              maxLength={35}
                              style={{
                                background: C.paper,
                                border: `1px solid ${C.line}`,
                                borderRadius: "0.65rem",
                                padding: "0.55rem 0.75rem",
                                fontSize: "0.85rem",
                                color: C.ink,
                                width: "100%",
                                outline: "none",
                              }}
                            />
                          </div>

                          <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.75rem" }}>
                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                ¿Quién paga? (Titular)
                              </label>
                              <select
                                value={installmentDraft.person}
                                onChange={(e) => setInstallmentDraft({ ...installmentDraft, person: e.target.value })}
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                }}
                              >
                                {editPeople.filter((p) => p.trim()).map((p) => (
                                  <option key={p} value={p}>
                                    {p}
                                  </option>
                                ))}
                              </select>
                            </div>

                            <div>
                              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.25rem" }}>
                                <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700 }}>
                                  ¿A quién se le paga? (Acreedor)
                                </label>
                                <button
                                  type="button"
                                  onClick={() => {
                                    const nextIsInternal = !installmentDraft.isInternal;
                                    const otherPerson = editPeople.find((p) => p.trim() && p !== installmentDraft.person) || "";
                                    setInstallmentDraft({
                                      ...installmentDraft,
                                      isInternal: nextIsInternal,
                                      recipient: nextIsInternal ? otherPerson : "",
                                    });
                                  }}
                                  style={{
                                    background: "none",
                                    border: "none",
                                    color: installmentDraft.isInternal ? C.green : C.inkSoft,
                                    fontSize: "0.65rem",
                                    fontWeight: 700,
                                    cursor: "pointer",
                                    padding: 0,
                                    textDecoration: "underline",
                                  }}
                                >
                                  {installmentDraft.isInternal ? "🤝 Entre nosotros" : "🏦 Entidad externa"}
                                </button>
                              </div>
                              {installmentDraft.isInternal ? (
                                <select
                                  value={installmentDraft.recipient}
                                  onChange={(e) => setInstallmentDraft({ ...installmentDraft, recipient: e.target.value })}
                                  style={{
                                    background: C.paper,
                                    border: `1.5px solid ${C.green}`,
                                    borderRadius: "0.65rem",
                                    padding: "0.55rem 0.75rem",
                                    fontSize: "0.85rem",
                                    color: C.ink,
                                    width: "100%",
                                    outline: "none",
                                    fontWeight: 600,
                                  }}
                                >
                                  <option value="">Selecciona quién prestó el dinero…</option>
                                  {editPeople.filter((p) => p.trim()).map((p) => (
                                    <option key={p} value={p} disabled={p === installmentDraft.person}>
                                      🤝 {p} {p === installmentDraft.person ? "(es quien paga)" : ""}
                                    </option>
                                  ))}
                                </select>
                              ) : (
                                <input
                                  value={installmentDraft.recipient}
                                  onChange={(e) => setInstallmentDraft({ ...installmentDraft, recipient: e.target.value })}
                                  placeholder="Ej. Banco, Tienda, Familiar..."
                                  maxLength={30}
                                  style={{
                                    background: C.paper,
                                    border: `1px solid ${C.line}`,
                                    borderRadius: "0.65rem",
                                    padding: "0.55rem 0.75rem",
                                    fontSize: "0.85rem",
                                    color: C.ink,
                                    width: "100%",
                                    outline: "none",
                                  }}
                                />
                              )}
                            </div>
                          </div>

                          <div>
                            <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                              Monto total de la deuda o préstamo <span style={{ textTransform: "none", fontWeight: 400 }}>(opcional)</span>
                            </label>
                            <div style={{ display: "flex", gap: "0.5rem" }}>
                              <select
                                value={installmentDraft.currency || editCurrency || "S/"}
                                onChange={(e) => setInstallmentDraft({ ...installmentDraft, currency: e.target.value })}
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.5rem",
                                  fontSize: "0.85rem",
                                  fontWeight: 700,
                                  color: C.ink,
                                  outline: "none",
                                  width: "5.5rem",
                                  flexShrink: 0,
                                }}
                              >
                                <option value="S/">S/ (Soles)</option>
                                <option value="$">$ (Dólares)</option>
                                <option value="€">€ (Euros)</option>
                              </select>
                              <input
                                type="number"
                                value={installmentDraft.totalAmount}
                                onChange={(e) => {
                                  const val = e.target.value;
                                  const totalDebtNum = parseFloat(val);
                                  const cuotasNum = parseInt(installmentDraft.totalInstallments, 10);
                                  let newAmount = installmentDraft.amount;
                                  if (isFinite(totalDebtNum) && totalDebtNum > 0 && isFinite(cuotasNum) && cuotasNum > 0) {
                                    newAmount = String(Math.round((totalDebtNum / cuotasNum) * 100) / 100);
                                  }
                                  setInstallmentDraft({ ...installmentDraft, totalAmount: val, amount: newAmount });
                                }}
                                placeholder="Ej. 5000 (monto total de deuda)"
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                  fontWeight: 600,
                                }}
                              />
                            </div>
                          </div>

                          <div style={{ display: "grid", gridTemplateColumns: "1.2fr 1fr", gap: "0.75rem" }}>
                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                Monto por cuota mensual
                              </label>
                              <input
                                type="number"
                                value={installmentDraft.amount}
                                onChange={(e) => {
                                  const val = e.target.value;
                                  const cuotaNum = parseFloat(val);
                                  const cuotasNum = parseInt(installmentDraft.totalInstallments, 10);
                                  let newTotal = installmentDraft.totalAmount;
                                  if (isFinite(cuotaNum) && cuotaNum > 0 && isFinite(cuotasNum) && cuotasNum > 0) {
                                    newTotal = String(Math.round(cuotaNum * cuotasNum * 100) / 100);
                                  }
                                  setInstallmentDraft({ ...installmentDraft, amount: val, totalAmount: newTotal });
                                }}
                                placeholder="Ej. 500"
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                  fontWeight: 600,
                                }}
                              />
                            </div>

                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                Día de pago (1–31)
                              </label>
                              <input
                                type="number"
                                min={1}
                                max={31}
                                value={installmentDraft.dueDay}
                                onChange={(e) => setInstallmentDraft({ ...installmentDraft, dueDay: Number(e.target.value) || 15 })}
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                }}
                              />
                            </div>
                          </div>

                          {/* Próximo mes de vencimiento / pago */}
                          <div>
                            <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                              Próximo mes de vencimiento / pago
                            </label>
                            <select
                              value={installmentDraft.targetDueMonth || installmentDraft.targetMonth || "auto"}
                              onChange={(e) => setInstallmentDraft({ ...installmentDraft, targetDueMonth: e.target.value, targetMonth: e.target.value })}
                              style={{
                                background: C.paper,
                                border: `1px solid ${C.line}`,
                                borderRadius: "0.65rem",
                                padding: "0.55rem 0.75rem",
                                fontSize: "0.85rem",
                                color: C.ink,
                                width: "100%",
                                outline: "none",
                                fontWeight: 600,
                              }}
                            >
                              <option value="auto">⚡ Automático (calcular según el día de hoy)</option>
                              {getUpcomingMonthsList(2, 12).map((m) => (
                                <option key={m.key} value={m.key}>
                                  🗓️ {m.label}
                                </option>
                              ))}
                            </select>
                            <span style={{ fontSize: "0.68rem", color: C.inkSoft, display: "block", marginTop: "0.2rem" }}>
                              Elige el mes que corresponda a tu cronograma de pago actual.
                            </span>
                          </div>

                          {/* Previsualización en vivo de la fecha de cuota */}
                          {(() => {
                            const preview = getInstallmentCycleInfo(installmentDraft);
                            if (!preview) return null;
                            return (
                              <div
                                style={{
                                  background: darkMode ? "rgba(245, 158, 11, 0.12)" : "#FFFBEB",
                                  border: `1px solid ${darkMode ? "rgba(245, 158, 11, 0.3)" : "#FDE68A"}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.74rem",
                                  color: darkMode ? "#FDE68A" : "#92400E",
                                  display: "flex",
                                  flexDirection: "column",
                                  gap: "0.25rem",
                                }}
                              >
                                <div style={{ fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                                  <span>⏰ Próximo vencimiento: <strong>{preview.dueFullLabel || preview.dueLabel}</strong> ({preview.daysToDue === 0 ? "¡Hoy!" : `en ${preview.daysToDue}d`})</span>
                                </div>
                                {installmentDraft.targetMonth && installmentDraft.targetMonth !== "auto" && (
                                  <div style={{ fontSize: "0.68rem", opacity: 0.9, marginTop: "0.1rem" }}>
                                    📌 Coordinado con <strong>{preview.dueFullLabel}</strong> (avanzará solo tras pasar la fecha).
                                  </div>
                                )}
                              </div>
                            );
                          })()}

                          {/* Tarjeta vinculada (opcional): la cuota se cobra en esa tarjeta */}
                          {(editCards || []).length > 0 && (
                            <div style={{ marginBottom: "0.75rem" }}>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                Cobrar en tarjeta <span style={{ textTransform: "none", fontWeight: 400 }}>(opcional)</span>
                              </label>
                              <select
                                value={installmentDraft.cardId || ""}
                                onChange={(e) => setInstallmentDraft({ ...installmentDraft, cardId: e.target.value })}
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                }}
                              >
                                <option value="">Efectivo (sin tarjeta)</option>
                                {editCards.map((c) => (
                                  <option key={c.id} value={c.id}>
                                    💳 {c.name}{c.person ? ` · ${c.person}` : ""}
                                  </option>
                                ))}
                              </select>
                              <div style={{ fontSize: "0.65rem", color: C.inkSoft, marginTop: "0.25rem" }}>
                                Cada pago de cuota se anota como consumo en esa tarjeta y se liquida al abonarla. Al terminar la cuota deja de cobrarse.
                              </div>
                            </div>
                          )}

                          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                Cuotas totales <span style={{ textTransform: "none", fontWeight: 400 }}>(opcional)</span>
                              </label>
                              <input
                                type="number"
                                min={1}
                                value={installmentDraft.totalInstallments}
                                onChange={(e) => {
                                  const val = e.target.value;
                                  const cuotasNum = parseInt(val, 10);
                                  const totalDebtNum = parseFloat(installmentDraft.totalAmount);
                                  let newAmount = installmentDraft.amount;
                                  if (isFinite(totalDebtNum) && totalDebtNum > 0 && isFinite(cuotasNum) && cuotasNum > 0) {
                                    newAmount = String(Math.round((totalDebtNum / cuotasNum) * 100) / 100);
                                  }
                                  setInstallmentDraft({ ...installmentDraft, totalInstallments: val, amount: newAmount });
                                }}
                                placeholder="Ej. 10 (vacío si indefinido)"
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                }}
                              />
                            </div>

                            <div>
                              <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.25rem" }}>
                                Cuotas ya pagadas
                              </label>
                              <input
                                type="number"
                                min={0}
                                value={installmentDraft.paidInstallments}
                                onChange={(e) => setInstallmentDraft({ ...installmentDraft, paidInstallments: Math.max(0, Number(e.target.value) || 0) })}
                                placeholder="Ej. 0"
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  padding: "0.55rem 0.75rem",
                                  fontSize: "0.85rem",
                                  color: C.ink,
                                  width: "100%",
                                  outline: "none",
                                }}
                              />
                            </div>
                          </div>

                          {/* Resumen dinámico de la deuda */}
                          {(() => {
                            const totDebt = parseFloat(installmentDraft.totalAmount) || ((parseFloat(installmentDraft.amount) || 0) * (parseInt(installmentDraft.totalInstallments, 10) || 1));
                            const cuota = parseFloat(installmentDraft.amount) || 0;
                            const paid = parseInt(installmentDraft.paidInstallments, 10) || 0;
                            const cur = installmentDraft.currency || editCurrency || "S/";
                            if (totDebt > 0 || cuota > 0) {
                              const paidAmt = cuota > 0 ? cuota * paid : 0;
                              const remaining = Math.max(0, totDebt - paidAmt);
                              return (
                                <div style={{ background: C.paper, border: `1px solid ${C.lineSoft}`, borderRadius: "0.75rem", padding: "0.65rem 0.85rem", fontSize: "0.74rem", display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: "0.4rem" }}>
                                  <div>
                                    <span style={{ color: C.inkSoft }}>Deuda total: </span>
                                    <strong style={{ color: C.ink }}>{fmtMoney(totDebt, cur)}</strong>
                                  </div>
                                  {paid > 0 && (
                                    <div>
                                      <span style={{ color: C.inkSoft }}>Pagado ({paid}): </span>
                                      <strong style={{ color: C.green }}>{fmtMoney(paidAmt, cur)}</strong>
                                    </div>
                                  )}
                                  <div>
                                    <span style={{ color: C.inkSoft }}>Saldo restante: </span>
                                    <strong style={{ color: "#D97706" }}>{fmtMoney(remaining, cur)}</strong>
                                  </div>
                                </div>
                              );
                            }
                            return null;
                          })()}

                          <div>
                            <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.35rem" }}>
                              Color y estilo
                            </label>
                            <div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: "0.45rem" }}>
                              {CARD_COLORS.map((col) => (
                                <button
                                  type="button"
                                  key={col.id}
                                  onClick={() => setInstallmentDraft({ ...installmentDraft, color: col.id })}
                                  style={{
                                    height: "2.1rem",
                                    borderRadius: "0.6rem",
                                    background: col.gradient,
                                    border: installmentDraft.color === col.id ? `2px solid ${C.ink}` : `1px solid ${col.border}`,
                                    cursor: "pointer",
                                    boxShadow: installmentDraft.color === col.id ? "0 0 0 2px rgba(16,185,129,0.5)" : "none",
                                  }}
                                  title={col.label}
                                />
                              ))}
                            </div>
                          </div>

                          <div style={{ display: "flex", gap: "0.5rem", marginTop: "0.5rem" }}>
                            <button
                              type="button"
                              onClick={() => setShowInstallmentModal(false)}
                              style={{
                                flex: 1,
                                background: "transparent",
                                border: `1px solid ${C.line}`,
                                borderRadius: "0.75rem",
                                padding: "0.55rem 0",
                                fontSize: "0.78rem",
                                color: C.inkSoft,
                                cursor: "pointer",
                              }}
                            >
                              Cancelar
                            </button>
                            <button
                              type="button"
                              onClick={() => {
                                if (!installmentDraft.name.trim()) return;
                                const instId = installmentDraft.id || uid();
                                const amt = Number(installmentDraft.amount) > 0 ? Number(installmentDraft.amount) : 0;
                                const totalInst = installmentDraft.totalInstallments && Number(installmentDraft.totalInstallments) > 0 ? Math.round(Number(installmentDraft.totalInstallments)) : undefined;
                                const totalDebt = Number(installmentDraft.totalAmount) > 0
                                  ? Number(installmentDraft.totalAmount)
                                  : (amt > 0 && totalInst ? amt * totalInst : amt);
                                const newInst = {
                                  id: instId,
                                  name: installmentDraft.name.trim(),
                                  person: installmentDraft.person.trim() || editPeople[0] || myName || "",
                                  recipient: installmentDraft.recipient.trim(),
                                  totalAmount: totalDebt > 0 ? totalDebt : undefined,
                                  amount: amt,
                                  currency: installmentDraft.currency || editCurrency || "S/",
                                  dueDay: Math.min(31, Math.max(1, Number(installmentDraft.dueDay) || 15)),
                                  targetMonth: installmentDraft.targetMonth && installmentDraft.targetMonth !== "auto" ? installmentDraft.targetMonth : undefined,
                                  totalInstallments: totalInst,
                                  paidInstallments: Math.max(0, Number(installmentDraft.paidInstallments) || 0),
                                  cardId: installmentDraft.cardId || undefined,
                                  notes: installmentDraft.notes.trim(),
                                  color: installmentDraft.color || "gold",
                                };
                                const exists = editInstallments.some((i) => i.id === instId);
                                if (exists) {
                                  setEditInstallments(editInstallments.map((i) => (i.id === instId ? newInst : i)));
                                } else {
                                  setEditInstallments([...editInstallments, newInst]);
                                }
                                setShowInstallmentModal(false);
                              }}
                              style={{
                                flex: 1.5,
                                background: BRAND_GRADIENT,
                                color: "#FFFFFF",
                                border: "none",
                                borderRadius: "0.75rem",
                                padding: "0.55rem 0",
                                fontSize: "0.78rem",
                                fontWeight: 700,
                                cursor: "pointer",
                              }}
                            >
                              Guardar cuota
                            </button>
                          </div>
                        </div>
                      </div>
                    )}

                    {/* Lista de cuotas agrupadas por titular */}
                    {(() => {
                      const editPeopleList = Array.from(
                        new Set([
                          ...editPeople.filter((p) => p.trim()),
                          ...editInstallments.map((i) => (i.person || "").trim()).filter(Boolean),
                        ])
                      );

                      if (editInstallments.length === 0) {
                        return (
                          <div
                            style={{
                              background: C.field,
                              border: `1px dashed ${C.line}`,
                              borderRadius: "1rem",
                              padding: "2rem 1.5rem",
                              textAlign: "center",
                              color: C.inkSoft,
                              fontSize: "0.82rem",
                            }}
                          >
                            <Calendar size={28} color={C.inkSoft} style={{ margin: "0 auto 0.6rem", opacity: 0.6 }} />
                            <p style={{ margin: "0 0 0.4rem", fontWeight: 600, color: C.ink }}>No hay cuotas registradas</p>
                            <p style={{ margin: 0, fontSize: "0.75rem" }}>
                              Pulsa en <strong>"Nueva cuota"</strong> para registrar préstamos o pagos recurrentes.
                            </p>
                          </div>
                        );
                      }

                      const filteredPeopleList = settingsInstallmentFilter === "todas"
                        ? editPeopleList
                        : editPeopleList.filter((p) => p === settingsInstallmentFilter);

                      return (
                        <div style={{ display: "flex", flexDirection: "column", gap: "0.85rem" }}>
                          {/* Filtro rápido por titular si hay más de 1 persona */}
                          {editPeopleList.length > 1 && (
                            <div style={{ display: "flex", gap: "0.35rem", flexWrap: "wrap", marginBottom: "0.25rem" }}>
                              <button
                                type="button"
                                onClick={() => setSettingsInstallmentFilter("todas")}
                                style={{
                                  background: settingsInstallmentFilter === "todas" ? C.ink : C.field,
                                  color: settingsInstallmentFilter === "todas" ? C.paper : C.inkSoft,
                                  border: `1px solid ${settingsInstallmentFilter === "todas" ? "transparent" : C.line}`,
                                  borderRadius: "999px",
                                  padding: "0.3rem 0.75rem",
                                  fontSize: "0.75rem",
                                  fontWeight: settingsInstallmentFilter === "todas" ? 700 : 500,
                                  cursor: "pointer",
                                }}
                                className="cc-btn-press"
                              >
                                Todas ({editInstallments.length})
                              </button>
                              {editPeopleList.map((p) => {
                                const isSel = settingsInstallmentFilter === p;
                                const count = editInstallments.filter((i) => (i.person || "").trim() === p).length;
                                const isTu = p === myName;
                                return (
                                  <button
                                    type="button"
                                    key={p}
                                    onClick={() => setSettingsInstallmentFilter(p)}
                                    style={{
                                      background: isSel ? C.ink : C.field,
                                      color: isSel ? C.paper : C.inkSoft,
                                      border: `1px solid ${isSel ? "transparent" : C.line}`,
                                      borderRadius: "999px",
                                      padding: "0.3rem 0.75rem",
                                      fontSize: "0.75rem",
                                      fontWeight: isSel ? 700 : 500,
                                      cursor: "pointer",
                                      display: "flex",
                                      alignItems: "center",
                                      gap: "0.35rem",
                                    }}
                                    className="cc-btn-press"
                                  >
                                    <span style={{ width: "0.5rem", height: "0.5rem", borderRadius: "50%", background: personColor(p) }} />
                                    <span>{isTu ? `⭐ ${p}` : p} ({count})</span>
                                  </button>
                                );
                              })}
                            </div>
                          )}

                          {/* Secciones separadas por persona */}
                          {filteredPeopleList.map((personName) => {
                            const personItems = editInstallments.filter((i) => (i.person || "").trim() === personName);
                            const isTu = personName === myName;
                            return (
                              <div
                                key={personName}
                                style={{
                                  background: C.field,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "1.1rem",
                                  padding: "0.95rem 1rem",
                                }}
                              >
                                {/* Cabecera del titular */}
                                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: personItems.length > 0 ? "0.75rem" : 0, paddingBottom: personItems.length > 0 ? "0.5rem" : 0, borderBottom: personItems.length > 0 ? `1px solid ${C.lineSoft}` : "none" }}>
                                  <div style={{ display: "flex", alignItems: "center", gap: "0.55rem" }}>
                                    <div
                                      style={{
                                        width: "1.8rem",
                                        height: "1.8rem",
                                        borderRadius: "50%",
                                        backgroundColor: personColor(personName),
                                        color: "#FFFFFF",
                                        display: "flex",
                                        alignItems: "center",
                                        justifyContent: "center",
                                        fontWeight: 700,
                                        fontSize: "0.82rem",
                                        flexShrink: 0,
                                      }}
                                    >
                                      {(personName || "?").trim().charAt(0).toUpperCase()}
                                    </div>
                                    <div>
                                      <div style={{ fontSize: "0.86rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.35rem" }}>
                                        <span>{personName}</span>
                                        {isTu && <span style={{ color: C.green, fontSize: "0.72rem", fontWeight: 600 }}>(tú)</span>}
                                      </div>
                                      <div style={{ fontSize: "0.68rem", color: C.inkSoft }}>
                                        {personItems.length} {personItems.length === 1 ? "compromiso registrado" : "compromisos registrados"}
                                      </div>
                                    </div>
                                  </div>

                                  <button
                                    type="button"
                                    onClick={() => {
                                      setInstallmentDraft({
                                        id: "",
                                        name: "",
                                        person: personName,
                                        recipient: "",
                                        amount: "",
                                        currency: editCurrency || "S/",
                                        dueDay: 15,
                                        totalInstallments: "",
                                        paidInstallments: 0,
                                        cardId: "",
                                        notes: "",
                                        color: "gold",
                                      });
                                      setShowInstallmentModal(true);
                                    }}
                                    style={{
                                      background: C.paper,
                                      border: `1px solid ${C.line}`,
                                      borderRadius: "0.6rem",
                                      padding: "0.3rem 0.65rem",
                                      fontSize: "0.72rem",
                                      fontWeight: 600,
                                      color: C.ink,
                                      cursor: "pointer",
                                      display: "flex",
                                      alignItems: "center",
                                      gap: "0.25rem",
                                    }}
                                    className="cc-btn-press"
                                  >
                                    <Plus size={11} />
                                    <span>Añadir</span>
                                  </button>
                                </div>

                                {/* Cuadrícula de cuotas de esta persona */}
                                {personItems.length === 0 ? (
                                  <div style={{ fontSize: "0.75rem", color: C.inkSoft, textAlign: "center", padding: "0.5rem 0" }}>
                                    {personName} no tiene cuotas o préstamos registrados.
                                  </div>
                                ) : (
                                  <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.65rem" }}>
                                    {personItems.map((inst) => {
                                      const pal = CARD_COLORS.find((p) => p.id === inst.color) || CARD_COLORS[1];
                                      const instCurr = inst.currency || editCurrency || "S/";
                                      const total = inst.totalInstallments || 0;
                                      const paid = inst.paidInstallments || 0;
                                      return (
                                        <div
                                          key={inst.id}
                                          style={{
                                            background: C.paper,
                                            border: `1px solid ${C.line}`,
                                            borderRadius: "0.9rem",
                                            padding: "0.8rem",
                                            display: "flex",
                                            flexDirection: "column",
                                            justifyContent: "space-between",
                                            gap: "0.6rem",
                                          }}
                                        >
                                          <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.5rem" }}>
                                            <div style={{ display: "flex", alignItems: "center", gap: "0.6rem", minWidth: 0 }}>
                                              <div
                                                style={{
                                                  width: "2.2rem",
                                                  height: "1.5rem",
                                                  borderRadius: "0.4rem",
                                                  background: pal.gradient,
                                                  border: `1px solid ${pal.border}`,
                                                  flexShrink: 0,
                                                  boxShadow: "0 2px 6px rgba(0,0,0,0.12)",
                                                }}
                                              />
                                              <div style={{ minWidth: 0 }}>
                                                <div style={{ fontSize: "0.88rem", fontWeight: 700, color: C.ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", display: "flex", alignItems: "center", gap: "0.35rem" }}>
                                                  <span>{inst.name}</span>
                                                  {inst.currency && inst.currency !== (editCurrency || "S/") && (
                                                    <span style={{ fontSize: "0.62rem", background: C.chipBg, padding: "0.1rem 0.35rem", borderRadius: "0.35rem", color: C.inkSoft }}>
                                                      {inst.currency}
                                                    </span>
                                                  )}
                                                </div>
                                                <div style={{ fontSize: "0.7rem", color: C.inkSoft, display: "flex", alignItems: "center", gap: "0.35rem", flexWrap: "wrap" }}>
                                                  {inst.targetMonth && inst.targetMonth !== "auto" && (
                                                    <span style={{ fontSize: "0.62rem", fontWeight: 700, background: darkMode ? "#451A03" : "#FEF3C7", color: darkMode ? "#FDE68A" : "#92400E", padding: "0.05rem 0.35rem", borderRadius: "0.35rem" }}>
                                                      🗓️ {inst.targetMonth}
                                                    </span>
                                                  )}
                                                  {inst.recipient ? <span>A: <strong>{inst.recipient}</strong> · </span> : ""}
                                                  <span>Vence: <strong>día {inst.dueDay}</strong></span>
                                                </div>
                                              </div>
                                            </div>

                                            <div style={{ display: "flex", alignItems: "center", gap: "0.25rem", flexShrink: 0 }}>
                                              <button
                                                type="button"
                                                onClick={() => {
                                                  setInstallmentDraft({
                                                    id: inst.id,
                                                    name: inst.name,
                                                    person: inst.person,
                                                    recipient: inst.recipient || "",
                                                    totalAmount: inst.totalAmount ? String(inst.totalAmount) : (inst.amount && inst.totalInstallments ? String(inst.amount * inst.totalInstallments) : ""),
                                                    amount: inst.amount ? String(inst.amount) : "",
                                                    currency: inst.currency || editCurrency || "S/",
                                                    dueDay: inst.dueDay,
                                                    targetMonth: inst.targetMonth || "auto",
                                                    totalInstallments: inst.totalInstallments ? String(inst.totalInstallments) : "",
                                                    paidInstallments: inst.paidInstallments || 0,
                                                    cardId: inst.cardId || "",
                                                    notes: inst.notes || "",
                                                    color: inst.color || "gold",
                                                  });
                                                  setShowInstallmentModal(true);
                                                }}
                                                style={{ color: C.inkSoft, background: C.field, border: `1px solid ${C.line}`, borderRadius: "0.5rem", cursor: "pointer", padding: "0.3rem" }}
                                                title="Editar cuota"
                                              >
                                                <Pencil size={12} />
                                              </button>
                                              <button
                                                type="button"
                                                onClick={() => setEditInstallments(editInstallments.filter((i) => i.id !== inst.id))}
                                                style={{ color: C.red, background: C.dangerBg, border: `1px solid ${C.line}`, borderRadius: "0.5rem", cursor: "pointer", padding: "0.3rem" }}
                                                title="Eliminar cuota"
                                              >
                                                <X size={12} />
                                              </button>
                                            </div>
                                          </div>

                                          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "0.72rem", color: C.inkSoft, background: C.field, padding: "0.35rem 0.6rem", borderRadius: "0.55rem", border: `1px solid ${C.lineSoft}`, flexWrap: "wrap", gap: "0.25rem" }}>
                                            <div>
                                              {inst.totalAmount ? <span>Deuda: <strong style={{ color: C.ink }}>{fmtMoney(inst.totalAmount, instCurr)}</strong> · </span> : ""}
                                              <span>Cuota: <strong style={{ color: C.ink }}>{fmtMoney(inst.amount, instCurr)}</strong></span>
                                            </div>
                                            
                                            {/* Contador interactivo de cuotas pagadas */}
                                            <div style={{ display: "flex", alignItems: "center", gap: "0.35rem" }}>
                                              <button
                                                type="button"
                                                disabled={paid <= 0}
                                                onClick={() => {
                                                  const nextPaid = Math.max(0, paid - 1);
                                                  setEditInstallments(editInstallments.map((i) => i.id === inst.id ? { ...i, paidInstallments: nextPaid } : i));
                                                }}
                                                style={{ border: `1px solid ${C.line}`, background: C.paper, borderRadius: "0.3rem", width: "1.2rem", height: "1.2rem", display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: paid > 0 ? "pointer" : "default", opacity: paid > 0 ? 1 : 0.4 }}
                                                title="Restar 1 cuota pagada"
                                              >
                                                -
                                              </button>
                                              <span style={{ fontWeight: 600, color: C.ink }}>
                                                {total > 0 ? `${paid}/${total}` : `${paid} pagadas`}
                                              </span>
                                              <button
                                                type="button"
                                                disabled={total > 0 && paid >= total}
                                                onClick={() => {
                                                  const nextPaid = Math.min(total || 9999, paid + 1);
                                                  setEditInstallments(editInstallments.map((i) => i.id === inst.id ? { ...i, paidInstallments: nextPaid } : i));
                                                }}
                                                style={{ border: `1px solid ${C.line}`, background: C.paper, borderRadius: "0.3rem", width: "1.2rem", height: "1.2rem", display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: (total === 0 || paid < total) ? "pointer" : "default", opacity: (total === 0 || paid < total) ? 1 : 0.4 }}
                                                title="Sumar 1 cuota pagada"
                                              >
                                                +
                                              </button>
                                            </div>
                                          </div>
                                        </div>
                                      );
                                    })}
                                  </div>
                                )}
                              </div>
                            );
                          })}
                        </div>
                      );
                    })()}
                  </div>
                )}

                {/* ===== PESTAÑA 4: PREFERENCIAS / OPCIONES RESUMIDAS ===== */}
                {settingsTab === "preferencias" && (() => {
                  const allNotifsOn = notifyEnabled || pushEnabled || soundEnabled;
                  return (
                    <div style={{ display: "flex", flexDirection: "column", gap: "0.45rem", marginBottom: "0.5rem" }}>
                      <p style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, letterSpacing: "0.08em" }} className="text-[10px] uppercase font-bold tracking-widest mb-0.5 flex items-center gap-1.5">
                        <Sparkles size={12} color={C.green} style={{ flexShrink: 0 }} />
                        Preferencias de este dispositivo
                      </p>

                      {/* 1. Modo minimalista */}
                      <div className="flex items-center justify-between gap-3 mb-2.5" style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.75rem", padding: "0.6rem 0.85rem" }}>
                        <div className="min-w-0">
                          <p style={{ margin: 0, fontFamily: "'Inter', sans-serif", color: C.ink, fontWeight: 600, fontSize: "0.8rem" }} className="flex items-center gap-1.5">
                            <Sparkles size={13} color={C.green} style={{ flexShrink: 0 }} />
                            Modo minimalista
                          </p>
                          <p id="cc-pref-minimal" style={{ margin: "0.15rem 0 0", fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontSize: "0.7rem", lineHeight: 1.35 }}>
                            Interfaz limpia y sin distracciones para registrar rápido.
                          </p>
                        </div>
                        <Toggle on={minimalMode} onToggle={setMinimalMode} label="Modo minimalista" describedBy="cc-pref-minimal" />
                      </div>

                      {/* 2. Modo oscuro */}
                      <div className="flex items-center justify-between gap-3 mb-2.5" style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.75rem", padding: "0.6rem 0.85rem" }}>
                        <div className="min-w-0">
                          <p style={{ margin: 0, fontFamily: "'Inter', sans-serif", color: C.ink, fontWeight: 600, fontSize: "0.8rem" }} className="flex items-center gap-1.5">
                            {dark ? <Moon size={13} color={C.violet} style={{ flexShrink: 0 }} /> : <Sun size={13} color="#F59E0B" style={{ flexShrink: 0 }} />}
                            Modo oscuro
                          </p>
                          <p id="cc-pref-dark" style={{ margin: "0.15rem 0 0", fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontSize: "0.7rem", lineHeight: 1.35 }}>
                            Tema oscuro para este dispositivo.
                          </p>
                        </div>
                        <Toggle on={dark} onToggle={setDark} label="Modo oscuro" describedBy="cc-pref-dark" />
                      </div>

                      {/* 3. Notificaciones unificadas como uno solo */}
                      <div className="flex items-center justify-between gap-3 mb-2.5" style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.75rem", padding: "0.6rem 0.85rem" }}>
                        <div className="min-w-0">
                          <p style={{ margin: 0, fontFamily: "'Inter', sans-serif", color: C.ink, fontWeight: 600, fontSize: "0.8rem" }} className="flex items-center gap-1.5">
                            <BellRing size={13} color={C.green} style={{ flexShrink: 0 }} />
                            Notificaciones y alertas
                          </p>
                          <p id="cc-pref-notifs" style={{ margin: "0.15rem 0 0", fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontSize: "0.7rem", lineHeight: 1.35 }}>
                            Avisos push, en segundo plano y sonidos al registrar.
                          </p>
                        </div>
                        <Toggle
                          on={allNotifsOn}
                          describedBy="cc-pref-notifs"
                          onToggle={(on) => {
                            setSoundEnabled(on);
                            storage.set("sound", on ? "1" : "0", false).catch(() => {});
                            if (on) {
                              (async () => {
                                try {
                                  if (typeof Notification !== "undefined") {
                                    let perm = Notification.permission;
                                    if (perm === "default") perm = await Notification.requestPermission();
                                    if (perm === "granted") {
                                      setNotifyEnabled(true);
                                      storage.set("notify", "1", false).catch(() => {});
                                      if (HAS_PUSH) setPushPref(true);
                                    } else {
                                      setNotifyEnabled(false);
                                    }
                                  } else {
                                    setNotifyEnabled(false);
                                  }
                                } catch (e) {
                                  setNotifyEnabled(false);
                                }
                              })();
                              playNoticeSound();
                              vibrateNotice();
                            } else {
                              setNotifyEnabled(false);
                              storage.set("notify", "0", false).catch(() => {});
                              if (HAS_PUSH) setPushPref(false);
                            }
                          }}
                          label="Notificaciones y alertas"
                        />
                      </div>

                      {/* 3.1. Notificaciones multi-cuenta globales */}
                      <div className="flex items-center justify-between gap-3 mb-2.5" style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.75rem", padding: "0.6rem 0.85rem" }}>
                        <div className="min-w-0">
                          <p style={{ margin: 0, fontFamily: "'Inter', sans-serif", color: C.ink, fontWeight: 600, fontSize: "0.8rem" }} className="flex items-center gap-1.5">
                            <BellRing size={13} color={globalNotifsEnabled ? C.green : C.inkSoft} style={{ flexShrink: 0 }} />
                            Avisos de todas mis cuentas guardadas
                          </p>
                          <p id="cc-pref-global-notifs" style={{ margin: "0.15rem 0 0", fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontSize: "0.7rem", lineHeight: 1.35 }}>
                            Recibe notificaciones de cualquier espacio guardado, incluso si estás en otra cuenta o fuera de ella.
                          </p>
                        </div>
                        <Toggle
                          on={globalNotifsEnabled}
                          describedBy="cc-pref-global-notifs"
                          onToggle={(on) => {
                            setGlobalNotifsEnabled(on);
                            try {
                              localStorage.setItem("cc:global_notifications", on ? "1" : "0");
                            } catch (e) {}
                          }}
                          label="Avisos de todas mis cuentas guardadas"
                        />
                      </div>

                      {/* 4. Repartir gastos a partes iguales */}
                      <div
                        onClick={() => handleToggleSplit(!editSplitEnabled)}
                        className="flex items-center justify-between gap-3 mb-2.5 cc-btn-press"
                        style={{
                          background: editSplitEnabled ? (darkMode ? "rgba(16, 185, 129, 0.12)" : "#ECFDF5") : C.field,
                          border: `1.5px solid ${editSplitEnabled ? C.green : C.lineSoft}`,
                          borderRadius: "0.85rem",
                          padding: "0.75rem 0.95rem",
                          cursor: "pointer",
                        }}
                      >
                        <div className="min-w-0">
                          <div style={{ display: "flex", alignItems: "center", gap: "0.45rem", marginBottom: "0.15rem" }}>
                            <p style={{ margin: 0, fontFamily: "'Inter', sans-serif", color: C.ink, fontWeight: 700, fontSize: "0.82rem" }} className="flex items-center gap-1.5">
                              <Users size={14} color={editSplitEnabled ? C.green : C.inkSoft} style={{ flexShrink: 0 }} />
                              Repartir gastos a partes iguales
                            </p>
                            <span style={{ fontSize: "0.62rem", fontWeight: 800, padding: "0.1rem 0.45rem", borderRadius: "999px", background: editSplitEnabled ? C.successBg : C.card, color: editSplitEnabled ? C.green : C.inkSoft }}>
                              {editSplitEnabled ? "ACTIVADO" : "DESACTIVADO"}
                            </span>
                          </div>
                          <p id="cc-pref-split" style={{ margin: 0, fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontSize: "0.7rem", lineHeight: 1.35 }}>
                            {editSplitEnabled ? "Calculando balances y quién le debe a quién en la cuenta." : "Cada quien paga lo suyo por separado (sin balances de deudas)."}
                          </p>
                        </div>
                        <div onClick={(e) => e.stopPropagation()}>
                          <Toggle on={editSplitEnabled} onToggle={handleToggleSplit} label="Repartir gastos a partes iguales" describedBy="cc-pref-split" />
                        </div>
                      </div>

                      {/* 5. Novedades y versión */}
                      <div style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.75rem", padding: "0.75rem 0.85rem", marginTop: "0.2rem" }}>
                        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                          <span style={{ fontSize: "0.8rem", fontWeight: 700, color: C.ink, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                            <Sparkles size={13} color={C.green} /> Versión {APP_CHANGELOG[0]?.version || "v25"}
                          </span>
                          <button
                            type="button"
                            onClick={openChangelog}
                            style={{
                              background: C.paper,
                              color: C.ink,
                              border: `1px solid ${C.line}`,
                              borderRadius: "0.6rem",
                              padding: "0.4rem 0.75rem",
                              fontSize: "0.74rem",
                              fontWeight: 600,
                              cursor: "pointer",
                            }}
                            className="cc-btn-press"
                          >
                            ¿Qué hay de nuevo?
                          </button>
                        </div>
                      </div>

                      {/* 6. Diagnóstico Realtime */}
                      {!minimalMode && (
                        <div style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.95rem", padding: "1rem", marginTop: "0.4rem" }}>
                          <p style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, margin: "0 0 0.75rem" }} className="text-[10px] uppercase tracking-widest font-bold flex items-center gap-1.5">
                            <Activity size={12} color={C.green} style={{ flexShrink: 0 }} />
                            Diagnóstico Realtime
                          </p>

                          <div style={{ display: "flex", flexDirection: "column", gap: "0.45rem", marginBottom: "0.75rem" }}>
                            <div className="flex items-center justify-between gap-3 mb-2" style={{ background: C.paper, padding: "0.5rem 0.75rem", borderRadius: "0.6rem", border: `1px solid ${C.lineSoft}` }}>
                              <span style={{ fontSize: "0.78rem", color: C.ink, fontWeight: 500, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                                <Wifi size={12} color={rtStatus === "connected" ? C.green : "#F59E0B"} /> Canal en vivo
                              </span>
                              <span style={{ fontWeight: 700, fontSize: "0.72rem", color: rtStatus === "connected" ? C.green : "#F59E0B" }}>
                                {rtStatus === "connected" ? "✓ En vivo" : "Sondeo"}
                              </span>
                            </div>

                            <div className="flex items-center justify-between gap-3 mb-2" style={{ background: C.paper, padding: "0.5rem 0.75rem", borderRadius: "0.6rem", border: `1px solid ${C.lineSoft}` }}>
                              <span style={{ fontSize: "0.78rem", color: C.ink, fontWeight: 500, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                                <Database size={12} color={C.green} /> Publicación supabase_realtime
                              </span>
                              <span style={{ fontWeight: 700, fontSize: "0.72rem", color: rtDiag && rtDiag.publicationExists ? C.green : C.inkSoft }}>
                                {rtDiag && rtDiag.publicationExists ? "✓ Activa" : "—"}
                              </span>
                            </div>

                            <div className="flex items-center justify-between gap-3 mb-2" style={{ background: C.paper, padding: "0.5rem 0.75rem", borderRadius: "0.6rem", border: `1px solid ${C.lineSoft}` }}>
                              <span style={{ fontSize: "0.78rem", color: C.ink, fontWeight: 500, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                                <BellRing size={12} color={C.green} /> Dispositivos con push
                              </span>
                              <span style={{ fontWeight: 700, fontSize: "0.75rem", color: C.ink }}>
                                {rtDiag && rtDiag.pushDevices != null ? rtDiag.pushDevices : "—"}
                              </span>
                            </div>
                          </div>

                          {rtDiagError && (
                            <p style={{ margin: "0.2rem 0 0.5rem", fontFamily: "'Inter', sans-serif", color: C.red, fontSize: "0.72rem" }}>{rtDiagError}</p>
                          )}

                          <button
                            type="button"
                            onClick={runRtDiag}
                            disabled={rtDiagBusy}
                            style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, border: `1px solid ${C.line}`, borderRadius: "0.65rem", background: C.paper }}
                            className="cc-btn-press w-full py-1.5 text-xs uppercase tracking-widest flex items-center justify-center gap-1.5 hover:opacity-70 transition-opacity disabled:opacity-40"
                          >
                            <RefreshCw size={12} style={rtDiagBusy ? { animation: "cc-spin 1s linear infinite" } : undefined} />
                            {rtDiagBusy ? "Comprobando…" : "Reintentar"}
                          </button>
                        </div>
                      )}
                    </div>
                  );
                })()}

                {/* ===== PESTAÑA IA & AUTOMATIZACIONES ===== */}
                {settingsTab === "ia" && (
                  <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
                    {/* Tarjeta 1: Asistente de Voz y Gemini */}
                    <div style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.95rem", padding: "1rem" }}>
                      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.6rem" }}>
                        <span style={{ fontSize: "0.76rem", color: C.ink, fontWeight: 700, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                          <Sparkles size={14} color={C.green} /> Asistente de Voz y Google Gemini
                        </span>
                        <span style={{ background: voiceSupported ? C.successBg : C.dangerBg, color: voiceSupported ? C.green : C.red, borderRadius: "999px", padding: "0.15rem 0.55rem", fontSize: "0.68rem", fontWeight: 700 }}>
                          {voiceSupported ? "🎙️ Micrófono listo" : "Sin micrófono"}
                        </span>
                      </div>
                      <p style={{ fontSize: "0.72rem", color: C.inkSoft, margin: "0 0 0.85rem", lineHeight: 1.45 }}>
                        Permite dictar o escribir tus gastos con lenguaje natural (ej. <em>"Hans pagó 35 soles en taxi con tarjeta"</em>). Basta con que tú configures la clave aquí; <strong>todos los miembros del grupo podrán usarla sin suscripción</strong>.
                      </p>

                      <div>
                        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.4rem" }}>
                          <label style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft }} className="text-[10px] uppercase tracking-widest block font-bold">
                            Google Gemini API Key
                          </label>
                          {editGeminiApiKey.trim() && !showEditGeminiKeyField && (
                            <span style={{ fontSize: "0.68rem", fontWeight: 700, color: editGeminiIsPrivate ? C.green : "#3B82F6", background: editGeminiIsPrivate ? C.successBg : "#EFF6FF", padding: "0.1rem 0.45rem", borderRadius: "999px" }}>
                              {editGeminiIsPrivate ? "🔒 Solo en tu dispositivo" : "👥 Compartida"}
                            </span>
                          )}
                        </div>

                        {/* Vista 1: Clave ya configurada y enmascarada para máxima privacidad */}
                        {editGeminiApiKey.trim() && !showEditGeminiKeyField ? (
                          <div style={{ background: C.paper, border: `1px solid ${C.line}`, borderRadius: "0.75rem", padding: "0.75rem 0.85rem" }}>
                            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", marginBottom: "0.5rem" }}>
                              <div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
                                <ShieldCheck size={16} color={C.green} />
                                <span style={{ fontFamily: "monospace", fontSize: "0.85rem", fontWeight: 700, color: C.ink, letterSpacing: "0.1em" }}>
                                  ••••••••••••{editGeminiApiKey.slice(-4)}
                                </span>
                              </div>
                              <span style={{ fontSize: "0.68rem", color: C.green, fontWeight: 700 }}>
                                ✓ Configurada
                              </span>
                            </div>

                            <div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
                              <button
                                type="button"
                                onClick={async () => {
                                  setGeminiTestStatus("Probando conexión...");
                                  try {
                                    const respText = await callGeminiApi("hola responde OK", editGeminiApiKey.trim(), false);
                                    if (respText) {
                                      setGeminiTestStatus("✓ Conexión con Gemini exitosa");
                                    } else {
                                      setGeminiTestStatus("⚠️ Clave no válida o sin permisos");
                                    }
                                  } catch (e) {
                                    setGeminiTestStatus("⚠️ Error de red al contactar Gemini");
                                  }
                                  setTimeout(() => setGeminiTestStatus(""), 4000);
                                }}
                                style={{
                                  background: C.field,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.6rem",
                                  padding: "0.35rem 0.65rem",
                                  fontSize: "0.72rem",
                                  fontWeight: 600,
                                  color: C.ink,
                                  cursor: "pointer",
                                }}
                                className="cc-btn-press"
                              >
                                Probar conexión
                              </button>
                              <button
                                type="button"
                                onClick={() => setShowEditGeminiKeyField(true)}
                                style={{
                                  background: C.field,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.6rem",
                                  padding: "0.35rem 0.65rem",
                                  fontSize: "0.72rem",
                                  fontWeight: 600,
                                  color: C.ink,
                                  cursor: "pointer",
                                }}
                                className="cc-btn-press"
                              >
                                Cambiar clave
                              </button>
                              <button
                                type="button"
                                onClick={() => {
                                  setEditGeminiApiKey("");
                                  try { localStorage.removeItem("cc:local_gemini_api_key"); } catch (e) {}
                                  setGeminiTestStatus("Clave eliminada");
                                  setTimeout(() => setGeminiTestStatus(""), 3000);
                                }}
                                style={{
                                  background: "transparent",
                                  border: `1px solid ${C.red}`,
                                  borderRadius: "0.6rem",
                                  padding: "0.35rem 0.65rem",
                                  fontSize: "0.72rem",
                                  fontWeight: 600,
                                  color: C.red,
                                  cursor: "pointer",
                                }}
                                className="cc-btn-press hover:bg-red-500/10"
                              >
                                Eliminar
                              </button>
                            </div>
                          </div>
                        ) : (
                          /* Vista 2: Campo para ingresar o reemplazar la clave con selector de privacidad */
                          <div style={{ display: "flex", flexDirection: "column", gap: "0.55rem" }}>
                            <div style={{ display: "flex", gap: "0.45rem" }}>
                              <div style={{ position: "relative", flex: 1 }}>
                                <input
                                  type={showGeminiKey ? "text" : "password"}
                                  value={editGeminiApiKey}
                                  onChange={(e) => setEditGeminiApiKey(e.target.value)}
                                  placeholder="Pega tu clave (AIzaSy... o AQ.Ab8...)"
                                  style={{
                                    fontFamily: "'Inter', sans-serif",
                                    background: C.paper,
                                    border: `1px solid ${C.line}`,
                                    borderRadius: "0.65rem",
                                    color: C.ink,
                                    padding: "0.48rem 2.2rem 0.48rem 0.75rem",
                                    fontSize: "0.8rem",
                                    outline: "none",
                                    width: "100%",
                                    boxSizing: "border-box",
                                  }}
                                />
                                <button
                                  type="button"
                                  onClick={() => setShowGeminiKey(!showGeminiKey)}
                                  title={showGeminiKey ? "Ocultar clave" : "Ver clave"}
                                  style={{
                                    position: "absolute",
                                    right: "0.5rem",
                                    top: "50%",
                                    transform: "translateY(-50%)",
                                    background: "none",
                                    border: "none",
                                    color: C.inkSoft,
                                    cursor: "pointer",
                                    padding: "0.2rem",
                                  }}
                                >
                                  {showGeminiKey ? <EyeOff size={14} /> : <Eye size={14} />}
                                </button>
                              </div>
                              <button
                                type="button"
                                onClick={async () => {
                                  if (!editGeminiApiKey.trim()) {
                                    setGeminiTestStatus("⚠️ Ingresa una clave primero");
                                    return;
                                  }
                                  setGeminiTestStatus("Probando clave con Google AI Studio...");
                                  try {
                                    const respText = await callGeminiApi("hola responde OK", editGeminiApiKey.trim(), false);
                                    if (respText) {
                                      setGeminiTestStatus("✓ Conexión con Gemini exitosa");
                                    } else {
                                      setGeminiTestStatus("⚠️ Clave no válida o sin permisos");
                                    }
                                  } catch (e) {
                                    setGeminiTestStatus("⚠️ Error de red al contactar Gemini");
                                  }
                                  setTimeout(() => setGeminiTestStatus(""), 4000);
                                }}
                                style={{
                                  background: C.paper,
                                  border: `1px solid ${C.line}`,
                                  borderRadius: "0.65rem",
                                  color: C.ink,
                                  padding: "0.48rem 0.75rem",
                                  fontSize: "0.72rem",
                                  fontWeight: 600,
                                  cursor: "pointer",
                                  whiteSpace: "nowrap",
                                }}
                                className="cc-btn-press"
                              >
                                Probar clave
                              </button>
                            </div>

                            {/* Selector de Privacidad */}
                            <div
                              onClick={() => setEditGeminiIsPrivate(!editGeminiIsPrivate)}
                              style={{
                                display: "flex",
                                alignItems: "flex-start",
                                gap: "0.5rem",
                                background: editGeminiIsPrivate ? (darkMode ? "rgba(16,185,129,0.12)" : "#ECFDF5") : C.paper,
                                border: `1px solid ${editGeminiIsPrivate ? C.green : C.line}`,
                                borderRadius: "0.65rem",
                                padding: "0.5rem 0.65rem",
                                cursor: "pointer",
                              }}
                            >
                              <input
                                type="checkbox"
                                checked={editGeminiIsPrivate}
                                onChange={(e) => setEditGeminiIsPrivate(e.target.checked)}
                                style={{ marginTop: "0.15rem", cursor: "pointer", accentColor: C.green }}
                              />
                              <div style={{ fontSize: "0.72rem", lineHeight: 1.35 }}>
                                <strong style={{ color: editGeminiIsPrivate ? (darkMode ? "#6EE7B7" : "#047857") : C.ink, display: "block" }}>
                                  🔒 Guardar solo en este dispositivo (Privada)
                                </strong>
                                <span style={{ color: C.inkSoft, fontSize: "0.68rem" }}>
                                  Tu clave <strong>no se subirá a la base de datos de la cuenta</strong>. Nadie más podrá verla ni usarla desde otros teléfonos.
                                </span>
                              </div>
                            </div>

                            {showEditGeminiKeyField && (
                              <button
                                type="button"
                                onClick={() => setShowEditGeminiKeyField(false)}
                                style={{ background: "transparent", border: "none", color: C.inkSoft, fontSize: "0.7rem", cursor: "pointer", textAlign: "left", padding: 0 }}
                              >
                                Cancelar cambio
                              </button>
                            )}
                          </div>
                        )}

                        {geminiTestStatus && (
                          <div style={{ fontSize: "0.72rem", marginTop: "0.4rem", color: geminiTestStatus.includes("✓") ? C.green : C.red, fontWeight: 600 }}>
                            {geminiTestStatus}
                          </div>
                        )}
                        <span style={{ fontSize: "0.66rem", color: C.inkFaint, marginTop: "0.3rem", display: "block" }}>
                          Obtén una clave gratuita en <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noreferrer" style={{ color: C.green, textDecoration: "underline" }}>aistudio.google.com</a>.
                        </span>
                      </div>
                    </div>

                    {/* Tarjeta 2: Bot de Telegram y Webhooks (Modo 4) */}
                    <div style={{ background: C.field, border: `1px solid ${C.lineSoft}`, borderRadius: "0.95rem", padding: "1rem" }}>
                      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.6rem" }}>
                        <span style={{ fontSize: "0.76rem", color: C.ink, fontWeight: 700, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                          <Bot size={14} color="#3B82F6" /> Bot de Telegram & Webhooks (Modo 4)
                        </span>
                        <span style={{ background: "#EFF6FF", color: "#3B82F6", borderRadius: "999px", padding: "0.15rem 0.55rem", fontSize: "0.68rem", fontWeight: 700 }}>
                          Segundo plano
                        </span>
                      </div>

                      <p style={{ fontSize: "0.72rem", color: C.inkSoft, margin: "0 0 0.85rem", lineHeight: 1.45 }}>
                        Puedes enviar mensajes de texto o notas de audio a tu propio bot de Telegram para que registre gastos automáticamente en la cuenta compartida.
                      </p>

                      <div style={{ display: "flex", flexDirection: "column", gap: "0.55rem", fontSize: "0.72rem", color: C.ink }}>
                        <div style={{ background: C.paper, padding: "0.6rem 0.75rem", borderRadius: "0.65rem", border: `1px solid ${C.lineSoft}` }}>
                          <div style={{ fontWeight: 700, marginBottom: "0.2rem" }}>1. Crea tu Bot en Telegram</div>
                          <div style={{ color: C.inkSoft }}>Abre Telegram, busca <code>@BotFather</code>, escribe <code>/newbot</code> y copia el Token API.</div>
                        </div>

                        <div style={{ background: C.paper, padding: "0.6rem 0.75rem", borderRadius: "0.65rem", border: `1px solid ${C.lineSoft}` }}>
                          <div style={{ fontWeight: 700, marginBottom: "0.2rem" }}>2. Conecta la Edge Function de Supabase</div>
                          <div style={{ color: C.inkSoft }}>URL del Webhook de tu cuenta:</div>
                          <code style={{ fontSize: "0.67rem", wordBreak: "break-all", background: C.field, padding: "0.25rem 0.4rem", borderRadius: "0.35rem", display: "block", marginTop: "0.25rem" }}>
                            https://[TU-SUPABASE-ID].supabase.co/functions/v1/webhook-ai
                          </code>
                        </div>

                        <div style={{ background: C.paper, padding: "0.6rem 0.75rem", borderRadius: "0.65rem", border: `1px solid ${C.lineSoft}` }}>
                          <div style={{ fontWeight: 700, marginBottom: "0.2rem" }}>3. ¡Listo para usar!</div>
                          <div style={{ color: C.inkSoft }}>Cualquier integrante del grupo le envía un mensaje al bot y el movimiento se guardará y sincronizará en vivo.</div>
                        </div>
                      </div>
                    </div>
                  </div>
                )}



                {/* Footer del panel de Ajustes */}
                <div style={{ borderTop: `1px solid ${C.line}`, paddingTop: "1rem", marginTop: "1rem", display: "flex", gap: "0.75rem", width: "100%" }}>
                  <button
                    type="button"
                    onClick={() => setShowSettings(false)}
                    style={{
                      flex: 1,
                      fontFamily: "'Inter', sans-serif",
                      color: C.inkSoft,
                      border: `1px solid ${C.line}`,
                      borderRadius: "0.75rem",
                      background: "transparent",
                      padding: "0.75rem 0.5rem",
                      cursor: "pointer",
                      fontSize: "0.75rem",
                      fontWeight: 700,
                      textTransform: "uppercase",
                      letterSpacing: "0.05em",
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                    }}
                    className="cc-btn-press hover:opacity-70 transition-opacity"
                  >
                    Cancelar
                  </button>
                  <button
                    type="button"
                    onClick={saveSettings}
                    style={{
                      flex: 1,
                      fontFamily: "'Inter', sans-serif",
                      background: BRAND_GRADIENT,
                      color: "#FFFFFF",
                      borderRadius: "0.75rem",
                      border: "none",
                      padding: "0.75rem 0.5rem",
                      fontWeight: 700,
                      fontSize: "0.75rem",
                      cursor: "pointer",
                      boxShadow: "0 8px 18px -8px rgba(15,81,50,0.55)",
                      textTransform: "uppercase",
                      letterSpacing: "0.05em",
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                    }}
                    className="cc-btn-press hover:brightness-105"
                  >
                    Guardar ajustes
                  </button>
                </div>
          </div>
        </div>
      )}

      {/* Modal de Novedades y Actualizaciones */}
      {showChangelog && (
        <div
          onClick={() => setShowChangelog(false)}
          className="cc-fade-in"
          style={{
            position: "fixed",
            inset: 0,
            background: "rgba(15, 23, 42, 0.72)",
            backdropFilter: "blur(5px)",
            WebkitBackdropFilter: "blur(5px)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            zIndex: 80,
            padding: "1rem",
          }}
        >
          <div
            onClick={(e) => e.stopPropagation()}
            className="cc-panel-in"
            style={{
              background: C.paper,
              borderRadius: "1.4rem",
              padding: isDesktop ? "1.5rem" : "1.25rem",
              border: `1px solid ${C.line}`,
              boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.35)",
              maxWidth: "520px",
              width: "100%",
              maxHeight: "88vh",
              display: "flex",
              flexDirection: "column",
              gap: "0.9rem",
            }}
          >
            <PanelHeader
              icon={<Sparkles size={16} />}
              title="Novedades y Actualizaciones"
              subtitle="Últimas funciones y mejoras añadidas a la app"
              onAction={() => setShowChangelog(false)}
              actionLabel="Cerrar novedades"
              actionTitle="Cerrar"
              actionIcon={<X size={14} />}
            />

            {/* Contenido scrolleable con las versiones */}
            <div
              style={{
                overflowY: "auto",
                maxHeight: "65vh",
                paddingRight: "0.25rem",
                display: "flex",
                flexDirection: "column",
                gap: "0.9rem",
              }}
            >
              {APP_CHANGELOG.map((ver, vIdx) => (
                <div
                  key={ver.version}
                  style={{
                    background: vIdx === 0 ? (darkMode ? "rgba(16, 185, 129, 0.08)" : "#F0FDF4") : C.field,
                    border: vIdx === 0 ? `1.5px solid ${C.green}` : `1px solid ${C.lineSoft}`,
                    borderRadius: "1rem",
                    padding: "0.95rem",
                    display: "flex",
                    flexDirection: "column",
                    gap: "0.55rem",
                  }}
                >
                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", flexWrap: "wrap" }}>
                    <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                      <span
                        style={{
                          background: vIdx === 0 ? C.green : C.ink,
                          color: "#FFFFFF",
                          padding: "0.15rem 0.5rem",
                          borderRadius: "999px",
                          fontSize: "0.72rem",
                          fontWeight: 800,
                        }}
                      >
                        {ver.version}
                      </span>
                      <span style={{ fontSize: "0.84rem", fontWeight: 700, color: C.ink }}>
                        {ver.title}
                      </span>
                    </div>
                    <span style={{ fontSize: "0.68rem", color: C.inkSoft, fontWeight: 500 }}>
                      {ver.date}
                    </span>
                  </div>

                  {ver.description && (
                    <p style={{ margin: 0, fontSize: "0.74rem", color: C.inkSoft, lineHeight: 1.4 }}>
                      {ver.description}
                    </p>
                  )}

                  <div style={{ display: "flex", flexDirection: "column", gap: "0.4rem", marginTop: "0.15rem" }}>
                    {ver.items.map((it, itIdx) => {
                      const tagBg =
                        it.tag === "Nuevo" ? (darkMode ? "rgba(16, 185, 129, 0.2)" : "#D1FAE5")
                        : it.tag === "Mejora" ? (darkMode ? "rgba(59, 130, 246, 0.2)" : "#DBEAFE")
                        : (darkMode ? "rgba(168, 85, 247, 0.2)" : "#F3E8FF");
                      const tagColor =
                        it.tag === "Nuevo" ? (darkMode ? "#6EE7B7" : "#065F46")
                        : it.tag === "Mejora" ? (darkMode ? "#93C5FD" : "#1E40AF")
                        : (darkMode ? "#D8B4FE" : "#6B21A8");
                      return (
                        <div key={itIdx} style={{ display: "flex", alignItems: "flex-start", gap: "0.45rem", fontSize: "0.74rem", lineHeight: 1.45 }}>
                          <span
                            style={{
                              background: tagBg,
                              color: tagColor,
                              padding: "0.08rem 0.38rem",
                              borderRadius: "5px",
                              fontSize: "0.62rem",
                              fontWeight: 700,
                              flexShrink: 0,
                              marginTop: "0.1rem",
                              textTransform: "uppercase",
                            }}
                          >
                            {it.tag}
                          </span>
                          <span style={{ color: C.ink }}>
                            {it.text}
                          </span>
                        </div>
                      );
                    })}
                  </div>
                </div>
              ))}
            </div>

            <button
              type="button"
              onClick={() => setShowChangelog(false)}
              style={{
                fontFamily: "'Inter', sans-serif",
                background: BRAND_GRADIENT,
                color: "#FFFFFF",
                border: "none",
                borderRadius: "0.75rem",
                padding: "0.65rem",
                fontSize: "0.82rem",
                fontWeight: 700,
                cursor: "pointer",
                textAlign: "center",
                marginTop: "0.15rem",
              }}
              className="cc-btn-press hover:opacity-95"
            >
              ¡Entendido!
            </button>
          </div>
        </div>
      )}

      {/* Contenido: una sola columna de tarjetas centradas (máx. 480px) */}
      <div className="max-w-[480px] mx-auto px-5">
        <div>
          {/* Tarjeta de saldo: en escritorio entra con una animación al abrir la app */}
          <section
            className={isDesktop ? CARD_ENTER_CLASS : "cc-card-hover"}
            style={{
              background: BRAND_GRADIENT_DEEP,
              borderRadius: "1.4rem",
              padding: minimalMode ? "1.25rem 1.35rem" : "1.5rem",
              boxShadow: "0 20px 40px -16px rgb(11 61 38 / 0.55)",
              marginBottom: "1.25rem",
              color: "#FFFFFF",
            }}
          >
            <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.5rem 0.75rem", flexWrap: "wrap" }}>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: "0.76rem", color: "rgba(255,255,255,0.85)", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", display: "flex", alignItems: "center", gap: "0.4rem" }}>
                  <span style={{ width: "0.45rem", height: "0.45rem", borderRadius: "50%", background: C.gold, display: "inline-block" }} />
                  {minimalMode ? "Saldo disponible" : "Dinero disponible (Efectivo / Cuentas)"}
                </div>
                <div className="cc-display cc-ledger-total" style={{ fontSize: minimalMode ? "2.2rem" : "2.6rem", fontWeight: 700, margin: "0.25rem 0 0.25rem 0", color: "#FFFFFF", letterSpacing: "-0.01em" }}>
                  {fmtMoney(totals.saldoEfectivo, settings.currency)}
                </div>
                <p style={{ margin: 0, fontSize: "0.75rem", color: "rgba(255,255,255,0.85)", lineHeight: 1.45 }}>
                  {totals.deudaTotal > 0 ? (
                    <span>
                      💳 Deuda general: <strong>{fmtMoney(totals.deudaTotal, settings.currency)}</strong>
                      {totals.deudaTarjetas > 0 && totals.deudaCuotas > 0 && !minimalMode ? (
                        <span style={{ opacity: 0.9 }}> ({fmtMoney(totals.deudaTarjetas, settings.currency)} tarj. + {fmtMoney(totals.deudaCuotas, settings.currency)} cuotas)</span>
                      ) : totals.deudaCuotas > 0 && totals.deudaTarjetas === 0 && !minimalMode ? (
                        <span style={{ opacity: 0.9 }}> (cuotas y préstamos)</span>
                      ) : !minimalMode ? (
                        <span style={{ opacity: 0.9 }}> (tarjetas)</span>
                      ) : null}
                      {" · Balance neto: "}
                      <strong style={{ color: totals.balanceNeto >= 0 ? "#D1FAE5" : "#FECACA" }}>
                        {fmtMoney(totals.balanceNeto, settings.currency)}
                      </strong>
                    </span>
                  ) : (
                    "Dinero en mano y bancos · Sin deudas pendientes"
                  )}
                </p>
              </div>
              {/* El personaje de la cuenta: reacciona a cada ingreso y gasto. */}
              <span
                style={{
                  display: "inline-block",
                  flexShrink: 0,
                  animation: isDesktop ? MASCOT_DANCE : MASCOT_DANCE_SOFT,
                }}
              >
                <Mascot kind={settings.mascot || "coin"} mood={mascotMood} burst={burst} size={minimalMode ? 52 : (isDesktop ? 96 : 68)} />
              </span>
              {/* Anuncio para lectores de pantalla */}
              <span role="status" className="sr-only">
                {burst === "happy"
                  ? "El personaje se alegra: ha entrado dinero."
                  : burst === "sad"
                  ? "El personaje se entristece: ha habido un gasto."
                  : ""}
              </span>
            </div>
            <div style={{ height: "1px", backgroundColor: "rgba(255,255,255,0.22)", margin: minimalMode ? "0.85rem 0" : "1.25rem 0" }}></div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.65rem" }}>
              <div style={{ backgroundColor: "rgba(255,255,255,0.14)", border: "1px solid rgba(255,255,255,0.18)", borderRadius: "0.85rem", padding: minimalMode ? "0.65rem 0.75rem" : "0.85rem" }}>
                <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", fontSize: "0.72rem", color: "rgba(255,255,255,0.85)", marginBottom: "0.2rem" }}>
                  <TrendingUp size={14} color="#FFFFFF" />
                  <span>Ingresos</span>
                </div>
                <div style={{ fontSize: minimalMode ? "1.05rem" : "1.2rem", fontWeight: 700, color: "#FFFFFF" }}>{fmtMoney(totals.totalIngresos, settings.currency)}</div>
              </div>
              <div style={{ backgroundColor: "rgba(255,255,255,0.14)", border: "1px solid rgba(255,255,255,0.18)", borderRadius: "0.85rem", padding: minimalMode ? "0.65rem 0.75rem" : "0.85rem" }}>
                <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", fontSize: "0.72rem", color: "rgba(255,255,255,0.85)", marginBottom: "0.2rem" }}>
                  <TrendingDown size={14} color="#FFFFFF" />
                  <span>Gastos totales</span>
                </div>
                <div style={{ fontSize: minimalMode ? "1.05rem" : "1.2rem", fontWeight: 700, color: "#FFFFFF" }}>{fmtMoney(totals.totalGastos, settings.currency)}</div>
                {!minimalMode && (totals.gastosTarjeta > 0 || totals.gastosEfectivo > 0) && (
                  <div style={{ fontSize: "0.68rem", color: "rgba(255,255,255,0.8)", marginTop: "0.2rem", whiteSpace: "nowrap" }}>
                    Efec: {fmtMoney(totals.gastosEfectivo, settings.currency)} · Tarj: {fmtMoney(totals.gastosTarjeta, settings.currency)}
                  </div>
                )}
              </div>
            </div>
            {byCurrency.some((c) => c.currency !== (settings.currency || "S/")) && (
              <>
                <div style={{ height: "1px", backgroundColor: "rgba(255,255,255,0.22)", margin: "1.25rem 0" }}></div>
                <div style={{ fontSize: "0.75rem", color: "rgba(255,255,255,0.85)", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: "0.5rem" }}>
                  Por moneda (sin convertir)
                </div>
                {byCurrency.map((c) => (
                  <div key={c.currency} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.75rem", padding: "0.35rem 0" }}>
                    <span style={{ fontWeight: 700, fontSize: "0.85rem", color: "#FFFFFF" }}>{c.currency}</span>
                    <span style={{ display: "flex", gap: "0.75rem", alignItems: "center", fontSize: "0.8rem" }}>
                      <span style={{ color: "#D1FAE5", display: "inline-flex", alignItems: "center", gap: "0.2rem" }}><ArrowUpRight size={12} style={{ flexShrink: 0 }} />{fmtMoney(c.ingresos, c.currency)}</span>
                      <span style={{ color: "#FECACA", display: "inline-flex", alignItems: "center", gap: "0.2rem" }}><ArrowDownRight size={12} style={{ flexShrink: 0 }} />{fmtMoney(c.gastos, c.currency)}</span>
                    </span>
                  </div>
                ))}
              </>
            )}
          </section>

          {/* ============================================================
              MÓDULO DESTACADO: PRÉSTAMOS Y DEUDAS ENTRE NOSOTROS
              Módulo independiente y resaltado en el dashboard para ver y saldar
              las deudas directas entre los miembros del equipo.
              ============================================================ */}
          {!minimalMode && (
          <section
            aria-label="Módulo de Préstamos y Deudas entre nosotros"
            className="cc-card-hover"
            style={{
              backgroundColor: darkMode ? "#1E1B4B" : "#FFFDF5",
              background: darkMode
                ? "linear-gradient(135deg, rgba(30, 27, 75, 0.95) 0%, rgba(69, 26, 3, 0.45) 100%)"
                : "linear-gradient(135deg, #FFFFFF 0%, #FEF9C3 100%)",
              borderRadius: "1.4rem",
              padding: peerLoansCollapsed ? "0.95rem 1.35rem" : "1.35rem",
              boxShadow: peerDebts.hasDebts
                ? "0 14px 32px -10px rgba(245, 158, 11, 0.28)"
                : "0 12px 28px -12px rgb(2 6 23 / 0.14)",
              border: peerDebts.hasDebts
                ? `1.5px solid ${darkMode ? "#F59E0B" : "#FBBF24"}`
                : `1px solid ${darkMode ? "rgba(245, 158, 11, 0.3)" : "#FDE68A"}`,
              marginBottom: "1.5rem",
              transition: "all 0.15s ease",
              position: "relative",
              overflow: "hidden",
            }}
          >
            {/* Cabecera del módulo */}
            <div
              onClick={() => {
                setPeerLoansCollapsed((prev) => {
                  const next = !prev;
                  try { localStorage.setItem("cc:collapse:peerloans", next ? "1" : "0"); } catch (e) {}
                  return next;
                });
              }}
              role="button"
              tabIndex={0}
              aria-expanded={!peerLoansCollapsed}
              aria-label={peerLoansCollapsed ? "Desplegar préstamos entre nosotros" : "Minimizar préstamos entre nosotros"}
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                marginBottom: peerLoansCollapsed ? 0 : "1rem",
                cursor: "pointer",
                userSelect: "none",
                gap: "0.5rem",
              }}
            >
              <div className="flex items-center gap-2.5">
                <div
                  style={{
                    width: "2.3rem",
                    height: "2.3rem",
                    borderRadius: "0.75rem",
                    background: BRAND_GRADIENT,
                    color: "#FFFFFF",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    fontSize: "1.15rem",
                    flexShrink: 0,
                    boxShadow: "0 4px 12px rgba(16,185,129,0.35)",
                  }}
                >
                  🤝
                </div>
                <div>
                  <h2 style={{ fontFamily: "'Inter', sans-serif", fontSize: "0.98rem", fontWeight: 800, color: C.ink, margin: 0, display: "flex", alignItems: "center", gap: "0.4rem", flexWrap: "wrap" }}>
                    <span>Préstamos entre nosotros</span>
                    {peerDebts.hasDebts ? (
                      <span
                        style={{
                          background: darkMode ? "#B45309" : "#F59E0B",
                          color: "#FFFFFF",
                          fontSize: "0.65rem",
                          fontWeight: 800,
                          padding: "0.12rem 0.55rem",
                          borderRadius: "999px",
                          letterSpacing: "0.03em",
                        }}
                      >
                        {peerDebts.list.length} {peerDebts.list.length === 1 ? "activo" : "activos"}
                      </span>
                    ) : (
                      <span
                        style={{
                          background: C.chipBg,
                          color: C.inkSoft,
                          fontSize: "0.65rem",
                          fontWeight: 600,
                          padding: "0.1rem 0.45rem",
                          borderRadius: "999px",
                        }}
                      >
                        Sin deudas
                      </span>
                    )}
                  </h2>
                  <p style={{ fontSize: "0.72rem", color: C.inkSoft, margin: 0 }}>
                    Deudas directas y préstamos entre los miembros del equipo
                  </p>
                </div>
              </div>

              <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
                {peerDebts.hasDebts && (
                  <button
                    type="button"
                    onClick={(e) => {
                      e.stopPropagation();
                      setShowMinimalTransfersModal(true);
                    }}
                    title="Calcular número mínimo de transferencias para saldar todo"
                    style={{
                      background: C.paper,
                      color: C.ink,
                      border: `1px solid ${C.line}`,
                      borderRadius: "0.55rem",
                      padding: "0.22rem 0.5rem",
                      fontSize: "0.68rem",
                      fontWeight: 700,
                      cursor: "pointer",
                      display: "flex",
                      alignItems: "center",
                      gap: "0.2rem",
                    }}
                    className="cc-btn-press"
                  >
                    <span>⚡ Min. Transferencias</span>
                  </button>
                )}
                {peerDebts.hasDebts ? (
                  <span
                    style={{
                      background: darkMode ? "#451A03" : "#FEF3C7",
                      color: darkMode ? "#FDE047" : "#B45309",
                      border: `1px solid ${darkMode ? "#B45309" : "#FDE68A"}`,
                      fontSize: "0.75rem",
                      fontWeight: 800,
                      padding: "0.22rem 0.65rem",
                      borderRadius: "999px",
                      letterSpacing: "0.02em",
                      whiteSpace: "nowrap",
                    }}
                  >
                    Total: {fmtMoney(peerDebts.totalPeerDebt, settings.currency)}
                  </span>
                ) : (
                  <span
                    style={{
                      background: C.successBg,
                      color: C.green,
                      fontSize: "0.72rem",
                      fontWeight: 700,
                      padding: "0.2rem 0.6rem",
                      borderRadius: "999px",
                    }}
                  >
                    Al día ✓
                  </span>
                )}
                <div
                  style={{
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.6rem",
                    color: C.inkSoft,
                    width: "1.75rem",
                    height: "1.75rem",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    flexShrink: 0,
                  }}
                  title={peerLoansCollapsed ? "Desplegar sección" : "Minimizar sección"}
                >
                  {peerLoansCollapsed ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
                </div>
              </div>
            </div>

            {/* Contenido desplegado */}
            {!peerLoansCollapsed && (
              <div>
                {peerDebts.list.length === 0 ? (
                  <div
                    style={{
                      background: darkMode ? "rgba(15, 23, 42, 0.45)" : C.field,
                      border: `1px dashed ${darkMode ? "rgba(245, 158, 11, 0.35)" : C.line}`,
                      borderRadius: "1rem",
                      padding: "1.25rem 1rem",
                      textAlign: "center",
                      display: "flex",
                      flexDirection: "column",
                      alignItems: "center",
                      gap: "0.6rem",
                    }}
                  >
                    <div style={{ fontSize: "1.5rem" }}>🤝</div>
                    <div style={{ fontSize: "0.82rem", fontWeight: 700, color: C.ink }}>
                      No hay préstamos ni deudas pendientes entre ustedes
                    </div>
                    <div style={{ fontSize: "0.72rem", color: C.inkSoft, maxWidth: "22rem" }}>
                      Si alguien le prestó dinero a otro integrante, regístralo para llevar el control y saldarlo en cualquier momento.
                    </div>
                    <button
                      type="button"
                      onClick={() => {
                        setEditingTx(null);
                        setFormType("prestamo");
                        const other = people.find((p) => p !== (myName || people[0])) || (people[1] || "");
                        setPerson(myName || (people[0] || ""));
                        setFormBorrower(other);
                        setFormCurrency(settings.currency || "S/");
                        setFormRate("");
                        setAmount("");
                        setDesc("");
                        setShowForm(true);
                      }}
                      style={{
                        background: BRAND_GRADIENT,
                        color: "#FFFFFF",
                        border: "none",
                        borderRadius: "0.75rem",
                        padding: "0.5rem 1rem",
                        fontSize: "0.76rem",
                        fontWeight: 700,
                        cursor: "pointer",
                        marginTop: "0.3rem",
                        boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                      }}
                      className="cc-btn-press hover:opacity-95 flex items-center gap-1.5"
                    >
                      <Plus size={14} />
                      <span>Registrar préstamo entre nosotros</span>
                    </button>
                  </div>
                ) : (
                  <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
                    {/* Lista de deudas entre personas */}
                    <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.75rem" }}>
                      {peerDebts.list.map((debt) => {
                        const isMyDebt = debt.borrower === myName;
                        const isMyCredit = debt.lender === myName;
                        return (
                          <div
                            key={debt.id}
                            style={{
                              background: darkMode ? "rgba(15, 23, 42, 0.75)" : "#FFFFFF",
                              border: `1.5px solid ${darkMode ? "rgba(245, 158, 11, 0.4)" : "#FDE68A"}`,
                              borderRadius: "1rem",
                              padding: "0.9rem 1rem",
                              display: "flex",
                              flexDirection: "column",
                              justifyContent: "space-between",
                              gap: "0.75rem",
                              boxShadow: "0 4px 14px rgba(0,0,0,0.05)",
                            }}
                          >
                            <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.6rem" }}>
                              <div style={{ minWidth: 0, display: "flex", flexDirection: "column", gap: "0.25rem" }}>
                                <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", flexWrap: "wrap" }}>
                                  <div style={{ display: "flex", alignItems: "center", gap: "0.25rem" }}>
                                    <span style={{ width: "0.6rem", height: "0.6rem", borderRadius: "50%", background: personColor(debt.borrower), flexShrink: 0 }} />
                                    <strong style={{ fontSize: "0.86rem", color: isMyDebt ? C.red : C.ink }}>
                                      {debt.borrower} {isMyDebt ? "(Tú)" : ""}
                                    </strong>
                                  </div>
                                  <span style={{ fontSize: "0.72rem", color: C.inkSoft, fontWeight: 700 }}>
                                    ➔ debe a
                                  </span>
                                  <div style={{ display: "flex", alignItems: "center", gap: "0.25rem" }}>
                                    <span style={{ width: "0.6rem", height: "0.6rem", borderRadius: "50%", background: personColor(debt.lender), flexShrink: 0 }} />
                                    <strong style={{ fontSize: "0.86rem", color: isMyCredit ? C.green : C.ink }}>
                                      {debt.lender} {isMyCredit ? "(Tú)" : ""}
                                    </strong>
                                  </div>
                                </div>

                                <div style={{ fontSize: "0.74rem", color: C.inkSoft, display: "flex", alignItems: "center", gap: "0.35rem", flexWrap: "wrap", marginTop: "0.1rem" }}>
                                  <span>Concepto: <strong style={{ color: C.ink }}>{debt.name}</strong></span>
                                  {debt.sourceType === "cuota" && debt.totalInstallments > 0 && (
                                    <span
                                      style={{
                                        background: darkMode ? "#451A03" : "#FEF3C7",
                                        color: darkMode ? "#FDE047" : "#B45309",
                                        padding: "0.05rem 0.4rem",
                                        borderRadius: "0.35rem",
                                        fontSize: "0.64rem",
                                        fontWeight: 700,
                                      }}
                                    >
                                      📅 Cuota {debt.paidInstallments + 1}/{debt.totalInstallments}
                                    </span>
                                  )}
                                </div>
                              </div>

                              <div style={{ textAlign: "right", flexShrink: 0 }}>
                                <div style={{ fontSize: "0.66rem", textTransform: "uppercase", color: C.inkSoft, fontWeight: 700, letterSpacing: "0.04em" }}>
                                  Deuda pendiente
                                </div>
                                <div style={{ fontSize: "1.15rem", fontWeight: 800, color: isMyDebt ? C.red : C.ink, whiteSpace: "nowrap" }}>
                                  {fmtMoney(debt.remainingAmount, debt.currency)}
                                </div>
                              </div>
                            </div>

                            {/* Botón de acción para saldar / pagar */}
                            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", paddingTop: "0.55rem", borderTop: `1px solid ${darkMode ? "rgba(255,255,255,0.08)" : C.lineSoft}`, gap: "0.5rem" }}>
                              <span style={{ fontSize: "0.7rem", color: C.inkSoft }}>
                                {debt.sourceType === "cuota" ? "Compromiso mensual" : "Préstamo único"}
                              </span>
                              <button
                                type="button"
                                onClick={() => setPendingSettleDebt(debt)}
                                style={{
                                  background: BRAND_GRADIENT,
                                  color: "#FFFFFF",
                                  border: "none",
                                  borderRadius: "0.65rem",
                                  padding: "0.35rem 0.85rem",
                                  fontSize: "0.74rem",
                                  fontWeight: 700,
                                  cursor: "pointer",
                                  display: "flex",
                                  alignItems: "center",
                                  gap: "0.3rem",
                                  boxShadow: "0 2px 8px rgba(16,185,129,0.25)",
                                }}
                                className="cc-btn-press hover:opacity-95"
                                title="Registrar pago o saldar esta deuda"
                              >
                                <span>💸 Saldar / Pagar</span>
                              </button>
                            </div>
                          </div>
                        );
                      })}
                    </div>

                    {/* Botón para añadir otro préstamo rápido */}
                    <div style={{ display: "flex", justifyContent: "flex-end", marginTop: "0.3rem" }}>
                      <button
                        type="button"
                        onClick={() => {
                          setEditingTx(null);
                          setFormType("prestamo");
                          const other = people.find((p) => p !== (myName || people[0])) || (people[1] || "");
                          setPerson(myName || (people[0] || ""));
                          setFormBorrower(other);
                          setFormCurrency(settings.currency || "S/");
                          setFormRate("");
                          setAmount("");
                          setDesc("");
                          setShowForm(true);
                        }}
                        style={{
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.7rem",
                          padding: "0.4rem 0.8rem",
                          fontSize: "0.74rem",
                          fontWeight: 700,
                          color: C.ink,
                          cursor: "pointer",
                          display: "flex",
                          alignItems: "center",
                          gap: "0.3rem",
                        }}
                        className="cc-btn-press"
                      >
                        <Plus size={13} />
                        <span>Nuevo préstamo entre nosotros</span>
                      </button>
                    </div>
                  </div>
                )}
              </div>
            )}
          </section>
          )}

          {/* Modal de confirmación para saldar deuda entre nosotros */}
          {pendingSettleDebt && (
            <div
              onClick={() => setPendingSettleDebt(null)}
              className="cc-fade-in"
              style={{
                position: "fixed",
                inset: 0,
                background: "rgba(15, 23, 42, 0.72)",
                backdropFilter: "blur(5px)",
                WebkitBackdropFilter: "blur(5px)",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                zIndex: 85,
                padding: "1rem",
              }}
            >
              <div
                onClick={(e) => e.stopPropagation()}
                className="cc-panel-in"
                style={{
                  background: C.paper,
                  borderRadius: "1.4rem",
                  padding: "1.5rem",
                  border: `1.5px solid ${darkMode ? "#F59E0B" : "#FCD34D"}`,
                  boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
                  maxWidth: "440px",
                  width: "100%",
                  display: "flex",
                  flexDirection: "column",
                  gap: "1rem",
                }}
              >
                <PanelHeader
                  icon={<Users size={16} />}
                  title="Saldar Préstamo"
                  subtitle={`Devolución directa de ${pendingSettleDebt.borrower} a ${pendingSettleDebt.lender}`}
                  onAction={() => setPendingSettleDebt(null)}
                  actionLabel="Cancelar"
                  actionTitle="Cancelar"
                  actionIcon={<X size={14} />}
                />

                <div
                  style={{
                    background: darkMode ? "rgba(245, 158, 11, 0.12)" : "#FEF3C7",
                    border: `1px solid ${darkMode ? "rgba(245, 158, 11, 0.3)" : "#FDE68A"}`,
                    borderRadius: "1rem",
                    padding: "1rem",
                    display: "flex",
                    flexDirection: "column",
                    gap: "0.5rem",
                  }}
                >
                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                    <span style={{ fontSize: "0.75rem", color: C.inkSoft }}>Concepto:</span>
                    <strong style={{ fontSize: "0.85rem", color: C.ink }}>{pendingSettleDebt.name}</strong>
                  </div>
                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                    <span style={{ fontSize: "0.75rem", color: C.inkSoft }}>Importe a devolver:</span>
                    <strong style={{ fontSize: "1.2rem", fontWeight: 800, color: darkMode ? "#FDE68A" : "#92400E" }}>
                      {fmtMoney(pendingSettleDebt.remainingAmount, pendingSettleDebt.currency)}
                    </strong>
                  </div>
                  <div style={{ fontSize: "0.72rem", color: C.inkSoft, borderTop: `1px solid ${darkMode ? "rgba(245,158,11,0.2)" : "#FDE68A"}`, paddingTop: "0.4rem" }}>
                    Al saldar, la deuda se retirará de los préstamos pendientes y se registrará la constancia de pago en el historial.
                  </div>
                </div>

                <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
                  <button
                    type="button"
                    disabled={saving}
                    onClick={() => saldarPeerDebt(pendingSettleDebt)}
                    style={{
                      background: BRAND_GRADIENT,
                      color: "#FFFFFF",
                      border: "none",
                      borderRadius: "0.75rem",
                      padding: "0.75rem 1rem",
                      fontSize: "0.82rem",
                      fontWeight: 700,
                      cursor: "pointer",
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      gap: "0.4rem",
                      boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                    }}
                    className="cc-btn-press hover:opacity-95"
                  >
                    <span>{saving ? "Saldando…" : "✅ Sí, saldar y marcar como pagado"}</span>
                  </button>

                  <button
                    type="button"
                    disabled={saving}
                    onClick={() => {
                      const debt = pendingSettleDebt;
                      setPendingSettleDebt(null);
                      if (debt.sourceType === "cuota" && debt.instId) {
                        setFormType("pago_cuota");
                        setFormInstallmentId(debt.instId);
                        setPerson(debt.borrower);
                        setAmount(String(debt.amount || debt.remainingAmount).replace(".", ","));
                        const cuotaStr = debt.totalInstallments > 0
                          ? `Pago cuota ${debt.paidInstallments + 1}/${debt.totalInstallments} - ${debt.name}`
                          : `Pago préstamo a ${debt.lender} - ${debt.name}`;
                        setDesc(cuotaStr);
                        pickCurrency(debt.currency || settings.currency || "S/");
                        setShowForm(true);
                      } else {
                        setFormType("gasto");
                        setFormPaymentMethod("prestamo_pago");
                        setFormLoanId(debt.txId || "");
                        setPerson(debt.borrower);
                        setAmount(String(debt.remainingAmount).replace(".", ","));
                        setDesc(`Devolución de préstamo a ${debt.lender} (${debt.name})`);
                        pickCurrency(debt.currency || settings.currency || "S/");
                        setShowForm(true);
                      }
                    }}
                    style={{
                      background: C.field,
                      border: `1px solid ${C.line}`,
                      borderRadius: "0.75rem",
                      padding: "0.6rem 1rem",
                      fontSize: "0.76rem",
                      fontWeight: 600,
                      color: C.ink,
                      cursor: "pointer",
                      textAlign: "center",
                    }}
                    className="cc-btn-press"
                  >
                    <span>📝 Abonar importe personalizado / formulario</span>
                  </button>

                  <button
                    type="button"
                    onClick={() => setPendingSettleDebt(null)}
                    style={{
                      background: "none",
                      border: "none",
                      padding: "0.4rem",
                      fontSize: "0.74rem",
                      color: C.inkSoft,
                      cursor: "pointer",
                    }}
                  >
                    Cancelar
                  </button>
                </div>
              </div>
            </div>
          )}

          {/* Módulo de Tarjetas de Crédito por Individuo */}
          {!minimalMode && (
          <section
            className="cc-card-hover"
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.4rem",
              padding: cardsCollapsed ? "0.95rem 1.35rem" : "1.35rem",
              boxShadow: "0 12px 28px -12px rgb(2 6 23 / 0.18)",
              border: `1px solid ${C.line}`,
              marginBottom: "1.5rem",
              transition: "padding 0.15s ease",
            }}
          >
            <div
              onClick={() => {
                setCardsCollapsed((prev) => {
                  const next = !prev;
                  try { localStorage.setItem("cc:collapse:cards", next ? "1" : "0"); } catch (e) {}
                  return next;
                });
              }}
              role="button"
              tabIndex={0}
              aria-expanded={!cardsCollapsed}
              aria-label={cardsCollapsed ? "Desplegar tarjetas y cuotas" : "Minimizar tarjetas y cuotas"}
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                marginBottom: cardsCollapsed ? 0 : "1rem",
                cursor: "pointer",
                userSelect: "none",
                gap: "0.5rem",
              }}
            >
              <div className="flex items-center gap-2.5">
                <div
                  style={{
                    width: "2.1rem",
                    height: "2.1rem",
                    borderRadius: "0.65rem",
                    background: C.chipBg,
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    color: C.green,
                    flexShrink: 0,
                  }}
                >
                  <CreditCard size={18} />
                </div>
                <div>
                  <h2 style={{ fontFamily: "'Inter', sans-serif", fontSize: "0.95rem", fontWeight: 700, color: C.ink, margin: 0, display: "flex", alignItems: "center", gap: "0.4rem" }}>
                    Tarjetas y cuotas
                    <span style={{ color: C.inkSoft, fontSize: "0.72rem", fontWeight: 500 }}>
                      ({cardStats.list.length} · {(settings.installments || []).length})
                    </span>
                  </h2>
                  <p style={{ fontSize: "0.72rem", color: C.inkSoft, margin: 0 }}>
                    Deuda, corte, pago y cuotas separados por persona
                  </p>
                </div>
              </div>

              <div style={{ display: "flex", alignItems: "center", gap: "0.45rem", flexWrap: "wrap", justifyContent: "flex-end" }}>
                {cardStats.totalCashbackAvailableAll > 0 && (
                  <span
                    style={{
                      background: darkMode ? "#064E3B" : "#ECFDF5",
                      color: darkMode ? "#6EE7B7" : "#047857",
                      border: `1px solid ${darkMode ? "#059669" : "#A7F3D0"}`,
                      fontSize: "0.72rem",
                      fontWeight: 700,
                      padding: "0.2rem 0.6rem",
                      borderRadius: "999px",
                      display: "flex",
                      alignItems: "center",
                      gap: "0.25rem",
                    }}
                  >
                    <span>🎁 Cashback:</span>
                    <strong>{fmtMoney(cardStats.totalCashbackAvailableAll, settings.currency)}</strong>
                  </span>
                )}
                {totals.deudaTotal > 0 ? (
                  <span
                    style={{
                      background: darkMode ? "#451A03" : "#FEF3C7",
                      color: darkMode ? "#FDE68A" : "#92400E",
                      border: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`,
                      fontSize: "0.72rem",
                      fontWeight: 700,
                      padding: "0.2rem 0.6rem",
                      borderRadius: "999px",
                      letterSpacing: "0.02em",
                    }}
                  >
                    Deuda: {fmtMoney(totals.deudaTotal, settings.currency)}
                  </span>
                ) : (
                  <span
                    style={{
                      background: C.successBg,
                      color: C.green,
                      fontSize: "0.72rem",
                      fontWeight: 700,
                      padding: "0.2rem 0.6rem",
                      borderRadius: "999px",
                    }}
                  >
                    Al día ✓
                  </span>
                )}
                <div
                  style={{
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.6rem",
                    color: C.inkSoft,
                    width: "1.75rem",
                    height: "1.75rem",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    flexShrink: 0,
                  }}
                  title={cardsCollapsed ? "Desplegar sección" : "Minimizar sección"}
                >
                  {cardsCollapsed ? <ChevronDown size={15} /> : <ChevronUp size={15} />}
                </div>
              </div>
            </div>

            {/* Contenido de tarjetas */}
            {!cardsCollapsed && (
              cardStats.list.length === 0 ? (
                <div
                  style={{
                    background: C.field,
                    border: `1px dashed ${C.line}`,
                    borderRadius: "1rem",
                    padding: "1.1rem",
                    textAlign: "center",
                  }}
                >
                  <p style={{ fontSize: "0.8rem", color: C.inkSoft, margin: "0 0 0.85rem 0", lineHeight: 1.45 }}>
                    ¿Pagas compras con tarjeta de crédito? Registra tus tarjetas para separar gastos en efectivo vs tarjeta y controlar tu deuda sin duplicar gastos al pagar tu estado de cuenta.
                  </p>
                  <button
                    type="button"
                    onClick={openSettings}
                    style={{
                      fontFamily: "'Inter', sans-serif",
                      background: C.chipBg,
                      color: C.ink,
                      border: `1px solid ${C.line}`,
                      borderRadius: "999px",
                      padding: "0.4rem 0.9rem",
                      fontSize: "0.75rem",
                      fontWeight: 600,
                      cursor: "pointer",
                    }}
                    className="cc-btn-press hover:opacity-85 transition-opacity"
                  >
                    <Plus size={12} style={{ display: "inline", verticalAlign: "middle", marginRight: "0.3rem" }} />
                    Configurar tarjetas en Ajustes
                  </button>
                </div>
              ) : (() => {
                const activeCardGroups = cardStats.byPerson.filter((g) => g.cards.length > 0);
                const myNameNorm = String(myName || "").trim().toLowerCase();
                // Determinar la persona seleccionada en el filtro
                const selectedPerson = cardTabPerson === "auto"
                  ? (activeCardGroups.some((g) => String(g.person || "").trim().toLowerCase() === myNameNorm)
                      ? (activeCardGroups.find((g) => String(g.person || "").trim().toLowerCase() === myNameNorm)?.person || activeCardGroups[0]?.person || "all")
                      : (activeCardGroups[0]?.person || "all"))
                  : cardTabPerson;

                const filteredGroups = selectedPerson === "all"
                  ? activeCardGroups
                  : activeCardGroups.filter((g) => String(g.person || "").trim().toLowerCase() === String(selectedPerson || "").trim().toLowerCase());

                return (
                  <div>
                    {/* Selector de personas / pestañas rápidas si hay más de 1 grupo o personas en la cuenta */}
                    {(activeCardGroups.length > 1 || people.length > 1) && (
                      <div
                        role="tablist"
                        aria-label="Filtrar tarjetas por persona"
                        style={{
                          display: "flex",
                          gap: "0.35rem",
                          overflowX: "auto",
                          paddingBottom: "0.5rem",
                          marginBottom: "0.85rem",
                          scrollbarWidth: "none",
                        }}
                      >
                        {/* Pestaña "Todas" */}
                        <button
                          type="button"
                          role="tab"
                          aria-selected={selectedPerson === "all"}
                          onClick={() => setCardTabPerson("all")}
                          style={{
                            display: "flex",
                            alignItems: "center",
                            gap: "0.35rem",
                            background: selectedPerson === "all" ? (darkMode ? "#1E293B" : "#FFFFFF") : C.field,
                            color: selectedPerson === "all" ? C.ink : C.inkSoft,
                            border: selectedPerson === "all" ? `1.5px solid ${C.green}` : `1px solid ${C.line}`,
                            borderRadius: "999px",
                            padding: "0.35rem 0.75rem",
                            fontSize: "0.74rem",
                            fontWeight: selectedPerson === "all" ? 700 : 500,
                            cursor: "pointer",
                            whiteSpace: "nowrap",
                            boxShadow: selectedPerson === "all" ? "0 2px 8px rgba(0,0,0,0.08)" : "none",
                            transition: "all 0.15s ease",
                          }}
                        >
                          <Users size={12} />
                          <span>Todas ({cardStats.list.length})</span>
                        </button>

                        {/* Pestañas individuales por persona */}
                        {cardStats.byPerson.map((g) => {
                          const isMe = String(g.person || "").trim().toLowerCase() === myNameNorm;
                          const isSelected = String(selectedPerson || "").trim().toLowerCase() === String(g.person || "").trim().toLowerCase();
                          const pColor = String(g.person || "").trim() ? personColor(String(g.person).trim()) : C.line;
                          const instGroup = installmentStats.byPerson.find((ib) => String(ib.person || "").trim().toLowerCase() === String(g.person || "").trim().toLowerCase());
                          const personCuotasDebt = instGroup ? instGroup.totalRemainingDebt : 0;
                          const personTotalDebt = g.totalDebt + personCuotasDebt;
                          return (
                            <button
                              key={g.person}
                              type="button"
                              role="tab"
                              aria-selected={isSelected}
                              onClick={() => setCardTabPerson(g.person)}
                              style={{
                                display: "flex",
                                alignItems: "center",
                                gap: "0.4rem",
                                background: isSelected ? (darkMode ? "#1E293B" : "#FFFFFF") : C.field,
                                color: isSelected ? C.ink : C.inkSoft,
                                border: isSelected ? `1.5px solid ${pColor}` : `1px solid ${C.line}`,
                                borderRadius: "999px",
                                padding: "0.35rem 0.75rem",
                                fontSize: "0.74rem",
                                fontWeight: isSelected ? 700 : 500,
                                cursor: "pointer",
                                whiteSpace: "nowrap",
                                boxShadow: isSelected ? "0 2px 8px rgba(0,0,0,0.08)" : "none",
                                transition: "all 0.15s ease",
                              }}
                            >
                              <span
                                style={{
                                  width: "0.55rem",
                                  height: "0.55rem",
                                  borderRadius: "50%",
                                  background: pColor,
                                  flexShrink: 0,
                                }}
                              />
                              <span>{isMe ? `Mis tarjetas (${g.person})` : g.person}</span>
                              <span
                                style={{
                                  fontSize: "0.65rem",
                                  background: isSelected ? C.chipBg : C.lineSoft,
                                  color: isSelected ? C.ink : C.inkSoft,
                                  borderRadius: "999px",
                                  padding: "0.05rem 0.35rem",
                                  fontWeight: 700,
                                }}
                              >
                                {g.cards.length}
                              </span>
                              {personTotalDebt > 0 && (
                                <span
                                  style={{
                                    width: "0.4rem",
                                    height: "0.4rem",
                                    borderRadius: "50%",
                                    background: C.red,
                                    flexShrink: 0,
                                  }}
                                  title="Tiene deuda pendiente (tarjetas / cuotas)"
                                />
                              )}
                            </button>
                          );
                        })}
                      </div>
                    )}

                    {/* Renderizado de las tarjetas filtradas */}
                    {filteredGroups.length === 0 ? (
                      <div
                        style={{
                          background: C.field,
                          border: `1px dashed ${C.line}`,
                          borderRadius: "1rem",
                          padding: "1.25rem",
                          textAlign: "center",
                        }}
                      >
                        <p style={{ fontSize: "0.78rem", color: C.inkSoft, margin: "0 0 0.6rem 0" }}>
                          <strong>{selectedPerson}</strong> no tiene tarjetas de crédito registradas todavía.
                        </p>
                        <button
                          type="button"
                          onClick={() => {
                            setCardDraft({
                              id: "",
                              name: "",
                              person: selectedPerson,
                              cutoffDay: 20,
                              dueDay: 10,
                              creditLimit: "",
                              color: "emerald",
                            });
                            setShowCardModal(true);
                            setSettingsTab("tarjetas");
                            setShowSettings(true);
                          }}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: BRAND_GRADIENT,
                            color: "#FFFFFF",
                            border: "none",
                            borderRadius: "999px",
                            padding: "0.35rem 0.85rem",
                            fontSize: "0.74rem",
                            fontWeight: 700,
                            cursor: "pointer",
                          }}
                          className="cc-btn-press hover:opacity-90"
                        >
                          <Plus size={11} style={{ display: "inline", verticalAlign: "middle", marginRight: "0.25rem" }} />
                          Añadir tarjeta para {selectedPerson}
                        </button>
                      </div>
                    ) : (
                      <div style={{ display: "flex", flexDirection: "column", gap: "1.2rem" }}>
                        {filteredGroups.map((group) => (
                          <div key={group.person} style={{ display: "flex", flexDirection: "column", gap: "0.55rem" }}>
                            {/* Cabecera del individuo (siempre visible para contexto) */}
                            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", paddingBottom: "0.25rem", borderBottom: `1px solid ${C.lineSoft}` }}>
                              <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                                <span
                                  style={{
                                    width: "0.55rem",
                                    height: "0.55rem",
                                    borderRadius: "50%",
                                    background: group.person.trim() ? personColor(group.person.trim()) : C.line,
                                    display: "inline-block",
                                  }}
                                />
                                <span style={{ fontSize: "0.82rem", fontWeight: 700, color: C.ink }}>
                                  {group.person}
                                </span>
                                <span style={{ fontSize: "0.7rem", color: C.inkSoft }}>
                                  ({group.cards.length} {group.cards.length === 1 ? "tarjeta" : "tarjetas"})
                                </span>
                              </div>
                              {(() => {
                                const instGroup = installmentStats.byPerson.find((ib) => String(ib.person || "").trim().toLowerCase() === String(group.person || "").trim().toLowerCase());
                                const personCuotasDebt = instGroup ? instGroup.totalRemainingDebt : 0;
                                const personTotalDebt = group.totalDebt + personCuotasDebt;
                                return (
                                  <div style={{ textAlign: "right" }}>
                                    <span style={{ fontSize: "0.75rem", fontWeight: 700, color: personTotalDebt > 0 ? C.red : C.green }}>
                                      {personTotalDebt > 0 ? `Debe ${fmtMoney(personTotalDebt, settings.currency)}` : "Al día ✓"}
                                    </span>
                                    {group.totalDebt > 0 && personCuotasDebt > 0 && (
                                      <div style={{ fontSize: "0.62rem", color: C.inkSoft }}>
                                        Tarj: {fmtMoney(group.totalDebt, settings.currency)} · Cuotas: {fmtMoney(personCuotasDebt, settings.currency)}
                                      </div>
                                    )}
                                  </div>
                                );
                              })()}
                            </div>

                            {/* Tarjetas de esta persona en cuadrícula de 2 columnas en desktop */}
                            <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "repeat(2, 1fr)" : "1fr", gap: "0.65rem" }}>
                              {group.cards.map(({ card, isDebit, currentDebt, currentMonthSpent, cycleInfo, available, currency: cardCurr, hasCashback, totalCashbackAccumulated, currentMonthCashback, availableCashback }) => {
                                const pal = CARD_COLORS.find((c) => c.id === card.color) || CARD_COLORS[0];
                                const curr = cardCurr || card.currency || settings.currency || "S/";
                                return (
                                  <div
                                    key={card.id}
                                    style={{
                                      background: pal.gradient,
                                      border: `1px solid ${pal.border}`,
                                      borderRadius: "1rem",
                                      padding: "0.95rem 1.05rem",
                                      color: pal.text,
                                      boxShadow: "0 6px 16px -4px rgba(0,0,0,0.2)",
                                    }}
                                  >
                                    <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.5rem" }}>
                                      <div>
                                        <div style={{ fontSize: "0.88rem", fontWeight: 700, letterSpacing: "0.02em", display: "flex", alignItems: "center", gap: "0.35rem", flexWrap: "wrap" }}>
                                          <span>{card.name}</span>
                                          {isDebit && (
                                            <span
                                              style={{
                                                fontSize: "0.62rem",
                                                fontWeight: 800,
                                                background: "rgba(255,255,255,0.22)",
                                                padding: "0.1rem 0.4rem",
                                                borderRadius: "0.4rem",
                                                letterSpacing: "0.02em",
                                              }}
                                            >
                                              🏧 Débito
                                            </span>
                                          )}
                                          {!isDebit && card.billingCycle === "quincenal" && (
                                            <span
                                              style={{
                                                fontSize: "0.62rem",
                                                fontWeight: 800,
                                                background: "rgba(255,255,255,0.22)",
                                                padding: "0.1rem 0.4rem",
                                                borderRadius: "0.4rem",
                                                letterSpacing: "0.02em",
                                              }}
                                            >
                                              ⚡ 15d
                                            </span>
                                          )}
                                          {curr !== (settings.currency || "S/") && (
                                            <span
                                              style={{
                                                fontSize: "0.62rem",
                                                fontWeight: 800,
                                                background: "rgba(255,255,255,0.25)",
                                                padding: "0.1rem 0.4rem",
                                                borderRadius: "0.4rem",
                                                letterSpacing: "0.04em",
                                              }}
                                            >
                                              {curr === "$" ? "$ USD" : curr}
                                            </span>
                                          )}
                                        </div>
                                        <div style={{ fontSize: "0.68rem", opacity: 0.85, marginTop: "0.1rem" }}>
                                          Titular: {card.person || "Sin titular"}
                                        </div>
                                        {(() => {
                                          // Cuotas activas cobradas en esta tarjeta: compromiso mensual visible.
                                          const linked = installmentStats.list.filter((s) => s.inst.cardId === card.id && !s.isCompleted);
                                          if (linked.length === 0) return null;
                                          const monthly = linked.reduce((s, i) => (i.currency === cardCurr ? s + (i.inst.amount || 0) : s + txToBase({ amount: i.inst.amount, currency: i.currency }, cardCurr)), 0);
                                          return (
                                            <div style={{ fontSize: "0.68rem", opacity: 0.9, marginTop: "0.15rem" }}>
                                              📅 {linked.length === 1 ? "Cuota" : "Cuotas"}: {linked.map((i) => i.inst.name).join(", ")} · {fmtMoney(monthly, cardCurr)} al mes
                                            </div>
                                          );
                                        })()}
                                      </div>
                                      <div style={{ textAlign: "right", display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "0.2rem" }}>
                                        <div style={{ display: "flex", alignItems: "center", gap: "0.3rem" }}>
                                          <button
                                            type="button"
                                            title="Editar tarjeta y cambiar reglas de Cashback"
                                            onClick={(e) => {
                                              e.stopPropagation();
                                              setCardDraft({
                                                id: card.id,
                                                name: card.name,
                                                cardType: card.cardType || "credito",
                                                person: card.person || "",
                                                billingCycle: card.billingCycle || "mensual",
                                                cutoffDay: card.cutoffDay,
                                                dueDay: card.dueDay,
                                                targetCutoffMonth: card.targetCutoffMonth || "auto",
                                                targetDueMonth: card.targetDueMonth || card.targetMonth || "auto",
                                                creditLimit: card.creditLimit ? String(card.creditLimit) : "",
                                                currency: card.currency || editCurrency || "S/",
                                                color: card.color || "emerald",
                                                hasCashback: card.hasCashback === true || (card.cashbackPercent != null && Number(card.cashbackPercent) > 0),
                                                cashbackPercent: card.cashbackPercent != null ? String(card.cashbackPercent) : "1.5",
                                                excludeServices: card.excludeServices !== false,
                                                minAmount: card.minAmount != null ? String(card.minAmount) : "",
                                                monthlyCap: card.monthlyCap != null ? String(card.monthlyCap) : "",
                                              });
                                              setEditCards(Array.isArray(settings.cards) ? settings.cards : []);
                                              setSettingsTab("tarjetas");
                                              setShowSettings(true);
                                              setShowCardModal(true);
                                            }}
                                            style={{
                                              background: "rgba(255,255,255,0.18)",
                                              border: "1px solid rgba(255,255,255,0.3)",
                                              borderRadius: "0.45rem",
                                              color: "#FFFFFF",
                                              padding: "0.15rem 0.4rem",
                                              cursor: "pointer",
                                              display: "inline-flex",
                                              alignItems: "center",
                                              gap: "0.25rem",
                                              fontSize: "0.62rem",
                                              fontWeight: 600,
                                            }}
                                            className="cc-btn-press hover:bg-white/30"
                                          >
                                            <Pencil size={10} />
                                            <span>Configurar</span>
                                          </button>
                                        </div>
                                        <div>
                                          <div style={{ fontSize: "0.66rem", textTransform: "uppercase", opacity: 0.85, letterSpacing: "0.05em" }}>
                                            {isDebit ? "Gasto este mes" : "Deuda actual"}
                                          </div>
                                          <div style={{ fontSize: "1.15rem", fontWeight: 800 }}>
                                            {isDebit ? fmtMoney(currentMonthSpent, curr) : fmtMoney(currentDebt, curr)}
                                          </div>
                                        </div>
                                      </div>
                                    </div>

                                    {/* Límite disponible si está configurado */}
                                    {!isDebit && card.creditLimit && (
                                      <div style={{ marginTop: "0.6rem" }}>
                                        <div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.65rem", opacity: 0.9, marginBottom: "0.2rem" }}>
                                          <span>Límite: {fmtMoney(card.creditLimit, curr)}</span>
                                          <span>Disp: {fmtMoney(available, curr)}</span>
                                        </div>
                                        <div style={{ height: "5px", background: "rgba(255,255,255,0.25)", borderRadius: "999px", overflow: "hidden" }}>
                                          <div
                                            style={{
                                              height: "100%",
                                              width: `${Math.min(100, Math.max(0, (currentDebt / card.creditLimit) * 100))}%`,
                                              background: currentDebt > card.creditLimit * 0.85 ? "#F87171" : "#FDE047",
                                              borderRadius: "999px",
                                            }}
                                          />
                                        </div>
                                      </div>
                                    )}

                                    {/* Bloque de Cashback acumulado en la tarjeta */}
                                    {(hasCashback || totalCashbackAccumulated > 0) ? (
                                      <div
                                        style={{
                                          marginTop: "0.65rem",
                                          background: "rgba(0, 0, 0, 0.22)",
                                          backdropFilter: "blur(4px)",
                                          borderRadius: "0.7rem",
                                          padding: "0.5rem 0.75rem",
                                          border: "1px solid rgba(255, 255, 255, 0.22)",
                                          display: "flex",
                                          alignItems: "center",
                                          justifyContent: "space-between",
                                          gap: "0.4rem",
                                        }}
                                      >
                                        <button
                                          type="button"
                                          onClick={() => setQuickCashbackCard({ ...card, hasCashback: card.hasCashback ?? (Number(card.cashbackPercent) > 0), cashbackPercent: card.cashbackPercent || "1.5", excludeServices: card.excludeServices !== false })}
                                          style={{ background: "none", border: "none", color: "inherit", padding: 0, cursor: "pointer", display: "flex", alignItems: "center", gap: "0.45rem", textAlign: "left" }}
                                          title="Toca para cambiar las reglas de cashback de esta tarjeta"
                                        >
                                          <span style={{ fontSize: "1.1rem" }}>🎁</span>
                                          <div>
                                            <div style={{ fontSize: "0.62rem", textTransform: "uppercase", opacity: 0.85, fontWeight: 700, letterSpacing: "0.03em", display: "flex", alignItems: "center", gap: "0.25rem" }}>
                                              <span>Cashback ({card.cashbackPercent || 1.5}% · {card.excludeServices === false ? "En todo" : "Menos servicios"})</span>
                                              <Pencil size={9} style={{ opacity: 0.8 }} />
                                            </div>
                                            <div style={{ fontSize: "0.86rem", fontWeight: 800, display: "flex", alignItems: "center", gap: "0.3rem" }}>
                                              <span>{fmtMoney(availableCashback, curr)} disponible</span>
                                              {currentMonthCashback > 0 && (
                                                <span style={{ fontSize: "0.62rem", fontWeight: 600, opacity: 0.85 }}>
                                                  (+{fmtMoney(currentMonthCashback, curr)} este mes)
                                                </span>
                                              )}
                                            </div>
                                          </div>
                                        </button>

                                        <div style={{ display: "flex", alignItems: "center", gap: "0.3rem" }}>
                                          <button
                                            type="button"
                                            onClick={() => setQuickCashbackCard({ ...card, hasCashback: card.hasCashback ?? (Number(card.cashbackPercent) > 0), cashbackPercent: card.cashbackPercent || "1.5", excludeServices: card.excludeServices !== false })}
                                            style={{
                                              background: "rgba(255,255,255,0.18)",
                                              border: "1px solid rgba(255,255,255,0.3)",
                                              borderRadius: "0.45rem",
                                              padding: "0.2rem 0.45rem",
                                              fontSize: "0.65rem",
                                              fontWeight: 700,
                                              color: "#FFFFFF",
                                              cursor: "pointer",
                                              whiteSpace: "nowrap",
                                            }}
                                            className="cc-btn-press hover:bg-white/30"
                                          >
                                            Cambiar %
                                          </button>
                                          {availableCashback > 0 && (
                                            <button
                                              type="button"
                                              onClick={() => setPendingCashbackRedeem({ card, availableCashback, currency: curr })}
                                              style={{
                                                background: "#FFFFFF",
                                                color: "#065F46",
                                                border: "none",
                                                borderRadius: "0.5rem",
                                                padding: "0.3rem 0.65rem",
                                                fontSize: "0.72rem",
                                                fontWeight: 800,
                                                cursor: "pointer",
                                                boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
                                                whiteSpace: "nowrap",
                                              }}
                                              className="cc-btn-press hover:opacity-95"
                                            >
                                              Canjear 🎁
                                            </button>
                                          )}
                                        </div>
                                      </div>
                                    ) : (
                                      /* Si la tarjeta NO tiene cashback, mostrar botón directo para activarlo */
                                      <div style={{ marginTop: "0.55rem", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                                        <span style={{ fontSize: "0.65rem", opacity: 0.8 }}>Sin cashback</span>
                                        <button
                                          type="button"
                                          onClick={() => setQuickCashbackCard({ ...card, hasCashback: true, cashbackPercent: "1.5", excludeServices: true })}
                                          style={{
                                            background: "rgba(255,255,255,0.12)",
                                            border: "1px dashed rgba(255,255,255,0.35)",
                                            borderRadius: "0.55rem",
                                            padding: "0.22rem 0.6rem",
                                            fontSize: "0.65rem",
                                            fontWeight: 600,
                                            color: "#FFFFFF",
                                            cursor: "pointer",
                                            display: "inline-flex",
                                            alignItems: "center",
                                            gap: "0.3rem",
                                            opacity: 0.9,
                                          }}
                                          className="cc-btn-press hover:opacity-100"
                                        >
                                          <span>🎁 Activar Cashback / Cambiar reglas</span>
                                        </button>
                                      </div>
                                    )}

                                    {/* Fechas de corte y pago + Botón Abonar */}
                                    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: "0.75rem", paddingTop: "0.6rem", borderTop: "1px solid rgba(255,255,255,0.2)", gap: "0.45rem" }}>
                                      <div style={{ display: "flex", gap: "0.55rem", fontSize: "0.68rem", opacity: 0.95, flexWrap: "wrap", alignItems: "center" }}>
                                        {isDebit || !cycleInfo ? (
                                          <span>{isDebit ? "🏧 Débito directo · Sin ciclo de corte" : "Sin ciclo configurado"}</span>
                                        ) : (<>
                                        {/* Corte con su propio avance de mes */}
                                        <div style={{ display: "inline-flex", alignItems: "center", gap: "0.25rem" }}>
                                          <span>✂️ Corte: <strong>{cycleInfo.cutoffLabel}</strong> ({cycleInfo.cutoffRelative})</span>
                                          <button
                                            type="button"
                                            title="Avanzar mes de corte (+1 mes)"
                                            onClick={async (ev) => {
                                              ev.stopPropagation();
                                              const currentKey = cycleInfo.currentCutoffMonthKey;
                                              if (!currentKey) return;
                                              const [y, m] = currentKey.split("-").map(Number);
                                              let nextM = m + 1; let nextY = y;
                                              if (nextM > 12) { nextM = 1; nextY++; }
                                              const nextKey = `${nextY}-${String(nextM).padStart(2, "0")}`;
                                              const updatedCards = (settings.cards || []).map((c) => (c.id === card.id ? { ...c, targetCutoffMonth: nextKey } : c));
                                              const ns = { ...settings, cards: updatedCards };
                                              try { await storage.set("settings", JSON.stringify(ns), true); setSettings(ns); } catch (err) {}
                                            }}
                                            style={{
                                              background: "rgba(255,255,255,0.18)",
                                              color: "#FFFFFF",
                                              border: "1px solid rgba(255,255,255,0.35)",
                                              borderRadius: "0.4rem",
                                              padding: "0.1rem 0.35rem",
                                              fontSize: "0.6rem",
                                              fontWeight: 700,
                                              cursor: "pointer",
                                            }}
                                            className="cc-btn-press hover:bg-white/30"
                                          >
                                            +1m
                                          </button>
                                          {card.targetCutoffMonth && card.targetCutoffMonth !== "auto" && (
                                            <button
                                              type="button"
                                              title="Corte automático según fecha actual"
                                              onClick={async (ev) => {
                                                ev.stopPropagation();
                                                const updatedCards = (settings.cards || []).map((c) => (c.id === card.id ? { ...c, targetCutoffMonth: undefined } : c));
                                                const ns = { ...settings, cards: updatedCards };
                                                try { await storage.set("settings", JSON.stringify(ns), true); setSettings(ns); } catch (err) {}
                                              }}
                                              style={{
                                                background: "rgba(255,255,255,0.12)",
                                                color: "#FFFFFF",
                                                border: "1px solid rgba(255,255,255,0.25)",
                                                borderRadius: "0.4rem",
                                                padding: "0.1rem 0.3rem",
                                                fontSize: "0.58rem",
                                                fontWeight: 600,
                                                cursor: "pointer",
                                                opacity: 0.85,
                                              }}
                                              className="cc-btn-press hover:opacity-100"
                                            >
                                              Auto
                                            </button>
                                          )}
                                        </div>

                                        {/* Pago con su propio avance de mes */}
                                        <div style={{ display: "inline-flex", alignItems: "center", gap: "0.25rem" }}>
                                          <span>⏰ Pago: <strong>{cycleInfo.dueLabel}</strong> ({cycleInfo.dueRelative})</span>
                                          <button
                                            type="button"
                                            title="Avanzar mes de pago (+1 mes)"
                                            onClick={async (ev) => {
                                              ev.stopPropagation();
                                              const currentKey = cycleInfo.currentDueMonthKey;
                                              if (!currentKey) return;
                                              const [y, m] = currentKey.split("-").map(Number);
                                              let nextM = m + 1; let nextY = y;
                                              if (nextM > 12) { nextM = 1; nextY++; }
                                              const nextKey = `${nextY}-${String(nextM).padStart(2, "0")}`;
                                              const updatedCards = (settings.cards || []).map((c) => (c.id === card.id ? { ...c, targetDueMonth: nextKey } : c));
                                              const ns = { ...settings, cards: updatedCards };
                                              try { await storage.set("settings", JSON.stringify(ns), true); setSettings(ns); } catch (err) {}
                                            }}
                                            style={{
                                              background: "rgba(255,255,255,0.18)",
                                              color: "#FFFFFF",
                                              border: "1px solid rgba(255,255,255,0.35)",
                                              borderRadius: "0.4rem",
                                              padding: "0.1rem 0.35rem",
                                              fontSize: "0.6rem",
                                              fontWeight: 700,
                                              cursor: "pointer",
                                            }}
                                            className="cc-btn-press hover:bg-white/30"
                                          >
                                            +1m
                                          </button>
                                          {(card.targetDueMonth || card.targetMonth) && (card.targetDueMonth || card.targetMonth) !== "auto" && (
                                            <button
                                              type="button"
                                              title="Pago automático según fecha actual"
                                              onClick={async (ev) => {
                                                ev.stopPropagation();
                                                const updatedCards = (settings.cards || []).map((c) => (c.id === card.id ? { ...c, targetDueMonth: undefined, targetMonth: undefined } : c));
                                                const ns = { ...settings, cards: updatedCards };
                                                try { await storage.set("settings", JSON.stringify(ns), true); setSettings(ns); } catch (err) {}
                                              }}
                                              style={{
                                                background: "rgba(255,255,255,0.12)",
                                                color: "#FFFFFF",
                                                border: "1px solid rgba(255,255,255,0.25)",
                                                borderRadius: "0.4rem",
                                                padding: "0.1rem 0.3rem",
                                                fontSize: "0.58rem",
                                                fontWeight: 600,
                                                cursor: "pointer",
                                                opacity: 0.85,
                                              }}
                                              className="cc-btn-press hover:opacity-100"
                                            >
                                              Auto
                                            </button>
                                          )}
                                        </div>
                                        </>)}
                                      </div>

                                      <button
                                        type="button"
                                        onClick={() => {
                                          setEditingTx(null);
                                          setFormType("pago_tarjeta");
                                          setFormCardId(card.id);
                                          setPerson(card.person || myName || (people[0] || ""));
                                          setAmount(currentDebt > 0 ? String(currentDebt).replace(".", ",") : "");
                                          setDesc(`Abono a ${card.name}`);
                                          pickCurrency(curr);
                                          setFormPaymentMethod("efectivo");
                                          setPhoto(null);
                                          setShowForm(true);
                                        }}
                                        style={{
                                          background: "rgba(255,255,255,0.22)",
                                          color: "#FFFFFF",
                                          border: "1px solid rgba(255,255,255,0.45)",
                                          borderRadius: "0.6rem",
                                          padding: "0.32rem 0.7rem",
                                          fontSize: "0.72rem",
                                          fontWeight: 700,
                                          cursor: "pointer",
                                          whiteSpace: "nowrap",
                                        }}
                                        className="cc-btn-press hover:bg-white/30 transition-colors"
                                      >
                                        {isDebit ? "Registrar gasto" : "Abonar / Pagar"}
                                      </button>
                                    </div>
                                  </div>
                                );
                              })}
                            </div>
                          </div>
                        ))}
                      </div>
                    )}
                  </div>
                );
              })()
            )}

            {/* Modal de Canje de Cashback */}
            {pendingCashbackRedeem && (
              <div
                onClick={() => setPendingCashbackRedeem(null)}
                className="cc-fade-in"
                style={{
                  position: "fixed",
                  inset: 0,
                  background: "rgba(15, 23, 42, 0.72)",
                  backdropFilter: "blur(5px)",
                  WebkitBackdropFilter: "blur(5px)",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  zIndex: 85,
                  padding: "1rem",
                }}
              >
                <div
                  onClick={(e) => e.stopPropagation()}
                  className="cc-panel-in"
                  style={{
                    background: C.paper,
                    borderRadius: "1.4rem",
                    padding: "1.5rem",
                    border: `1.5px solid ${darkMode ? "#059669" : "#10B981"}`,
                    boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
                    maxWidth: "440px",
                    width: "100%",
                    display: "flex",
                    flexDirection: "column",
                    gap: "1rem",
                  }}
                >
                  <PanelHeader
                    icon={<Gift size={16} />}
                    title="Canjear Cashback"
                    subtitle={`Recompensa acumulada en ${pendingCashbackRedeem.card.name}`}
                    onAction={() => setPendingCashbackRedeem(null)}
                    actionLabel="Cerrar"
                    actionTitle="Cerrar"
                    actionIcon={<X size={14} />}
                  />

                  <div
                    style={{
                      background: darkMode ? "rgba(16, 185, 129, 0.12)" : "#ECFDF5",
                      border: `1px solid ${darkMode ? "rgba(16, 185, 129, 0.3)" : "#A7F3D0"}`,
                      borderRadius: "1rem",
                      padding: "1rem",
                      display: "flex",
                      flexDirection: "column",
                      gap: "0.5rem",
                    }}
                  >
                    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                      <span style={{ fontSize: "0.75rem", color: C.inkSoft }}>Cashback acumulado:</span>
                      <strong style={{ fontSize: "1.25rem", fontWeight: 800, color: darkMode ? "#6EE7B7" : "#047857" }}>
                        {fmtMoney(pendingCashbackRedeem.availableCashback, pendingCashbackRedeem.currency)}
                      </strong>
                    </div>
                    <div style={{ fontSize: "0.72rem", color: C.inkSoft, borderTop: `1px solid ${darkMode ? "rgba(16,185,129,0.2)" : "#A7F3D0"}`, paddingTop: "0.4rem" }}>
                      Elige cómo deseas canjear o disponer de tu cashback acumulado:
                    </div>
                  </div>

                  <div style={{ display: "flex", flexDirection: "column", gap: "0.65rem" }}>
                    <button
                      type="button"
                      disabled={saving}
                      onClick={() => redeemCashback(pendingCashbackRedeem.card, pendingCashbackRedeem.availableCashback, "tarjeta")}
                      style={{
                        background: BRAND_GRADIENT,
                        color: "#FFFFFF",
                        border: "none",
                        borderRadius: "0.85rem",
                        padding: "0.85rem 1rem",
                        fontSize: "0.82rem",
                        fontWeight: 700,
                        cursor: "pointer",
                        display: "flex",
                        flexDirection: "column",
                        alignItems: "flex-start",
                        gap: "0.2rem",
                        boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                        textAlign: "left",
                      }}
                      className="cc-btn-press hover:opacity-95"
                    >
                      <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", fontWeight: 800 }}>
                        <span>💳 Descontar de la deuda de la tarjeta</span>
                      </div>
                      <div style={{ fontSize: "0.7rem", opacity: 0.9, fontWeight: 400 }}>
                        Abona {fmtMoney(pendingCashbackRedeem.availableCashback, pendingCashbackRedeem.currency)} directamente a la tarjeta {pendingCashbackRedeem.card.name} para reducir lo que debes sin sacar dinero de tu bolsillo.
                      </div>
                    </button>

                    <button
                      type="button"
                      disabled={saving}
                      onClick={() => redeemCashback(pendingCashbackRedeem.card, pendingCashbackRedeem.availableCashback, "ingreso")}
                      style={{
                        background: C.field,
                        color: C.ink,
                        border: `1px solid ${C.line}`,
                        borderRadius: "0.85rem",
                        padding: "0.85rem 1rem",
                        fontSize: "0.82rem",
                        fontWeight: 700,
                        cursor: "pointer",
                        display: "flex",
                        flexDirection: "column",
                        alignItems: "flex-start",
                        gap: "0.2rem",
                        textAlign: "left",
                      }}
                      className="cc-btn-press hover:opacity-90"
                    >
                      <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", fontWeight: 800 }}>
                        <span>💵 Cobrar como ingreso en efectivo / cuenta</span>
                      </div>
                      <div style={{ fontSize: "0.7rem", color: C.inkSoft, fontWeight: 400 }}>
                        Registra un ingreso de {fmtMoney(pendingCashbackRedeem.availableCashback, pendingCashbackRedeem.currency)} en el saldo general disponible del grupo.
                      </div>
                    </button>

                    <button
                      type="button"
                      onClick={() => setPendingCashbackRedeem(null)}
                      style={{
                        background: "none",
                        border: "none",
                        padding: "0.4rem",
                        fontSize: "0.74rem",
                        color: C.inkSoft,
                        cursor: "pointer",
                        marginTop: "0.2rem",
                      }}
                    >
                      Cancelar
                    </button>
                  </div>
                </div>
              </div>
            )}

            {/* Modal de Configuración Rápida de Cashback */}
            {quickCashbackCard && (
              <div
                onClick={() => setQuickCashbackCard(null)}
                className="cc-fade-in"
                style={{
                  position: "fixed",
                  inset: 0,
                  background: "rgba(15, 23, 42, 0.72)",
                  backdropFilter: "blur(5px)",
                  WebkitBackdropFilter: "blur(5px)",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  zIndex: 85,
                  padding: "1rem",
                }}
              >
                <div
                  onClick={(e) => e.stopPropagation()}
                  className="cc-panel-in"
                  style={{
                    background: C.paper,
                    borderRadius: "1.4rem",
                    padding: "1.5rem",
                    border: `1.5px solid ${darkMode ? "#059669" : "#10B981"}`,
                    boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.4)",
                    maxWidth: "440px",
                    width: "100%",
                    display: "flex",
                    flexDirection: "column",
                    gap: "1rem",
                  }}
                >
                  <PanelHeader
                    icon={<Gift size={16} />}
                    title={`Cashback · ${quickCashbackCard.name}`}
                    subtitle="Activa, desactiva o cambia las reglas de cashback de esta tarjeta"
                    onAction={() => setQuickCashbackCard(null)}
                    actionLabel="Cerrar"
                    actionTitle="Cerrar"
                    actionIcon={<X size={14} />}
                  />

                  {/* Selector de Estado: ¿Tiene o no tiene Cashback? */}
                  <div>
                    <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.35rem" }}>
                      Estado del Cashback
                    </label>
                    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem" }}>
                      <button
                        type="button"
                        onClick={() => setQuickCashbackCard({ ...quickCashbackCard, hasCashback: false })}
                        style={{
                          padding: "0.6rem 0.75rem",
                          borderRadius: "0.75rem",
                          border: !quickCashbackCard.hasCashback ? `2px solid ${C.red}` : `1px solid ${C.line}`,
                          background: !quickCashbackCard.hasCashback ? (darkMode ? "rgba(220,38,38,0.15)" : "#FEE2E2") : C.field,
                          color: !quickCashbackCard.hasCashback ? (darkMode ? "#FCA5A5" : "#991B1B") : C.inkSoft,
                          fontWeight: !quickCashbackCard.hasCashback ? 700 : 500,
                          fontSize: "0.78rem",
                          cursor: "pointer",
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          gap: "0.35rem",
                        }}
                        className="cc-btn-press"
                      >
                        <span>❌ Sin Cashback</span>
                      </button>
                      <button
                        type="button"
                        onClick={() => setQuickCashbackCard({ ...quickCashbackCard, hasCashback: true })}
                        style={{
                          padding: "0.6rem 0.75rem",
                          borderRadius: "0.75rem",
                          border: quickCashbackCard.hasCashback ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                          background: quickCashbackCard.hasCashback ? (darkMode ? "rgba(16,185,129,0.15)" : "#D1FAE5") : C.field,
                          color: quickCashbackCard.hasCashback ? (darkMode ? "#6EE7B7" : "#065F46") : C.inkSoft,
                          fontWeight: quickCashbackCard.hasCashback ? 700 : 500,
                          fontSize: "0.78rem",
                          cursor: "pointer",
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          gap: "0.35rem",
                        }}
                        className="cc-btn-press"
                      >
                        <span>🎁 Con Cashback</span>
                      </button>
                    </div>
                  </div>

                  {quickCashbackCard.hasCashback && (
                    <>
                      {/* Porcentaje */}
                      <div>
                        <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.35rem" }}>
                          Porcentaje de Cashback (%)
                        </label>
                        <div style={{ display: "flex", gap: "0.35rem", flexWrap: "wrap", marginBottom: "0.45rem" }}>
                          {["1", "1.5", "2", "3", "5"].map((pct) => {
                            const isSel = String(quickCashbackCard.cashbackPercent) === pct;
                            return (
                              <button
                                type="button"
                                key={pct}
                                onClick={() => setQuickCashbackCard({ ...quickCashbackCard, cashbackPercent: pct })}
                                style={{
                                  background: isSel ? C.ink : C.field,
                                  color: isSel ? C.paper : C.ink,
                                  border: `1px solid ${isSel ? "transparent" : C.line}`,
                                  borderRadius: "0.55rem",
                                  padding: "0.35rem 0.65rem",
                                  fontSize: "0.78rem",
                                  fontWeight: isSel ? 700 : 500,
                                  cursor: "pointer",
                                }}
                                className="cc-btn-press"
                              >
                                {pct}%
                              </button>
                            );
                          })}
                        </div>
                        <div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
                          <input
                            type="number"
                            step="0.1"
                            min="0"
                            max="100"
                            value={quickCashbackCard.cashbackPercent != null ? quickCashbackCard.cashbackPercent : ""}
                            onChange={(e) => setQuickCashbackCard({ ...quickCashbackCard, cashbackPercent: e.target.value })}
                            placeholder="Otro %"
                            style={{
                              background: C.field,
                              border: `1px solid ${C.line}`,
                              borderRadius: "0.55rem",
                              padding: "0.45rem 0.65rem",
                              fontSize: "0.82rem",
                              color: C.ink,
                              width: "5.5rem",
                              outline: "none",
                              fontWeight: 700,
                            }}
                          />
                          <span style={{ fontSize: "0.75rem", color: C.inkSoft }}>% en compras con esta tarjeta</span>
                        </div>
                      </div>

                      {/* Reglas de exclusión */}
                      <div>
                        <label style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, display: "block", marginBottom: "0.35rem" }}>
                          Regla de Cobertura
                        </label>
                        <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.5rem" }}>
                          <button
                            type="button"
                            onClick={() => setQuickCashbackCard({ ...quickCashbackCard, excludeServices: true })}
                            style={{
                              fontFamily: "'Inter', sans-serif",
                              padding: "0.6rem 0.65rem",
                              borderRadius: "0.75rem",
                              border: quickCashbackCard.excludeServices !== false ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                              background: quickCashbackCard.excludeServices !== false ? (darkMode ? "rgba(16, 185, 129, 0.15)" : "#ECFDF5") : C.field,
                              color: quickCashbackCard.excludeServices !== false ? C.ink : C.inkSoft,
                              fontSize: "0.75rem",
                              fontWeight: quickCashbackCard.excludeServices !== false ? 700 : 500,
                              cursor: "pointer",
                              textAlign: "left",
                              display: "flex",
                              flexDirection: "column",
                              gap: "0.2rem",
                            }}
                            className="cc-btn-press"
                          >
                            <span style={{ fontWeight: 700, color: quickCashbackCard.excludeServices !== false ? C.green : C.ink }}>
                              🛡️ Menos servicios
                            </span>
                            <span style={{ fontSize: "0.65rem", color: C.inkSoft, lineHeight: 1.3 }}>
                              Excluye luz, agua, internet, gas y tributos.
                            </span>
                          </button>
                          <button
                            type="button"
                            onClick={() => setQuickCashbackCard({ ...quickCashbackCard, excludeServices: false })}
                            style={{
                              fontFamily: "'Inter', sans-serif",
                              padding: "0.6rem 0.65rem",
                              borderRadius: "0.75rem",
                              border: quickCashbackCard.excludeServices === false ? `2px solid ${C.green}` : `1px solid ${C.line}`,
                              background: quickCashbackCard.excludeServices === false ? (darkMode ? "rgba(16, 185, 129, 0.15)" : "#ECFDF5") : C.field,
                              color: quickCashbackCard.excludeServices === false ? C.ink : C.inkSoft,
                              fontSize: "0.75rem",
                              fontWeight: quickCashbackCard.excludeServices === false ? 700 : 500,
                              cursor: "pointer",
                              textAlign: "left",
                              display: "flex",
                              flexDirection: "column",
                              gap: "0.2rem",
                            }}
                            className="cc-btn-press"
                          >
                            <span style={{ fontWeight: 700, color: quickCashbackCard.excludeServices === false ? C.green : C.ink }}>
                              🌟 En TODO (100%)
                            </span>
                            <span style={{ fontSize: "0.65rem", color: C.inkSoft, lineHeight: 1.3 }}>
                              Acumula en todo sin ninguna excepción.
                            </span>
                          </button>
                        </div>
                      </div>
                    </>
                  )}

                  {/* Botones de acción */}
                  <div style={{ display: "flex", gap: "0.5rem", marginTop: "0.3rem" }}>
                    <button
                      type="button"
                      onClick={() => setQuickCashbackCard(null)}
                      style={{
                        flex: 1,
                        background: "none",
                        border: `1px solid ${C.line}`,
                        borderRadius: "0.75rem",
                        padding: "0.65rem 0",
                        fontSize: "0.78rem",
                        color: C.inkSoft,
                        cursor: "pointer",
                      }}
                      className="cc-btn-press"
                    >
                      Cancelar
                    </button>
                    <button
                      type="button"
                      onClick={() => saveQuickCashback(quickCashbackCard.id, quickCashbackCard.hasCashback, quickCashbackCard.cashbackPercent, quickCashbackCard.excludeServices)}
                      style={{
                        flex: 1.5,
                        background: BRAND_GRADIENT,
                        color: "#FFFFFF",
                        border: "none",
                        borderRadius: "0.75rem",
                        padding: "0.65rem 0",
                        fontSize: "0.78rem",
                        fontWeight: 700,
                        cursor: "pointer",
                        boxShadow: "0 4px 12px rgba(16,185,129,0.3)",
                      }}
                      className="cc-btn-press hover:opacity-90"
                    >
                      Guardar regla
                    </button>
                  </div>
                </div>
              </div>
            )}

            {/* Cuotas y préstamos: mismo módulo que las tarjetas (un solo bloque,
                separadas por un divisor; se pliegan junto con la sección). */}
            {!cardsCollapsed && (settings.installments || []).length > 0 && (
            <div aria-label="Cuotas y Préstamos" style={{ marginTop: "1.25rem", paddingTop: "1.25rem", borderTop: `1px solid ${C.line}` }}>
              {/* Cabecera de Cuotas */}
              <div
                onClick={() => {
                  setInstallmentsCollapsed((prev) => {
                    const next = !prev;
                    try { localStorage.setItem("cc:collapse:installments", next ? "1" : "0"); } catch (e) {}
                    return next;
                  });
                }}
                role="button"
                tabIndex={0}
                aria-expanded={!installmentsCollapsed}
                aria-label={installmentsCollapsed ? "Desplegar cuotas y préstamos" : "Minimizar cuotas y préstamos"}
                style={{
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "space-between",
                  cursor: "pointer",
                  userSelect: "none",
                  marginBottom: installmentsCollapsed ? 0 : "1rem",
                  gap: "0.5rem",
                }}
              >
                <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                  <Calendar size={15} color={C.green} />
                  <span
                    style={{
                      fontSize: "0.85rem",
                      color: C.ink,
                      fontWeight: 700,
                      letterSpacing: "0.03em",
                      textTransform: "uppercase",
                    }}
                  >
                    Cuotas y Préstamos ({installmentStats.activeList.length})
                  </span>
                </div>

                <div style={{ display: "flex", alignItems: "center", gap: "0.45rem", flexWrap: "wrap", justifyContent: "flex-end" }}>
                  {installmentStats.totalMonthlyAll > 0 && (
                    <span style={{ fontSize: "0.78rem", fontWeight: 700, color: "#D97706" }}>
                      Mensual: {fmtMoney(installmentStats.totalMonthlyAll, settings.currency)}
                    </span>
                  )}
                  {installmentStats.totalRemainingDebtAll > 0 && (
                    <span
                      style={{
                        background: darkMode ? "#451A03" : "#FEF3C7",
                        color: darkMode ? "#FDE68A" : "#92400E",
                        border: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`,
                        fontSize: "0.72rem",
                        fontWeight: 700,
                        padding: "0.15rem 0.55rem",
                        borderRadius: "999px",
                        letterSpacing: "0.02em",
                      }}
                    >
                      Deuda: {fmtMoney(installmentStats.totalRemainingDebtAll, settings.currency)}
                    </span>
                  )}
                  <div
                    style={{
                      background: C.field,
                      border: `1px solid ${C.line}`,
                      borderRadius: "0.6rem",
                      color: C.inkSoft,
                      width: "1.75rem",
                      height: "1.75rem",
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      flexShrink: 0,
                    }}
                    title={installmentsCollapsed ? "Desplegar sección" : "Minimizar sección"}
                  >
                    {installmentsCollapsed ? <ChevronDown size={15} /> : <ChevronUp size={15} />}
                  </div>
                </div>
              </div>

              {!installmentsCollapsed && (
                <div>
                  {/* Selector / Filtro por persona si hay varios integrantes */}
                  {installmentStats.byPerson.filter((g) => g.items.length > 0).length > 1 && (
                    <div style={{ display: "flex", gap: "0.35rem", flexWrap: "wrap", marginBottom: "1rem" }}>
                      <button
                        type="button"
                        onClick={() => setSelectedInstallmentPerson("todas")}
                        style={{
                          background: selectedInstallmentPerson === "todas" ? C.ink : C.field,
                          color: selectedInstallmentPerson === "todas" ? C.paper : C.inkSoft,
                          border: `1px solid ${selectedInstallmentPerson === "todas" ? "transparent" : C.line}`,
                          borderRadius: "999px",
                          padding: "0.3rem 0.75rem",
                          fontSize: "0.75rem",
                          fontWeight: selectedInstallmentPerson === "todas" ? 700 : 500,
                          cursor: "pointer",
                        }}
                        className="cc-btn-press"
                      >
                        Todas ({installmentStats.list.length})
                      </button>
                      {installmentStats.byPerson.filter((g) => g.items.length > 0).map((g) => {
                        const isSel = selectedInstallmentPerson === g.person;
                        const isTu = g.person === myName;
                        return (
                          <button
                            type="button"
                            key={g.person}
                            onClick={() => setSelectedInstallmentPerson(g.person)}
                            style={{
                              background: isSel ? C.ink : C.field,
                              color: isSel ? C.paper : C.inkSoft,
                              border: `1px solid ${isSel ? "transparent" : C.line}`,
                              borderRadius: "999px",
                              padding: "0.3rem 0.75rem",
                              fontSize: "0.75rem",
                              fontWeight: isSel ? 700 : 500,
                              cursor: "pointer",
                              display: "flex",
                              alignItems: "center",
                              gap: "0.35rem",
                            }}
                            className="cc-btn-press"
                          >
                            <span style={{ width: "0.5rem", height: "0.5rem", borderRadius: "50%", background: personColor(g.person) }} />
                            <span>{isTu ? `⭐ ${g.person}` : g.person} ({g.items.length})</span>
                          </button>
                        );
                      })}
                    </div>
                  )}

                  {/* Renderizado de cuotas */}
                  <div style={{ display: "flex", flexDirection: "column", gap: "1.25rem" }}>
                    {installmentStats.byPerson
                      .filter((g) => selectedInstallmentPerson === "todas" ? g.items.length > 0 : g.person === selectedInstallmentPerson)
                      .map((group) => (
                        <div key={group.person} style={{ display: "flex", flexDirection: "column", gap: "0.55rem" }}>
                          {/* Cabecera del individuo */}
                          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", paddingBottom: "0.25rem", borderBottom: `1px solid ${C.lineSoft}` }}>
                            <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                              <span
                                style={{
                                  width: "0.55rem",
                                  height: "0.55rem",
                                  borderRadius: "50%",
                                  background: group.person.trim() ? personColor(group.person.trim()) : C.line,
                                  display: "inline-block",
                                }}
                              />
                              <span style={{ fontSize: "0.82rem", fontWeight: 700, color: C.ink }}>
                                {group.person}
                              </span>
                              <span style={{ fontSize: "0.7rem", color: C.inkSoft }}>
                                ({group.items.length} {group.items.length === 1 ? "compromiso" : "compromisos"})
                              </span>
                            </div>
                            {group.totalMonthly > 0 && (
                              <span style={{ fontSize: "0.75rem", fontWeight: 700, color: C.ink }}>
                                Mensual: {fmtMoney(group.totalMonthly, settings.currency)}
                              </span>
                            )}
                          </div>

                          {/* Cuadrícula de tarjetas de cuotas */}
                          <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "repeat(2, 1fr)" : "1fr", gap: "0.65rem" }}>
                            {group.items.map(({ inst, currency: instCurr, cycleInfo, total, paid, totalDebt, paidAmount, remainingInstallments, progressPercent, remainingAmount, isCompleted }) => {
                              const pal = CARD_COLORS.find((c) => c.id === inst.color) || CARD_COLORS[1]; // gold by default
                              const linkedCard = inst.cardId ? (settings.cards || []).find((c) => c.id === inst.cardId) : null;
                              return (
                                <div
                                  key={inst.id}
                                  style={{
                                    background: pal.gradient,
                                    border: `1px solid ${pal.border}`,
                                    borderRadius: "1rem",
                                    padding: "0.95rem 1.05rem",
                                    color: pal.text,
                                    boxShadow: "0 6px 16px -4px rgba(0,0,0,0.2)",
                                    opacity: isCompleted ? 0.75 : 1,
                                    position: "relative",
                                  }}
                                >
                                  <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "0.5rem" }}>
                                    <div>
                                      <div style={{ fontSize: "0.9rem", fontWeight: 700, letterSpacing: "0.02em", display: "flex", alignItems: "center", gap: "0.35rem" }}>
                                        <span>{inst.name}</span>
                                        {instCurr !== (settings.currency || "S/") && (
                                          <span
                                            style={{
                                              fontSize: "0.62rem",
                                              fontWeight: 800,
                                              background: "rgba(255,255,255,0.25)",
                                              padding: "0.1rem 0.4rem",
                                              borderRadius: "0.4rem",
                                            }}
                                          >
                                            {instCurr}
                                          </span>
                                        )}
                                      </div>
                                      <div style={{ fontSize: "0.7rem", opacity: 0.88, marginTop: "0.15rem" }}>
                                        Paga: <strong>{inst.person}</strong> {inst.recipient ? `➔ A: ${inst.recipient}` : ""}
                                      </div>
                                      {linkedCard && (
                                        <div style={{ fontSize: "0.68rem", opacity: 0.9, marginTop: "0.15rem" }}>
                                          💳 Cobrada en: <strong>{linkedCard.name}</strong>
                                        </div>
                                      )}
                                      {totalDebt > 0 && (
                                        <div style={{ fontSize: "0.68rem", opacity: 0.9, marginTop: "0.2rem" }}>
                                          Deuda total: <strong>{fmtMoney(totalDebt, instCurr)}</strong>
                                        </div>
                                      )}
                                    </div>

                                    <div style={{ textAlign: "right" }}>
                                      <div style={{ fontSize: "0.65rem", textTransform: "uppercase", opacity: 0.85, letterSpacing: "0.05em" }}>
                                        Cuota mensual
                                      </div>
                                      <div style={{ fontSize: "1.15rem", fontWeight: 800 }}>
                                        {fmtMoney(inst.amount, instCurr)}
                                      </div>
                                    </div>
                                  </div>

                                  {/* Progreso de cuotas si tiene total o deuda */}
                                  {(total > 0 || totalDebt > 0) && (
                                    <div style={{ marginTop: "0.65rem" }}>
                                      <div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.68rem", opacity: 0.92, marginBottom: "0.25rem", flexWrap: "wrap", gap: "0.3rem" }}>
                                        <span>
                                          {isCompleted
                                            ? "🎉 ¡Deuda cancelada!"
                                            : total > 0
                                            ? `Cuota ${paid} de ${total} (${progressPercent}%)`
                                            : `Pagado: ${fmtMoney(paidAmount, instCurr)} (${progressPercent}%)`}
                                        </span>
                                        {remainingAmount != null && !isCompleted && (
                                          <span>Resta: <strong>{fmtMoney(remainingAmount, instCurr)}</strong></span>
                                        )}
                                      </div>
                                      <div style={{ height: "5px", background: "rgba(255,255,255,0.25)", borderRadius: "999px", overflow: "hidden" }}>
                                        <div
                                          style={{
                                            height: "100%",
                                            width: `${progressPercent}%`,
                                            background: isCompleted ? "#4ADE80" : "#FDE047",
                                            borderRadius: "999px",
                                            transition: "width 0.3s ease",
                                          }}
                                        />
                                      </div>
                                    </div>
                                  )}

                                  {/* Fecha de vencimiento + Botón Pagar */}
                                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: "0.75rem", paddingTop: "0.6rem", borderTop: "1px solid rgba(255,255,255,0.2)", gap: "0.4rem" }}>
                                    <div style={{ fontSize: "0.7rem", opacity: 0.95, display: "flex", alignItems: "center", gap: "0.4rem", flexWrap: "wrap" }}>
                                      {cycleInfo ? (
                                        <span>
                                          ⏰ Vence: <strong>{cycleInfo.dueLabel}</strong> ({cycleInfo.daysToDue === 0 ? "¡Vence hoy!" : `en ${cycleInfo.daysToDue}d`})
                                        </span>
                                      ) : (
                                        <span>⏰ Sin fecha fijada</span>
                                      )}
                                      {!isCompleted && cycleInfo && (
                                        <button
                                          type="button"
                                          title="Avanzar mes de vencimiento (+1 mes)"
                                          onClick={async (ev) => {
                                            ev.stopPropagation();
                                            const currentKey = cycleInfo.currentDueMonthKey;
                                            const [y, m] = currentKey.split("-").map(Number);
                                            let nextM = m + 1;
                                            let nextY = y;
                                            if (nextM > 12) { nextM = 1; nextY++; }
                                            const nextKey = `${nextY}-${String(nextM).padStart(2, "0")}`;
                                            const updated = (settings.installments || []).map((i) => (i.id === inst.id ? { ...i, targetMonth: nextKey } : i));
                                            const ns = { ...settings, installments: updated };
                                            try {
                                              await storage.set("settings", JSON.stringify(ns), true);
                                              setSettings(ns);
                                            } catch (err) {}
                                          }}
                                          style={{
                                            background: "rgba(255,255,255,0.18)",
                                            color: "#FFFFFF",
                                            border: "1px solid rgba(255,255,255,0.35)",
                                            borderRadius: "0.4rem",
                                            padding: "0.12rem 0.4rem",
                                            fontSize: "0.62rem",
                                            fontWeight: 700,
                                            cursor: "pointer",
                                            display: "inline-flex",
                                            alignItems: "center",
                                            gap: "0.2rem",
                                          }}
                                          className="cc-btn-press hover:bg-white/30"
                                        >
                                          <span>⏭️ +1 mes</span>
                                        </button>
                                      )}
                                      {inst.targetMonth && inst.targetMonth !== "auto" && (
                                        <button
                                          type="button"
                                          title="Volver a cálculo automático de mes"
                                          onClick={async (ev) => {
                                            ev.stopPropagation();
                                            const updated = (settings.installments || []).map((i) => (i.id === inst.id ? { ...i, targetMonth: undefined } : i));
                                            const ns = { ...settings, installments: updated };
                                            try {
                                              await storage.set("settings", JSON.stringify(ns), true);
                                              setSettings(ns);
                                            } catch (err) {}
                                          }}
                                          style={{
                                            background: "rgba(255,255,255,0.12)",
                                            color: "#FFFFFF",
                                            border: "1px solid rgba(255,255,255,0.25)",
                                            borderRadius: "0.4rem",
                                            padding: "0.12rem 0.35rem",
                                            fontSize: "0.6rem",
                                            fontWeight: 600,
                                            cursor: "pointer",
                                            opacity: 0.85,
                                          }}
                                          className="cc-btn-press hover:opacity-100"
                                        >
                                          <span>⚡ Auto</span>
                                        </button>
                                      )}
                                    </div>

                                    {!isCompleted ? (
                                      <button
                                        type="button"
                                        onClick={() => {
                                          setEditingTx(null);
                                          setFormType("pago_cuota");
                                          setFormInstallmentId(inst.id);
                                          setPerson(inst.person || myName || (people[0] || ""));
                                          setAmount(inst.amount > 0 ? String(inst.amount).replace(".", ",") : "");
                                          const cuotaStr = total > 0 ? `Cuota ${paid + 1}/${total} - ${inst.name}` : `Cuota ${inst.name}`;
                                          setDesc(cuotaStr);
                                          pickCurrency(instCurr);
                                          // Si la cuota está vinculada a una tarjeta, el pago se
                                          // anota como consumo de ESA tarjeta (lo liquidas al abonarla).
                                          setFormPaymentMethod(linkedCard ? "tarjeta" : "efectivo");
                                          setFormCardId(linkedCard ? linkedCard.id : "");
                                          setInstallmentPayingId(inst.id);
                                          setPhoto(null);
                                          setShowForm(true);
                                        }}
                                        style={{
                                          background: "rgba(255,255,255,0.22)",
                                          color: "#FFFFFF",
                                          border: "1px solid rgba(255,255,255,0.45)",
                                          borderRadius: "0.6rem",
                                          padding: "0.32rem 0.75rem",
                                          fontSize: "0.72rem",
                                          fontWeight: 700,
                                          cursor: "pointer",
                                          whiteSpace: "nowrap",
                                        }}
                                        className="cc-btn-press hover:bg-white/30 transition-colors"
                                      >
                                        Pagar cuota
                                      </button>
                                    ) : (
                                      <span style={{ fontSize: "0.72rem", fontWeight: 700, background: "rgba(255,255,255,0.2)", padding: "0.2rem 0.5rem", borderRadius: "0.4rem" }}>
                                        Pagado ✓
                                      </span>
                                    )}
                                  </div>
                                </div>
                              );
                            })}
                          </div>
                        </div>
                      ))}
                  </div>
                </div>
              )}
            </div>
            )}
          </section>
          )}

          {/* Barra Inteligente de Entrada por Voz y Texto (Smart Input / Gemini Spark) */}
          <div
            className="cc-rise"
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.2rem",
              padding: "0.55rem 0.65rem 0.55rem 0.85rem",
              border: `1.5px solid ${isListening ? (darkMode ? "#10B981" : "#059669") : C.line}`,
              boxShadow: isListening
                ? "0 0 0 3px rgba(16,185,129,0.25), 0 8px 20px -6px rgba(16,185,129,0.3)"
                : "0 6px 16px -6px rgb(2 6 23 / 0.08)",
              marginBottom: "1rem",
              display: "flex",
              alignItems: "center",
              gap: "0.5rem",
              transition: "all 0.2s ease",
            }}
          >
            <div
              style={{
                width: "1.9rem",
                height: "1.9rem",
                borderRadius: "0.6rem",
                background: isListening
                  ? (darkMode ? "#064E3B" : "#ECFDF5")
                  : (darkMode ? "#1E293B" : "#F1F5F9"),
                color: isListening ? C.green : C.inkSoft,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                flexShrink: 0,
              }}
            >
              {isListening ? (
                <Sparkles size={14} color={C.green} />
              ) : (
                <Wand2 size={14} />
              )}
            </div>

            <input
              type="text"
              value={smartPromptText}
              onChange={(e) => setSmartPromptText(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") {
                  e.preventDefault();
                  processSmartExpense();
                }
              }}
              placeholder={
                isListening
                  ? "🎙️ Escuchando... habla ahora..."
                  : '✨ Dicta o escribe: "Ingreso 500 sueldo" o "Hans gastó 40 en Uber"'
              }
              style={{
                flex: 1,
                background: "transparent",
                border: "none",
                outline: "none",
                color: C.ink,
                fontSize: "0.82rem",
                fontWeight: 500,
                minWidth: 0,
              }}
            />

            {/* Botón de micrófono */}
            {voiceSupported && (
              <button
                type="button"
                onClick={toggleVoiceRecording}
                aria-label={isListening ? "Detener micrófono" : "Dictar por voz"}
                title={isListening ? "Detener dictado" : "Dictar gasto o ingreso con micrófono"}
                style={{
                  background: isListening
                    ? (darkMode ? "#EF4444" : "#DC2626")
                    : C.field,
                  color: isListening ? "#FFFFFF" : C.inkSoft,
                  border: `1px solid ${isListening ? "#DC2626" : C.line}`,
                  borderRadius: "0.65rem",
                  width: "2.1rem",
                  height: "2.1rem",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  cursor: "pointer",
                  flexShrink: 0,
                  boxShadow: isListening ? "0 0 10px rgba(220,38,38,0.5)" : "none",
                }}
                className="cc-btn-press"
              >
                {isListening ? <MicOff size={14} /> : <Mic size={14} />}
              </button>
            )}

            {/* Botón de procesar */}
            {smartPromptText.trim().length > 0 && (
              <button
                type="button"
                onClick={() => processSmartExpense()}
                disabled={smartProcessing}
                style={{
                  background: BRAND_GRADIENT,
                  color: "#FFFFFF",
                  border: "none",
                  borderRadius: "0.65rem",
                  padding: "0.35rem 0.65rem",
                  fontSize: "0.74rem",
                  fontWeight: 700,
                  display: "flex",
                  alignItems: "center",
                  gap: "0.25rem",
                  cursor: "pointer",
                  flexShrink: 0,
                }}
                className="cc-btn-press"
              >
                <span>{smartProcessing ? "..." : "Interpretar"}</span>
                <Sparkles size={12} />
              </button>
            )}
          </div>

          {/* Feedback de Smart Prompt */}
          {smartFeedback && (
            <div
              style={{
                fontSize: "0.76rem",
                fontWeight: 600,
                padding: "0.5rem 0.85rem",
                borderRadius: "0.75rem",
                marginBottom: "0.85rem",
                background: smartFeedback.type === "success"
                  ? (darkMode ? "#064E3B" : "#ECFDF5")
                  : (darkMode ? "#451A03" : "#FEF3C7"),
                color: smartFeedback.type === "success"
                  ? (darkMode ? "#6EE7B7" : "#047857")
                  : (darkMode ? "#FDE68A" : "#92400E"),
                border: `1px solid ${smartFeedback.type === "success" ? (darkMode ? "#059669" : "#A7F3D0") : (darkMode ? "#92400E" : "#FCD34D")}`,
              }}
              className="cc-pop"
            >
              {smartFeedback.text}
            </div>
          )}

          {/* Formulario y botones de acción principales */}
          {minimalMode ? (
            <div style={{ display: "flex", gap: "0.55rem", marginBottom: "1.25rem" }}>
              <button
                type="button"
                onClick={() => {
                  setEditingTx(null);
                  setFormType("gasto");
                  setFormPaymentMethod("efectivo");
                  setFormCardId("");
                  setFormCurrency(settings.currency || "S/");
                  setFormRate("");
                  setPhoto(null);
                  setShowForm(true);
                }}
                style={{
                  flex: 1,
                  background: darkMode ? "#7F1D1D" : "#EF4444",
                  color: "#FFFFFF",
                  border: "none",
                  borderRadius: "0.85rem",
                  padding: "0.85rem 0.5rem",
                  fontWeight: 700,
                  fontSize: "0.82rem",
                  cursor: "pointer",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  gap: "0.4rem",
                  boxShadow: "0 4px 14px rgba(239,68,68,0.25)",
                }}
                className="cc-btn-press hover:brightness-105"
              >
                <TrendingDown size={15} />
                <span>− Anotar Gasto</span>
              </button>

              <button
                type="button"
                onClick={() => {
                  setEditingTx(null);
                  setFormType("ingreso");
                  setFormPaymentMethod("efectivo");
                  setFormCardId("");
                  setFormCurrency(settings.currency || "S/");
                  setFormRate("");
                  setPhoto(null);
                  setShowForm(true);
                }}
                style={{
                  flex: 1,
                  background: BRAND_GRADIENT,
                  color: "#FFFFFF",
                  border: "none",
                  borderRadius: "0.85rem",
                  padding: "0.85rem 0.5rem",
                  fontWeight: 700,
                  fontSize: "0.82rem",
                  cursor: "pointer",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  gap: "0.4rem",
                  boxShadow: "0 4px 14px rgba(16,185,129,0.25)",
                }}
                className="cc-btn-press hover:brightness-105"
              >
                <TrendingUp size={15} />
                <span>+ Anotar Ingreso</span>
              </button>

              <button
                type="button"
                onClick={() => {
                  setEditingTx(null);
                  setFormType((settings.cards || []).length > 0 ? "pago_tarjeta" : "prestamo");
                  setFormPaymentMethod("efectivo");
                  setFormCurrency(settings.currency || "S/");
                  setFormRate("");
                  setPhoto(null);
                  setShowForm(true);
                }}
                title="Otras opciones: Tarjeta, Cuota o Préstamo"
                style={{
                  background: C.paper,
                  border: `1px solid ${C.line}`,
                  borderRadius: "0.85rem",
                  padding: "0.85rem 0.85rem",
                  color: C.ink,
                  cursor: "pointer",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                }}
                className="cc-btn-press"
              >
                <CreditCard size={15} />
              </button>
            </div>
          ) : (
            <div
              className="cc-card-hover"
              style={{ backgroundColor: C.paper, borderRadius: "1.4rem", padding: "1.5rem", boxShadow: "0 12px 28px -12px rgb(2 6 23 / 0.18)", border: `1px solid ${C.line}`, marginBottom: "1.5rem" }}
            >
              <button
                onClick={() => {
                  setEditingTx(null);
                  setFormType("gasto");
                  setFormPaymentMethod("efectivo");
                  setFormCardId("");
                  setFormCurrency(settings.currency || "S/");
                  setFormRate("");
                  setPhoto(null);
                  setShowForm(true);
                }}
                style={{
                  background: BRAND_GRADIENT,
                  color: "#FFFFFF",
                  border: "none",
                  borderRadius: "0.9rem",
                  cursor: "pointer",
                  fontWeight: 700,
                  boxShadow: "0 10px 22px -10px rgba(15,81,50,0.6)",
                }}
                className="cc-btn-press hover:brightness-105 w-full py-3 text-sm font-semibold flex items-center justify-center gap-2"
              >
                <Plus size={16} /> Anotar movimiento
              </button>
            </div>
          )}

          {/* Ventana modal del formulario: hoja anclada abajo y centrada en
              horizontal (si se centrara en vertical, la tarjeta taparía al
              personaje). El fondo translúcido + el scroll bloqueado mantienen la
              tarjeta de saldo quieta y al personaje siempre visible arriba. Se
              cierra con el fondo, Cancelar o Escape. */}
          {showForm && (
            <div
              onClick={() => { setEditingTx(null); setShowForm(false); }}
              role="dialog"
              aria-modal="true"
              aria-label={editingTx ? "Editar movimiento" : "Anotar movimiento"}
              className="cc-fade-in"
              style={{
                position: "fixed",
                inset: 0,
                background: "rgba(15, 23, 42, 0.5)",
                display: "flex",
                alignItems: "flex-end",
                justifyContent: "center",
                zIndex: 60,
                padding: "1rem",
              }}
            >
              <div
                onClick={(e) => e.stopPropagation()}
                className="cc-sheet-in"
                style={{
                  backgroundColor: C.paper,
                  borderRadius: "1.4rem",
                  padding: "1.5rem 1.5rem 0",
                  width: "100%",
                  maxWidth: "28rem",
                  maxHeight: "min(52vh, 40rem)",
                  overflowY: "auto",
                  boxShadow: "0 24px 60px -12px rgb(2 6 23 / 0.4)",
                  border: `1px solid ${C.line}`,
                }}
              >
                {/* Cabecera del modal */}
                <PanelHeader
                  icon={
                    editingTx ? (
                      <Pencil size={16} />
                    ) : formType === "pago_tarjeta" ? (
                      <CreditCard size={16} />
                    ) : formType === "pago_cuota" ? (
                      <Calendar size={16} />
                    ) : formType === "prestamo" ? (
                      <Users size={16} />
                    ) : (
                      <Plus size={16} />
                    )
                  }
                  title={
                    editingTx
                      ? "Editar movimiento"
                      : formType === "pago_tarjeta"
                      ? "Abonar a tarjeta de crédito"
                      : formType === "pago_cuota"
                      ? "Pagar cuota o compromiso"
                      : formType === "prestamo"
                      ? "Préstamo entre nosotros"
                      : "Anotar movimiento"
                  }
                  subtitle={
                    formType === "gasto"
                      ? "Un gasto baja el saldo y entristece al personaje"
                      : formType === "ingreso"
                      ? "Un ingreso sube el saldo y alegra al personaje"
                      : formType === "prestamo"
                      ? "Registra una deuda directa entre dos personas del equipo"
                      : formType === "pago_cuota"
                      ? "Registra el pago de tu cuota o préstamo y avanza tu contador"
                      : "Reduce la deuda acumulada de la tarjeta sin inflar los gastos del mes"
                  }
                  tileStyle={editingTx ? { background: C.chipBg, color: C.inkSoft, boxShadow: "none" } : undefined}
                  onAction={() => { setEditingTx(null); setPerson(myNameRef.current || (people[0] || "")); setShowForm(false); }}
                  actionLabel="Cerrar"
                  actionTitle="Cerrar"
                  actionIcon={<X size={14} />}
                  style={{ marginBottom: "1.1rem", gap: "0.6rem" }}
                />

                {/* Asistente Rápido de Voz y Texto dentro del modal */}
                {!editingTx && (
                  <div
                    style={{
                      display: "flex",
                      alignItems: "center",
                      gap: "0.4rem",
                      marginBottom: "1rem",
                      background: isListening ? (darkMode ? "rgba(16,185,129,0.12)" : "#ECFDF5") : C.field,
                      border: `1px solid ${isListening ? (darkMode ? "#059669" : "#10B981") : C.line}`,
                      borderRadius: "0.85rem",
                      padding: "0.4rem 0.5rem",
                    }}
                  >
                    <input
                      type="text"
                      value={smartPromptText}
                      onChange={(e) => setSmartPromptText(e.target.value)}
                      onKeyDown={(e) => {
                        if (e.key === "Enter") {
                          e.preventDefault();
                          processSmartExpense();
                        }
                      }}
                      placeholder={isListening ? "🎙️ Escuchando... habla..." : '✨ Dictar: Ej. "Ingreso 500 sueldo" o "45 almuerzo"'}
                      style={{
                        flex: 1,
                        background: "transparent",
                        border: "none",
                        outline: "none",
                        color: C.ink,
                        fontSize: "0.78rem",
                        fontWeight: 500,
                        minWidth: 0,
                        padding: "0 0.35rem",
                      }}
                    />
                    {voiceSupported && (
                      <button
                        type="button"
                        onClick={toggleVoiceRecording}
                        aria-label={isListening ? "Detener micrófono" : "Dictar"}
                        style={{
                          background: isListening ? (darkMode ? "#EF4444" : "#DC2626") : C.paper,
                          color: isListening ? "#FFFFFF" : C.inkSoft,
                          border: `1px solid ${isListening ? "#DC2626" : C.line}`,
                          borderRadius: "0.55rem",
                          width: "1.85rem",
                          height: "1.85rem",
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          cursor: "pointer",
                          flexShrink: 0,
                        }}
                        className="cc-btn-press"
                      >
                        {isListening ? <MicOff size={12} /> : <Mic size={12} />}
                      </button>
                    )}
                    {smartPromptText.trim().length > 0 && (
                      <button
                        type="button"
                        onClick={() => processSmartExpense()}
                        disabled={smartProcessing}
                        style={{
                          background: BRAND_GRADIENT,
                          color: "#FFFFFF",
                          border: "none",
                          borderRadius: "0.55rem",
                          padding: "0.3rem 0.6rem",
                          fontSize: "0.7rem",
                          fontWeight: 700,
                          cursor: "pointer",
                          flexShrink: 0,
                        }}
                        className="cc-btn-press"
                      >
                        {smartProcessing ? "..." : "Llenar"}
                      </button>
                    )}
                  </div>
                )}

                <form onSubmit={addTransaction}>
                  {/* Tipo: control segmentado Gasto / Ingreso / Préstamo / Pago de Tarjeta / Cuota */}
                  <div
                    style={{
                      display: "flex",
                      backgroundColor: C.field,
                      border: `1px solid ${C.line}`,
                      borderRadius: "0.9rem",
                      padding: "0.25rem",
                      gap: "0.25rem",
                      marginBottom: "1.1rem",
                      overflowX: "auto",
                    }}
                  >
                    {[
                      { id: "gasto", label: "Gasto", icon: <TrendingDown size={13} style={{ flexShrink: 0 }} />, color: C.red, shadow: "rgba(220,38,38,0.5)" },
                      { id: "ingreso", label: "Ingreso", icon: <TrendingUp size={13} style={{ flexShrink: 0 }} />, color: C.green, shadow: "rgba(22,163,74,0.5)" },
                      { id: "prestamo", label: "Préstamo", icon: <Users size={13} style={{ flexShrink: 0 }} />, color: "#F59E0B", shadow: "rgba(245,158,11,0.5)" },
                      { id: "pago_tarjeta", label: "Tarjeta", icon: <CreditCard size={13} style={{ flexShrink: 0 }} />, color: "#4F46E5", shadow: "rgba(79,70,229,0.5)" },
                      ...((settings.installments || []).length > 0 ? [
                        { id: "pago_cuota", label: "Cuota", icon: <Calendar size={13} style={{ flexShrink: 0 }} />, color: "#D97706", shadow: "rgba(217,119,6,0.5)" },
                      ] : []),
                    ].map((btn) => {
                      const activo = formType === btn.id;
                      return (
                        <button
                          type="button"
                          key={btn.id}
                          onClick={() => {
                            setFormType(btn.id);
                            if (btn.id === "prestamo") {
                              const other = people.find((p) => p !== person) || (people[0] || "");
                              if (!formBorrower || formBorrower === person) {
                                setFormBorrower(other);
                              }
                            } else if (btn.id === "pago_tarjeta") {
                              if (!formCardId && (settings.cards || []).length > 0) {
                                setFormCardId(settings.cards[0].id);
                              }
                            } else if (btn.id === "pago_cuota") {
                              const instList = settings.installments || [];
                              if (instList.length > 0) {
                                const target = formInstallmentId ? instList.find((i) => i.id === formInstallmentId) || instList[0] : instList[0];
                                setFormInstallmentId(target.id);
                                if (target.person) setPerson(target.person);
                                if (target.amount) setAmount(String(target.amount).replace(".", ","));
                                const total = target.totalInstallments || 0;
                                const paid = target.paidInstallments || 0;
                                const cuotaStr = total > 0 ? `Cuota ${paid + 1}/${total} - ${target.name}` : `Cuota ${target.name}`;
                                setDesc(cuotaStr);
                                pickCurrency(target.currency || settings.currency || "S/");
                              }
                            }
                          }}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            flex: 1,
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            gap: "0.25rem",
                            background: activo ? btn.color : "transparent",
                            color: activo ? "#FFFFFF" : C.inkSoft,
                            border: "none",
                            borderRadius: "0.65rem",
                            padding: "0.55rem 0.2rem",
                            fontSize: "0.72rem",
                            fontWeight: activo ? 700 : 600,
                            boxShadow: activo ? `0 6px 14px -6px ${btn.shadow}` : "none",
                            cursor: "pointer",
                            transition: "all 0.15s ease",
                            whiteSpace: "nowrap",
                          }}
                        >
                          {btn.icon}
                          {btn.label}
                        </button>
                      );
                    })}
                  </div>

                  {/* Banner explicativo al prestar dinero entre nosotros */}
                  {formType === "prestamo" && (
                    <div
                      style={{
                        background: darkMode ? "rgba(245, 158, 11, 0.15)" : "#FEF3C7",
                        border: `1px solid ${darkMode ? "#B45309" : "#FCD34D"}`,
                        borderRadius: "0.85rem",
                        padding: "0.75rem 0.9rem",
                        marginBottom: "1rem",
                        color: darkMode ? "#FDE68A" : "#92400E",
                        fontSize: "0.75rem",
                        lineHeight: 1.45,
                      }}
                    >
                      <div style={{ fontWeight: 700, display: "flex", alignItems: "center", gap: "0.4rem", marginBottom: "0.2rem" }}>
                        <Users size={14} /> 🤝 Préstamo directo entre ustedes
                      </div>
                      Registra el dinero que una persona prestó a otra. Se <strong>resaltará de forma destacada</strong> en el panel de deudas para saber exactamente quién debe a quién y saldarla fácilmente.
                    </div>
                  )}

                  {/* Selectores de Prestamista y Deudor si es préstamo */}
                  {formType === "prestamo" && (
                    <div style={{ display: "grid", gridTemplateColumns: isDesktop ? "1fr 1fr" : "1fr", gap: "0.75rem", marginBottom: "1rem" }}>
                      <div>
                        <label
                          style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                          className="text-[10px] uppercase tracking-widest block mb-1.5"
                        >
                          ¿Quién prestó el dinero? (Acreedor)
                        </label>
                        <select
                          value={person}
                          onChange={(e) => {
                            const newLender = e.target.value;
                            setPerson(newLender);
                            if (formBorrower === newLender) {
                              const other = people.find((p) => p !== newLender) || "";
                              setFormBorrower(other);
                            }
                          }}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.75rem",
                            color: C.ink,
                            padding: "0.7rem 0.85rem",
                          }}
                          className="w-full outline-none"
                        >
                          {people.map((p) => (
                            <option key={p} value={p}>
                              {p}
                            </option>
                          ))}
                        </select>
                      </div>

                      <div>
                        <label
                          style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                          className="text-[10px] uppercase tracking-widest block mb-1.5"
                        >
                          ¿A quién se le prestó? (Deudor)
                        </label>
                        <select
                          value={formBorrower}
                          onChange={(e) => setFormBorrower(e.target.value)}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.75rem",
                            color: C.ink,
                            padding: "0.7rem 0.85rem",
                          }}
                          className="w-full outline-none"
                        >
                          {people.map((p) => (
                            <option key={p} value={p} disabled={p === person}>
                              {p} {p === person ? "(es quien presta)" : ""}
                            </option>
                          ))}
                        </select>
                      </div>
                    </div>
                  )}

                  {/* Opción de seguimiento en cuotas si es préstamo */}
                  {formType === "prestamo" && !editingTx && (
                    <div
                      style={{
                        background: C.field,
                        border: `1px solid ${C.lineSoft}`,
                        borderRadius: "0.85rem",
                        padding: "0.75rem 0.9rem",
                        marginBottom: "1rem",
                        display: "flex",
                        flexDirection: "column",
                        gap: "0.5rem",
                      }}
                    >
                      <label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", fontSize: "0.78rem", fontWeight: 600, color: C.ink }}>
                        <input
                          type="checkbox"
                          checked={formCreateInstallment}
                          onChange={(e) => setFormCreateInstallment(e.target.checked)}
                          style={{ accentColor: "#F59E0B", width: "1rem", height: "1rem", cursor: "pointer" }}
                        />
                        <span>Crear seguimiento en Cuotas / Préstamos</span>
                      </label>
                      {formCreateInstallment && (
                        <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginTop: "0.2rem", fontSize: "0.72rem", color: C.inkSoft }}>
                          <span>Dividir en:</span>
                          <select
                            value={formInstallmentCount}
                            onChange={(e) => setFormInstallmentCount(e.target.value)}
                            style={{
                              background: C.paper,
                              border: `1px solid ${C.line}`,
                              borderRadius: "0.4rem",
                              padding: "0.2rem 0.5rem",
                              fontSize: "0.75rem",
                              color: C.ink,
                              fontWeight: 700,
                            }}
                          >
                            <option value="1">1 solo pago total</option>
                            <option value="2">2 cuotas mensuales</option>
                            <option value="3">3 cuotas mensuales</option>
                            <option value="4">4 cuotas mensuales</option>
                            <option value="6">6 cuotas mensuales</option>
                            <option value="12">12 cuotas mensuales</option>
                          </select>
                        </div>
                      )}
                    </div>
                  )}

                  {/* Banner explicativo al pagar cuota */}
                  {formType === "pago_cuota" && (
                    <div
                      style={{
                        background: darkMode ? "#451A03" : "#FEF3C7",
                        border: `1px solid ${darkMode ? "#B45309" : "#FDE68A"}`,
                        borderRadius: "0.85rem",
                        padding: "0.75rem 0.9rem",
                        marginBottom: "1rem",
                        color: darkMode ? "#FDE047" : "#92400E",
                        fontSize: "0.75rem",
                        lineHeight: 1.45,
                      }}
                    >
                      <div style={{ fontWeight: 700, display: "flex", alignItems: "center", gap: "0.4rem", marginBottom: "0.2rem" }}>
                        <Calendar size={14} /> Pago de cuota o compromiso mensual
                      </div>
                      Anota el pago de tu cuota o préstamo y <strong>avanza automáticamente el contador</strong> de cuotas abonadas.
                    </div>
                  )}

                  {/* Selector de cuota si es pago_cuota */}
                  {formType === "pago_cuota" && (
                    <div className="mb-4">
                      <label
                        style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                        className="text-[10px] uppercase tracking-widest block mb-1.5"
                      >
                        ¿A qué cuota o préstamo corresponde?
                      </label>
                      {(settings.installments || []).length === 0 ? (
                        <div style={{ fontSize: "0.75rem", color: C.red, padding: "0.5rem 0" }}>
                          No hay cuotas configuradas. Puedes registrarlas en Ajustes.
                        </div>
                      ) : (
                        <select
                          value={formInstallmentId}
                          onChange={(e) => {
                            const id = e.target.value;
                            setFormInstallmentId(id);
                            const found = (settings.installments || []).find((i) => i.id === id);
                            if (found) {
                              if (found.person) setPerson(found.person);
                              if (found.amount) setAmount(String(found.amount).replace(".", ","));
                              const total = found.totalInstallments || 0;
                              const paid = found.paidInstallments || 0;
                              const cuotaStr = total > 0 ? `Cuota ${paid + 1}/${total} - ${found.name}` : `Cuota ${found.name}`;
                              setDesc(cuotaStr);
                              pickCurrency(found.currency || settings.currency || "S/");
                            }
                          }}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.75rem",
                            color: C.ink,
                            padding: "0.7rem 0.85rem",
                          }}
                          className="w-full outline-none"
                        >
                          <option value="">Selecciona una cuota…</option>
                          {(settings.installments || []).map((inst) => {
                            const total = inst.totalInstallments || 0;
                            const paid = inst.paidInstallments || 0;
                            const instCurr = inst.currency || settings.currency || "S/";
                            const cuotaInfo = total > 0 ? `(Cuota ${paid + 1}/${total})` : "";
                            return (
                              <option key={inst.id} value={inst.id}>
                                📅 {inst.name} · {inst.person} {inst.recipient ? `➔ ${inst.recipient}` : ""} · {fmtMoney(inst.amount, instCurr)} {cuotaInfo}
                              </option>
                            );
                          })}
                        </select>
                      )}
                    </div>
                  )}

                  {/* Banner explicativo al abonar a tarjeta */}
                  {formType === "pago_tarjeta" && (
                    <div
                      style={{
                        background: darkMode ? "#1E1B4B" : "#EEF2FF",
                        border: `1px solid ${darkMode ? "#4338CA" : "#C7D2FE"}`,
                        borderRadius: "0.85rem",
                        padding: "0.75rem 0.9rem",
                        marginBottom: "1rem",
                        color: darkMode ? "#E0E7FF" : "#3730A3",
                        fontSize: "0.75rem",
                        lineHeight: 1.45,
                      }}
                    >
                      <div style={{ fontWeight: 700, display: "flex", alignItems: "center", gap: "0.4rem", marginBottom: "0.2rem" }}>
                        <CreditCard size={14} /> Control de deuda sin duplicar
                      </div>
                      Este abono disminuye la deuda de la tarjeta y descuenta el dinero de tu saldo, <strong>sin sumar a los gastos del mes</strong> para no duplicar consumos.
                    </div>
                  )}

                  {/* Selector de tarjeta de destino si es pago_tarjeta */}
                  {formType === "pago_tarjeta" && (
                    <div className="mb-4">
                      <label
                        style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                        className="text-[10px] uppercase tracking-widest block mb-1.5"
                      >
                        ¿A qué tarjeta se abona?
                      </label>
                      {(settings.cards || []).length === 0 ? (
                        <div style={{ fontSize: "0.75rem", color: C.red, padding: "0.5rem 0" }}>
                          No hay tarjetas registradas. Puedes configurarlas en Ajustes.
                        </div>
                      ) : (
                        <select
                          value={formCardId}
                          onChange={(e) => {
                            setFormCardId(e.target.value);
                            const found = (settings.cards || []).find((c) => c.id === e.target.value);
                            if (found) {
                              if (found.person) setPerson(found.person);
                              if (found.currency) pickCurrency(found.currency);
                            }
                          }}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.75rem",
                            color: C.ink,
                            padding: "0.7rem 0.85rem",
                          }}
                          className="w-full outline-none"
                        >
                          <option value="">Selecciona una tarjeta…</option>
                          {(settings.cards || []).map((c) => {
                            const st = cardStats.list.find((s) => s.card.id === c.id);
                            const cardCurr = st ? st.currency : (c.currency || settings.currency || "S/");
                            const debtStr = st ? fmtMoney(st.currentDebt, cardCurr) : "";
                            return (
                              <option key={c.id} value={c.id}>
                                💳 {c.name} ({c.person || "Sin titular"}) {c.currency && c.currency !== (settings.currency || "S/") ? `[${c.currency}] ` : ""}{st && st.currentDebt > 0 ? `· Deuda: ${debtStr}` : "· Al día"}
                              </option>
                            );
                          })}
                        </select>
                      )}
                    </div>
                  )}

                  {/* Desglose de la deuda al abonar: cuánto corresponde a cuotas
                      cobradas este mes en la tarjeta elegida y cuánto a otros
                      consumos (incluye cuotas de meses anteriores). */}
                  {formType === "pago_tarjeta" && formCardId && (() => {
                    const selCard = (settings.cards || []).find((c) => c.id === formCardId);
                    const st = selCard ? cardStats.list.find((s) => s.card.id === selCard.id) : null;
                    if (!selCard || !st) return null;
                    const nowD = new Date();
                    const mesKey = nowD.getFullYear() + "-" + String(nowD.getMonth() + 1).padStart(2, "0");
                    const cuotasMes = transactions.filter((t) => t.cardId === selCard.id && t.installmentId && t.type === "gasto" && String(t.date || "").startsWith(mesKey));
                    const totalCuotas = cuotasMes.reduce((s, t) => {
                      const cur = t.currency || settings.currency || "S/";
                      return cur === st.currency ? s + (Number(t.amount) || 0) : s + txToBase(t, st.currency);
                    }, 0);
                    const otros = Math.max(0, st.currentDebt - totalCuotas);
                    if (st.currentDebt <= 0 && cuotasMes.length === 0) return null;
                    const instNames = [];
                    cuotasMes.forEach((t) => {
                      const found = (settings.installments || []).find((i) => i.id === t.installmentId);
                      if (found && found.name && !instNames.includes(found.name)) instNames.push(found.name);
                    });
                    return (
                      <div
                        style={{
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.85rem",
                          padding: "0.75rem 0.9rem",
                          marginBottom: "1rem",
                          fontSize: "0.75rem",
                          color: C.ink,
                        }}
                      >
                        <div style={{ fontWeight: 700, marginBottom: "0.35rem" }}>📊 Desglose de la deuda · {selCard.name}</div>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem" }}>
                          <span>Cuotas de este mes{instNames.length > 0 ? " (" + instNames.join(", ") + ")" : ""}</span>
                          <strong>{fmtMoney(totalCuotas, st.currency)}</strong>
                        </div>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem", marginTop: "0.2rem" }}>
                          <span>Otros consumos y cuotas anteriores</span>
                          <strong>{fmtMoney(otros, st.currency)}</strong>
                        </div>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: "0.5rem", marginTop: "0.35rem", paddingTop: "0.35rem", borderTop: `1px solid ${C.line}` }}>
                          <span style={{ fontWeight: 700 }}>Total en la tarjeta</span>
                          <strong>{fmtMoney(st.currentDebt, st.currency)}</strong>
                        </div>
                      </div>
                    );
                  })()}

                                    {/* 1. Descripción o Detalle (Arriba del gasto) */}
                  <div className="mb-4">
                    <label
                      style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                      className="text-[10px] uppercase tracking-widest block mb-1.5"
                    >
                      📝 Descripción o concepto
                    </label>
                    <input
                      value={desc}
                      onChange={(e) => setDesc(e.target.value)}
                      placeholder="Ej. Almuerzo, Gasolina, Supermercado, Cine..."
                      maxLength={80}
                      autoFocus={!editingTx}
                      style={{
                        fontFamily: "'Inter', sans-serif",
                        background: C.field,
                        border: `1px solid ${C.line}`,
                        borderRadius: "0.75rem",
                        color: C.ink,
                        padding: "0.7rem 0.85rem",
                        fontSize: "0.95rem",
                        fontWeight: 600,
                      }}
                      className="w-full outline-none"
                    />
                  </div>

                  {/* 2. Importe + Fecha (El gasto) */}
                  <div className="mb-4">
                    <div className="flex gap-3">
                      <div style={{ flex: 1.35, minWidth: 0 }}>
                        <label
                          style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                          className="text-[10px] uppercase tracking-widest block mb-1.5"
                        >
                          Importe
                        </label>
                        <div style={{ position: "relative" }}>
                          <input
                            type="text"
                            inputMode="decimal"
                            value={amount}
                            onChange={(e) => setAmount(e.target.value)}
                            placeholder="0,00"
                            style={{
                              fontFamily: "'Inter', sans-serif",
                              background: C.field,
                              border: `1px solid ${C.line}`,
                              borderRadius: "0.75rem",
                              color: C.ink,
                              padding: "0.7rem 2.8rem 0.7rem 0.85rem",
                              fontSize: "1.1rem",
                              fontWeight: 700,
                            }}
                            className="w-full outline-none"
                            
                          />
                          <span
                            style={{
                              position: "absolute",
                              right: "0.85rem",
                              top: "50%",
                              transform: "translateY(-50%)",
                              color: C.inkSoft,
                              fontWeight: 700,
                              fontSize: "0.95rem",
                              pointerEvents: "none",
                            }}
                          >
                            {formCurrency || settings.currency || "S/"}
                          </span>
                        </div>
                      </div>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <label
                          style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                          className="text-[10px] uppercase tracking-widest block mb-1.5"
                        >
                          Fecha
                        </label>
                        <input
                          type="date"
                          value={date}
                          onChange={(e) => setDate(e.target.value)}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.75rem",
                            color: C.ink,
                            padding: "0.62rem 0.75rem",
                          }}
                          className="w-full outline-none"
                        />
                      </div>
                    </div>

                    {/* Selector de moneda rápida bajo el importe */}
                    <div style={{ display: "flex", gap: "0.35rem", marginTop: "0.45rem", alignItems: "center", flexWrap: "wrap" }}>
                      {["S/", "$", "€"].map((cur) => {
                        const isSel = (formCurrency || settings.currency || "S/") === cur;
                        return (
                          <button
                            type="button"
                            key={cur}
                            onClick={() => pickCurrency(cur)}
                            style={{
                              background: isSel ? C.ink : C.field,
                              color: isSel ? C.paper : C.inkSoft,
                              border: `1px solid ${isSel ? "transparent" : C.line}`,
                              borderRadius: "0.45rem",
                              padding: "0.2rem 0.55rem",
                              fontSize: "0.72rem",
                              fontWeight: isSel ? 700 : 500,
                              cursor: "pointer",
                            }}
                            className="cc-btn-press"
                          >
                            {cur}
                          </button>
                        );
                      })}
                      {formCurrency && formCurrency !== (settings.currency || "S/") && rateLoading && (
                        <span style={{ fontSize: "0.68rem", color: C.inkSoft, display: "flex", alignItems: "center", gap: "0.25rem" }}>
                          <RefreshCw size={11} style={{ animation: "cc-spin 1s linear infinite" }} />
                          <span>Consultando cambio…</span>
                        </span>
                      )}
                    </div>

                    {/* Previsualización del tipo de cambio en tiempo real */}
                    {formCurrency && formCurrency !== (settings.currency || "S/") && (
                      <div
                        style={{
                          marginTop: "0.45rem",
                          background: C.field,
                          border: `1px solid ${C.lineSoft}`,
                          borderRadius: "0.65rem",
                          padding: "0.4rem 0.65rem",
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "space-between",
                          gap: "0.5rem",
                          fontSize: "0.74rem",
                          flexWrap: "wrap",
                        }}
                      >
                        <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", color: C.ink }}>
                          <span>1 {formCurrency} =</span>
                          <input
                            value={formRate}
                            onChange={(e) => setFormRate(e.target.value)}
                            placeholder="T.C."
                            inputMode="decimal"
                            maxLength={8}
                            style={{
                              width: "4.5rem",
                              background: C.paper,
                              border: `1px solid ${C.line}`,
                              borderRadius: "0.45rem",
                              padding: "0.15rem 0.4rem",
                              fontSize: "0.74rem",
                              fontWeight: 700,
                              color: C.ink,
                              outline: "none",
                              textAlign: "center",
                            }}
                          />
                          <span>{settings.currency || "S/"}</span>
                          <button
                            type="button"
                            onClick={() => pickCurrency(formCurrency)}
                            title="Actualizar tipo de cambio"
                            style={{ background: "none", border: "none", cursor: "pointer", padding: "0.1rem", display: "flex", alignItems: "center", color: C.inkSoft }}
                          >
                            <RefreshCw size={12} style={rateLoading ? { animation: "cc-spin 1s linear infinite" } : undefined} />
                          </button>
                        </div>

                        {(() => {
                          const numAmount = parseFloat(String(amount).replace(",", "."));
                          const numRate = parseFloat(String(formRate).replace(",", "."));
                          if (isFinite(numAmount) && numAmount > 0 && isFinite(numRate) && numRate > 0) {
                            return (
                              <div style={{ fontWeight: 700, color: C.green, display: "flex", alignItems: "center", gap: "0.25rem" }}>
                                <span>≈</span>
                                <strong>{fmtMoney(numAmount * numRate, settings.currency || "S/")}</strong>
                              </div>
                            );
                          }
                          return null;
                        })()}
                      </div>
                    )}
                  </div>

                  {/* Método de pago (Efectivo / Tarjeta / Transferencia) si es gasto */}
                  {formType === "gasto" && (
                    <div className="mb-4">
                      <label
                        style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                        className="text-[10px] uppercase tracking-widest block mb-1.5"
                      >
                        Método de pago
                      </label>
                      <div className="flex gap-2">
                        {[
                          { id: "efectivo", label: "Efectivo", icon: <Banknote size={13} /> },
                          { id: "tarjeta", label: "Tarjeta", icon: <CreditCard size={13} /> },
                          { id: "transferencia", label: "Transfer", icon: <Landmark size={13} /> },
                        ].map((m) => {
                          const activo = (formPaymentMethod || "efectivo") === m.id;
                          return (
                            <button
                              type="button"
                              key={m.id}
                              onClick={() => {
                                setFormPaymentMethod(m.id);
                                if (m.id === "tarjeta") {
                                  let targetCard = null;
                                  if (!formCardId) {
                                    const userCards = (settings.cards || []).filter((c) => !person || c.person === person);
                                    if (userCards.length > 0) targetCard = userCards[0];
                                    else if ((settings.cards || []).length > 0) targetCard = settings.cards[0];
                                    if (targetCard) setFormCardId(targetCard.id);
                                  } else {
                                    targetCard = (settings.cards || []).find((c) => c.id === formCardId);
                                  }
                                  if (targetCard && targetCard.currency) pickCurrency(targetCard.currency);
                                }
                              }}
                              style={{
                                fontFamily: "'Inter', sans-serif",
                                flex: 1,
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                                gap: "0.3rem",
                                background: activo ? C.ink : C.field,
                                color: activo ? C.paper : C.inkSoft,
                                border: `1px solid ${activo ? "transparent" : C.line}`,
                                borderRadius: "0.75rem",
                                padding: "0.55rem 0.2rem",
                                fontSize: "0.75rem",
                                fontWeight: activo ? 700 : 500,
                                cursor: "pointer",
                              }}
                              className="cc-btn-press"
                            >
                              {m.icon}
                              <span>{m.label}</span>
                            </button>
                          );
                        })}
                      </div>

                      {/* Selector de tarjeta cuando el método es Tarjeta */}
                      {formPaymentMethod === "tarjeta" && (
                        <div style={{ marginTop: "0.6rem" }}>
                          {(settings.cards || []).length === 0 ? (
                            <div style={{ fontSize: "0.72rem", color: C.inkSoft, padding: "0.2rem 0" }}>
                              No tienes tarjetas configuradas. Puedes registrarlas en Ajustes.
                            </div>
                          ) : (
                            <select
                              value={formCardId}
                              onChange={(e) => {
                                setFormCardId(e.target.value);
                                const found = (settings.cards || []).find((c) => c.id === e.target.value);
                                if (found && found.currency) pickCurrency(found.currency);
                              }}
                              style={{
                                fontFamily: "'Inter', sans-serif",
                                background: C.field,
                                border: `1px solid ${C.line}`,
                                borderRadius: "0.75rem",
                                color: C.ink,
                                padding: "0.55rem 0.8rem",
                                fontSize: "0.8rem",
                              }}
                              className="w-full outline-none"
                            >
                              <option value="">Selecciona tarjeta…</option>
                              {(settings.cards || []).map((c) => (
                                <option key={c.id} value={c.id}>
                                  {c.cardType === "debito" ? "🏧" : "💳"} {c.name} {c.cardType === "debito" ? "[Débito]" : "[Crédito]"} ({c.person || "Sin titular"})
                                </option>
                              ))}
                            </select>
                          )}

                          {/* Banner interactivo de Cashback al comprar con Tarjeta */}
                          {formPaymentMethod === "tarjeta" && formCardId && (() => {
                            const selCard = (settings.cards || []).find((c) => c.id === formCardId);
                            if (!selCard) return null;
                            const isCardWithCb = selCard.hasCashback || (selCard.cashbackPercent != null && Number(selCard.cashbackPercent) > 0);
                            
                            if (!isCardWithCb && !formCashbackCustom) {
                              return (
                                <div style={{ marginTop: "0.45rem", display: "flex", justifyContent: "flex-end" }}>
                                  <button
                                    type="button"
                                    onClick={() => {
                                      setFormCashbackEnabled(true);
                                      setFormCashbackCustom(true);
                                      setFormCashbackPercent("1.5");
                                    }}
                                    style={{
                                      background: "none",
                                      border: "none",
                                      color: C.green,
                                      fontSize: "0.72rem",
                                      fontWeight: 600,
                                      cursor: "pointer",
                                      display: "inline-flex",
                                      alignItems: "center",
                                      gap: "0.25rem",
                                      padding: "0.2rem 0",
                                    }}
                                  >
                                    <span>🎁 ¿Esta compra o tarjeta tiene cashback? Aplicar</span>
                                  </button>
                                </div>
                              );
                            }

                            const isService = isServicePayment(desc);
                            const cardPct = selCard.cashbackPercent != null ? Number(selCard.cashbackPercent) : 1.5;
                            const effectivePct = formCashbackPercent !== "" ? Number(formCashbackPercent) : cardPct;
                            const isAutoExcluded = isService && selCard.excludeServices !== false;
                            const isApplying = formCashbackEnabled && (!isAutoExcluded || formCashbackCustom);
                            const amtVal = parseFloat(String(amount).replace(",", ".")) || 0;
                            const cbEst = isApplying && amtVal > 0 ? Math.round((amtVal * (effectivePct / 100)) * 100) / 100 : 0;

                            return (
                              <div
                                style={{
                                  marginTop: "0.6rem",
                                  background: isApplying
                                    ? (darkMode ? "rgba(16, 185, 129, 0.12)" : "#ECFDF5")
                                    : (darkMode ? "rgba(245, 158, 11, 0.12)" : "#FEF3C7"),
                                  border: `1px solid ${
                                    isApplying
                                      ? (darkMode ? "rgba(16, 185, 129, 0.35)" : "#A7F3D0")
                                      : (darkMode ? "rgba(245, 158, 11, 0.35)" : "#FDE68A")
                                  }`,
                                  borderRadius: "0.75rem",
                                  padding: "0.55rem 0.75rem",
                                  display: "flex",
                                  flexDirection: "column",
                                  gap: "0.35rem",
                                  fontSize: "0.74rem",
                                }}
                              >
                                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem" }}>
                                  <div style={{ display: "flex", alignItems: "center", gap: "0.35rem" }}>
                                    <span>{isApplying ? "🎁" : "🚫"}</span>
                                    <strong style={{ color: isApplying ? (darkMode ? "#6EE7B7" : "#047857") : (darkMode ? "#FDE68A" : "#B45309") }}>
                                      {isApplying
                                        ? `+ ${fmtMoney(cbEst, formCurrency || settings.currency || "S/")} de Cashback (${effectivePct}% · ${selCard.excludeServices === false ? "En todo" : "Compra general"})`
                                        : (isAutoExcluded ? "0% Cashback (Pago de servicios excluido en esta tarjeta)" : "Cashback desactivado para esta compra")}
                                    </strong>
                                  </div>

                                  <button
                                    type="button"
                                    onClick={() => {
                                      if (!isApplying) {
                                        setFormCashbackEnabled(true);
                                        setFormCashbackCustom(true);
                                      } else {
                                        setFormCashbackEnabled(false);
                                        setFormCashbackCustom(true);
                                      }
                                    }}
                                    style={{
                                      background: C.paper,
                                      border: `1px solid ${C.line}`,
                                      borderRadius: "0.45rem",
                                      padding: "0.15rem 0.5rem",
                                      fontSize: "0.68rem",
                                      fontWeight: 600,
                                      color: C.ink,
                                      cursor: "pointer",
                                    }}
                                  >
                                    {isApplying ? "Excluir / Desactivar" : "Aplicar igualmente"}
                                  </button>
                                </div>

                                {isApplying && (
                                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "0.68rem", color: C.inkSoft, borderTop: `1px solid ${darkMode ? "rgba(255,255,255,0.06)" : C.lineSoft}`, paddingTop: "0.3rem" }}>
                                    <span>Dinero de vuelta acumulable en {selCard.name}</span>
                                    <div style={{ display: "flex", alignItems: "center", gap: "0.25rem" }}>
                                      <span>Tasa:</span>
                                      <select
                                        value={effectivePct}
                                        onChange={(e) => {
                                          setFormCashbackPercent(e.target.value);
                                          setFormCashbackCustom(true);
                                        }}
                                        style={{
                                          background: C.paper,
                                          border: `1px solid ${C.line}`,
                                          borderRadius: "0.35rem",
                                          padding: "0.05rem 0.3rem",
                                          fontSize: "0.68rem",
                                          fontWeight: 700,
                                          color: C.ink,
                                        }}
                                      >
                                        {[1, 1.5, 2, 3, 5, 10].map((p) => (
                                          <option key={p} value={p}>{p}%</option>
                                        ))}
                                      </select>
                                    </div>
                                  </div>
                                )}
                              </div>
                            );
                          })()}
                        </div>
                      )}
                    </div>
                  )}

                  {/* Origen del dinero si es pago_tarjeta */}
                  {formType === "pago_tarjeta" && (
                    <div className="mb-4">
                      <label
                        style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                        className="text-[10px] uppercase tracking-widest block mb-1.5"
                      >
                        ¿De dónde sale el dinero para pagar?
                      </label>
                      <div className="flex gap-2">
                        {[
                          { id: "efectivo", label: "Efectivo", icon: <Banknote size={13} /> },
                          { id: "transferencia", label: "Transferencia / Cuenta", icon: <Landmark size={13} /> },
                        ].map((m) => {
                          const activo = (formPaymentMethod || "efectivo") === m.id;
                          return (
                            <button
                              type="button"
                              key={m.id}
                              onClick={() => setFormPaymentMethod(m.id)}
                              style={{
                                fontFamily: "'Inter', sans-serif",
                                flex: 1,
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                                gap: "0.3rem",
                                background: activo ? C.ink : C.field,
                                color: activo ? C.paper : C.inkSoft,
                                border: `1px solid ${activo ? "transparent" : C.line}`,
                                borderRadius: "0.75rem",
                                padding: "0.55rem 0.3rem",
                                fontSize: "0.75rem",
                                fontWeight: activo ? 700 : 500,
                                cursor: "pointer",
                              }}
                              className="cc-btn-press"
                            >
                              {m.icon}
                              <span>{m.label}</span>
                            </button>
                          );
                        })}
                      </div>
                    </div>
                  )}

                  {/* Selector visual de Categorías (100% independiente de la descripción) */}
                  <div className="mb-3.5">
                    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.35rem" }}>
                      <label
                        style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                        className="text-[10px] uppercase tracking-widest block"
                      >
                        🏷️ Categoría {category ? <span style={{ color: C.green, fontWeight: 700 }}>({category})</span> : <span style={{ fontWeight: 400 }}>(opcional)</span>}
                      </label>
                      <button
                        type="button"
                        onClick={() => {
                          const newCatName = prompt("Escribe el nombre de la nueva categoría (ej. Mascotas, Taxis, Gimnasio, Viajes):");
                          if (newCatName && newCatName.trim()) {
                            const clean = cleanCategoryName(newCatName.trim());
                            const currentCats = Array.isArray(settings.categories) && settings.categories.length > 0 ? settings.categories : DEFAULT_CATEGORIES;
                            if (!currentCats.some((c) => c.toLowerCase() === clean.toLowerCase())) {
                              const updated = [...currentCats, clean];
                              setSettings((prev) => ({ ...prev, categories: updated }));
                              storage.set("settings", JSON.stringify({ ...settings, categories: updated }), true).catch(() => {});
                            }
                            setCategory(clean);
                          }
                        }}
                        style={{
                          background: "none",
                          border: "none",
                          color: C.green,
                          fontSize: "0.72rem",
                          fontWeight: 700,
                          cursor: "pointer",
                          display: "inline-flex",
                          alignItems: "center",
                          gap: "0.2rem",
                          padding: "0.1rem 0.3rem",
                        }}
                        className="hover:underline"
                      >
                        + Nueva categoría
                      </button>
                    </div>

                    <div
                      style={{
                        display: "flex",
                        flexWrap: "wrap",
                        gap: "0.4rem",
                        maxHeight: "160px",
                        overflowY: "auto",
                        padding: "0.25rem 0",
                      }}
                      className="cc-custom-scrollbar"
                    >
                      {(Array.isArray(settings.categories) && settings.categories.length > 0 ? settings.categories : DEFAULT_CATEGORIES).map((c) => {
                        const isSelected = (category || "").toLowerCase() === c.toLowerCase();
                        const icon = getCategoryIcon(c);
                        return (
                          <button
                            type="button"
                            key={c}
                            onClick={() => setCategory((prev) => (prev && prev.toLowerCase() === c.toLowerCase() ? "" : c))}
                            style={{
                              fontFamily: "'Inter', sans-serif",
                              background: isSelected ? BRAND_GRADIENT : C.field,
                              color: isSelected ? "#FFFFFF" : C.ink,
                              border: `1.5px solid ${isSelected ? C.green : C.line}`,
                              borderRadius: "999px",
                              padding: "0.35rem 0.75rem",
                              fontSize: "0.75rem",
                              fontWeight: isSelected ? 700 : 500,
                              cursor: "pointer",
                              display: "inline-flex",
                              alignItems: "center",
                              gap: "0.35rem",
                              boxShadow: isSelected ? "0 2px 8px rgba(16,185,129,0.35)" : "none",
                              transition: "all 0.15s ease",
                            }}
                            className="cc-btn-press"
                          >
                            <span>{icon}</span>
                            <span>{c}</span>
                            {isSelected && <span style={{ fontSize: "0.7rem", marginLeft: "0.15rem" }}>✓</span>}
                          </button>
                        );
                      })}
                    </div>
                  </div>



                  {/* Nota */}
                  <label
                    style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                    className="text-[10px] uppercase tracking-widest block mb-1.5"
                  >
                    Nota{" "}
                    <span style={{ fontWeight: 400, textTransform: "none", letterSpacing: "0.02em" }}>(opcional)</span>
                  </label>
                  <input
                    value={note}
                    onChange={(e) => setNote(e.target.value)}
                    placeholder="Ej. cena del sábado"
                    maxLength={120}
                    style={{
                      fontFamily: "'Inter', sans-serif",
                      background: C.field,
                      border: `1px solid ${C.line}`,
                      borderRadius: "0.75rem",
                      color: C.ink,
                      padding: "0.7rem 0.85rem",
                    }}
                    className="w-full outline-none mb-4"
                  />

                  {/* Foto del recibo (opcional) */}
                  <label
                    style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                    className="text-[10px] uppercase tracking-widest block mb-1.5"
                  >
                    Foto del recibo{" "}
                    <span style={{ fontWeight: 400, textTransform: "none", letterSpacing: "0.02em" }}>(opcional)</span>
                  </label>
                  <div className="mb-4">
                    {photo ? (
                      <div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
                        <img
                          src={photo}
                          alt="Recibo"
                          onClick={() => setViewPhoto(photo)}
                          style={{
                            width: "3.5rem",
                            height: "3.5rem",
                            objectFit: "cover",
                            borderRadius: "0.75rem",
                            cursor: "zoom-in",
                            border: `1px solid ${C.line}`,
                          }}
                        />
                        <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
                          <span style={{ fontSize: "0.75rem", color: C.inkSoft }}>Recibo adjunto</span>
                          <button
                            type="button"
                            onClick={() => setPhoto(null)}
                            style={{ fontFamily: "'Inter', sans-serif", color: C.red, background: "none", border: "none", cursor: "pointer", padding: 0, textAlign: "left", fontSize: "0.8rem" }}
                          >
                            Quitar foto
                          </button>
                        </div>
                      </div>
                    ) : (
                      <label
                        style={{
                          fontFamily: "'Inter', sans-serif",
                          display: "flex",
                          alignItems: "center",
                          gap: "0.5rem",
                          color: C.inkSoft,
                          fontSize: "0.85rem",
                          cursor: "pointer",
                          padding: "0.7rem 0.85rem",
                          borderRadius: "0.75rem",
                          border: `1px dashed ${C.line}`,
                          backgroundColor: C.field,
                        }}
                        className="cc-btn-press"
                      >
                        <Camera size={16} style={{ flexShrink: 0 }} />
                        <span>{photoBusy ? "Procesando foto…" : "Adjuntar foto del recibo (opcional)"}</span>
                        <input
                          type="file"
                          accept="image/*"
                          capture="environment"
                          onChange={onPickPhoto}
                          style={{ display: "none" }}
                        />
                      </label>
                    )}
                  </div>

                  {/* Moneda del movimiento */}
                  <div className="mb-4">
                    <p
                      style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                      className="text-[10px] uppercase tracking-widest mb-1.5 flex items-center gap-1.5"
                    >
                      <CircleDollarSign size={12} color={C.inkSoft} style={{ flexShrink: 0 }} />
                      Moneda del movimiento
                    </p>
                    <div className="flex flex-wrap gap-2 mb-2">
                      {[...CURRENCY_OPTIONS, "Otro"].map((c) => {
                        const esOtro = c === "Otro";
                        const activo = esOtro
                          ? !CURRENCY_OPTIONS.includes(formCurrency)
                          : formCurrency === c;
                        return (
                          <button
                            type="button"
                            key={c}
                            onClick={() => pickCurrency(esOtro ? (CURRENCY_OPTIONS.includes(formCurrency) ? "" : formCurrency) : c)}
                            style={{
                              fontFamily: "'Inter', sans-serif",
                              background: activo ? C.ink : C.field,
                              color: activo ? C.paper : C.inkSoft,
                              border: `1px solid ${activo ? "transparent" : C.line}`,
                              borderRadius: "9999px",
                              padding: "0.35rem 0.85rem",
                              fontWeight: activo ? 700 : 600,
                              cursor: "pointer",
                              transition: "all 0.15s ease",
                            }}
                            className="text-xs"
                          >
                            {c}
                          </button>
                        );
                      })}
                    </div>
                    {!CURRENCY_OPTIONS.includes(formCurrency) && (
                      <input
                        value={formCurrency}
                        onChange={(e) => {
                          const v = e.target.value;
                          setFormCurrency(v);
                          if (toIsoCurrency(v)) pickCurrency(v);
                        }}
                        placeholder="Símbolo (ej. MX$)"
                        maxLength={8}
                        style={{
                          fontFamily: "'Inter', sans-serif",
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.75rem",
                          color: C.ink,
                          padding: "0.6rem 0.85rem",
                        }}
                        className="w-full outline-none mb-2"
                      />
                    )}
                    {formCurrency && formCurrency !== (settings.currency || "S/") && (
                      <div className="flex items-center gap-2">
                        <input
                          value={formRate}
                          onChange={(e) => setFormRate(e.target.value)}
                          placeholder="Tipo de cambio"
                          inputMode="decimal"
                          maxLength={10}
                          style={{
                            fontFamily: "'Inter', sans-serif",
                            background: C.field,
                            border: `1px solid ${C.line}`,
                            borderRadius: "0.75rem",
                            color: C.ink,
                            padding: "0.6rem 0.85rem",
                          }}
                          className="w-1/2 outline-none text-sm"
                        />
                        {toIsoCurrency(formCurrency) && (
                          <button
                            type="button"
                            onClick={() => pickCurrency(formCurrency)}
                            title="Actualizar tipo de cambio"
                            style={{ background: "none", border: "none", cursor: "pointer", padding: "0.1rem", display: "flex", alignItems: "center", color: C.inkSoft }}
                          >
                            <RefreshCw size={14} style={rateLoading ? { animation: "cc-spin 1s linear infinite" } : undefined} />
                          </button>
                        )}
                        <span style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontSize: "0.72rem" }}>
                          {rateLoading && toIsoCurrency(formCurrency)
                            ? "Obteniendo tipo de cambio…"
                            : `1 ${formCurrency} = ? ${settings.currency || "S/"}`}
                        </span>
                      </div>
                    )}
                  </div>

                  {/* Persona (solo si no es préstamo entre nosotros) */}
                  {formType !== "prestamo" && (
                    <>
                      <label
                        style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, fontWeight: 600 }}
                        className="text-[10px] uppercase tracking-widest block mb-1.5"
                      >
                        Persona
                      </label>
                      <select
                        value={person}
                        onChange={(e) => setPerson(e.target.value)}
                        style={{
                          fontFamily: "'Inter', sans-serif",
                          background: C.field,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.75rem",
                          color: C.ink,
                          padding: "0.7rem 0.85rem",
                        }}
                        className="w-full outline-none mb-4"
                      >
                        {(people.length ? people : [myName]).map((p) => (
                          <option key={p} value={p}>
                            {p}
                          </option>
                        ))}
                      </select>
                    </>
                  )}

                  {error && (
                    <p
                      style={{
                        fontFamily: "'Inter', sans-serif",
                        color: C.red,
                        backgroundColor: C.dangerBg,
                        borderRadius: "0.6rem",
                        padding: "0.5rem 0.7rem",
                      }}
                      className="text-xs mb-3"
                    >
                      {error}
                    </p>
                  )}

                  {/* Acciones: pegadas abajo de la hoja para verlas siempre */}
                  <div
                    className="flex gap-2"
                    style={{
                      position: "sticky",
                      bottom: 0,
                      background: C.paper,
                      borderTop: `1px solid ${C.line}`,
                      boxShadow: "0 -10px 24px -14px rgb(2 6 23 / 0.3)",
                      margin: "0.5rem -1.5rem 0",
                      padding: "0.85rem 1.5rem 1.5rem",
                    }}
                  >
                    <button
                      type="button"
                      onClick={() => { setEditingTx(null); setPerson(myNameRef.current || (people[0] || "")); setShowForm(false); }}
                      style={{ fontFamily: "'Inter', sans-serif", color: C.inkSoft, border: `1px solid ${C.line}`, borderRadius: "0.85rem", background: "transparent" }}
                      className="flex-1 py-3 text-xs uppercase tracking-widest cc-btn-press"
                    >
                      Cancelar
                    </button>
                    <button
                      type="submit"
                      disabled={saving}
                      style={{
                        fontFamily: "'Inter', sans-serif",
                        background: BRAND_GRADIENT,
                        color: "#FFFFFF",
                        borderRadius: "0.85rem",
                        fontWeight: 700,
                        boxShadow: "0 10px 22px -10px rgba(15,81,50,0.6)",
                      }}
                      className="flex-1 py-3 text-xs uppercase tracking-widest cc-btn-press hover:brightness-105 disabled:opacity-40 disabled:shadow-none"
                    >
                      {saving ? "Guardando…" : editingTx ? "Guardar cambios" : "Anotar"}
                    </button>
                  </div>
                </form>
              </div>
            </div>
          )}

          {/* Modal del Consejero Financiero & Resumen Inteligente con IA */}
          {showAdvisorModal && (
            <div
              onClick={() => setShowAdvisorModal(false)}
              role="dialog"
              aria-modal="true"
              aria-label="Consejero Financiero con IA"
              className="cc-fade-in"
              style={{
                position: "fixed",
                inset: 0,
                background: "rgba(15, 23, 42, 0.55)",
                display: "flex",
                alignItems: isDesktop ? "center" : "flex-end",
                justifyContent: "center",
                zIndex: 60,
                padding: "1rem",
              }}
            >
              <div
                onClick={(e) => e.stopPropagation()}
                className="cc-sheet-in"
                style={{
                  backgroundColor: C.paper,
                  borderRadius: "1.4rem",
                  padding: "1.4rem",
                  width: "100%",
                  maxWidth: "30rem",
                  maxHeight: "min(85vh, 44rem)",
                  overflowY: "auto",
                  boxShadow: "0 24px 60px -12px rgb(2 6 23 / 0.4)",
                  border: `1px solid ${C.line}`,
                }}
              >
                {/* Cabecera */}
                <PanelHeader
                  icon={<Lightbulb size={16} />}
                  title="Consejero Financiero IA"
                  subtitle={`Análisis y recomendaciones de ${monthLabel(summaryMonth)}`}
                  onAction={() => setShowAdvisorModal(false)}
                  actionLabel="Cerrar consejero"
                  actionTitle="Cerrar"
                  actionIcon={<X size={14} />}
                  style={{ marginBottom: "1rem", gap: "0.6rem" }}
                />

                {advisorLoading ? (
                  <div style={{ padding: "3rem 1rem", textAlign: "center", color: C.inkSoft }}>
                    <RefreshCw size={28} color={C.green} style={{ margin: "0 auto 0.8rem", animation: "cc-spin 1.2s linear infinite" }} />
                    <p style={{ fontWeight: 700, color: C.ink, margin: "0 0 0.3rem", fontSize: "0.95rem" }}>
                      Analizando tus finanzas con IA...
                    </p>
                    <p style={{ margin: 0, fontSize: "0.75rem" }}>
                      Evaluando gastos, tendencias y detectando oportunidades de ahorro.
                    </p>
                  </div>
                ) : advisorData ? (
                  <div style={{ display: "flex", flexDirection: "column", gap: "0.9rem" }}>
                    {/* Tarjeta de Salud Financiera y Resumen */}
                    <div
                      style={{
                        background: darkMode ? "rgba(255,255,255,0.03)" : C.field,
                        border: `1px solid ${C.lineSoft}`,
                        borderRadius: "1rem",
                        padding: "0.9rem 1rem",
                      }}
                    >
                      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.5rem" }}>
                        <span style={{ fontSize: "0.7rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, letterSpacing: "0.05em" }}>
                          Diagnóstico del Mes
                        </span>
                        <span
                          style={{
                            background: advisorData.healthColor + "22",
                            color: advisorData.healthColor,
                            border: `1px solid ${advisorData.healthColor}55`,
                            borderRadius: "999px",
                            padding: "0.15rem 0.55rem",
                            fontSize: "0.7rem",
                            fontWeight: 700,
                          }}
                        >
                          {advisorData.healthScore}
                        </span>
                      </div>
                      <p style={{ margin: 0, fontSize: "0.82rem", color: C.ink, lineHeight: 1.45, fontWeight: 500 }}>
                        {advisorData.summaryText}
                      </p>
                    </div>

                    {/* Métricas clave en 2 columnas */}
                    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.6rem" }}>
                      {/* Top Categoría */}
                      <div
                        style={{
                          background: C.paper,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.9rem",
                          padding: "0.75rem",
                        }}
                      >
                        <div style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, marginBottom: "0.2rem" }}>
                          Mayor Gasto
                        </div>
                        <div style={{ fontSize: "0.92rem", fontWeight: 800, color: C.ink }}>
                          {advisorData.topCategory ? advisorData.topCategory.name : "—"}
                        </div>
                        <div style={{ fontSize: "0.7rem", color: C.red, fontWeight: 600, marginTop: "0.1rem" }}>
                          {advisorData.topCategory ? fmtMoney(advisorData.topCategory.amount, settings.currency) : "S/ 0.00"}
                        </div>
                      </div>

                      {/* Promedio Diario / Proyección */}
                      <div
                        style={{
                          background: C.paper,
                          border: `1px solid ${C.line}`,
                          borderRadius: "0.9rem",
                          padding: "0.75rem",
                        }}
                      >
                        <div style={{ fontSize: "0.68rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, marginBottom: "0.2rem" }}>
                          Ritmo Diario
                        </div>
                        <div style={{ fontSize: "0.92rem", fontWeight: 800, color: C.ink }}>
                          {fmtMoney(advisorData.dailyAvg, settings.currency)}/día
                        </div>
                        <div style={{ fontSize: "0.7rem", color: C.inkSoft, marginTop: "0.1rem" }}>
                          Cierre est.: {fmtMoney(advisorData.projectedGastos, settings.currency)}
                        </div>
                      </div>
                    </div>

                    {/* Fugas y Gastos Hormiga si existen */}
                    {advisorData.microCount > 0 && (
                      <div
                        style={{
                          background: darkMode ? "rgba(245, 158, 11, 0.12)" : "#FEF3C7",
                          border: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`,
                          borderRadius: "0.9rem",
                          padding: "0.75rem 0.9rem",
                          color: darkMode ? "#FDE68A" : "#92400E",
                          fontSize: "0.76rem",
                          display: "flex",
                          alignItems: "center",
                          gap: "0.5rem",
                        }}
                      >
                        <AlertTriangle size={16} style={{ flexShrink: 0 }} />
                        <div>
                          <strong>{advisorData.microCount} compras pequeñas</strong> menores a 30 soles que suman <strong>{fmtMoney(advisorData.totalMicro, settings.currency)}</strong> este mes.
                        </div>
                      </div>
                    )}

                    {/* Lista de Consejos Accionables */}
                    <div>
                      <div style={{ fontSize: "0.72rem", color: C.inkSoft, textTransform: "uppercase", fontWeight: 700, letterSpacing: "0.05em", marginBottom: "0.45rem" }}>
                        Consejos de Ahorro
                      </div>
                      <div style={{ display: "flex", flexDirection: "column", gap: "0.45rem" }}>
                        {(advisorData.tips || []).map((tip, idx) => (
                          <div
                            key={idx}
                            style={{
                              background: C.field,
                              border: `1px solid ${C.lineSoft}`,
                              borderRadius: "0.85rem",
                              padding: "0.7rem 0.85rem",
                            }}
                          >
                            <div style={{ fontWeight: 700, fontSize: "0.8rem", color: C.ink, marginBottom: "0.2rem", display: "flex", alignItems: "center", gap: "0.35rem" }}>
                              <Target size={13} color={C.green} />
                              <span>{tip.title}</span>
                            </div>
                            <div style={{ fontSize: "0.73rem", color: C.inkSoft, lineHeight: 1.4 }}>
                              {tip.desc}
                            </div>
                          </div>
                        ))}
                      </div>
                    </div>

                    {/* Botón de recálculo / refresco */}
                    <button
                      type="button"
                      onClick={() => openFinancialAdvisor(summaryMonth)}
                      style={{
                        background: C.paper,
                        color: C.ink,
                        border: `1px solid ${C.line}`,
                        borderRadius: "0.75rem",
                        padding: "0.55rem 0",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        cursor: "pointer",
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "center",
                        gap: "0.4rem",
                        marginTop: "0.3rem",
                      }}
                      className="cc-btn-press"
                    >
                      <RefreshCw size={12} />
                      <span>Volver a analizar</span>
                    </button>
                  </div>
                ) : null}
              </div>
            </div>
          )}

          {/* Resumen mensual: navega por meses para ver cómo fue cada uno */}
          <section
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.25rem",
              padding: summaryCollapsed ? "0.95rem 1.35rem" : "1.5rem",
              boxShadow: C.cardShadow,
              border: `1px solid ${C.line}`,
              marginBottom: "1.5rem",
              transition: "padding 0.15s ease",
            }}
          >
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", marginBottom: summaryCollapsed ? 0 : "0.75rem" }}>
              <div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
                <button
                  type="button"
                  onClick={() => shiftMonth(-1)}
                  aria-label="Mes anterior"
                  title="Mes anterior"
                  style={{
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.5rem",
                    cursor: "pointer",
                    color: C.inkSoft,
                    fontSize: "1.1rem",
                    lineHeight: 1,
                    padding: "0.2rem 0.55rem",
                  }}
                  className="cc-btn-press"
                >
                  ‹
                </button>
                <div>
                  <div style={{ fontSize: "0.68rem", color: C.inkSoft, fontWeight: 600, letterSpacing: "0.05em", textTransform: "uppercase" }}>
                    Resumen mensual
                  </div>
                  <div style={{ fontSize: "0.95rem", fontWeight: 700, color: C.ink }}>{monthLabel(summaryMonth)}</div>
                </div>
                <button
                  type="button"
                  onClick={() => shiftMonth(1)}
                  aria-label="Mes siguiente"
                  title="Mes siguiente"
                  style={{
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.5rem",
                    cursor: "pointer",
                    color: C.inkSoft,
                    fontSize: "1.1rem",
                    lineHeight: 1,
                    padding: "0.2rem 0.55rem",
                  }}
                  className="cc-btn-press"
                >
                  ›
                </button>
              </div>

              <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                <button
                  type="button"
                  onClick={() => exportTransactionsToCsv(transactions, settings.currency, settings.accountName, summaryMonth, person)}
                  title="Exportar únicamente mis movimientos del mes a Excel / CSV"
                  style={{
                    background: C.field,
                    color: C.ink,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.6rem",
                    padding: "0.3rem 0.5rem",
                    fontSize: "0.7rem",
                    fontWeight: 700,
                    display: "flex",
                    alignItems: "center",
                    gap: "0.25rem",
                    cursor: "pointer",
                  }}
                  className="cc-btn-press"
                >
                  <Download size={11} />
                  <span>Mis Datos (Excel)</span>
                </button>

                <button
                  type="button"
                  onClick={() => {
                    const msg = generateWhatsAppSummary(summaryMonth, monthLabel(summaryMonth), transactions, people, totals, peerDebts.list, settings.currency, settings.accountName);
                    window.open(`https://api.whatsapp.com/send?text=${encodeURIComponent(msg)}`, "_blank");
                  }}
                  title="Compartir resumen del mes por WhatsApp"
                  style={{
                    background: "#25D366",
                    color: "#FFFFFF",
                    border: "none",
                    borderRadius: "0.6rem",
                    padding: "0.3rem 0.55rem",
                    fontSize: "0.7rem",
                    fontWeight: 700,
                    display: "flex",
                    alignItems: "center",
                    gap: "0.25rem",
                    cursor: "pointer",
                    boxShadow: "0 4px 10px -2px rgba(37,211,102,0.4)",
                  }}
                  className="cc-btn-press hover:brightness-105"
                >
                  <Share2 size={11} />
                  <span>WhatsApp</span>
                </button>

                <button
                  type="button"
                  onClick={() => openFinancialAdvisor(summaryMonth)}
                  title="Consejos y análisis financiero con IA"
                  style={{
                    background: BRAND_GRADIENT,
                    color: "#FFFFFF",
                    border: "none",
                    borderRadius: "0.6rem",
                    padding: "0.3rem 0.55rem",
                    fontSize: "0.7rem",
                    fontWeight: 700,
                    display: "flex",
                    alignItems: "center",
                    gap: "0.25rem",
                    cursor: "pointer",
                    boxShadow: "0 4px 10px -2px rgba(15,81,50,0.4)",
                  }}
                  className="cc-btn-press"
                >
                  <Sparkles size={11} />
                  <span>Consejero IA</span>
                </button>

                <span style={{ fontSize: "0.85rem", fontWeight: 700, color: monthSummary.neto >= 0 ? C.green : C.red }}>
                  Saldo: {fmtMoney(monthSummary.neto, settings.currency)}
                </span>
                <button
                  type="button"
                  onClick={() => {
                    setSummaryCollapsed((prev) => {
                      const next = !prev;
                      try { localStorage.setItem("cc:collapse:summary", next ? "1" : "0"); } catch (e) {}
                      return next;
                    });
                  }}
                  aria-label={summaryCollapsed ? "Desplegar resumen mensual" : "Minimizar resumen mensual"}
                  style={{
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.6rem",
                    color: C.inkSoft,
                    width: "1.75rem",
                    height: "1.75rem",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    cursor: "pointer",
                    padding: 0,
                  }}
                  title={summaryCollapsed ? "Desplegar sección" : "Minimizar sección"}
                >
                  {summaryCollapsed ? <ChevronDown size={15} /> : <ChevronUp size={15} />}
                </button>
              </div>
            </div>

            {!summaryCollapsed && (
              <>
                <div style={{ height: "1px", backgroundColor: C.line, margin: "0 0 1rem 0" }}></div>
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
                  <div>
                    <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", fontSize: "0.75rem", color: C.inkSoft, marginBottom: "0.15rem" }}>
                      <TrendingUp size={14} color={C.green} />
                      <span>Ingresos del mes</span>
                    </div>
                    <div style={{ fontSize: "1.1rem", fontWeight: 700, color: C.green }}>{fmtMoney(monthSummary.ingresos, settings.currency)}</div>
                  </div>
                  <div>
                    <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", fontSize: "0.75rem", color: C.inkSoft, marginBottom: "0.15rem" }}>
                      <TrendingDown size={14} color={C.red} />
                      <span>Gastos del mes</span>
                    </div>
                    <div style={{ fontSize: "1.1rem", fontWeight: 700, color: C.red }}>{fmtMoney(monthSummary.gastos, settings.currency)}</div>
                    {monthSummary.gastosTarjeta > 0 && (
                      <div style={{ fontSize: "0.68rem", color: C.inkSoft, marginTop: "0.1rem" }}>
                        Efec: {fmtMoney(monthSummary.gastosEfectivo, settings.currency)} · Tarj: {fmtMoney(monthSummary.gastosTarjeta, settings.currency)}
                      </div>
                    )}
                  </div>
                </div>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginTop: "0.85rem" }}>
                  <div>
                    <span style={{ fontSize: "0.8rem", color: C.inkSoft }}>Saldo del mes (Efectivo)</span>
                    {monthSummary.gastosTarjeta > 0 && (
                      <div style={{ fontSize: "0.68rem", color: C.inkSoft }}>
                        Balance neto: {fmtMoney(monthSummary.ingresos - monthSummary.gastos, settings.currency)}
                      </div>
                    )}
                  </div>
                  <span style={{ fontSize: "1.2rem", fontWeight: 700, color: monthSummary.neto >= 0 ? C.green : C.red }}>
                    {fmtMoney(monthSummary.neto, settings.currency)}
                  </span>
                </div>
                {monthSummary.porPersona.length > 0 && (
                  <>
                    <div style={{ height: "1px", backgroundColor: C.line, margin: "1rem 0" }}></div>
                    <div style={{ fontSize: "0.75rem", color: C.inkSoft, fontWeight: 500, letterSpacing: "0.05em", textTransform: "uppercase", marginBottom: "0.5rem" }}>
                      Por persona (este mes)
                    </div>
                    {monthSummary.porPersona.map(([n, v]) => (
                      <div
                        key={n}
                        style={{
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "space-between",
                          gap: "0.75rem",
                          padding: "0.45rem 0",
                          borderBottom: `1px solid ${C.lineSoft}`,
                        }}
                      >
                        <span style={{ display: "flex", alignItems: "center", gap: "0.45rem", fontWeight: 600, fontSize: "0.82rem", color: C.ink }}>
                          <span style={{ width: "0.55rem", height: "0.55rem", borderRadius: "50%", background: personColor(n), flexShrink: 0 }} />
                          {n}
                        </span>
                        <div style={{ display: "flex", gap: "0.75rem", alignItems: "center", fontSize: "0.76rem" }}>
                          <span style={{ color: C.green, fontWeight: 600 }}>+{fmtMoney(v.ingresos, settings.currency)}</span>
                          <span style={{ color: C.red, fontWeight: 600 }}>−{fmtMoney(v.gastos, settings.currency)}</span>
                          <span style={{ fontWeight: 700, color: v.saldoEfectivo >= 0 ? C.green : C.red }}>
                            Saldo: {v.saldoEfectivo >= 0 ? "+" : ""}{fmtMoney(v.saldoEfectivo, settings.currency)}
                          </span>
                        </div>
                      </div>
                    ))}
                  </>
                )}
              </>
            )}
          </section>

          {/* Desglose por persona: cada uno con sus ingresos y gastos resumidos. */}
          {!minimalMode && (porPersona.length > 0 || porPersonaHuérfanos.total > 0) && (
          <section
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.25rem",
              padding: peopleCollapsed ? "0.95rem 1.35rem" : "1.35rem",
              boxShadow: C.cardShadow,
              border: `1px solid ${C.line}`,
              marginBottom: "1.5rem",
              transition: "padding 0.15s ease",
            }}
            className="cc-card-hover"
          >
            <div
              onClick={() => {
                setPeopleCollapsed((prev) => {
                  const next = !prev;
                  try { localStorage.setItem("cc:collapse:people", next ? "1" : "0"); } catch (e) {}
                  return next;
                });
              }}
              role="button"
              tabIndex={0}
              aria-expanded={!peopleCollapsed}
              aria-label={peopleCollapsed ? "Desplegar desglose por persona" : "Minimizar desglose por persona"}
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                cursor: "pointer",
                userSelect: "none",
                marginBottom: peopleCollapsed ? 0 : "0.9rem",
                gap: "0.5rem",
              }}
            >
              <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                <Users size={15} color={C.green} />
                <span
                  style={{
                    fontSize: "0.86rem",
                    color: C.ink,
                    fontWeight: 700,
                    letterSpacing: "0.02em",
                  }}
                >
                  Resumen por persona ({porPersona.length})
                </span>
              </div>
              <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
                <span style={{ fontSize: "0.72rem", color: C.inkSoft }}>Saldo y aportes</span>
                <div
                  style={{
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.6rem",
                    color: C.inkSoft,
                    width: "1.75rem",
                    height: "1.75rem",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    flexShrink: 0,
                  }}
                  title={peopleCollapsed ? "Desplegar sección" : "Minimizar sección"}
                >
                  {peopleCollapsed ? <ChevronDown size={15} /> : <ChevronUp size={15} />}
                </div>
              </div>
            </div>

            {!peopleCollapsed && (
              <div style={{ display: "flex", flexDirection: "column", gap: "0.65rem" }}>
                {porPersona.map((p) => {
                  const esTu = p.nombre === person;
                  return (
                    <div
                      key={p.nombre}
                      style={{
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "space-between",
                        gap: "0.75rem",
                        padding: "0.65rem 0.75rem",
                        background: C.field,
                        borderRadius: "0.85rem",
                        border: `1px solid ${C.lineSoft}`,
                      }}
                    >
                      <div style={{ display: "flex", alignItems: "center", gap: "0.65rem", minWidth: 0 }}>
                        <div
                          style={{
                            width: "2.3rem",
                            height: "2.3rem",
                            borderRadius: "50%",
                            backgroundColor: personColor(p.nombre),
                            color: "#FFFFFF",
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            fontWeight: 700,
                            fontSize: "0.95rem",
                            flexShrink: 0,
                            boxShadow: "0 2px 6px rgba(0,0,0,0.1)",
                          }}
                        >
                          {(p.nombre || "?").trim().charAt(0).toUpperCase()}
                        </div>
                        <div style={{ minWidth: 0 }}>
                          <div
                            style={{
                              fontSize: "0.88rem",
                              fontWeight: 700,
                              color: C.ink,
                              whiteSpace: "nowrap",
                              overflow: "hidden",
                              textOverflow: "ellipsis",
                              display: "flex",
                              alignItems: "center",
                              gap: "0.35rem",
                            }}
                          >
                            <span>{p.nombre}</span>
                            {esTu && (
                              <span
                                style={{
                                  fontSize: "0.65rem",
                                  fontWeight: 800,
                                  background: darkMode ? "rgba(16,185,129,0.2)" : "#D1FAE5",
                                  color: darkMode ? "#6EE7B7" : "#065F46",
                                  padding: "0.05rem 0.35rem",
                                  borderRadius: "999px",
                                }}
                              >
                                Tú
                              </span>
                            )}
                          </div>

                          {/* Subtítulo simple y resumido */}
                          <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", flexWrap: "wrap", fontSize: "0.72rem", color: C.inkSoft, marginTop: "0.15rem" }}>
                            <span>Aportó: <strong style={{ color: C.green }}>{fmtMoney(p.ingresos, settings.currency)}</strong></span>
                            <span>·</span>
                            <span>Gastó: <strong style={{ color: C.red }}>{fmtMoney(p.gastos, settings.currency)}</strong></span>
                            {p.deudaTotal > 0 && (
                              <span
                                style={{
                                  background: darkMode ? "#451A03" : "#FEF3C7",
                                  color: darkMode ? "#FDE047" : "#B45309",
                                  fontSize: "0.64rem",
                                  fontWeight: 700,
                                  padding: "0.05rem 0.35rem",
                                  borderRadius: "0.35rem",
                                }}
                              >
                                💳 Debe: {fmtMoney(p.deudaTotal, settings.currency)}
                              </span>
                            )}
                          </div>
                        </div>
                      </div>

                      {/* Saldo de la persona a la derecha */}
                      <div style={{ textAlign: "right", flexShrink: 0 }}>
                        <div style={{ fontSize: "0.62rem", textTransform: "uppercase", color: C.inkSoft, fontWeight: 700, letterSpacing: "0.04em" }}>
                          Saldo
                        </div>
                        <div
                          style={{
                            fontSize: "1rem",
                            fontWeight: 800,
                            color: p.saldo >= 0 ? C.green : C.red,
                            whiteSpace: "nowrap",
                          }}
                        >
                          {p.saldo >= 0 ? "+" : ""}{fmtMoney(p.saldo, settings.currency)}
                        </div>
                      </div>
                    </div>
                  );
                })}

                {/* Movimientos sin persona asignada (huérfanos) si existen */}
                {porPersonaHuérfanos.total > 0 && (
                  <div
                    key="cc-orphan-row"
                    style={{
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "space-between",
                      gap: "0.75rem",
                      padding: "0.65rem 0.75rem",
                      background: darkMode ? "rgba(69, 26, 3, 0.2)" : "#FFFBEB",
                      border: `1px dashed ${darkMode ? "#92400E" : "#FCD34D"}`,
                      borderRadius: "0.85rem",
                    }}
                  >
                    <div style={{ display: "flex", alignItems: "center", gap: "0.65rem", minWidth: 0 }}>
                      <div
                        style={{
                          width: "2.3rem",
                          height: "2.3rem",
                          borderRadius: "50%",
                          backgroundColor: C.inkSoft,
                          color: C.paper,
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          fontWeight: 700,
                          fontSize: "0.95rem",
                          flexShrink: 0,
                        }}
                      >
                        ?
                      </div>
                      <div style={{ minWidth: 0 }}>
                        <div style={{ fontSize: "0.86rem", fontWeight: 700, color: C.ink }}>
                          Sin persona asignada
                        </div>
                        <div style={{ fontSize: "0.7rem", color: C.inkSoft, marginTop: "0.15rem" }}>
                          {porPersonaHuérfanos.count} mov{porPersonaHuérfanos.count === 1 ? "" : "s"} antiguos · Aportó {fmtMoney(porPersonaHuérfanos.ingresos, settings.currency)} · Gastó {fmtMoney(porPersonaHuérfanos.gastos, settings.currency)}
                        </div>
                      </div>
                    </div>
                    <div style={{ textAlign: "right", flexShrink: 0 }}>
                      <div style={{ fontSize: "0.62rem", textTransform: "uppercase", color: C.inkSoft, fontWeight: 700 }}>
                        Saldo
                      </div>
                      <div
                        style={{
                          fontSize: "1rem",
                          fontWeight: 800,
                          color: porPersonaHuérfanos.saldo >= 0 ? C.green : C.red,
                          whiteSpace: "nowrap",
                        }}
                      >
                        {porPersonaHuérfanos.saldo >= 0 ? "+" : ""}{fmtMoney(porPersonaHuérfanos.saldo, settings.currency)}
                      </div>
                    </div>
                  </div>
                )}
              </div>
            )}
          </section>
          )}

          {/* Sellos de balance (N personas) */}
          {settleUp && (
            <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem", marginBottom: "1.5rem" }}>
              {settleUp.map((s, idx) => (
                <div
                  key={idx}
                  style={{
                    backgroundColor: s.neutral ? C.field : C.dangerBg,
                    color: s.neutral ? C.inkSoft : C.red,
                    borderRadius: "0.5rem",
                    fontSize: "0.85rem",
                    fontWeight: 600,
                  }}
                  className="px-3 py-2"
                >
                  {s.text}
                </div>
              ))}
            </div>
          )}

          {/* ============================================================
              METAS DE AHORRO Y ALCANCÍAS COMPARTIDAS
              ============================================================ */}
          {!minimalMode && (
          <section
            aria-label="Metas de Ahorro y Alcancías"
            style={{
              backgroundColor: C.paper,
              borderRadius: "1.25rem",
              padding: savingsCollapsed ? "0.95rem 1.35rem" : "1.35rem",
              boxShadow: C.cardShadow,
              border: `1px solid ${C.line}`,
              marginBottom: "1.5rem",
              transition: "padding 0.15s ease",
            }}
            className="cc-card-hover"
          >
            <div
              onClick={() => {
                setSavingsCollapsed((prev) => {
                  const next = !prev;
                  try { localStorage.setItem("cc:collapse:savings", next ? "1" : "0"); } catch (e) {}
                  return next;
                });
              }}
              role="button"
              tabIndex={0}
              aria-expanded={!savingsCollapsed}
              aria-label={savingsCollapsed ? "Desplegar metas de ahorro" : "Minimizar metas de ahorro"}
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                cursor: "pointer",
                userSelect: "none",
                marginBottom: savingsCollapsed ? 0 : "0.9rem",
                gap: "0.5rem",
              }}
            >
              <div style={{ display: "flex", alignItems: "center", gap: "0.45rem" }}>
                <span style={{ fontSize: "1.2rem" }}>🎯</span>
                <div>
                  <div style={{ fontSize: "0.86rem", color: C.ink, fontWeight: 700, letterSpacing: "0.02em" }}>
                    Metas de Ahorro y Alcancías ({(settings.savingsGoals || []).length})
                  </div>
                  <div style={{ fontSize: "0.68rem", color: C.inkSoft }}>Fondos comunes y proyectos compartidos</div>
                </div>
              </div>

              <div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
                <button
                  type="button"
                  onClick={(e) => {
                    e.stopPropagation();
                    setSavingsDraft({ id: "", name: "", targetAmount: "", currentAmount: "", targetDate: "", color: "emerald" });
                    setShowSavingsModal(true);
                  }}
                  style={{
                    background: BRAND_GRADIENT,
                    color: "#FFFFFF",
                    border: "none",
                    borderRadius: "0.55rem",
                    padding: "0.22rem 0.5rem",
                    fontSize: "0.68rem",
                    fontWeight: 700,
                    cursor: "pointer",
                    display: "flex",
                    alignItems: "center",
                    gap: "0.2rem",
                  }}
                  className="cc-btn-press"
                >
                  <Plus size={11} />
                  <span>Nueva Meta</span>
                </button>
                <div
                  style={{
                    background: C.field,
                    border: `1px solid ${C.line}`,
                    borderRadius: "0.6rem",
                    color: C.inkSoft,
                    width: "1.75rem",
                    height: "1.75rem",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    flexShrink: 0,
                  }}
                >
                  {savingsCollapsed ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
                </div>
              </div>
            </div>

            {!savingsCollapsed && (
              <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
                {(settings.savingsGoals || []).length === 0 ? (
                  <div style={{ textAlign: "center", padding: "1.2rem", background: C.field, borderRadius: "0.85rem", border: `1px dashed ${C.line}` }}>
                    <div style={{ fontSize: "1.6rem", marginBottom: "0.2rem" }}>🏖️</div>
                    <p style={{ margin: 0, fontSize: "0.82rem", fontWeight: 700, color: C.ink }}>No hay metas de ahorro activas</p>
                    <p style={{ margin: "0.2rem 0 0", fontSize: "0.72rem", color: C.inkSoft }}>
                      Crea una alcancía compartida (ej. Vacaciones, Fondo de emergencia, Compras).
                    </p>
                  </div>
                ) : (
                  (settings.savingsGoals || []).map((goal) => {
                    const target = Number(goal.targetAmount) || 0;
                    const current = Number(goal.currentAmount) || 0;
                    const pct = target > 0 ? Math.min(100, Math.round((current / target) * 100)) : 100;
                    const isDone = pct >= 100;

                    return (
                      <div
                        key={goal.id}
                        style={{
                          background: C.field,
                          border: `1px solid ${isDone ? C.green : C.line}`,
                          borderRadius: "0.85rem",
                          padding: "0.85rem",
                          display: "flex",
                          flexDirection: "column",
                          gap: "0.5rem",
                        }}
                      >
                        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
                          <div>
                            <div style={{ fontWeight: 800, color: C.ink, fontSize: "0.88rem", display: "flex", alignItems: "center", gap: "0.35rem" }}>
                              <span>{goal.name}</span>
                              {isDone && <span style={{ background: C.successBg, color: C.green, fontSize: "0.62rem", padding: "0.1rem 0.4rem", borderRadius: "999px", fontWeight: 700 }}>¡Completada! 🎉</span>}
                            </div>
                            {goal.targetDate && (
                              <div style={{ fontSize: "0.68rem", color: C.inkSoft }}>Meta: {goal.targetDate}</div>
                            )}
                          </div>
                          <div style={{ textAlign: "right" }}>
                            <div style={{ fontSize: "0.92rem", fontWeight: 900, color: C.ink }}>
                              {fmtMoney(current, settings.currency)} <span style={{ fontSize: "0.72rem", fontWeight: 600, color: C.inkSoft }}>/ {fmtMoney(target, settings.currency)}</span>
                            </div>
                            <div style={{ fontSize: "0.7rem", fontWeight: 700, color: isDone ? C.green : C.brand }}>
                              {pct}% acumulado
                            </div>
                          </div>
                        </div>

                        {/* Barra de progreso */}
                        <div style={{ width: "100%", height: "0.55rem", background: C.paper, borderRadius: "999px", overflow: "hidden", border: `1px solid ${C.lineSoft}` }}>
                          <div
                            style={{
                              width: `${pct}%`,
                              height: "100%",
                              background: isDone ? "linear-gradient(90deg, #10B981 0%, #059669 100%)" : BRAND_GRADIENT,
                              borderRadius: "999px",
                              transition: "width 0.3s ease",
                            }}
                          />
                        </div>

                        {/* Acciones de la meta */}
                        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", paddingTop: "0.2rem" }}>
                          <button
                            type="button"
                            onClick={() => {
                              const amountStr = prompt(`¿Cuánto deseas aportar a "${goal.name}" (${settings.currency})?:`);
                              if (!amountStr) return;
                              const addVal = Number(amountStr.replace(",", "."));
                              if (isNaN(addVal) || addVal <= 0) return;
                              const updatedGoals = (settings.savingsGoals || []).map(g => {
                                if (g.id === goal.id) {
                                  return { ...g, currentAmount: (Number(g.currentAmount) || 0) + addVal };
                                }
                                return g;
                              });
                              updateSettings({ savingsGoals: updatedGoals });
                            }}
                            style={{
                              background: C.paper,
                              color: C.ink,
                              border: `1px solid ${C.line}`,
                              borderRadius: "0.5rem",
                              padding: "0.2rem 0.55rem",
                              fontSize: "0.68rem",
                              fontWeight: 700,
                              cursor: "pointer",
                            }}
                            className="cc-btn-press"
                          >
                            + Aportar dinero
                          </button>

                          <button
                            type="button"
                            onClick={() => {
                              if (confirm(`¿Eliminar la meta "${goal.name}"?`)) {
                                const updatedGoals = (settings.savingsGoals || []).filter(g => g.id !== goal.id);
                                updateSettings({ savingsGoals: updatedGoals });
                              }
                            }}
                            style={{ background: "none", border: "none", color: C.red, fontSize: "0.68rem", cursor: "pointer", padding: "0.2rem" }}
                          >
                            Eliminar
                          </button>
                        </div>
                      </div>
                    );
                  })
                )}
              </div>
            )}
          </section>
          )}

          {/* Pie de página permanente con enlaces rápidos a Ajustes y Novedades */}
          <div className="mb-8 md:mb-0">
            <div style={{ display: "flex", alignItems: "center", gap: "1.25rem", flexWrap: "wrap" }}>
              <button
                type="button"
                onClick={openSettings}
                style={{ color: C.inkSoft, cursor: "pointer", border: "none", background: "none", padding: 0 }}
                className="text-xs font-semibold uppercase tracking-widest flex items-center gap-1 hover:opacity-80"
              >
                <Settings size={12} /> Ajustes
              </button>
              <button
                type="button"
                onClick={openChangelog}
                style={{ color: C.inkSoft, cursor: "pointer", border: "none", background: "none", padding: 0 }}
                className="text-xs font-semibold uppercase tracking-widest flex items-center gap-1.5 hover:opacity-80"
              >
                <Sparkles size={12} color={C.green} /> Novedades y cambios
                <span
                  style={{
                    background: hasUnseenChangelog ? C.green : C.chipBg,
                    color: hasUnseenChangelog ? "#FFFFFF" : C.inkSoft,
                    borderRadius: "999px",
                    padding: "0.05rem 0.35rem",
                    fontSize: "0.62rem",
                    fontWeight: 700,
                  }}
                >
                  {APP_CHANGELOG[0]?.version || "v25"}
                </span>
              </button>
            </div>

        </div>
      </div>

        {/* Aviso: hay cambios guardados solo en este dispositivo (nube caída).
            Se suben automáticamente al recuperar la conexión. */}
        {pendingWrites > 0 && (
          <div
            role="status"
            style={{
              display: "flex",
              alignItems: "center",
              gap: "0.4rem",
              backgroundColor: darkMode ? "#422006" : "#FEF3C7",
              border: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`,
              color: darkMode ? "#FDE68A" : "#92400E",
              borderRadius: "0.75rem",
              padding: "0.5rem 0.75rem",
              marginBottom: "0.75rem",
              fontSize: "0.8rem",
            }}
          >
            <RefreshCw size={14} />
            <span>
              <strong>{pendingWrites} cambio{pendingWrites === 1 ? "" : "s"} sin subir a la nube</strong> — se subirá{pendingWrites === 1 ? "" : "n"} automáticamente al recuperar la conexión{pendingElapsedLabel(pendingSince)}.
            </span>
          </div>
        )}

        {/* Aviso de movimientos duplicados/fantasma (limpieza de antes del arreglo) */}
        {dupGroups.length > 0 && (
          <div
            id="cc-dup-banner"
            style={{
              backgroundColor: darkMode ? "#451A03" : "#FEF3C7",
              border: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`,
              color: darkMode ? "#FDE68A" : "#92400E",
              borderRadius: "1.4rem",
              padding: "1.25rem 1.5rem",
              marginBottom: "1.25rem",
            }}
          >
            {/* Cabecera del panel */}
            <PanelHeader
              icon={<Trash2 size={16} />}
              title={<>
                Movimientos duplicados{" "}
                <span style={{ fontWeight: 600, color: C.inkSoft }}>({dupCount})</span>
              </>}
              subtitle="Misma descripción, importe y fecha con distinto id: deja solo uno de cada grupo"
              subtitleColor={darkMode ? "#FDE68A" : "#92400E"}
              expanded={showDupPanel}
              onAction={() => setShowDupPanel((v) => !v)}
              actionLabel={showDupPanel ? "Cerrar revisión de duplicados" : "Revisar duplicados"}
              actionTitle={showDupPanel ? "Cerrar" : "Revisar"}
              actionIcon={showDupPanel ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
              actionBg={darkMode ? "rgba(253,230,138,0.14)" : "rgba(146,64,14,0.10)"}
              actionColor={darkMode ? "#FDE68A" : "#92400E"}
            />

            {showDupPanel && (
              <div style={{ marginTop: "0.75rem", borderTop: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}`, paddingTop: "0.75rem" }}>
                {dupGroups.map((g) => {
                  const peopleIn = [...new Set(g.items.map((i) => i.person || "Alguien"))];
                  const keep = g.keep;
                  const ghosts = dupGhostItemsOf(g);
                  const target = (people || []).includes(dupTargets[g.sig]) ? dupTargets[g.sig] : dupDefaultTarget(g);
                  const askConsolidate = dupGhostConfirm === g.sig;
                  return (
                    <div key={g.sig} className="flex items-center justify-between gap-3 mb-2">
                      <div className="text-xs leading-snug">
                        <div>
                          <strong>{keep.description || "Sin descripción"}</strong> · {fmtDate(keep.date)} ·{" "}
                          {fmtMoney(keep.amount, txCurrency(keep, settings.currency))}
                        </div>
                        <div style={{ opacity: 0.8 }}>
                          {peopleIn.join(" y ")} · {g.items.length} copias
                          {ghosts.length > 0 && (
                            <span> · {ghosts.length} de nombre antiguo</span>
                          )}
                        </div>
                      </div>
                      <div className="flex gap-2 whitespace-nowrap items-center">
                        {ghosts.length > 0 && (
                          <>
                            <select
                              value={target}
                              onChange={(e) => setDupTargets((prev) => ({ ...(prev || {}), [g.sig]: e.target.value }))}
                              disabled={dupBusy}
                              style={{ background: "transparent", border: `1px solid ${darkMode ? "#A855F7" : "#D8B4FE"}`, color: "inherit", borderRadius: "8px", padding: "0.3rem 0.5rem" }}
                              className="text-xs"
                            >
                              {(people || []).map((p) => (
                                <option key={p} value={p}>{p}</option>
                              ))}
                            </select>
                            <button
                              onClick={() => (askConsolidate ? consolidateDupGhost(g, target) : setDupGhostConfirm(g.sig))}
                              disabled={dupBusy || !target}
                              style={{ background: darkMode ? "#A855F7" : "#6B21A8", color: "#FAF5FF", border: "none", cursor: "pointer", borderRadius: "8px", padding: "0.35rem 0.7rem" }}
                              className="text-xs"
                            >
                              {askConsolidate ? "¿Consolidar en " + target + "?" : "Consolidar"}
                            </button>
                            {askConsolidate && (
                              <button
                                onClick={() => setDupGhostConfirm(null)}
                                disabled={dupBusy}
                                style={{ background: "transparent", border: "none", cursor: "pointer" }}
                                className="text-xs"
                              >
                                Cancelar
                              </button>
                            )}
                          </>
                        )}
                        <button
                          onClick={() => omitDupGroup(g.sig)}
                          disabled={dupBusy}
                          style={{ background: "transparent", border: "none", cursor: "pointer" }}
                          className="text-xs"
                        >
                          Omitir
                        </button>
                        <button
                          onClick={() => cleanDuplicates([g])}
                          disabled={dupBusy}
                          style={{ background: darkMode ? "#F59E0B" : "#92400E", color: darkMode ? "#1F2937" : "#FFFBEB", border: "none", cursor: "pointer", borderRadius: "8px", padding: "0.35rem 0.7rem" }}
                          className="text-xs"
                        >
                          Dejar 1 de {g.items.length}
                        </button>
                      </div>
                    </div>
                  );
                })}
                <div className="flex items-center justify-between flex-wrap gap-2 mt-3 pt-2" style={{ borderTop: `1px solid ${darkMode ? "#92400E" : "#FCD34D"}` }}>
                  <button
                    type="button"
                    onClick={() => {
                      const allSigs = dupGroups.map(g => g.sig);
                      setDupDismissed((prev) => {
                        const next = [...new Set([...(Array.isArray(prev) ? prev : []), ...allSigs])];
                        try {
                          localStorage.setItem("cc:dupDismissed", JSON.stringify(next));
                        } catch (e) {}
                        return next;
                      });
                      setShowDupPanel(false);
                    }}
                    disabled={dupBusy}
                    style={{ background: "transparent", color: "inherit", border: `1px solid ${darkMode ? "#FDE68A" : "#92400E"}`, cursor: "pointer", borderRadius: "8px", padding: "0.4rem 0.8rem", opacity: 0.9 }}
                    className="text-xs font-semibold cc-btn-press"
                  >
                    ✓ Son válidos (descartar aviso)
                  </button>
                  <button
                    type="button"
                    onClick={() => cleanDuplicates(dupGroups)}
                    disabled={dupBusy}
                    style={{ background: darkMode ? "#EF4444" : "#B91C1C", color: "#FEF2F2", border: "none", cursor: "pointer", borderRadius: "8px", padding: "0.4rem 0.8rem" }}
                    className="text-xs uppercase tracking-widest font-bold cc-btn-press"
                  >
                    {dupBusy ? "Limpiando…" : "Limpiar todos los duplicados"}
                  </button>
                </div>
              </div>
            )}
          </div>
        )}

        {/* Aviso de movimientos huérfanos (persona ya no está en la cuenta) */}
        {/* Sin el gate de people: si la lista está vacía (caso borde) los
            huérfanos también deben poder revisarse. */}
        {orphanGroups.length > 0 && (
          <div
            id="cc-orphan-banner"
            style={{
              backgroundColor: darkMode ? "#3B0764" : "#F3E8FF",
              border: `1px solid ${darkMode ? "#A855F7" : "#D8B4FE"}`,
              color: darkMode ? "#E9D5FF" : "#6B21A8",
              borderRadius: "1.4rem",
              padding: "1.25rem 1.5rem",
              marginBottom: "1.25rem",
            }}
          >
            {/* Cabecera del panel */}
            <PanelHeader
              icon={<Users size={16} />}
              title={<>
                Movimientos sin persona{" "}
                <span style={{ fontWeight: 600, color: C.inkSoft }}>({orphanCount})</span>
              </>}
              subtitle={orphanGroups.map((g) => g.person).join(", ") + ": asígnalos a una persona actual o bórralos"}
              subtitleColor={darkMode ? "#E9D5FF" : "#6B21A8"}
              expanded={showOrphanPanel}
              onAction={() => setShowOrphanPanel((v) => !v)}
              actionLabel={showOrphanPanel ? "Cerrar revisión de movimientos sin persona" : "Revisar movimientos sin persona"}
              actionTitle={showOrphanPanel ? "Cerrar" : "Revisar"}
              actionIcon={showOrphanPanel ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
              actionBg={darkMode ? "rgba(233,213,255,0.14)" : "rgba(107,33,168,0.10)"}
              actionColor={darkMode ? "#E9D5FF" : "#6B21A8"}
            />

            {showOrphanPanel && (
              <div style={{ marginTop: "0.75rem", borderTop: `1px solid ${darkMode ? "#A855F7" : "#D8B4FE"}`, paddingTop: "0.75rem" }}>
                {orphanGroups.map((g) => {
                  const key = String(g.person || "").trim().toLowerCase();
                  // El destino debe seguir en la lista: si se guardó uno que ya
                  // no existe (lo quitaron de Ajustes), se usa el destino por
                  // defecto para no renombrar a un no-miembro (re-huérfano).
                  const target = (people || []).includes(orphanTargets[key])
                    ? orphanTargets[key]
                    : orphanDefaultTarget(key);
                  const total = g.items.reduce((s, t) => s + txToBase(t, settings.currency || "S/"), 0);
                  const confirming = orphanConfirm && orphanConfirm.key === key;
                  const askDelete = confirming && orphanConfirm.action === "delete";
                  const askMigrate = confirming && orphanConfirm.action === "migrate";
                  const expanded = !!orphanExpanded[key];
                  return (
                    <div key={key} className="mb-2">
                      <div className="flex items-center justify-between gap-3">
                        <div className="text-xs leading-snug">
                          <div>
                            <strong>{g.person}</strong> · {g.items.length} movimiento{g.items.length === 1 ? "" : "s"} · total {fmtMoney(total, settings.currency || "S/")}
                          </div>
                          <div style={{ opacity: 0.8 }}>
                            {g.items.slice(0, 2).map((t) => t.description).join(" · ")}{g.items.length > 2 ? "…" : ""}
                          </div>
                          <button
                            type="button"
                            aria-expanded={!!expanded}
                            onClick={() => setOrphanExpanded((prev) => ({ ...(prev || {}), [key]: !prev[key] }))}
                            disabled={orphanBusy}
                            style={{ background: "transparent", border: "none", cursor: "pointer", color: "inherit", fontWeight: 600, padding: "0.15rem 0 0" }}
                            className="text-xs"
                          >
                            {expanded ? "Ocultar movimientos ▲" : "Ver " + g.items.length + " movimiento" + (g.items.length === 1 ? "" : "s") + " ▼"}
                          </button>
                        </div>
                      <div className="flex gap-2 whitespace-nowrap items-center">
                        <select
                          value={target}
                          onChange={(e) => setOrphanTargets((prev) => ({ ...(prev || {}), [key]: e.target.value }))}
                          disabled={orphanBusy}
                          style={{ background: "transparent", border: `1px solid ${darkMode ? "#A855F7" : "#D8B4FE"}`, color: "inherit", borderRadius: "8px", padding: "0.3rem 0.5rem" }}
                          className="text-xs"
                        >
                          {(people || []).map((p) => (
                            <option key={p} value={p}>{p}</option>
                          ))}
                        </select>
                        <button
                          onClick={() => (askMigrate ? migrateOrphans(g, target) : setOrphanConfirm({ key, action: "migrate" }))}
                          disabled={orphanBusy || !target}
                          style={{ background: darkMode ? "#A855F7" : "#6B21A8", color: "#FAF5FF", border: "none", cursor: "pointer", borderRadius: "8px", padding: "0.35rem 0.7rem" }}
                          className="text-xs"
                        >
                          {askMigrate ? "¿Asignar a " + target + "?" : "Asignar"}
                        </button>
                        <button
                          onClick={() => (askDelete ? deleteOrphans(g) : setOrphanConfirm({ key, action: "delete" }))}
                          disabled={orphanBusy}
                          style={{ background: darkMode ? "#EF4444" : "#B91C1C", color: "#FEF2F2", border: "none", cursor: "pointer", borderRadius: "8px", padding: "0.35rem 0.7rem" }}
                          className="text-xs"
                        >
                          {askDelete ? "¿Borrar?" : "Borrar"}
                        </button>
                        {confirming && (
                          <button
                            onClick={() => setOrphanConfirm(null)}
                            disabled={orphanBusy}
                            style={{ background: "transparent", border: "none", cursor: "pointer" }}
                            className="text-xs"
                          >
                            Cancelar
                          </button>
                        )}
                        <button
                          onClick={() => omitOrphanGroup(key)}
                          disabled={orphanBusy}
                          style={{ background: "transparent", border: "none", cursor: "pointer" }}
                          className="text-xs"
                        >
                          Omitir
                        </button>
                      </div>
                    </div>
                    {expanded && (
                      <div
                        style={{
                          marginTop: "0.4rem",
                          marginLeft: "0.35rem",
                          paddingLeft: "0.75rem",
                          borderLeft: `2px solid ${darkMode ? "#A855F7" : "#D8B4FE"}`,
                          display: "flex",
                          flexDirection: "column",
                          gap: "0.3rem",
                        }}
                      >
                        {g.items.map((t) => (
                          <div key={t.id} className="flex items-center justify-between gap-2 text-xs">
                            <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }}>
                              {t.description || "Sin descripción"} · {fmtDate(t.date)}
                            </span>
                            <span style={{ color: t.type === "gasto" ? C.red : C.green, whiteSpace: "nowrap" }}>
                              {t.type === "gasto" ? "−" : "+"} {fmtMoney(Number(t.amount) || 0, txCurrency(t, settings.currency))}
                            </span>
                          </div>
                        ))}
                      </div>
                    )}
                  </div>
                );
                })}
              </div>
            )}
          </div>
        )}

        {/* Lista de movimientos */}
        <div>
          {grouped.length === 0 && (
            <div className="text-center py-10 cc-rise" style={{ color: C.inkSoft }}>
              <div
                style={{
                  width: "3.2rem",
                  height: "3.2rem",
                  borderRadius: "1rem",
                  background: C.chipBg,
                  color: C.inkFaint,
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  margin: "0 auto 0.8rem auto",
                }}
              >
                <Receipt size={22} />
              </div>
              <p style={{ fontFamily: "'Inter', sans-serif", fontSize: "0.9rem", fontWeight: 600, color: C.ink, margin: "0 0 0.25rem 0" }}>
                Todavía no hay movimientos
              </p>
              <p style={{ fontFamily: "'Inter', sans-serif", fontSize: "0.8rem", margin: 0 }} className="m-0">
                Anota el primero y se verá aquí al instante.
              </p>
            </div>
          )}
          {grouped.map(([d, items]) => {
            const dayIngresos = items.filter((t) => t.type === "ingreso").reduce((acc, t) => acc + txToBase(t, settings.currency || "S/"), 0);
            const dayGastos = items.filter((t) => t.type === "gasto" && !isPeerLoanTx(t)).reduce((acc, t) => acc + txToBase(t, settings.currency || "S/"), 0);
            const dayNet = dayIngresos - dayGastos;
            return (
              <div
                key={d}
                style={{
                  backgroundColor: C.paper,
                  borderRadius: "1.25rem",
                  padding: "1rem 1.15rem",
                  boxShadow: C.cardShadow,
                  border: `1px solid ${C.line}`,
                  marginBottom: "1.1rem",
                }}
                className="cc-card-hover"
              >
                {/* Cabecera del día con balance resumido */}
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem", marginBottom: "0.75rem" }}>
                  <span
                    style={{
                      background: C.chipBg,
                      color: C.inkSoft,
                      fontSize: "0.7rem",
                      fontWeight: 700,
                      letterSpacing: "0.04em",
                      textTransform: "uppercase",
                      padding: "0.22rem 0.6rem",
                      borderRadius: "999px",
                      display: "inline-flex",
                      alignItems: "center",
                      gap: "0.35rem",
                    }}
                  >
                    <Calendar size={12} color={C.green} />
                    {fmtDate(d)}
                  </span>

                  <div style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
                    {dayNet !== 0 && (
                      <span
                        style={{
                          fontSize: "0.72rem",
                          fontWeight: 700,
                          color: dayNet > 0 ? C.green : C.red,
                        }}
                      >
                        {dayNet > 0 ? `+${fmtMoney(dayNet, settings.currency || "S/")}` : fmtMoney(dayNet, settings.currency || "S/")}
                      </span>
                    )}
                    <span style={{ color: C.inkFaint, fontSize: "0.68rem", fontWeight: 600 }}>
                      {items.length} {items.length === 1 ? "mov." : "movs."}
                    </span>
                  </div>
                </div>

                {/* Lista de movimientos del día */}
                <div style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}>
                  {items.map((t, tIdx) => {
                    const tCard = (settings.cards || []).find((c) => c && c.id === t.cardId);
                    const tInst = (settings.installments || []).find((i) => i && i.id === (t.installmentId || t.installment_id))
                      || (settings.installments || []).find((i) => i && t.description && ((i.name && String(t.description).toLowerCase().includes(String(i.name).toLowerCase())) || (i.recipient && String(t.description).toLowerCase().includes(String(i.recipient).toLowerCase()))));
                    const isCardPayment = t.type === "pago_tarjeta";
                    const isPrestamo = t.type === "prestamo";
                    const isGasto = t.type === "gasto";
                    const isIngreso = t.type === "ingreso";

                    const badgeBg = isIngreso
                      ? (darkMode ? "rgba(16,185,129,0.15)" : "#ECFDF5")
                      : isPrestamo
                      ? (darkMode ? "rgba(245,158,11,0.15)" : "#FEF3C7")
                      : isCardPayment
                      ? (darkMode ? "rgba(99,102,241,0.15)" : "#EEF2FF")
                      : (darkMode ? "rgba(239,68,68,0.12)" : "#FEE2E2");

                    const badgeColor = isIngreso
                      ? (darkMode ? "#6EE7B7" : "#047857")
                      : isPrestamo
                      ? (darkMode ? "#FDE047" : "#B45309")
                      : isCardPayment
                      ? (darkMode ? "#A5B4FC" : "#4338CA")
                      : (darkMode ? "#FCA5A5" : "#B91C1C");

                    return (
                      <div
                        key={t.id}
                        id={"tx-row-" + t.id}
                        style={{
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "space-between",
                          gap: "0.65rem",
                          padding: "0.5rem 0.55rem",
                          borderRadius: "0.75rem",
                          background: tIdx % 2 === 0 ? "transparent" : (darkMode ? "rgba(255,255,255,0.015)" : "rgba(0,0,0,0.015)"),
                          border: `1px solid transparent`,
                          transition: "all 0.15s ease",
                        }}
                        className={"dotted-row group" + (highlightTx === String(t.id) ? " cc-tx-highlight" : "")}
                      >
                        {/* Icono de tipo */}
                        <div
                          style={{
                            width: "2.1rem",
                            height: "2.1rem",
                            borderRadius: "0.65rem",
                            background: badgeBg,
                            color: badgeColor,
                            display: "flex",
                            alignItems: "center",
                            justifyContent: "center",
                            flexShrink: 0,
                          }}
                        >
                          {isIngreso ? (
                            <TrendingUp size={14} strokeWidth={2.4} />
                          ) : isPrestamo ? (
                            <Users size={14} strokeWidth={2.4} />
                          ) : isCardPayment ? (
                            <CreditCard size={14} strokeWidth={2.4} />
                          ) : t.paymentMethod === "tarjeta" || t.cardId ? (
                            <CreditCard size={14} strokeWidth={2.2} />
                          ) : t.paymentMethod === "transferencia" ? (
                            <Landmark size={14} strokeWidth={2.2} />
                          ) : (
                            <TrendingDown size={14} strokeWidth={2.4} />
                          )}
                        </div>

                        {/* Descripción y Metadata */}
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", flexWrap: "wrap" }}>
                            <span style={{ fontFamily: "'Inter', sans-serif", fontSize: "0.84rem", fontWeight: 700, color: C.ink, wordBreak: "break-word" }}>
                              {t.description || "Sin descripción"}
                            </span>

                            {(t.hasPhoto || t.photo) && (
                              <button
                                type="button"
                                onClick={() => openPhoto(t)}
                                aria-label="Ver recibo"
                                title="Ver recibo"
                                style={{
                                  background: "none",
                                  border: "none",
                                  cursor: "pointer",
                                  padding: 0,
                                  display: "inline-flex",
                                  alignItems: "center",
                                }}
                              >
                                {t.photo ? (
                                  <img
                                    src={t.photo}
                                    alt="Recibo"
                                    style={{ width: "1.15rem", height: "1.15rem", objectFit: "cover", borderRadius: "0.25rem", border: `1px solid ${C.line}` }}
                                  />
                                ) : (
                                  <Camera size={13} color={C.inkSoft} />
                                )}
                              </button>
                            )}
                          </div>

                          {/* Fila secundaria: persona, etiquetas y métodos */}
                          <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", flexWrap: "wrap", marginTop: "0.15rem", fontSize: "0.7rem", color: C.inkSoft }}>
                            <span style={{ display: "inline-flex", alignItems: "center", gap: "0.2rem", fontWeight: 600 }}>
                              <span style={{ width: "0.45rem", height: "0.45rem", borderRadius: "50%", background: personColor(t.person), display: "inline-block" }} />
                              {t.person}
                            </span>

                            {t.category && (
                              <span
                                style={{
                                  background: C.chipBg,
                                  color: C.inkSoft,
                                  fontSize: "0.64rem",
                                  padding: "0.06rem 0.35rem",
                                  borderRadius: "999px",
                                  fontWeight: 600,
                                  display: "inline-flex",
                                  alignItems: "center",
                                  gap: "0.15rem",
                                }}
                              >
                                <span>{getCategoryIcon(t.category)}</span>
                                <span>{t.category}</span>
                              </span>
                            )}

                            {isPrestamo ? (
                              <span
                                style={{
                                  background: darkMode ? "rgba(245, 158, 11, 0.2)" : "#FEF3C7",
                                  color: darkMode ? "#FCD34D" : "#B45309",
                                  fontSize: "0.64rem",
                                  padding: "0.06rem 0.35rem",
                                  borderRadius: "999px",
                                  fontWeight: 700,
                                }}
                              >
                                ➔ {t.recipient || "otro"}
                              </span>
                            ) : isCardPayment ? (
                              <span
                                style={{
                                  background: darkMode ? "#312E81" : "#EEF2FF",
                                  color: darkMode ? "#C7D2FE" : "#4338CA",
                                  fontSize: "0.64rem",
                                  padding: "0.06rem 0.35rem",
                                  borderRadius: "999px",
                                  fontWeight: 700,
                                }}
                              >
                                💳 {tCard ? tCard.name : "Tarjeta"}
                              </span>
                            ) : t.paymentMethod === "tarjeta" || t.cardId ? (
                              <span
                                style={{
                                  background: C.chipBg,
                                  color: C.inkSoft,
                                  fontSize: "0.64rem",
                                  padding: "0.06rem 0.35rem",
                                  borderRadius: "999px",
                                  fontWeight: 600,
                                }}
                              >
                                💳 {tCard ? tCard.name : "Tarjeta"}
                              </span>
                            ) : t.paymentMethod === "transferencia" ? (
                              <span
                                style={{
                                  background: C.chipBg,
                                  color: C.inkSoft,
                                  fontSize: "0.64rem",
                                  padding: "0.06rem 0.35rem",
                                  borderRadius: "999px",
                                  fontWeight: 600,
                                }}
                              >
                                🏦 Transfer
                              </span>
                            ) : null}

                            {tInst ? (
                              <span
                                style={{
                                  background: darkMode ? "#451A03" : "#FEF3C7",
                                  color: darkMode ? "#FDE047" : "#B45309",
                                  fontSize: "0.64rem",
                                  padding: "0.06rem 0.35rem",
                                  borderRadius: "999px",
                                  fontWeight: 700,
                                }}
                              >
                                📅 {tInst.name}
                              </span>
                            ) : (t.description && /cuota\s*\d+/i.test(t.description)) ? (
                              <span
                                style={{
                                  background: darkMode ? "#451A03" : "#FEF3C7",
                                  color: darkMode ? "#FDE047" : "#B45309",
                                  fontSize: "0.64rem",
                                  padding: "0.06rem 0.35rem",
                                  borderRadius: "999px",
                                  fontWeight: 700,
                                }}
                              >
                                📅 Cuota
                              </span>
                            ) : null}
                          </div>

                          {/* Nota / Comentario */}
                          {t.note && (() => {
                            const cleanNote = t.note.replace(/\[canje_cashback\]/g, "").replace(/\[saldado\]/g, "").trim();
                            if (!cleanNote) return null;
                            return (
                              <div
                                style={{
                                  fontFamily: "'Inter', sans-serif",
                                  color: C.inkSoft,
                                  fontSize: "0.71rem",
                                  fontStyle: "italic",
                                  marginTop: "0.12rem",
                                  lineHeight: 1.3,
                                  display: "flex",
                                  alignItems: "flex-start",
                                  gap: "0.25rem",
                                  wordBreak: "break-word",
                                }}
                              >
                                <span style={{ opacity: 0.65, fontStyle: "normal", fontSize: "0.65rem", flexShrink: 0 }}>💬</span>
                                <span>{cleanNote}</span>
                              </div>
                            );
                          })()}
                        </div>

                        {/* Importe y Acciones */}
                        <div style={{ display: "flex", alignItems: "center", gap: "0.45rem", flexShrink: 0 }}>
                          <div style={{ textAlign: "right" }}>
                            <div
                              style={{
                                fontFamily: "'Inter', sans-serif",
                                color: isPrestamo
                                  ? (darkMode ? "#FBBF24" : "#D97706")
                                  : isCardPayment
                                  ? (darkMode ? "#818CF8" : "#4F46E5")
                                  : isGasto
                                  ? C.red
                                  : C.green,
                                fontSize: "0.88rem",
                                fontWeight: 800,
                                whiteSpace: "nowrap",
                              }}
                            >
                              {isPrestamo ? "🤝 " : isCardPayment ? "💳 " : isGasto ? "−" : "+"}
                              {(Number(t.amount) || 0).toFixed(2).replace(".", ",")} {txCurrency(t, settings.currency)}
                            </div>
                            {txCurrency(t, settings.currency) !== (settings.currency || "S/") && (
                              <div style={{ color: C.inkSoft, fontSize: "0.68rem", fontWeight: 500 }}>
                                ≈ {fmtMoney(txToBase(t, settings.currency), settings.currency)}
                              </div>
                            )}
                          </div>

                          {/* Botones de acción compactos */}
                          <div style={{ display: "flex", alignItems: "center", gap: "0.15rem" }}>
                            <button
                              type="button"
                              onClick={() => openEdit(t)}
                              aria-label="Editar"
                              title="Editar movimiento"
                              style={{
                                background: "none",
                                border: "none",
                                color: C.inkSoft,
                                cursor: "pointer",
                                padding: "0.3rem",
                                borderRadius: "0.4rem",
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                              }}
                              className="opacity-40 hover:opacity-100 hover:bg-black/5 dark:hover:bg-white/5 transition-opacity"
                            >
                              <Pencil size={13} />
                            </button>
                            <button
                              type="button"
                              onClick={() => deleteTransaction(t.id)}
                              aria-label="Borrar"
                              title="Borrar movimiento"
                              style={{
                                background: "none",
                                border: "none",
                                color: C.inkSoft,
                                cursor: "pointer",
                                padding: "0.3rem",
                                borderRadius: "0.4rem",
                                display: "flex",
                                alignItems: "center",
                                justifyContent: "center",
                              }}
                              className="opacity-40 hover:opacity-100 hover:bg-black/5 dark:hover:bg-white/5 hover:text-red-500 transition-opacity"
                            >
                              <Trash2 size={13} />
                            </button>
                          </div>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// Capturador global de errores en tiempo de ejecución para evitar pantallas en blanco
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, errorInfo) {
    console.error("ErrorBoundary ha capturado un error:", error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: "2rem", fontFamily: "sans-serif", textAlign: "center", minHeight: "100vh", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", background: "#F5F4EE", color: "#161A16" }}>
          <div style={{ background: "#FFFFFF", padding: "1.75rem", borderRadius: "1.2rem", maxWidth: "440px", border: "1px solid #E4E1D6", boxShadow: "0 10px 25px rgba(0,0,0,0.1)" }}>
            <h2 style={{ fontSize: "1.2rem", fontWeight: 700, margin: "0 0 0.5rem 0", color: "#B3392B" }}>Ocurrió un error inesperado</h2>
            <p style={{ fontSize: "0.82rem", color: "#5B6459", margin: "0 0 1rem 0", wordBreak: "break-all" }}>
              {this.state.error && this.state.error.message ? this.state.error.message : String(this.state.error || "Error desconocido")}
            </p>
            <div style={{ display: "flex", gap: "0.6rem" }}>
              <button
                type="button"
                onClick={() => window.location.reload()}
                style={{ flex: 1, background: "linear-gradient(135deg,#0F5132,#10B981)", color: "#FFFFFF", border: "none", borderRadius: "0.6rem", padding: "0.65rem", fontWeight: 700, cursor: "pointer", fontSize: "0.8rem" }}
              >
                Recargar
              </button>
              <button
                type="button"
                onClick={() => {
                  try {
                    localStorage.removeItem("cc:my-name");
                    localStorage.removeItem("cc:transactions");
                    localStorage.removeItem("cc:settings");
                    localStorage.removeItem("cc:people");
                  } catch (e) {}
                  window.location.reload();
                }}
                style={{ flex: 1, background: "#F1EFE5", color: "#161A16", border: "1px solid #E4E1D6", borderRadius: "0.6rem", padding: "0.65rem", fontWeight: 600, cursor: "pointer", fontSize: "0.8rem" }}
              >
                Limpiar datos locales
              </button>
            </div>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}

// Auto-montaje cuando se ejecuta en un navegador con <div id="root">
// (demo del backend, Netlify, etc.). Se omite en Claude Artifacts: además de
// no haber #root, la existencia de window.storage (HAS_LOCAL_BRIDGE) indica que
// Claude ya se encarga de renderizar el componente, evitando doble renderizado.
if (typeof document !== "undefined" && !HAS_LOCAL_BRIDGE && document.getElementById("root")) {
  import("react-dom/client").then(({ createRoot }) => {
    createRoot(document.getElementById("root")).render(
      <ErrorBoundary>
        <CuentaCompartida />
      </ErrorBoundary>
    );
  });
}
