// === scenes/historial.jsx — COMPONENTES COMPARTIDOS de la familia «histórico por campo» ===
// Los cargan DOS decks: public/historico-por-campo/ (escenas 02 y 03) y public/que-es-una-seccion/
// (su escena 04). Se escriben UNA sola vez y NO se reescriben: aquí dentro no hay ni un dato de
// ninguna pieza — ni Compresor, ni Precio de venta, ni 1290. Todo entra por props.
//
// Fidelidad al producto (verificado en el plan, Fase 2):
//   DnzHistoricoDialog.razor       → estructura, textos literales y orden de los bloques
//   DnzHistoricoDialog.razor.css   → espaciados, rojo tachado con opacidad 0.75, verde en negrita
//   DnzAvatarInitial.razor:22-50   → el color del avatar NO es libre: sale de la inicial
//   DnzControl.razor:279-283       → el menú contextual tiene UN item: history + «Historial de cambios»
// Hex decididos en la revisión de coherencia del 13/08: rojo #EF4444 · verde #10B981.
// Tamaños: tabla de la Fase 5 del plan (diálogo 780×900 a escala 1, sobre lienzo 1920×1080).
//
// REGLAS DEL MOTOR QUE ESTE ARCHIVO CUMPLE:
//   · función pura del reloj t — sin Math.random, sin Date, sin useState/useEffect
//   · ningún NaN/undefined en el DOM: todo prop tiene default y toda entrada se sanea
//   · todo beat es OPCIONAL: si no se pasa, esa parte se pinta en ESTADO FINAL
//     (así el mini de la escena 03 se usa sin animar nada por dentro)

// Paleta del diálogo. El producto usa var(--rz-danger)/var(--rz-success) del tema; aquí van fijos
// porque el vídeo no tiene tema, y son los de la casa (CREAR-PRESENTACIONES.md:246).
const HIST_COL = {
  rojo:   '#EF4444',
  verde:  '#10B981',
  azul:   '#0085FF',
  texto:  '#0F172A',
  texto2: '#475569',
  texto3: '#94A3B8',
  borde:  '#E2E8F0',
  bordeSuave: '#EDF1F6',
};

// Tabla fija de color por inicial — copiada de DnzAvatarInitial.razor:22-50. Se pasa por props
// `avatarColores` solo para AÑADIR o corregir; lo normal es no tocarla.
const HIST_AVATAR_COLORES = {
  '?':['#B3B3B3','#8a8a8a'],
  'A':['#f44336','#c62828'], 'B':['#e91e63','#ad1457'], 'C':['#9c27b0','#6a1b9a'],
  'D':['#673ab7','#4527a0'], 'E':['#3f51b5','#283593'], 'F':['#2196f3','#1565c0'],
  'G':['#03a9f4','#0277bd'], 'H':['#00bcd4','#00838f'], 'I':['#009688','#00695c'],
  'J':['#4caf50','#2e7d32'], 'K':['#8bc34a','#558b2f'], 'L':['#cddc39','#9e9d24'],
  'M':['#ffeb3b','#f9a825'], 'N':['#ffc107','#ff8f00'], 'O':['#ff9800','#ef6c00'],
  'P':['#ff5722','#d84315'], 'Q':['#795548','#4e342e'], 'R':['#9e9e9e','#616161'],
  'S':['#607d8b','#37474f'], 'T':['#00796b','#004d40'], 'U':['#303f9f','#1a237e'],
  'V':['#8e24aa','#6a1b9a'], 'W':['#d32f2f','#b71c1c'], 'X':['#7b1fa2','#4a148c'],
  'Y':['#512da8','#311b92'], 'Z':['#303030','#1a1a1a'],
};

// ── Progreso 0→1 de un beat OPCIONAL. start null/undefined ⇒ 1 (estado final). ──
function histProg(t, start, dur){
  if (start === null || start === undefined) return 1;
  return clamp(((t ?? 0) - start) / (dur || 0.4), 0, 1);
}

// ── Intensidad 0→1 de un halo. start null ⇒ 0 (un halo apagado ES su estado final).
//    Cuando arranca un halo posterior, este se queda atenuado (plan, escena 02). ──
function histHalo(t, start, posteriores){
  if (start === null || start === undefined) return 0;
  const e = Easing.easeOutCubic(clamp(((t ?? 0) - start) / 0.3, 0, 1));
  let at = 1;
  const lista = posteriores || [];
  for (let i = 0; i < lista.length; i++){
    if (lista[i] !== null && lista[i] !== undefined && (t ?? 0) >= lista[i]) at = 0.34;
  }
  return e * at;
}

// ── Iconos del diálogo (material: history, check_circle, arrow_forward, close) ──
function HistIcono({ nombre, size = 24, color = 'currentColor', grosor = 2, style }){
  const box = { width:size, height:size, display:'block', flexShrink:0, ...style };
  if (nombre === 'history'){
    return (
      <svg viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={grosor} strokeLinecap="round" strokeLinejoin="round" style={box}>
        <path d="M4.48 9.26 A8 8 0 1 1 4.48 14.74"/>
        <path d="M4.48 9.26 L1.6 7.9"/>
        <path d="M4.48 9.26 L5.9 6.3"/>
        <path d="M12 7.4 V12.5 L15.7 14.6"/>
      </svg>
    );
  }
  if (nombre === 'check_circle'){
    return (
      <svg viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={grosor} strokeLinecap="round" strokeLinejoin="round" style={box}>
        <circle cx="12" cy="12" r="9"/>
        <path d="M7.8 12.3 L10.7 15.2 L16.2 9.3"/>
      </svg>
    );
  }
  if (nombre === 'arrow_forward'){
    return (
      <svg viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={grosor} strokeLinecap="round" strokeLinejoin="round" style={box}>
        <path d="M4 12 H19"/>
        <path d="M13.4 6.4 L19 12 L13.4 17.6"/>
      </svg>
    );
  }
  // close
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={grosor} strokeLinecap="round" style={box}>
      <path d="M6.5 6.5 L17.5 17.5"/>
      <path d="M17.5 6.5 L6.5 17.5"/>
    </svg>
  );
}

// ── Avatar del historial: FOTO si la entrada la trae, INICIAL con degradado si no ──
// La bifurcación es la del producto: FotoUsuarioU.razor:9-24 pinta la imagen redonda con borde
// fino cuando hay FotoURL y cae a <DnzAvatarInitial/> cuando no. El degradado por letra
// (HIST_AVATAR_COLORES) queda INTACTO: sin `foto` este componente pinta exactamente lo de antes.
//   foto        — ruta local; cualquier cosa que no sea texto no vacío se trata como «sin foto»
//   colorBorde  — opcional: anillo del color de presencia de esa persona (rima con su cursor).
//                 Sin él, la foto lleva un borde neutro fino y la inicial ningún anillo.
// LICENCIA CONSCIENTE, declarada: DnzHistoricoDialog pinta hoy SIEMPRE la inicial porque su DTO
// no trae foto. La foto aquí es coherente con AvatarU/FotoUsuarioU del resto del producto, pero
// no con ese diálogo tal como está hoy.
function HistAvatar({ texto, inicial, tam = 40, colores, foto = null, colorBorde = null, grosorBorde = null }){
  const base = (inicial || (texto || '?').charAt(0) || '?').toUpperCase();
  const ruta   = (typeof foto === 'string' && foto !== '') ? foto : null;
  const anillo = (typeof colorBorde === 'string' && colorBorde !== '') ? colorBorde : null;
  const grosor = (typeof grosorBorde === 'number' && isFinite(grosorBorde))
    ? grosorBorde
    : (anillo ? 3 : 2);   // el 1px gris del producto no se ve en vídeo; 2 px sigue siendo fino

  if (ruta){
    return (
      <span style={{
        width:tam, height:tam, flexShrink:0, borderRadius:'50%', boxSizing:'border-box',
        border:`${grosor}px solid ${anillo || HIST_COL.borde}`,
        background:`#E2E8F0 url(${ruta}) center/cover no-repeat`,
        display:'block',
      }}/>
    );
  }

  const tabla = colores ? { ...HIST_AVATAR_COLORES, ...colores } : HIST_AVATAR_COLORES;
  const par = tabla[base] || tabla['?'];
  return (
    <span className="f-plex" style={{
      width:tam, height:tam, flexShrink:0, borderRadius:'50%',
      background:`linear-gradient(135deg, ${par[0]}, ${par[1]})`,
      color:'#fff', fontWeight:600, fontSize:Math.round(tam*0.42),
      display:'flex', alignItems:'center', justifyContent:'center', lineHeight:1,
      // anillo por boxShadow: no ocupa sitio, así el círculo mide igual con y sin color
      boxShadow: anillo ? `0 0 0 ${grosor}px ${anillo}` : 'none',
    }}>{base}</span>
  );
}

// ── Halo azul de realce (va DETRÁS del contenido; el azul evita sumar un tercer verde) ──
function HistHalo({ v }){
  if (v <= 0) return null;
  return (
    <span style={{
      position:'absolute', left:-12, right:-12, top:-8, bottom:-8,
      borderRadius:12, pointerEvents:'none',
      background:`rgba(0,133,255,${0.14*v})`,
      border:`1px solid rgba(0,133,255,${0.45*v})`,
      boxShadow:`0 0 ${26*v}px rgba(0,133,255,${0.35*v})`,
      transform:`scale(${lerp(0.94,1,v)})`,
    }}/>
  );
}

// =====================================================================================
// <DialogoHistorial/> — el diálogo «Historial» del producto, fiel y PARAMETRIZADO.
//
//   entradas: [{ grupo, inicial, autor, fecha, antes, despues, foto, borde }]
//     · grupo   — literal del producto: 'Hoy' | 'Ayer' | 'Esta semana' | 'Este mes' | 'mes año'
//                 REGLA DURA: la cabecera de grupo SOLO se dibuja si ese grupo tiene entradas
//                 (el producto la pinta dentro del bucle, DnzHistoricoDialog.razor:73-84).
//     · inicial — opcional; si falta se usa la primera letra de `autor`.
//     · antes / despues — el valor TAL COMO ESTÁ GUARDADO (sin € y sin separadores).
//     · foto    — opcional: ruta local del retrato ('/_engine/assets/avatares/marta.jpg').
//                 Sin ella se pinta la inicial con su degradado, igual que siempre.
//     · borde   — opcional: color del anillo de ESA persona, para rimar con el color de su
//                 cursor de presencia. Si no está, manda `bordeAvatar`; si tampoco, borde neutro.
//
//   Beats (todos opcionales; el que no se pasa se pinta en estado final):
//     aparicion · bloqueActual · rail · haloQuien · haloCuando · haloValores
//     tachado · valorNuevo · badge · entradasResto
//   Los beats de entrada/valor actúan sobre la entrada `indiceAnimado` (por defecto la 0).
// =====================================================================================
function DialogoHistorial({
  t = 0,
  campo = '',
  entradas = [],
  valorActual = '',
  avatarColores = null,
  bordeAvatar = null,            // color de anillo por defecto para TODOS los avatares del diálogo
                                 // (cada entrada lo puede pisar con su propia clave `borde`)
  escala = 1,
  x = 570, y = 90,               // 780 de ancho centrado en la columna segura 4:5 (568→1352)
  ancho = 780, alto = 900,
  transformOrigin = '0% 0%',
  escalaEntrada = 0.72,
  opacidad = 1,
  indiceAnimado = 0,
  zIndex = 20,
  estilo = null,
  // ── beats ──
  aparicion = null,
  bloqueActual = null,
  rail = null,
  haloQuien = null,
  haloCuando = null,
  haloValores = null,
  tachado = null,
  valorNuevo = null,
  badge = null,
  entradasResto = null,
}){
  const tt = t ?? 0;

  // Aparición del diálogo (continuidad espacial con el menú: transformOrigin lo pone la escena)
  if (aparicion !== null && aparicion !== undefined && tt < aparicion) return null;
  const apP = histProg(tt, aparicion, 0.55);
  const apE = Easing.easeOutBack(apP);
  const apEscala = (aparicion === null || aparicion === undefined) ? 1 : lerp(escalaEntrada, 1, apE);
  const apOp = clamp(apP*1.6, 0, 1);

  const lista = Array.isArray(entradas) ? entradas : [];
  const primera = lista.length > 0 ? lista[0] : null;

  // Badge del contador — literal del producto: «1 cambio» / «N cambios»
  const badgeTexto = lista.length === 1 ? '1 cambio' : `${lista.length} cambios`;
  const badgePulse = (badge === null || badge === undefined || tt < badge || tt > badge + 0.9)
    ? 0 : Math.sin((tt - badge)/0.9*Math.PI);

  // Bloque «Valor actual»
  const vaP = histProg(tt, bloqueActual, 0.45);
  const vaE = Easing.easeOutCubic(vaP);

  // Halos (azul): quién → cuándo → valores; el anterior se queda atenuado
  const hQuien   = histHalo(tt, haloQuien,   [haloCuando, haloValores]);
  const hCuando  = histHalo(tt, haloCuando,  [haloValores]);
  const hValores = histHalo(tt, haloValores, []);

  // EL MOMENTO: el tachado se DIBUJA de izquierda a derecha sobre el valor viejo
  const tachaP = Easing.easeOutCubic(histProg(tt, tachado, 0.35));
  const tachaGlow = (tachado === null || tachado === undefined || tt < tachado || tt > tachado + 0.9)
    ? 0 : Math.sin((tt - tachado)/0.9*Math.PI);

  // El valor nuevo aterriza en verde y la flecha se dibuja
  const nvP = histProg(tt, valorNuevo, 0.4);
  const nvE = Easing.easeOutBack(nvP);
  const nvVisible = (valorNuevo === null || valorNuevo === undefined) ? 1 : clamp(nvP*2.2, 0, 1);

  // Filas: cabecera de grupo + entrada, exactamente como el bucle del producto
  const filas = [];
  let ultimoGrupo = null;
  for (let i = 0; i < lista.length; i++){
    const en = lista[i] || {};
    const grupo = en.grupo || '';
    if (grupo !== ultimoGrupo){
      ultimoGrupo = grupo;
      filas.push({ tipo:'grupo', grupo, i });
    }
    filas.push({ tipo:'entrada', en, i });
  }

  // Cascada de las entradas posteriores a la animada (+0.12 s) y dibujado de sus líneas de grupo
  const restoOp = (i) => {
    if (entradasResto === null || entradasResto === undefined) return 1;
    if (i <= indiceAnimado) return 1;
    const k = i - indiceAnimado - 1;
    return lerp(0.45, 1, Easing.easeOutCubic(clamp((tt - entradasResto - k*0.12)/0.5, 0, 1)));
  };
  const lineaGrupo = (i) => {
    if (entradasResto === null || entradasResto === undefined) return 1;
    if (i <= indiceAnimado) return 1;
    const k = i - indiceAnimado - 1;
    return Easing.easeOutCubic(clamp((tt - entradasResto - k*0.12)/0.45, 0, 1));
  };
  // Raíl (punto + conector) autodibujándose de arriba abajo
  const railP = (i) => (rail === null || rail === undefined)
    ? 1 : Easing.easeInOutCubic(clamp((tt - rail - i*0.28)/0.4, 0, 1));

  return (
    <div style={{
      position:'absolute', left:x, top:y, width:ancho, minHeight:alto,
      transform:`scale(${escala * apEscala})`, transformOrigin,
      opacity: opacidad * apOp,
      background:'#fff', borderRadius:16, border:`1px solid ${HIST_COL.borde}`,
      boxShadow:'0 40px 100px rgba(15,23,42,0.30)', overflow:'hidden',
      zIndex,
      ...(estilo || {}),
    }}>

      {/* ── Cabecera: history + «Historial» + chip del campo + badge «N cambios» ── */}
      <div style={{display:'flex', alignItems:'center', gap:14, padding:'24px 28px', borderBottom:`1px solid ${HIST_COL.bordeSuave}`}}>
        <HistIcono nombre="history" size={28} color={HIST_COL.azul} grosor={2.1}/>
        <span className="f-plex" style={{fontSize:26, fontWeight:600, color:HIST_COL.texto, whiteSpace:'nowrap'}}>Historial</span>
        {campo ? (
          <span className="f-plex" style={{
            fontSize:18, fontWeight:500, padding:'4px 14px', borderRadius:999,
            background:'rgba(0,133,255,0.10)', color:HIST_COL.azul, whiteSpace:'nowrap',
            maxWidth:300, overflow:'hidden', textOverflow:'ellipsis',
          }}>{campo}</span>
        ) : null}
        {lista.length > 0 ? (
          <span className="f-plex" style={{
            fontSize:17, fontWeight:600, padding:'4px 12px', borderRadius:7,
            background: badgePulse > 0 ? `rgba(0,133,255,${0.10 + 0.16*badgePulse})` : '#EEF2F6',
            color: badgePulse > 0 ? HIST_COL.azul : HIST_COL.texto2,
            whiteSpace:'nowrap', display:'inline-block',
            transform:`scale(${1 + 0.14*badgePulse})`,
          }}>{badgeTexto}</span>
        ) : null}
        <span style={{flex:1}}/>
        <HistIcono nombre="close" size={24} color={HIST_COL.texto3} grosor={2} style={{opacity:0.6}}/>
      </div>

      {/* ── Bloque destacado: check_circle + «Valor actual» + el valor + autor/fecha del último ── */}
      {primera && vaP > 0 ? (
        <div style={{
          margin:'22px 26px 0', padding:'20px 22px', borderRadius:14,
          background:'rgba(16,185,129,0.06)', border:'1px solid rgba(16,185,129,0.25)',
          opacity:vaE, transform:`translateY(${(1-vaE)*10}px)`,
        }}>
          <div style={{display:'flex', alignItems:'center', gap:8}}>
            <HistIcono nombre="check_circle" size={20} color={HIST_COL.verde} grosor={2.2}/>
            <span className="f-plex" style={{fontSize:17, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase', color:HIST_COL.verde}}>
              Valor actual
            </span>
          </div>
          <div className="f-plex" style={{fontSize:44, fontWeight:600, color:HIST_COL.texto, marginTop:10, lineHeight:1.15, wordBreak:'break-word'}}>
            {valorActual !== '' && valorActual !== null && valorActual !== undefined
              ? valorActual
              : <span style={{fontWeight:400, fontStyle:'italic', color:HIST_COL.texto3}}>(vacío)</span>}
          </div>
          <div style={{display:'flex', alignItems:'center', gap:10, marginTop:12}}>
            <HistAvatar texto={primera.autor} inicial={primera.inicial} tam={30} colores={avatarColores}
                        foto={primera.foto} colorBorde={primera.borde || bordeAvatar}/>
            <span className="f-plex" style={{fontSize:19, color:HIST_COL.texto2}}>{primera.autor || ''}</span>
            <span className="f-plex" style={{fontSize:16, color:HIST_COL.texto3}}>{primera.fecha || ''}</span>
          </div>
        </div>
      ) : null}

      {/* ── Línea de tiempo ── */}
      <div style={{display:'flex', flexDirection:'column', padding:'18px 26px 26px'}}>
        {filas.map((f, k) => {
          if (f.tipo === 'grupo'){
            const dib = lineaGrupo(f.i);
            // La cabecera acompaña a SU entrada (misma opacidad que ella): si la entrada está en
            // pantalla al 0.45 esperando su beat, su cabecera también. Lo que se anima aparte es la
            // LÍNEA, que es lo que el plan dice que «se dibuja» en el beat de «Tres cambios».
            return (
              <div key={`g${k}`} style={{
                display:'flex', alignItems:'center', gap:12,
                margin: f.i === 0 ? '0 0 14px 0' : '22px 0 14px 0', paddingLeft:4,
                opacity: restoOp(f.i),
              }}>
                <span className="f-plex" style={{
                  fontSize:15, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase',
                  color:HIST_COL.texto2, whiteSpace:'nowrap',
                }}>{f.grupo}</span>
                <span style={{flex:1, height:1, background:HIST_COL.borde, transform:`scaleX(${dib})`, transformOrigin:'left'}}/>
              </div>
            );
          }

          const en = f.en;
          const i = f.i;
          const animada = i === indiceAnimado;
          const esUltima = i === lista.length - 1;
          const primeraDeGrupo = k > 0 && filas[k-1].tipo === 'grupo';
          const op = restoOp(i);
          const rp = railP(i);

          // Estado del cambio inline de ESTA entrada
          const tacha  = animada ? tachaP : 1;
          const glow   = animada ? tachaGlow : 0;
          const verdeP = animada ? nvE : 1;
          const verdeOp= animada ? nvVisible : 1;
          const flecha = animada ? Easing.easeOutCubic(nvP) : 1;

          return (
            <div key={`e${k}`} style={{display:'flex', gap:16, alignItems:'stretch', minHeight:60, opacity:op}}>
              {/* raíl: punto + conector (DnzHistoricoDialog.razor:90-93) */}
              <div style={{display:'flex', flexDirection:'column', alignItems:'center', width:14, flexShrink:0, paddingTop:8}}>
                {/* punto MACIZO — nada de anillo con centro hueco: el mock con «círculo de radio»
                    es una de las cinco contradicciones que el plan manda no copiar */}
                <span style={{
                  width:14, height:14, borderRadius:'50%', flexShrink:0,
                  background: primeraDeGrupo ? HIST_COL.azul : '#CBD5E1',
                  boxShadow: primeraDeGrupo ? '0 0 0 5px rgba(0,133,255,0.16)' : '0 0 0 5px rgba(203,213,225,0.28)',
                  opacity: clamp(rp*2, 0, 1),
                  transform:`scale(${lerp(0.4,1,rp)})`,
                }}/>
                <span style={{
                  width:3, flex:1, marginTop:5, borderRadius:2,
                  background: esUltima ? 'transparent' : HIST_COL.borde,
                  transform:`scaleY(${rp})`, transformOrigin:'top',
                }}/>
              </div>

              {/* contenido: cabecera (avatar + autor + fecha) y DEBAJO el cambio */}
              <div style={{flex:1, minWidth:0, paddingBottom:26}}>
                <div style={{display:'flex', alignItems:'center', gap:12}}>
                  <span style={{position:'relative', display:'flex', alignItems:'center', gap:12}}>
                    {animada ? <HistHalo v={hQuien}/> : null}
                    <span style={{position:'relative', display:'flex', alignItems:'center', gap:12}}>
                      <HistAvatar texto={en.autor} inicial={en.inicial} tam={40} colores={avatarColores}
                                  foto={en.foto} colorBorde={en.borde || bordeAvatar}/>
                      <span className="f-plex" style={{fontSize:21, fontWeight:600, color:HIST_COL.texto, whiteSpace:'nowrap'}}>
                        {en.autor || ''}
                      </span>
                    </span>
                  </span>
                  <span style={{position:'relative', display:'inline-flex', alignItems:'center'}}>
                    {animada ? <HistHalo v={hCuando}/> : null}
                    <span className="f-plex" style={{position:'relative', fontSize:16, color:HIST_COL.texto3, whiteSpace:'nowrap'}}>
                      {en.fecha || ''}
                    </span>
                  </span>
                </div>

                {/* cambio inline: antes tachado en rojo + arrow_forward + después en verde */}
                <div style={{position:'relative', display:'flex', alignItems:'center', gap:10, marginTop:10, minWidth:0}}>
                  {animada ? <HistHalo v={hValores}/> : null}
                  <span style={{position:'relative', display:'inline-block', opacity:0.75}}>
                    <span className="f-plex" style={{
                      fontSize:30, color:HIST_COL.rojo, wordBreak:'break-word',
                      textShadow: glow > 0 ? `0 0 ${16*glow}px rgba(239,68,68,${0.75*glow})` : 'none',
                    }}>{en.antes !== null && en.antes !== undefined && en.antes !== '' ? en.antes : 'vacío'}</span>
                    <span style={{
                      position:'absolute', left:0, right:0, top:'53%', height:3, borderRadius:2,
                      background:HIST_COL.rojo, transform:`scaleX(${tacha})`, transformOrigin:'left',
                    }}/>
                  </span>
                  <span style={{
                    position:'relative', display:'inline-flex', opacity:verdeOp,
                    clipPath:`inset(0 ${(1-flecha)*100}% 0 0)`, WebkitClipPath:`inset(0 ${(1-flecha)*100}% 0 0)`,
                  }}>
                    <HistIcono nombre="arrow_forward" size={24} color={HIST_COL.texto3} grosor={2.2}/>
                  </span>
                  <span className="f-plex" style={{
                    position:'relative', fontSize:30, fontWeight:700, color:HIST_COL.verde, wordBreak:'break-word',
                    opacity:verdeOp, display:'inline-block',
                    transform:`scale(${lerp(1.12, 1, verdeP)})`, transformOrigin:'left center',
                    textShadow:`0 0 ${18*verdeOp}px rgba(16,185,129,0.45)`,
                  }}>{en.despues !== null && en.despues !== undefined && en.despues !== '' ? en.despues : 'vacío'}</span>
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// =====================================================================================
// <MenuContextualUnItem/> — el menú del clic derecho sobre la ETIQUETA del campo.
// UN solo item: icono history + «Historial de cambios» (DnzControl.razor:279-283).
// (x, y) = punto del puntero: el menú crece desde ahí (transformOrigin arriba-izquierda).
// =====================================================================================
function MenuContextualUnItem({
  t = 0,
  x = 0, y = 0,
  escala = 1,
  texto = 'Historial de cambios',
  ancho = 372,
  transformOrigin = '0% 0%',
  zIndex = 30,
  estilo = null,
  // ── beats ──
  start = null,     // entrada: scale .9→1 easeOutBack 0.35 s
  resalte = null,   // el item se resalta (0.15 s antes del fade)
  salida = null,    // fade de salida 0.2 s; después no se pinta
}){
  const tt = t ?? 0;
  if (start !== null && start !== undefined && tt < start) return null;
  if (salida !== null && salida !== undefined && tt > salida + 0.2) return null;

  const p = histProg(tt, start, 0.35);
  const e = Easing.easeOutBack(p);
  const entradaEscala = (start === null || start === undefined) ? 1 : lerp(0.9, 1, e);
  const opIn = clamp(p*2.2, 0, 1);
  const opOut = (salida === null || salida === undefined) ? 1 : 1 - clamp((tt - salida)/0.2, 0, 1);
  const resaltado = resalte !== null && resalte !== undefined && tt >= resalte;

  return (
    <div style={{
      position:'absolute', left:x, top:y, width:ancho,
      transform:`scale(${escala * entradaEscala})`, transformOrigin,
      opacity: opIn * opOut,
      background:'#fff', borderRadius:12, border:`1px solid ${HIST_COL.borde}`,
      boxShadow:'0 24px 60px rgba(15,23,42,0.24)', overflow:'hidden', padding:'8px 0',
      zIndex,
      ...(estilo || {}),
    }}>
      <div style={{
        display:'flex', alignItems:'center', gap:14, padding:'14px 22px',
        background: resaltado ? 'rgba(0,133,255,0.10)' : 'transparent',
      }}>
        <HistIcono nombre="history" size={24} color={resaltado ? HIST_COL.azul : HIST_COL.texto2} grosor={2.1}/>
        <span className="f-plex" style={{fontSize:26, fontWeight:500, color: resaltado ? HIST_COL.azul : HIST_COL.texto, whiteSpace:'nowrap'}}>
          {texto}
        </span>
      </div>
    </div>
  );
}

// =====================================================================================
// <AnilloClicDerecho/> — el anillo del clic derecho sobre la etiqueta:
// círculo azul scale 0.4 → 1 con opacity 1 → 0 en 0.45 s. Centrado en (x, y).
// Sin `start` no se pinta: el estado final de un anillo es no estar.
// =====================================================================================
function AnilloClicDerecho({
  t = 0,
  x = 0, y = 0,
  start = null,
  duracion = 0.45,
  tamano = 118,
  color = '#0085FF',
  zIndex = 32,
}){
  const tt = t ?? 0;
  if (start === null || start === undefined) return null;
  if (tt < start || tt > start + duracion) return null;
  const p = clamp((tt - start)/duracion, 0, 1);
  const e = Easing.easeOutCubic(p);
  return (
    <span style={{
      position:'absolute', left:x, top:y, width:tamano, height:tamano, borderRadius:'50%',
      border:`4px solid ${color}`, boxShadow:`0 0 26px rgba(0,133,255,${0.45*(1-p)})`,
      transform:`translate(-50%,-50%) scale(${lerp(0.4, 1, e)})`,
      opacity: 1 - p, pointerEvents:'none', zIndex,
    }}/>
  );
}

Object.assign(window, { DialogoHistorial, MenuContextualUnItem, AnilloClicDerecho });
