// Workout Player — Phase 7
// Full-screen exercise-by-exercise workout execution player

const WP = {
  page:   '#060D18',
  card:   '#0B1628',
  border: 'rgba(255,255,255,0.07)',
  text:   '#E8EDF8',
  sub:    'rgba(232,237,248,0.55)',
  muted:  'rgba(232,237,248,0.55)',
  green:  '#22C55E',
  blue:   '#3B82F6',
  red:    '#EF4444',
  input:  '#0A1422',
};

// ── Helpers ────────────────────────────────────────────────────────────────────

function wpFmtTime(sec) {
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  const s = sec % 60;
  const pad = n => String(n).padStart(2, '0');
  if (h > 0) return `${pad(h)}:${pad(m)}:${pad(s)}`;
  return `${pad(m)}:${pad(s)}`;
}

function wpMuscleColor(name) {
  const n = (name || '').toLowerCase();
  if (n.includes('pectoral') || n.includes('pecho')) return '#F97316';
  if (n.includes('dorsal') || n.includes('espalda')) return '#8B5CF6';
  if (n.includes('deltoid') || n.includes('hombro')) return '#3B82F6';
  if (n.includes('bíceps') || n.includes('biceps')) return '#EC4899';
  if (n.includes('tríceps') || n.includes('triceps')) return '#F59E0B';
  if (n.includes('cuádric') || n.includes('cuadric') || n.includes('pierna')) return '#22C55E';
  if (n.includes('glút') || n.includes('glut')) return '#10B981';
  if (n.includes('core') || n.includes('abdom')) return '#06B6D4';
  return '#6B7280';
}

// ── Loading spinner ────────────────────────────────────────────────────────────

function WpSpinner() {
  return (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', height:'100vh', background:WP.page }}>
      <div style={{ width:36, height:36, border:`3px solid ${WP.border}`, borderTop:`3px solid ${WP.green}`, borderRadius:'50%', animation:'spin 0.8s linear infinite' }} />
    </div>
  );
}

// ── Rest Timer Banner ─────────────────────────────────────────────────────────

function WpRestTimer({ restTimer, onSkip, onAdjust, isMobile }) {
  if (!restTimer) return null;
  const { rem, total } = restTimer;
  const pct = total > 0 ? Math.max(0, rem / total) : 0;

  // Conic-gradient circle
  const deg = Math.round(pct * 360);
  const circleStyle = {
    width: 72,
    height: 72,
    borderRadius: '50%',
    background: `conic-gradient(${WP.green} ${deg}deg, rgba(34,197,94,0.15) ${deg}deg)`,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    flexShrink: 0,
  };

  return (
    <div className="wp-rest-enter" style={{
      background: 'rgba(11,22,40,0.97)',
      borderBottom: `1px solid rgba(34,197,94,0.20)`,
      padding: '12px 20px',
      display: 'flex',
      alignItems: 'center',
      gap: 16,
      flexShrink: 0,
    }}>
      <div style={circleStyle}>
        <div style={{ width:58, height:58, borderRadius:'50%', background:WP.card, display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column' }}>
          <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:17, fontWeight:800, color:WP.green, lineHeight:1 }}>{wpFmtTime(rem)}</span>
        </div>
      </div>
      <div style={{ flex:1 }}>
        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, fontWeight:700, color:'rgba(34,197,94,0.70)', letterSpacing:1.6, marginBottom:6 }}>DESCANSO</div>
        <div style={{ display:'flex', gap:6 }}>
          <button className="wp-press" onClick={() => onAdjust(-15)} style={{ minWidth: isMobile ? 52 : undefined, minHeight: isMobile ? 44 : undefined, padding: isMobile ? '0 12px' : '5px 10px', borderRadius:8, border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.06)', color:WP.sub, fontFamily:'Inter,system-ui', fontSize: isMobile ? 13 : 11, fontWeight:700, cursor:'pointer' }}>−15s</button>
          <button className="wp-press" onClick={onSkip} style={{ minHeight: isMobile ? 44 : undefined, padding: isMobile ? '0 18px' : '5px 14px', borderRadius:8, border:'none', background:WP.green, color:'#fff', fontFamily:'Inter,system-ui', fontSize: isMobile ? 13 : 11, fontWeight:700, cursor:'pointer' }}>Skip →</button>
          <button className="wp-press" onClick={() => onAdjust(15)} style={{ minWidth: isMobile ? 52 : undefined, minHeight: isMobile ? 44 : undefined, padding: isMobile ? '0 12px' : '5px 10px', borderRadius:8, border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.06)', color:WP.sub, fontFamily:'Inter,system-ui', fontSize: isMobile ? 13 : 11, fontWeight:700, cursor:'pointer' }}>+15s</button>
        </div>
      </div>
    </div>
  );
}

// ── Set Status Mini-grid ───────────────────────────────────────────────────────

function WpSetGrid({ sets, currentSetIdx, onSelectSet }) {
  return (
    <div style={{ display:'flex', gap:6, flexWrap:'wrap' }}>
      {sets.map((s, i) => {
        const isCurrent = i === currentSetIdx;
        const isDone    = s.done;
        return (
          <button
            key={i}
            className="wp-press"
            onClick={() => onSelectSet(i)}
            style={{
              width: 28, height: 28, borderRadius: '50%', border: 'none',
              cursor: 'pointer', fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, fontWeight:700,
              background: isDone ? WP.green : isCurrent ? 'rgba(59,130,246,0.22)' : 'rgba(255,255,255,0.07)',
              color: isDone ? '#fff' : isCurrent ? WP.blue : WP.muted,
              outline: isCurrent ? `2px solid ${WP.blue}` : 'none',
              outlineOffset: 2,
            }}
          >
            {isDone ? '✓' : i + 1}
          </button>
        );
      })}
    </div>
  );
}

// ── RIR Selector ──────────────────────────────────────────────────────────────
// El objetivo del programa está en RIR, así que el registro opcional por serie
// también es RIR (0–5), no RPE. 0 = al fallo · 5 = muy fácil.
function WpRirSelector({ value, onChange }) {
  return (
    <div>
      <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:WP.muted, letterSpacing:1.2, marginBottom:6 }}>RIR (OPCIONAL) · REPS EN RESERVA</div>
      <div role="radiogroup" aria-label="Repeticiones en reserva (opcional)" style={{ display:'flex', gap:4, flexWrap:'wrap' }}>
        {[0,1,2,3,4,5].map(r => (
          <button className="wp-press"
            key={r} role="radio" aria-checked={value === r} aria-label={`RIR ${r}`}
            onClick={() => onChange(value === r ? null : r)}
            style={{
              width:34, height:28, borderRadius:7, border: value === r ? 'none' : `1px solid ${WP.border}`,
              cursor:'pointer', fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, fontWeight:700,
              background: value === r ? WP.blue : 'rgba(255,255,255,0.05)',
              color: value === r ? '#fff' : WP.muted,
              transition:'background 120ms ease, color 120ms ease, border-color 120ms ease',
            }}
          >
            {r}
          </button>
        ))}
      </div>
    </div>
  );
}

// ── Finish Screen ─────────────────────────────────────────────────────────────

// Returns per-exercise comparison rows against the most recent matching session
function wpBuildComparison(finished) {
  const history = WorkoutSessionStore.getHistory(20);
  // Skip the just-completed session itself: anything logged at/after this
  // workout started is the current session (it may or may not have been
  // persisted to the store yet when this renders).
  const startTs = finished?.startTime || finished?.dateTs || 0;
  const currNames = new Set((finished?.exercises || []).map(e => e.name));
  let prev = null;
  for (let i = 0; i < history.length; i++) {
    const h = history[i];
    if ((h.dateTs || 0) >= startTs) continue;
    const overlap = (h.exercises || []).filter(e => currNames.has(e.name)).length;
    if (overlap >= Math.min(2, Math.floor(currNames.size * 0.4))) { prev = h; break; }
  }
  if (!prev) return null;

  // Build previous-session map. Weighted exercises compare by max kg; isometric
  // (time) exercises compare by max seconds held.
  const isTimeEx = ex => ex.unit === 'time' || (ex.sets || []).some(s => s.sec != null && s.sec !== '' && !(Number(s.reps) > 0));
  const prevMap = {};
  (prev.exercises || []).forEach(ex => {
    if (isTimeEx(ex)) {
      const done = (ex.sets || []).filter(s => s.done && Number(s.sec) > 0);
      if (!done.length) return;
      prevMap[ex.name] = { time: true, sec: Math.max(...done.map(s => Number(s.sec))), sets: done.length };
    } else {
      const doneSets = (ex.sets || []).filter(s => s.done && Number(s.kg) > 0);
      if (!doneSets.length) return;
      prevMap[ex.name] = {
        kg:   Math.max(...doneSets.map(s => Number(s.kg))),
        reps: Math.round(doneSets.reduce((a, s) => a + Number(s.reps || 0), 0) / doneSets.length),
        sets: doneSets.length,
      };
    }
  });

  const rows = (finished?.exercises || []).map(ex => {
    const p = prevMap[ex.name];
    if (isTimeEx(ex)) {
      const done = (ex.sets || []).filter(s => s.done && Number(s.sec) > 0);
      if (!done.length) return null;
      const currSec = Math.max(...done.map(s => Number(s.sec)));
      const delta   = (p && p.time) ? +(currSec - p.sec).toFixed(0) : null;
      return { name: ex.name, time: true, currSec, currSets: done.length, prevSec: p?.time ? p.sec : null, delta };
    }
    const doneSets = (ex.sets || []).filter(s => s.done && Number(s.kg) > 0);
    if (!doneSets.length) return null;
    const currKg   = Math.max(...doneSets.map(s => Number(s.kg)));
    const currReps = Math.round(doneSets.reduce((a, s) => a + Number(s.reps || 0), 0) / doneSets.length);
    const delta    = (p && !p.time) ? +(currKg - p.kg).toFixed(1) : null;
    return { name: ex.name, currKg, currReps, currSets: doneSets.length, prevKg: p?.time ? null : p?.kg, prevReps: p?.reps, delta };
  }).filter(Boolean);

  return rows.length ? { rows, prevDate: prev.date, prevDateTs: prev.dateTs || Date.parse(prev.date) || 0 } : null;
}

// Fecha corta en español, como en el resto de la app: "mar, 10 nov".
function wpFmtShortDate(ts, fallback) {
  try {
    const d = new Date(ts || fallback);
    if (isNaN(d.getTime())) return fallback || '';
    return d.toLocaleDateString('es-ES', { weekday: 'short', day: 'numeric', month: 'short' }).replace('.', '');
  } catch { return fallback || ''; }
}

// Gemas de récord realmente acreditadas esta sesión (misma lógica que la store:
// computeSessionPRBonus sobre el historial anterior). Permite integrar el
// "+N por récord" en la tarjeta del récord y no contradecir el aviso "Sin gemas".
function wpRecordGems(finished) {
  if (typeof computeSessionPRBonus !== 'function') return 0;
  let history = [];
  try { history = WorkoutSessionStore.getHistory(100); } catch (e) { return 0; }
  const startTs = finished?.startTime || finished?.dateTs || 0;
  const prior = history.filter(h => (h.dateTs || 0) < startTs);
  const current = history.find(h => (h.dateTs || 0) >= startTs);
  const exercises = current ? current.exercises : (finished?.exercises || []);
  try { return computeSessionPRBonus(prior, exercises) || 0; } catch (e) { return 0; }
}

// Nuevos récords de peso de la sesión (competimos con uno mismo, nunca con otros).
// La primera vez que se registra un ejercicio no cuenta como récord.
function wpSessionRecords(finished) {
  let history = [];
  try { history = WorkoutSessionStore.getHistory(100); } catch (e) { return []; }
  const startTs = finished?.startTime || finished?.dateTs || 0;
  const prior = history.filter(h => (h.dateTs || 0) < startTs);
  const bestPrior = {};
  for (const h of prior) for (const ex of (h.exercises || [])) {
    if (!ex.name) continue;
    const sets = (ex.sets || []).filter(s => Number(s.kg) > 0 && Number(s.reps) > 0);
    if (!sets.length) continue;
    const kg = Math.max(...sets.map(s => Number(s.kg)));
    const b = bestPrior[ex.name] || { kg: 0 };
    bestPrior[ex.name] = { kg: Math.max(b.kg, kg) };
  }
  const fmtKg = v => (v % 1 === 0 ? String(v) : String(Math.round(v * 10) / 10).replace('.', ','));
  const records = [];
  for (const ex of (finished?.exercises || [])) {
    const done = (ex.sets || []).filter(s => s.done && Number(s.kg) > 0 && Number(s.reps) > 0);
    if (!done.length) continue;
    const prev = bestPrior[ex.name];
    if (!prev) continue;   // primera vez → no es récord
    const top = done.slice().sort((a, b) => (Number(b.kg) - Number(a.kg)) || (Number(b.reps) - Number(a.reps)))[0];
    const kg = Number(top.kg), reps = Number(top.reps);
    if (kg > prev.kg + 0.01) records.push({ name: ex.name, text: `${fmtKg(kg)} kg × ${reps}` });
  }
  return records;
}

// Aviso suave de rendimiento a la baja (≥3 ejercicios peor en la última sesión),
// mostrado como mucho una vez por semana. Nunca lenguaje médico ni diagnósticos.
function wpPerfDropNotice() {
  try {
    if (typeof progPerformanceDrop !== 'function') return false;
    const log = WorkoutSessionStore.getHistory(60);
    const drop = progPerformanceDrop(log);
    if (drop.count < 3) return false;
    const now = new Date();
    const wk = `${now.getFullYear()}-W${Math.ceil(((now - new Date(now.getFullYear(), 0, 1)) / 86400000 + now.getDay() + 1) / 7)}`;
    const KEY = 'atlas.perfdrop.week.v1';
    if (localStorage.getItem(KEY) === wk) return false;
    localStorage.setItem(KEY, wk);
    return true;
  } catch (e) { return false; }
}

function WpFinishScreen({ finished, onHome, onCoach, onOpenArticle, addOffer, onAddToRoutine, onDismissOffer }) {
  const dur = finished?.duration || 0;
  const partial = !!finished?.partial;
  // "Músculos trabajados": solo los de ejercicios con alguna serie completada.
  const muscles = [...new Set(
    (finished?.exercises || [])
      .filter(ex => (ex.sets || []).some(s => s.done))
      .flatMap(ex => ex.muscles?.primary || [])
  )].slice(0, 6);

  const comparison = React.useMemo(() => wpBuildComparison(finished), [finished]);
  const records    = React.useMemo(() => wpSessionRecords(finished), [finished]);
  const recordGems = React.useMemo(() => wpRecordGems(finished), [finished]);
  const perfDrop   = React.useMemo(() => wpPerfDropNotice(), [finished]);

  return (
    <div style={{ minHeight:'100vh', background:WP.page, display:'flex', alignItems:'center', justifyContent:'center', padding:'24px 20px' }}>
      <div style={{ maxWidth:480, width:'100%', textAlign:'center' }}>
        {/* Checkmark */}
        <div style={{ width:80, height:80, borderRadius:'50%', background:'rgba(34,197,94,0.15)', border:`2px solid ${WP.green}`, display:'flex', alignItems:'center', justifyContent:'center', margin:'0 auto 24px', fontSize:36 }}>
          ✓
        </div>

        <div style={{ fontFamily:'Inter,system-ui', fontSize:24, fontWeight:800, color:WP.text, letterSpacing:-0.5, marginBottom:6 }}>
          {partial ? 'Sesión registrada' : 'Entrenamiento completado'}
        </div>
        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:28, fontWeight:800, color:WP.green, marginBottom:20 }}>
          {wpFmtTime(dur)}
        </div>

        {/* Stats row */}
        <div style={{ display:'flex', justifyContent:'center', gap:24, marginBottom:24, flexWrap:'wrap' }}>
          <div style={{ textAlign:'center' }}>
            <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:20, fontWeight:800, color:WP.text }}>{finished?.completedSets || 0}/{finished?.totalSets || 0}</div>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:11, color:WP.muted }}>series</div>
          </div>
          <div style={{ textAlign:'center' }}>
            <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:20, fontWeight:800, color:WP.text }}>{(finished?.totalVolume || 0).toLocaleString('es-ES')}</div>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:11, color:WP.muted }}>kg vol</div>
          </div>
        </div>

        {/* Session reward — effort-tiered, capped to one per day */}
        {(() => {
          const earned = finished?.gemsAwarded || 0;
          const tier   = finished?.gemsTier || 0;
          let label, sub;
          if (earned > 0) {
            label = `+${earned} 💎`;
            sub = tier >= 20 ? 'Sesión completa' : 'Sesión parcial';
          } else if (recordGems > 0) {
            // No hay gemas por % de series, pero sí por récord: no mostramos el
            // pill de "Sin gemas" (lo integra la tarjeta del récord, abajo).
            return null;
          } else if (finished?.rewardCapped) {
            label = 'Sin gemas';
            sub = 'Ya ganaste tu recompensa de hoy';
          } else {
            label = 'Sin gemas';
            sub = 'Completa al menos el 30% de las series';
          }
          const pos = earned > 0;
          return (
            <div style={{ display:'inline-flex', alignItems:'center', gap:10, margin:'0 auto 24px', padding:'10px 18px', borderRadius:14,
              background: pos ? 'rgba(34,197,94,0.12)' : 'rgba(255,255,255,0.05)',
              border:`1px solid ${pos ? 'rgba(34,197,94,0.4)' : WP.border}` }}>
              <span style={{ fontFamily:'Inter,system-ui', fontSize:16, fontWeight:800, color: pos ? WP.green : WP.muted }}>{label}</span>
              <span style={{ fontFamily:'Inter,system-ui', fontSize:12, color:WP.muted }}>{sub}</span>
            </div>
          );
        })()}

        {/* Muscles */}
        {muscles.length > 0 && (
          <div style={{ marginBottom:28 }}>
            <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:WP.muted, letterSpacing:1.2, marginBottom:10 }}>MÚSCULOS TRABAJADOS</div>
            <div style={{ display:'flex', flexWrap:'wrap', gap:6, justifyContent:'center' }}>
              {muscles.map(m => (
                <span key={m} style={{ padding:'3px 10px', borderRadius:999, fontFamily:'Inter,system-ui', fontSize:11, fontWeight:600, background:`${wpMuscleColor(m)}22`, border:`1px solid ${wpMuscleColor(m)}44`, color:wpMuscleColor(m) }}>{m}</span>
              ))}
            </div>
          </div>
        )}

        {/* Aviso suave de rendimiento a la baja (máx. 1/semana) */}
        {perfDrop && (
          <div style={{ marginBottom:18, textAlign:'left', padding:'14px 16px', borderRadius:12, background:'rgba(245,158,11,0.08)', border:'1px solid rgba(245,158,11,0.28)' }}>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:13, color:WP.text, lineHeight:1.55 }}>
              Tu rendimiento ha bajado estos días. Puede ser fatiga, sueño o estrés. Si se repite, valora una semana de descarga.
            </div>
            <button onClick={() => onOpenArticle && onOpenArticle('deload')} style={{ marginTop:8, padding:0, background:'none', border:'none', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:12.5, fontWeight:700, cursor:'pointer', textDecoration:'underline' }}>
              Cómo hacer una descarga →
            </button>
          </div>
        )}

        {/* Nuevos récords de la sesión */}
        {records.length > 0 && (
          <div style={{ marginBottom:20, textAlign:'left', display:'flex', flexDirection:'column', gap:8 }}>
            {recordGems > 0 && (
              <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8 }}>
                <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:WP.muted, letterSpacing:1.2 }}>NUEVOS RÉCORDS</span>
                <span style={{ fontFamily:'Inter,system-ui', fontSize:12, fontWeight:800, color:'#EAB308' }}>+{recordGems} 💎 por récord</span>
              </div>
            )}
            {records.map((r, i) => (
              <div key={i} style={{ display:'flex', alignItems:'center', gap:10, padding:'11px 14px', borderRadius:12, background:'rgba(234,179,8,0.10)', border:'1px solid rgba(234,179,8,0.32)' }}>
                <span aria-hidden="true" style={{ fontSize:18, flexShrink:0 }}>🏆</span>
                <div style={{ fontFamily:'Inter,system-ui', fontSize:13, color:WP.text, lineHeight:1.4 }}>
                  <span style={{ fontWeight:800, color:'#EAB308' }}>Nuevo récord</span> en {r.name}: <span style={{ fontWeight:700 }}>{r.text}</span>
                </div>
              </div>
            ))}
          </div>
        )}

        {/* Per-exercise comparison */}
        {comparison && (
          <div style={{ marginBottom:24, textAlign:'left' }}>
            <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:WP.muted, letterSpacing:1.2, marginBottom:10, textAlign:'center' }}>
              VS SESIÓN ANTERIOR · {wpFmtShortDate(comparison.prevDateTs, comparison.prevDate)}
            </div>
            <div style={{ display:'flex', flexDirection:'column', gap:1, borderRadius:12, overflow:'hidden', border:`1px solid ${WP.border}` }}>
              {comparison.rows.map((row, i) => {
                const hasPrev    = row.time ? (row.prevSec != null) : (row.prevKg != null);
                const improved   = hasPrev && row.delta > 0;
                const regressed  = hasPrev && row.delta < 0;
                const indicator  = improved ? '↑' : regressed ? '↓' : hasPrev ? '=' : null;
                const indColor   = improved ? WP.green : regressed ? WP.red : WP.muted;
                const fmtKg      = v => (v % 1 === 0 ? String(v) : String(Math.round(v * 10) / 10).replace('.', ','));
                return (
                  <div key={i} style={{ display:'flex', alignItems:'center', padding:'9px 14px', background: i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent', gap:8 }}>
                    <div style={{ flex:1, fontFamily:'Inter,system-ui', fontSize:11, fontWeight:600, color:WP.text, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                      {row.name}
                    </div>
                    <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, color:WP.sub, flexShrink:0 }}>
                      {row.time ? `${row.currSets}×${row.currSec}s` : `${row.currSets}×${row.currReps} · ${fmtKg(row.currKg)} kg`}
                    </div>
                    {indicator && (
                      <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, fontWeight:800, color:indColor, flexShrink:0, minWidth:36, textAlign:'right' }}>
                        {indicator}{row.delta !== 0 ? ` ${fmtKg(Math.abs(row.delta))}` : ''}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        )}

        {/* Offer to save mid-session-added exercises to the saved routine */}
        {addOffer && (
          <div style={{ marginBottom:20, textAlign:'left', borderRadius:14, border:`1px solid rgba(59,130,246,0.28)`, background:'rgba(59,130,246,0.06)', padding:'16px 18px' }}>
            {addOffer.done ? (
              <div style={{ fontFamily:'Inter,system-ui', fontSize:13.5, fontWeight:700, color:WP.green, textAlign:'center' }}>
                ✓ Guardado en {addOffer.routineName}
              </div>
            ) : (addOffer.kind === 'changes' || addOffer.kind === 'newRoutine') ? (
              <>
                <div style={{ fontFamily:'Inter,system-ui', fontSize:13.5, fontWeight:700, color:WP.text, marginBottom:4 }}>
                  Cambiaste la sesión de hoy
                </div>
                <div style={{ fontFamily:'Inter,system-ui', fontSize:12, color:WP.sub, lineHeight:1.5, marginBottom:12 }}>
                  {addOffer.kind === 'newRoutine'
                    ? <>Series o ejercicios distintos de <b style={{ color:WP.text }}>{addOffer.routineName}</b> (una predeterminada, no se edita) · ¿guardarla como rutina tuya?</>
                    : <>Series o ejercicios distintos de la plantilla · ¿guardar estos cambios en <b style={{ color:WP.text }}>{addOffer.routineName}</b>?</>}
                </div>
                <div style={{ display:'flex', gap:8 }}>
                  <button onClick={onAddToRoutine} className="wp-press" style={{ flex:1, minHeight:44, borderRadius:11, border:'none', cursor:'pointer', background:'#2563EB', color:'#fff', fontFamily:'Inter,system-ui', fontSize:13.5, fontWeight:800 }}>
                    {addOffer.kind === 'newRoutine' ? 'Guardar como rutina mía' : 'Sí, guardar'}
                  </button>
                  <button onClick={onDismissOffer} className="wp-press" style={{ flex:1, minHeight:44, borderRadius:11, border:`1px solid ${WP.border}`, cursor:'pointer', background:'transparent', color:WP.sub, fontFamily:'Inter,system-ui', fontSize:13.5, fontWeight:700 }}>
                    Solo esta vez
                  </button>
                </div>
              </>
            ) : (
              <>
                <div style={{ fontFamily:'Inter,system-ui', fontSize:13.5, fontWeight:700, color:WP.text, marginBottom:4 }}>
                  Añadiste {addOffer.names.length === 1 ? 'un ejercicio' : `${addOffer.names.length} ejercicios`} sobre la marcha
                </div>
                <div style={{ fontFamily:'Inter,system-ui', fontSize:12, color:WP.sub, lineHeight:1.5, marginBottom:12 }}>
                  {addOffer.names.slice(0,3).join(', ')}{addOffer.names.length>3?'…':''} · ¿guardarlo también en <b style={{ color:WP.text }}>{addOffer.routineName}</b> para la próxima?
                </div>
                <button onClick={onAddToRoutine} className="wp-press" style={{ width:'100%', minHeight:44, borderRadius:11, border:'none', cursor:'pointer', background:'#2563EB', color:'#fff', fontFamily:'Inter,system-ui', fontSize:13.5, fontWeight:800 }}>
                  Añadir a mi rutina
                </button>
              </>
            )}
          </div>
        )}

        {/* CTAs */}
        <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
          <button className="wp-press" onClick={onHome} style={{ padding:'13px 20px', borderRadius:12, border:'none', cursor:'pointer', background:WP.green, color:'#fff', fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700, boxShadow:'0 4px 18px -4px rgba(34,197,94,0.45)' }}>
            Guardar y volver al inicio
          </button>
          <button className="wp-press" onClick={onCoach} style={{ padding:'12px 20px', borderRadius:12, border:`1px solid rgba(59,130,246,0.30)`, cursor:'pointer', background:'transparent', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700 }}>
            Volver al Coach →
          </button>
        </div>
      </div>
    </div>
  );
}

// ── History Screen ────────────────────────────────────────────────────────────

function WpHistoryScreen({ onNewWorkout }) {
  const history = React.useMemo(() => WorkoutSessionStore.getHistory(20), []);

  return (
    <div style={{ minHeight:'100vh', background:WP.page, padding:'24px 20px 80px' }}>
      <div style={{ maxWidth:600, margin:'0 auto' }}>
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:24 }}>
          <div>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:20, fontWeight:800, color:WP.text, letterSpacing:-0.5 }}>Historial</div>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:12, color:WP.muted, marginTop:2 }}>Entrenamientos completados</div>
          </div>
          <button className="wp-press" onClick={onNewWorkout} style={{ padding:'9px 16px', borderRadius:10, border:`1px solid rgba(34,197,94,0.30)`, background:'rgba(34,197,94,0.08)', color:WP.green, fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor:'pointer' }}>
            Ir al Coach
          </button>
        </div>

        {history.length === 0 ? (
          <div style={{ textAlign:'center', padding:'60px 20px' }}>
            <div style={{ fontSize:40, marginBottom:16 }}>🏋️</div>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:16, fontWeight:700, color:WP.text, marginBottom:8 }}>
              No hay entrenamientos completados todavía
            </div>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:13, color:WP.muted, marginBottom:24 }}>
              Genera una rutina en el Coach y empieza a entrenar
            </div>
            <button className="wp-press" onClick={onNewWorkout} style={{ padding:'11px 24px', borderRadius:10, border:'none', background:WP.green, color:'#fff', fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700, cursor:'pointer' }}>
              Ir al Coach
            </button>
          </div>
        ) : (
          <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
            {history.map((entry, i) => (
              <div key={entry.id || i} style={{ borderRadius:14, border:`1px solid ${WP.border}`, background:WP.card, padding:'14px 18px' }}>
                <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:8 }}>
                  <div>
                    <div style={{ display:'flex', alignItems:'center', gap:8 }}>
                      <span style={{ fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700, color:WP.text }}>{entry.routineName || 'Entrenamiento'}</span>
                      {entry.partial && (
                        <span style={{ fontFamily:'Inter,system-ui', fontSize:9, fontWeight:800, letterSpacing:0.4, color:'#FBBF24', background:'rgba(251,191,36,0.14)', border:'1px solid rgba(251,191,36,0.30)', borderRadius:999, padding:'2px 7px' }}>PARCIAL</span>
                      )}
                    </div>
                    <div style={{ fontFamily:'Inter,system-ui', fontSize:11, color:WP.muted, marginTop:2 }}>{entry.sessionName || ''}</div>
                  </div>
                  <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:10, color:WP.muted, textAlign:'right' }}>
                    {entry.date || ''}
                  </div>
                </div>
                <div style={{ display:'flex', gap:16, flexWrap:'wrap' }}>
                  {[
                    entry.duration ? wpFmtTime(entry.duration) : null,
                    (entry.completedSets != null && entry.totalSets != null) ? `${entry.completedSets}/${entry.totalSets} series` : null,
                    entry.totalVolume ? `${entry.totalVolume.toLocaleString('es-ES')} kg` : null,
                  ].filter(Boolean).map(v => (
                    <span key={v} style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:10, fontWeight:700, color:WP.muted }}>{v}</span>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ── Active View ───────────────────────────────────────────────────────────────

// Exercise demonstration image for the active workout. Uses ExerciseMedia.Thumbnail
// so exercises WITH a free-exercise-db photo get the animated crossfade, and those
// WITHOUT one (or whose photo fails to load) get the elegant navy silhouette
// placeholder — never an empty gap or a broken image.
function wpImgGroup(ex) {
  const p = ((ex?.muscles?.primary?.[0]) || ex?.group || '').toLowerCase();
  if (/pec|pecho/.test(p)) return 'pecho';
  if (/dorsal|espalda|trapecio|romboid|lumbar|erector/.test(p)) return 'espalda';
  if (/delto|hombro/.test(p)) return 'hombro';
  if (/b[íi]cep|braquial/.test(p)) return 'biceps';
  if (/tr[íi]cep/.test(p)) return 'triceps';
  if (/cu[áa]dri|isqui|gl[úu]te|femoral|pierna|gemelo|aductor|abductor|pantorr|soleo/.test(p)) return 'pierna';
  return 'core';
}
function WpExerciseImage({ id, name }) {
  if (typeof ExerciseMedia === 'undefined') return null;
  // Enrich from the DB (muscles/compound) so the placeholder silhouette matches.
  const info = (typeof ExerciseService !== 'undefined' && ExerciseService.getById)
    ? ExerciseService.getById(id) : null;
  const ex = { id, name, muscles: info?.muscles, compound: info?.compound };
  return (
    <div style={{ borderRadius:12, overflow:'hidden', marginBottom:14 }}>
      <ExerciseMedia.Thumbnail exercise={ex} group={wpImgGroup(info || ex)} height={180} animate />
    </div>
  );
}

// A tappable substitute option in the swap sheet (comfortable 44px+ target)
function WpSwapOption({ ex, onPick }) {
  const patternMeta = (ExerciseService.META?.PATTERN_META || {})[ex.pattern];
  const primary = ex.muscles?.primary?.[0] || '';
  const equip = ex.equipment ? ex.equipment[0].toUpperCase() + ex.equipment.slice(1) : '';
  return (
    <button className="wp-press" onClick={onPick} style={{
      display:'flex', alignItems:'center', gap:11, width:'100%', textAlign:'left',
      minHeight:44, padding:'9px 11px', borderRadius:11, cursor:'pointer',
      border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.04)', color:WP.text }}>
      <WpExThumb id={ex.id} name={ex.name} />
      <div style={{ flex:1, minWidth:0 }}>
        <div style={{ fontSize:13, fontWeight:700, color:WP.text, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{ex.name}</div>
        <div style={{ fontSize:10, color:WP.muted, marginTop:2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
          {patternMeta?.short ? patternMeta.short : ''}
          {primary ? `${patternMeta?.short ? ' · ' : ''}${primary}` : ''}
          {equip ? ` · ${equip}` : ''}
        </div>
      </div>
      <span style={{ flexShrink:0, fontSize:11, fontWeight:800, color:WP.blue, fontFamily:'Inter,system-ui' }}>Elegir</span>
    </button>
  );
}

// Small thumbnail for swap options (mirrors WpExerciseImage but compact).
// No image / load error → la ilustración del GRUPO muscular (igual que en las
// tarjetas), no un círculo vacío que parezca una imagen cargando.
function WpExThumb({ id, name }) {
  const [ok, setOk] = React.useState(true);
  const url = (typeof ExerciseImages !== 'undefined') ? ExerciseImages.urlFor(id) : null;
  if (!url || !ok) {
    let ex = null; try { ex = (typeof ExerciseService !== 'undefined') ? ExerciseService.getById(id) : null; } catch (e) {}
    const group = (typeof wpImgGroup === 'function') ? wpImgGroup(ex || { id }) : 'core';
    const SvgComp = (typeof ExerciseMedia !== 'undefined' && ExerciseMedia.SVGS) ? ExerciseMedia.SVGS[group] : null;
    return (
      <div style={{ width:38, height:38, borderRadius:8, flexShrink:0, position:'relative', overflow:'hidden',
        background:'linear-gradient(150deg, #16233B 0%, #0F1A2E 100%)' }}>
        {SvgComp && <div style={{ position:'absolute', inset:0 }}><SvgComp /></div>}
        {SvgComp && <div style={{ position:'absolute', inset:0, background:'#2563EB', mixBlendMode:'multiply', pointerEvents:'none' }} />}
      </div>
    );
  }
  return (
    <div style={{ width:38, height:38, borderRadius:8, overflow:'hidden', flexShrink:0, background:'#0A1422' }}>
      <img src={url} alt={name} loading="lazy" onError={() => setOk(false)}
        style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
    </div>
  );
}

// ── Time field (isometric / duration-based sets) ───────────────────────────────
// Records DURATION in seconds instead of reps: a live stopwatch plus a manual
// seconds input. The value lives in the set (`sec`) so it persists exactly like
// kg/reps and survives reload; the stopwatch just writes into it.
function WpTimeField({ set, target, onChange, isMobile, disabled }) {
  const [running, setRunning] = React.useState(false);
  const tickRef  = React.useRef(null);
  const baseRef  = React.useRef(0);   // sec value when the stopwatch started
  const startRef = React.useRef(0);   // Date.now() at start
  const lastRef  = React.useRef(-1);  // last whole-second emitted (throttle writes)

  function stop() {
    if (tickRef.current) { clearInterval(tickRef.current); tickRef.current = null; }
    setRunning(false);
  }
  React.useEffect(() => () => { if (tickRef.current) clearInterval(tickRef.current); }, []);
  // If the set is completed elsewhere while the clock runs, stop it.
  React.useEffect(() => { if (disabled && tickRef.current) stop(); }, [disabled]);

  function start() {
    baseRef.current  = parseInt(set?.sec) || 0;
    startRef.current = Date.now();
    lastRef.current  = -1;
    setRunning(true);
    // Timestamp-based so backgrounding doesn't drift; only write on second change.
    tickRef.current = setInterval(() => {
      const elapsed = Math.round((Date.now() - startRef.current) / 1000);
      const total   = baseRef.current + elapsed;
      if (total !== lastRef.current) { lastRef.current = total; onChange(String(total)); }
    }, 300);
  }
  function toggle() { running ? stop() : start(); }
  function reset()  { stop(); onChange(''); }

  const secVal  = set?.sec || '';
  const btnBase = { minHeight: isMobile ? 50 : 42, borderRadius:10, cursor: disabled ? 'default' : 'pointer', fontFamily:'Inter,system-ui', fontWeight:800, border:'none', transition:'background 150ms ease, color 150ms ease, border-color 150ms ease' };

  return (
    <div style={{ marginBottom:14 }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:5 }}>
        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:WP.muted, letterSpacing:1 }}>DURACIÓN (SEG)</div>
        {target && <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:'rgba(34,197,94,0.7)', letterSpacing:0.6 }}>OBJETIVO {target}</div>}
      </div>
      {/* Seconds value on its own row (full width) */}
      <input
        type="number" inputMode="numeric" value={secVal} disabled={disabled || running}
        onChange={e => onChange(e.target.value)} placeholder="0"
        style={{ width:'100%', boxSizing:'border-box', padding: isMobile ? '13px 12px' : '10px 12px', minHeight: isMobile ? 50 : undefined, borderRadius:10, border:`1px solid ${running ? 'rgba(34,197,94,0.5)' : WP.border}`, background:WP.input, color: running ? WP.green : WP.text, fontFamily:'ui-monospace,Menlo,monospace', fontSize: isMobile ? 22 : 20, fontWeight:800, textAlign:'center' }} />
      {/* Stopwatch controls below, so they never crowd the value on narrow screens */}
      <div style={{ display:'flex', gap:8, marginTop:8 }}>
        <button className="wp-press" onClick={() => !disabled && toggle()} disabled={disabled}
          style={{ ...btnBase, flex:1, minWidth:0, background: running ? WP.red : 'rgba(34,197,94,0.16)', color: running ? '#fff' : WP.green, fontSize: isMobile ? 15 : 13, border: running ? 'none' : `1px solid rgba(34,197,94,0.35)` }}>
          {running ? '⏸ Detener' : '▶ Iniciar cronómetro'}
        </button>
        <button className="wp-press" onClick={() => !disabled && reset()} disabled={disabled} aria-label="Reiniciar"
          style={{ ...btnBase, width: isMobile ? 54 : 46, flexShrink:0, background:'rgba(255,255,255,0.05)', color:WP.sub, border:`1px solid ${WP.border}`, fontSize:16 }}>
          ↺
        </button>
      </div>
    </div>
  );
}

// ── Drop sets (optional advanced technique) ────────────────────────────────────
// A drop set = the main set + one or more "drops" (less weight, no rest). Opt-in
// per set, so it never gets in the way of standard logging. Sub-tramos are stored
// on the set as `drops: [{kg, reps}]`.
function WpDropSets({ set, onChange, isMobile, disabled }) {
  const drops = set?.drops || [];
  const add = () => onChange([...(set?.drops || []), { kg: '', reps: '' }]);
  const upd = (i, patch) => onChange(drops.map((d, di) => di === i ? { ...d, ...patch } : d));
  const del = (i) => { const next = drops.filter((_, di) => di !== i); onChange(next.length ? next : undefined); };

  // Mismo input que la serie normal: ancho suficiente para "102,5"/"15", números
  // tabulares y coma decimal. minWidth:0 deja que la rejilla reparta, pero el
  // contenido no se corta porque la columna nunca baja de su mínimo legible.
  // Fuente/padding ajustados para que "102,5" quepa incluso a 320 px (el botón de
  // borrar mantiene 44 px de objetivo táctil, así que el ancho se gana en el input).
  const numSty = { width:'100%', minWidth:0, boxSizing:'border-box', padding: isMobile ? '11px 6px' : '9px 10px', minHeight: isMobile ? 44 : 38, borderRadius:9, border:`1px solid ${WP.border}`, background:WP.input, color:WP.text, fontFamily:'ui-monospace,Menlo,monospace', fontSize: isMobile ? 15 : 15, fontWeight:800, textAlign:'center', fontVariantNumeric:'tabular-nums' };
  const colLab = t => (<div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:7, color:WP.muted, letterSpacing:1 }}>{t}</div>);
  // Rejilla: marcador | PESO | REPS | (borrar). Mismas columnas en cabecera y filas.
  const grid = { display:'grid', gridTemplateColumns: `20px minmax(0,1fr) minmax(0,1fr) ${disabled ? '0px' : '44px'}`, gap:6, alignItems:'center' };

  if (!drops.length) {
    if (disabled) return null;
    return (
      <button className="wp-press" onClick={add}
        style={{ display:'flex', alignItems:'center', justifyContent:'center', gap:7, width:'100%', minHeight: isMobile ? 44 : 36, marginTop:4, borderRadius:10, cursor:'pointer', border:`1px dashed rgba(245,158,11,0.4)`, background:'rgba(245,158,11,0.05)', color:'#F59E0B', fontFamily:'Inter,system-ui', fontSize: isMobile ? 13 : 12, fontWeight:700 }}>
        + Convertir en drop set
      </button>
    );
  }
  return (
    <div style={{ marginTop:4, padding:'11px 12px', borderRadius:12, border:`1px solid rgba(245,158,11,0.28)`, background:'rgba(245,158,11,0.05)' }}>
      <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:'#F59E0B', letterSpacing:1.2, marginBottom:9 }}>DROP SET · SIN DESCANSO ENTRE BAJADAS</div>
      {/* Cabecera de columnas (una sola vez, alineada con las filas) */}
      <div style={{ ...grid, marginBottom:4 }}>
        <span />{colLab('PESO (KG)')}{colLab('REPS')}<span />
      </div>
      {drops.map((d, i) => (
        <div key={i} style={{ ...grid, marginBottom:8 }}>
          <span style={{ textAlign:'center', fontFamily:'ui-monospace,Menlo,monospace', fontSize:11, fontWeight:800, color:'#F59E0B' }}>↓{i + 1}</span>
          <input type="text" inputMode="decimal" value={d.kg || ''} disabled={disabled}
            aria-label={`Peso en kilos · bajada ${i + 1}`}
            onChange={e => upd(i, { kg: e.target.value })} placeholder="0" style={numSty} />
          <input type="text" inputMode="numeric" value={d.reps || ''} disabled={disabled}
            aria-label={`Repeticiones · bajada ${i + 1}`}
            onChange={e => upd(i, { reps: e.target.value })} placeholder="0" style={numSty} />
          {!disabled && (
            <button className="wp-press" onClick={() => del(i)} aria-label={`Quitar bajada ${i + 1}`}
              style={{ width: isMobile ? 44 : 36, height: isMobile ? 44 : 36, borderRadius:9, border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.04)', color:WP.muted, cursor:'pointer', fontSize:14, display:'flex', alignItems:'center', justifyContent:'center' }}>✕</button>
          )}
        </div>
      ))}
      {!disabled && (
        <button className="wp-press" onClick={add}
          style={{ width:'100%', minHeight: isMobile ? 44 : 36, marginTop:2, borderRadius:9, cursor:'pointer', border:`1px dashed rgba(245,158,11,0.4)`, background:'transparent', color:'#F59E0B', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700 }}>
          + Añadir bajada
        </button>
      )}
    </div>
  );
}

// ── Seguimiento de cargas: última vez + sugerencia de progresión ──────────────
// Calcula, para un ejercicio, el historial, si es de peso corporal y la
// sugerencia de hoy (suggestNext). Pura; se reutiliza en la precarga y en la UI.
function wpSuggestFor(ex, log) {
  if (!ex || typeof suggestNext !== 'function' || typeof peGetExerciseHistory !== 'function') return null;
  const history = peGetExerciseHistory(log || [], ex.name);
  const tr = (typeof progParseRepRange === 'function') ? progParseRepRange(ex.repsRange) : { min: 8, max: 12 };
  let equip = '';
  try { equip = (ExerciseService.getById(ex.id)?.equipment) || ''; } catch (e) {}
  const histBodyweight = history[0] && (history[0].sets || []).length > 0 &&
    (history[0].sets || []).every(s => !(Number(s.kg) > 0));
  const isBodyweight = /peso corporal|bodyweight|sin peso|calistenia/i.test(equip) || !!histBodyweight;
  const increment = (typeof AtlasSettings !== 'undefined') ? AtlasSettings.incrementFor(ex) : 2.5;
  const sug = suggestNext(history, {
    repMin: tr.min, repMax: tr.max,
    targetRir: (ex.rir != null ? ex.rir : null),
    increment, isBodyweight,
  });
  return { sug, history, isBodyweight, tr };
}

// Precarga el kg de las series NO hechas con la sugerencia (o el peso de la
// última vez). Solo en sesiones nuevas; nunca pisa lo que el usuario ya hizo.
function wpPrefillSuggestions(session, log) {
  if (!session || !Array.isArray(session.exercises)) return session;
  session.exercises.forEach(ex => {
    const info = wpSuggestFor(ex, log);
    if (!info || info.isBodyweight) return;
    const kg = (info.sug && info.sug.kg != null) ? info.sug.kg
             : (info.history[0] ? info.history[0].maxKg : null);
    if (kg == null || !(kg > 0)) return;
    const str = String(kg);
    (ex.sets || []).forEach(set => {
      if (set.done) return;
      set.kg = str;
      if (set.left)  set.left.kg  = set.left.kg  || str;
      if (set.right) set.right.kg = set.right.kg || str;
    });
  });
  return session;
}

// Formatea la línea "Última vez": "60 kg × 10 · 10 · 9 — RIR 2".
function wpFmtLastTime(entry) {
  if (!entry) return null;
  const sets = (entry.sets || []).filter(s => Number(s.reps) > 0 || Number(s.sec) > 0);
  if (!sets.length) return null;
  const kg = Math.max(...sets.map(s => Number(s.kg) || 0));
  const reps = sets.map(s => Number(s.reps) || Number(s.sec) || 0);
  const rirs = sets.map(s => s.rir).filter(r => r != null && r !== '').map(Number);
  const fmtKg = v => (v % 1 === 0 ? String(v) : String(v).replace('.', ','));
  const head = kg > 0 ? `${fmtKg(kg)} kg × ${reps[0]}` : `${reps[0]}${entry.sets?.[0]?.sec ? ' s' : ' reps'}`;
  const tail = reps.slice(1).join(' · ');
  const rirTxt = rirs.length ? ` — RIR ${Math.min(...rirs)}` : '';
  return `${head}${tail ? ' · ' + tail : ''}${rirTxt}`;
}

// Bloque en la tarjeta del ejercicio: última vez + sugerencia de hoy + ¿Por qué?
function WpLastSuggestion({ ex, log, onOpenArticle }) {
  const [why, setWhy] = React.useState(false);
  const info = React.useMemo(() => wpSuggestFor(ex, log), [ex.id, ex.name, ex.repsRange, ex.rir, log]);
  if (!info) return null;
  const { sug, history, tr } = info;
  const last = history[0];
  const lastLine = wpFmtLastTime(last);
  const fmtKg = v => (v == null ? '' : (v % 1 === 0 ? String(v) : String(v).replace('.', ',')));
  const arrow = sug.action === 'subir' ? '↑' : sug.action === 'bajar' ? '↓' : '→';
  const color = sug.action === 'subir' ? '#22C55E' : sug.action === 'bajar' ? '#F59E0B' : '#93C5FD';

  const fmtDate = (d) => { try { return new Date(last.dateTs || d).toLocaleDateString('es-ES', { weekday:'short', day:'numeric', month:'short' }); } catch { return d; } };

  return (
    <div style={{ marginBottom:12, padding:'10px 12px', borderRadius:12, background:'rgba(59,130,246,0.06)', border:`1px solid ${WP.border}` }}>
      {lastLine ? (
        <div style={{ fontFamily:'Inter,system-ui', fontSize:12, color:WP.sub, lineHeight:1.5 }}>
          <span style={{ color:WP.muted }}>Última vez</span>
          {last?.date ? <span style={{ color:WP.muted }}> ({fmtDate(last.date)})</span> : null}: <span style={{ color:WP.text, fontWeight:600 }}>{lastLine}</span>
        </div>
      ) : (
        <div style={{ fontFamily:'Inter,system-ui', fontSize:12, color:WP.sub, lineHeight:1.5 }}>
          Primera vez: elige un peso con el que llegues a <strong style={{ color:WP.text }}>{tr.max} reps</strong> dejando <strong style={{ color:WP.text }}>{ex.rir != null ? ex.rir : 2}</strong> en reserva.
        </div>
      )}
      {sug.action !== 'primera' && (
        <div style={{ display:'flex', alignItems:'center', gap:8, flexWrap:'wrap', marginTop:6 }}>
          <span aria-hidden="true" style={{ fontSize:14 }}>💡</span>
          <span style={{ fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700, color:WP.text }}>
            Sugerencia de hoy: <span style={{ color }}>{arrow} {sug.kg != null ? `${fmtKg(sug.kg)} kg` : `${sug.targetReps} reps`}</span>
          </span>
          <button onClick={() => setWhy(w => !w)} aria-expanded={why}
            style={{ minHeight:28, padding:'2px 8px', borderRadius:8, border:`1px solid ${WP.border}`, background:'transparent', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor:'pointer' }}>
            ¿Por qué?
          </button>
        </div>
      )}
      {why && (
        <div style={{ marginTop:8, fontFamily:'Inter,system-ui', fontSize:12.5, color:WP.sub, lineHeight:1.6 }}>
          {sug.reason}
          <div style={{ marginTop:6, display:'flex', gap:10, flexWrap:'wrap' }}>
            <button onClick={() => onOpenArticle && onOpenArticle('sobrecarga-progresiva')} style={{ padding:0, background:'none', border:'none', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor:'pointer', textDecoration:'underline' }}>Sobrecarga progresiva</button>
            <button onClick={() => onOpenArticle && onOpenArticle('rpe-rir')} style={{ padding:0, background:'none', border:'none', color:'#93C5FD', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor:'pointer', textDecoration:'underline' }}>RPE y RIR</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ── Historial por ejercicio: gráfica SVG propia + récords + lista de sesiones ──
function WpExHistoryChart({ points, metric }) {
  const vals = points.map(p => p[metric]);
  const W = 300, H = 150, padL = 8, padR = 8, padT = 14, padB = 22;
  const max = Math.max(...vals, 1), min = Math.min(...vals, 0);
  const span = max - min || 1;
  const n = points.length;
  const x = i => padL + (n === 1 ? (W - padL - padR) / 2 : (i * (W - padL - padR)) / (n - 1));
  const y = v => padT + (1 - (v - min) / span) * (H - padT - padB);
  const color = metric === 'kg' ? '#2563EB' : metric === 'e1rm' ? '#7c3aed' : '#059669';
  const line = points.map((p, i) => `${x(i).toFixed(1)},${y(p[metric]).toFixed(1)}`).join(' ');
  const fmtDate = d => { try { return new Date(d).toLocaleDateString('es-ES', { day:'numeric', month:'short' }); } catch { return ''; } };
  const label = metric === 'kg' ? 'mejor serie (kg)' : metric === 'e1rm' ? '1RM estimado' : 'volumen (kg)';
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width="100%" role="img" aria-label={`Evolución de ${label} por sesión`} style={{ display:'block' }}>
      <line x1={padL} y1={H - padB} x2={W - padR} y2={H - padB} stroke="rgba(255,255,255,0.12)" strokeWidth="1" />
      <polyline points={line} fill="none" stroke={color} strokeWidth="2.4" strokeLinejoin="round" strokeLinecap="round" />
      {points.map((p, i) => <circle key={i} cx={x(i)} cy={y(p[metric])} r="3" fill={color} />)}
      <text x={padL} y={H - 6} fill="rgba(255,255,255,0.45)" fontSize="9" fontFamily="Inter,system-ui">{fmtDate(points[0]?.dateTs)}</text>
      {n > 1 && <text x={W - padR} y={H - 6} textAnchor="end" fill="rgba(255,255,255,0.45)" fontSize="9" fontFamily="Inter,system-ui">{fmtDate(points[n - 1]?.dateTs)}</text>}
      {metric === 'e1rm' && <text x={W - padR} y={11} textAnchor="end" fill="rgba(255,255,255,0.4)" fontSize="8.5" fontStyle="italic" fontFamily="Inter,system-ui">estimado</text>}
    </svg>
  );
}

function WpExerciseHistory({ exercise, log, onClose }) {
  if (typeof useEscClose === 'function') useEscClose(onClose);
  const [metric, setMetric] = React.useState('kg');
  const isMobile = useIsMobile();
  const hist = (typeof peGetExerciseHistory === 'function') ? peGetExerciseHistory(log || [], exercise.name) : [];
  const chrono = hist.slice().reverse();  // más antigua → más reciente
  const fmtKg = v => (v % 1 === 0 ? String(v) : String(Math.round(v * 10) / 10).replace('.', ','));
  const points = chrono.map(h => {
    const sets = (h.sets || []).filter(s => Number(s.reps) > 0);
    const kg = Math.max(0, ...sets.map(s => Number(s.kg) || 0));
    const e1rm = Math.max(0, ...sets.map(s => (typeof progEstimate1RM === 'function' ? (progEstimate1RM(s.kg, s.reps) || 0) : 0)));
    const vol = sets.reduce((a, s) => a + (Number(s.kg) || 0) * (Number(s.reps) || 0), 0);
    return { date: h.date, dateTs: h.dateTs, kg, e1rm, vol };
  });
  const recKg  = Math.max(0, ...points.map(p => p.kg));
  const recE1  = Math.max(0, ...points.map(p => p.e1rm));
  const recVol = Math.max(0, ...points.map(p => p.vol));
  const metrics = [
    { k: 'kg',   label: 'Peso' },
    { k: 'e1rm', label: '1RM est.' },
    { k: 'vol',  label: 'Volumen' },
  ];

  if (typeof ReactDOM === 'undefined') return null;
  return ReactDOM.createPortal(
    <div onClick={onClose} style={{ position:'fixed', inset:0, zIndex:6000, background:'rgba(6,13,24,0.72)', display:'flex', alignItems: isMobile ? 'flex-end' : 'center', justifyContent:'center', padding: isMobile ? 0 : 20 }}>
      <div role="dialog" aria-modal="true" aria-label={`Historial de ${exercise.name}`} onClick={e => e.stopPropagation()}
        style={{ width: isMobile ? '100%' : 460, maxWidth:'100%', maxHeight: isMobile ? '88vh' : '86vh', overflowY:'auto', background:WP.card, border:`1px solid ${WP.border}`, borderRadius: isMobile ? '18px 18px 0 0' : 18, padding:'20px 20px calc(20px + env(safe-area-inset-bottom))' }}>
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:4 }}>
          <div style={{ fontFamily:'Inter,system-ui', fontSize:17, fontWeight:800, color:WP.text, letterSpacing:-0.3 }}>{exercise.name}</div>
          <button onClick={onClose} aria-label="Cerrar" style={{ width:32, height:32, borderRadius:9, border:`1px solid ${WP.border}`, background:'transparent', color:WP.sub, cursor:'pointer', fontSize:15 }}>✕</button>
        </div>
        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, color:WP.muted, letterSpacing:1, marginBottom:14 }}>HISTORIAL · {points.length} {points.length === 1 ? 'SESIÓN' : 'SESIONES'}</div>

        {points.length === 0 ? (
          <div style={{ fontFamily:'Inter,system-ui', fontSize:14, color:WP.sub, padding:'20px 0' }}>Aún no tienes sesiones registradas de este ejercicio.</div>
        ) : (
          <>
            {/* Récords personales */}
            <div style={{ display:'flex', gap:8, marginBottom:16 }}>
              {[['Mejor peso', recKg > 0 ? `${fmtKg(recKg)} kg` : '—'], ['Mejor 1RM est.', recE1 > 0 ? `${fmtKg(recE1)} kg` : '—'], ['Mejor volumen', recVol > 0 ? `${Math.round(recVol).toLocaleString('es-ES')} kg` : '—']].map(([l, v], i) => (
                <div key={i} style={{ flex:1, textAlign:'center', padding:'10px 6px', borderRadius:12, background:'rgba(255,255,255,0.03)', border:`1px solid ${WP.border}` }}>
                  <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:WP.muted, letterSpacing:0.5, marginBottom:4 }}>{l.toUpperCase()}</div>
                  <div style={{ fontFamily:'Inter,system-ui', fontSize:13.5, fontWeight:800, color:WP.text }}>{v}</div>
                </div>
              ))}
            </div>

            {/* Gráfica (≥3 sesiones) o mensaje */}
            {points.length >= 3 ? (
              <div style={{ marginBottom:16 }}>
                <div style={{ display:'flex', gap:6, marginBottom:8 }}>
                  {metrics.map(m => (
                    <button key={m.k} onClick={() => setMetric(m.k)} aria-pressed={metric === m.k}
                      style={{ flex:1, minHeight:34, borderRadius:9, cursor:'pointer', fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700,
                        border:`1px solid ${metric === m.k ? 'transparent' : WP.border}`, background: metric === m.k ? '#2563EB' : 'transparent', color: metric === m.k ? '#fff' : WP.sub }}>
                      {m.label}
                    </button>
                  ))}
                </div>
                <div style={{ borderRadius:12, border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.02)', padding:'10px 8px' }}>
                  <WpExHistoryChart points={points} metric={metric} />
                </div>
              </div>
            ) : (
              <div style={{ marginBottom:16, padding:'16px 14px', borderRadius:12, border:`1px dashed ${WP.border}`, textAlign:'center', fontFamily:'Inter,system-ui', fontSize:13, color:WP.sub }}>
                Con 3 sesiones empezamos a mostrar tu tendencia.
              </div>
            )}

            {/* Lista de sesiones */}
            <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
              {hist.map((h, i) => {
                const sets = (h.sets || []).filter(s => Number(s.reps) > 0 || Number(s.sec) > 0);
                return (
                  <div key={i} style={{ padding:'10px 12px', borderRadius:11, background:'rgba(255,255,255,0.03)', border:`1px solid ${WP.border}` }}>
                    <div style={{ fontFamily:'Inter,system-ui', fontSize:11, fontWeight:700, color:WP.muted, marginBottom:5 }}>{(() => { try { return new Date(h.dateTs || h.date).toLocaleDateString('es-ES', { weekday:'short', day:'numeric', month:'short' }); } catch { return h.date; } })()}</div>
                    <div style={{ display:'flex', flexWrap:'wrap', gap:'4px 8px' }}>
                      {sets.map((s, j) => (
                        <span key={j} style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:11.5, color:WP.text }}>
                          {Number(s.kg) > 0 ? `${fmtKg(Number(s.kg))}×${Number(s.reps)}` : (Number(s.sec) > 0 ? `${Number(s.sec)}s` : `${Number(s.reps)}`)}
                          {s.rir != null && s.rir !== '' ? <span style={{ color:WP.muted }}> (RIR {Number(s.rir)})</span> : null}
                          {j < sets.length - 1 ? <span style={{ color:WP.muted }}> ·</span> : null}
                        </span>
                      ))}
                    </div>
                  </div>
                );
              })}
            </div>
          </>
        )}
      </div>
    </div>,
    document.body
  );
}

function WpActiveView({ session, log, onOpenArticle, exIdx, setIdx, elapsed, restTimer, restFlash, onCompleteSet, onUpdateSet, onUpdateSide, onSelectSet, onNavigateEx, onFinish, onExitPause, onExitSave, onExitDiscard, onRestAdjust, onRestSkip, onSwap, onAddExercise, swapRoutineName, onAddSet, onRemoveLastSet, undoSet, onUndoRemove, onDismissUndo, confirmSet, onConfirmRemove, onCancelConfirm }) {
  const isMobile = useIsMobile();
  const TABBAR = 'calc(56px + env(safe-area-inset-bottom))';

  // Exit dialog state
  const [exitOpen, setExitOpen] = React.useState(false);
  const [confirmDiscard, setConfirmDiscard] = React.useState(false);
  const [confirmFinish, setConfirmFinish] = React.useState(false);
  const doneCount = session.exercises.reduce((t, e) => t + e.sets.filter(s => s.done).length, 0);
  const totalCount = session.exercises.reduce((t, e) => t + e.sets.length, 0);
  // Terminar con menos de la mitad de las series → pedir confirmación.
  function handleFinishClick() {
    if (totalCount > 0 && doneCount < totalCount / 2) setConfirmFinish(true);
    else onFinish();
  }
  function handleSalir() {
    // Nothing logged yet → nothing to lose, exit straight away (discard).
    if (doneCount === 0) { onExitDiscard(); return; }
    setConfirmDiscard(false);
    setExitOpen(true);
  }

  // Exercise picker sheet — one selector reused for two modes:
  //   'swap' → replace the current exercise
  //   'add'  → add an extra exercise to today's session
  const [sheetMode, setSheetMode] = React.useState(null); // null | 'swap' | 'add'
  const [histOpen, setHistOpen] = React.useState(false);  // "Ver historial" del ejercicio actual
  const [sheetQuery, setSheetQuery] = React.useState('');
  const [applyToRoutine, setApplyToRoutine] = React.useState(false);
  const sheetEx = sheetMode ? session.exercises[exIdx] : null;   // seeds smart suggestions
  const alternatives = React.useMemo(
    () => (sheetEx ? ExerciseService.alternativesFor(sheetEx, 12) : []),
    [sheetEx?.id, exIdx, sheetMode]
  );
  const searchResults = React.useMemo(() => {
    const q = sheetQuery.trim();
    if (q.length < 2) return [];
    const altIds = new Set(alternatives.map(a => a.id));
    return ExerciseService.getAll({ query: q })
      .filter(e => (sheetMode === 'add' || e.id !== sheetEx?.id) && !altIds.has(e.id))
      .slice(0, 20);
  }, [sheetQuery, alternatives, sheetEx?.id, sheetMode]);
  function openSwap()   { setSheetQuery(''); setApplyToRoutine(false); setSheetMode('swap'); }
  function openAdd()    { setSheetQuery(''); setSheetMode('add'); }
  function closeSheet() { setSheetMode(null); setSheetQuery(''); }
  function pickExercise(dbEx) {
    if (sheetMode === 'add') onAddExercise(dbEx);
    else onSwap(dbEx, applyToRoutine);
    closeSheet();
  }

  // Immersive training: hide the app's mobile top header during the active view
  React.useEffect(() => {
    document.body.classList.add('atlas-training');
    return () => document.body.classList.remove('atlas-training');
  }, []);

  const ex = session.exercises[exIdx];
  if (!ex) return null;
  const set = ex.sets[setIdx];
  const isTime = ex.unit === 'time';
  const isUni  = !isTime && ex.unilateral === true;
  const totalExs = session.exercises.length;
  const doneSets = session.exercises.reduce((t, e) => t + e.sets.filter(s => s.done).length, 0);
  const totalSets = session.exercises.reduce((t, e) => t + e.sets.length, 0);
  const pct = totalSets > 0 ? Math.round((doneSets / totalSets) * 100) : 0;

  // Look up cues from ExerciseService
  const cues = React.useMemo(() => {
    try {
      const info = ExerciseService.getById(ex.id);
      return (info?.cues || []).slice(0, 3);
    } catch { return []; }
  }, [ex.id]);

  return (
    <div style={{ display:'flex', flexDirection:'column', background:WP.page,
      // Mobile: fill the screen; the app top header is hidden via body.atlas-training
      minHeight: isMobile ? '100dvh' : '100vh' }}>

      <h1 className="sr-only">Entrenamiento en curso{session?.sessionName ? ` — ${session.sessionName}` : ''}</h1>

      {/* Rest-finished visual flash — the silent-mode fallback (no external assets) */}
      {restFlash && (
        <div className="wp-flash" style={{ position:'fixed', inset:0, zIndex:500, pointerEvents:'none',
          background:'rgba(34,197,94,0.28)' }}>
          <div style={{ position:'absolute', top:'42%', left:0, right:0, textAlign:'center',
            fontFamily:'Inter,system-ui', fontWeight:900, fontSize:'clamp(28px,9vw,48px)', color:'#fff',
            textShadow:'0 4px 24px rgba(0,0,0,0.5)', letterSpacing:-1 }}>
            ¡Siguiente serie!
          </div>
        </div>
      )}

      {/* Fixed header — training hides the app's mobile header (which carried the
          safe-area inset), so pad the top here or the notch/Dynamic Island covers
          "← Salir". The dark bg fills the inset zone too (no transparent strip). */}
      <div style={{ background:'rgba(6,13,24,0.97)', borderBottom:`1px solid ${WP.border}`, flexShrink:0, position:'sticky', top:0, zIndex:10,
        paddingTop: isMobile ? 'env(safe-area-inset-top)' : undefined }}>
        {/* Top bar — "← Salir" y el cronómetro no se parten; el nombre largo del
            ejercicio es quien se trunca (antes a 320 px "Salir" caía a dos líneas). */}
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:10, padding:'10px 16px' }}>
          <button className="wp-press" onClick={handleSalir} style={{ flexShrink:0, whiteSpace:'nowrap', padding:'6px 12px', borderRadius:8, border:`1px solid ${WP.border}`, background:'transparent', color:WP.muted, fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor:'pointer' }}>
            ← Salir
          </button>
          <div style={{ flex:1, minWidth:0, fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700, color:WP.text, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', textAlign:'center' }}>
            {ex.name}
          </div>
          <div style={{ flexShrink:0, whiteSpace:'nowrap', fontFamily:'ui-monospace,Menlo,monospace', fontSize:14, fontWeight:800, color:WP.green }}>
            {wpFmtTime(elapsed)}
          </div>
        </div>
        {/* Progress info */}
        <div style={{ padding:'4px 16px 8px', display:'flex', alignItems:'center', gap:10 }}>
          <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, color:WP.muted, whiteSpace:'nowrap' }}>
            Ej {exIdx + 1} de {totalExs} · {doneSets}/{totalSets} series
          </div>
          <div style={{ flex:1, height:4, borderRadius:999, background:'rgba(255,255,255,0.08)', overflow:'hidden' }}>
            <div style={{ height:'100%', width:'100%', background:WP.green, transform:`scaleX(${Math.max(0, Math.min(1, pct/100))})`, transformOrigin:'left', transition:'transform 250ms ease' }} />
          </div>
          <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, color:WP.green, whiteSpace:'nowrap' }}>{pct}%</div>
        </div>
      </div>

      {/* Rest timer banner */}
      <WpRestTimer restTimer={restTimer} onSkip={onRestSkip} onAdjust={onRestAdjust} isMobile={isMobile} />

      {/* Scrollable content — clears the fixed mobile tab bar */}
      <div style={{ flex:1, overflowY:'auto', padding: isMobile ? `20px 16px calc(40px + ${TABBAR})` : '20px 16px 100px' }}>
        <div style={{ maxWidth:540, margin:'0 auto' }}>

          {/* Exercise card — keyed by exercise id so it fades in when the active
              exercise changes (navigation, auto-advance, swap). */}
          <div key={ex.id} className="wp-ex-enter" style={{ borderRadius:16, border:`1px solid ${WP.border}`, background:WP.card, padding:'18px 18px 14px', marginBottom:16 }}>
            <WpExerciseImage id={ex.id} name={ex.name} />
            <div style={{ fontFamily:'Inter,system-ui', fontSize:20, fontWeight:800, color:WP.text, letterSpacing:-0.5, marginBottom:10 }}>
              {ex.name}
            </div>

            {/* Muscles + equipment */}
            <div style={{ display:'flex', alignItems:'center', gap:8, flexWrap:'wrap', marginBottom: cues.length ? 12 : 0 }}>
              {(ex.muscles?.primary || []).map(m => (
                <span key={m} style={{ display:'inline-flex', alignItems:'center', gap:4, fontFamily:'Inter,system-ui', fontSize:11, fontWeight:600, color:wpMuscleColor(m) }}>
                  <span style={{ width:6, height:6, borderRadius:'50%', background:wpMuscleColor(m), display:'block', flexShrink:0 }} />
                  {m}
                </span>
              ))}
              {ex.group && (
                <span style={{ fontFamily:'Inter,system-ui', fontSize:10, color:WP.muted }}>· {ex.group}</span>
              )}
            </div>

            {/* Seguimiento de cargas: última vez + sugerencia de progresión */}
            {!isTime && <WpLastSuggestion ex={ex} log={log} onOpenArticle={onOpenArticle} />}

            {/* Cues */}
            {cues.length > 0 && (
              <div style={{ display:'flex', flexDirection:'column', gap:4 }}>
                {cues.map((cue, ci) => (
                  <div key={ci} style={{ display:'flex', gap:6, alignItems:'flex-start' }}>
                    <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, color:WP.muted, flexShrink:0, marginTop:2 }}>•</span>
                    <span style={{ fontFamily:'Inter,system-ui', fontSize:11, color:WP.sub, lineHeight:1.5 }}>{cue}</span>
                  </div>
                ))}
              </div>
            )}
          </div>

          {/* Cambiar ejercicio + Ver historial — justo encima de las series */}
          <div style={{ display:'flex', gap:8, marginBottom:14 }}>
            <button className="wp-press" onClick={openSwap}
              style={{ flex:1, display:'flex', alignItems:'center', justifyContent:'center', gap:8,
                minHeight: isMobile ? 46 : 38, borderRadius:12, cursor:'pointer',
                border:`1px solid rgba(59,130,246,0.30)`, background:'rgba(59,130,246,0.08)', color:WP.blue,
                fontFamily:'Inter,system-ui', fontSize: isMobile ? 14 : 13, fontWeight:700, letterSpacing:-0.1 }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M7 4 3 8l4 4" /><path d="M3 8h13a4 4 0 0 1 4 4" />
                <path d="M17 20l4-4-4-4" /><path d="M21 16H8a4 4 0 0 1-4-4" />
              </svg>
              Cambiar ejercicio
            </button>
            <button className="wp-press" onClick={() => setHistOpen(true)} aria-label={`Ver historial de ${ex.name}`}
              style={{ flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center', gap:7, padding:'0 14px',
                minHeight: isMobile ? 46 : 38, borderRadius:12, cursor:'pointer',
                border:`1px solid ${WP.border}`, background:'transparent', color:WP.sub,
                fontFamily:'Inter,system-ui', fontSize: isMobile ? 14 : 13, fontWeight:700 }}>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M3 3v18h18" /><path d="M7 14l4-4 4 3 5-6" />
              </svg>
              Historial
            </button>
          </div>
          {histOpen && <WpExerciseHistory exercise={ex} log={log} onClose={() => setHistOpen(false)} />}

          {/* Set panel */}
          <div style={{ borderRadius:16, border:`1px solid ${WP.border}`, background:WP.card, padding:'18px', marginBottom:14 }}>
            {isTime ? (
              <>
                {/* Set info — isometric */}
                <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:10, fontWeight:700, color:WP.muted, letterSpacing:0.8, marginBottom:14 }}>
                  SERIE {setIdx + 1} DE {ex.sets.length} · ISOMÉTRICO{ex.timeTarget ? ` · OBJETIVO ${ex.timeTarget}` : ''}
                </div>

                {/* Duration (stopwatch + manual) + optional load */}
                <WpTimeField set={set} target={ex.timeTarget} isMobile={isMobile} disabled={!!set?.done}
                  onChange={sec => onUpdateSet(exIdx, setIdx, { sec })} />

                <div style={{ marginBottom:16 }}>
                  <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:WP.muted, letterSpacing:1, marginBottom:5 }}>LASTRE (KG) · OPCIONAL</div>
                  <input
                    type="number" inputMode="decimal" value={set?.kg || ''} aria-label={`Lastre en kilos (opcional) · serie ${setIdx + 1}`}
                    onChange={e => onUpdateSet(exIdx, setIdx, { kg: e.target.value })} placeholder="0"
                    style={{ width:'100%', padding: isMobile ? '11px 12px' : '9px 12px', minHeight: isMobile ? 46 : undefined, borderRadius:10, border:`1px solid ${WP.border}`, background:WP.input, color:WP.text, fontFamily:'ui-monospace,Menlo,monospace', fontSize: isMobile ? 18 : 16, fontWeight:800, textAlign:'center', boxSizing:'border-box' }}
                  />
                </div>
              </>
            ) : isUni ? (
              <>
                {/* Set info — unilateral */}
                <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:14, gap:8, flexWrap:'wrap' }}>
                  <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:10, fontWeight:700, color:WP.muted, letterSpacing:0.8 }}>
                    SERIE {setIdx + 1} DE {ex.sets.length} · POR LADO · OBJETIVO {ex.repsRange} REPS
                  </div>
                  <button className="wp-press"
                    onClick={() => { if (set?.done) return; const l = set?.left || {}; onUpdateSide(exIdx, setIdx, 'right', { kg: l.kg || '', reps: l.reps || '' }); }}
                    disabled={!!set?.done}
                    style={{ fontFamily:'Inter,system-ui', fontSize:10, fontWeight:700, color:WP.blue, background:'rgba(59,130,246,0.08)', border:`1px solid rgba(59,130,246,0.25)`, borderRadius:8, padding:'5px 9px', cursor: set?.done ? 'default':'pointer' }}>
                    Copiar izq → der
                  </button>
                </div>

                {[['left','IZQUIERDO'],['right','DERECHO']].map(([side, label]) => (
                  <div key={side} style={{ marginBottom:12, padding:'11px 12px', borderRadius:12, border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.02)' }}>
                    <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:9, fontWeight:700, color: side==='left' ? '#60A5FA' : '#F59E0B', letterSpacing:1.2, marginBottom:8 }}>{label}</div>
                    <div style={{ display:'flex', gap:10 }}>
                      <div style={{ flex:1 }}>
                        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:WP.muted, letterSpacing:1, marginBottom:5 }}>PESO (KG)</div>
                        <input type="number" inputMode="decimal" value={set?.[side]?.kg || ''} aria-label={`Peso en kilos · lado ${label.toLowerCase()} · serie ${setIdx + 1}`}
                          onChange={e => onUpdateSide(exIdx, setIdx, side, { kg: e.target.value })} placeholder="0"
                          style={{ width:'100%', boxSizing:'border-box', padding: isMobile ? '12px 10px' : '9px 10px', minHeight: isMobile ? 48 : undefined, borderRadius:10, border:`1px solid ${WP.border}`, background:WP.input, color:WP.text, fontFamily:'ui-monospace,Menlo,monospace', fontSize: isMobile ? 20 : 18, fontWeight:800, textAlign:'center' }} />
                      </div>
                      <div style={{ flex:1 }}>
                        <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:WP.muted, letterSpacing:1, marginBottom:5 }}>REPS</div>
                        <input type="number" inputMode="numeric" value={set?.[side]?.reps || ''} aria-label={`Repeticiones · lado ${label.toLowerCase()} · serie ${setIdx + 1}`}
                          onChange={e => onUpdateSide(exIdx, setIdx, side, { reps: e.target.value })} placeholder="0"
                          style={{ width:'100%', boxSizing:'border-box', padding: isMobile ? '12px 10px' : '9px 10px', minHeight: isMobile ? 48 : undefined, borderRadius:10, border:`1px solid ${WP.border}`, background:WP.input, color:WP.text, fontFamily:'ui-monospace,Menlo,monospace', fontSize: isMobile ? 20 : 18, fontWeight:800, textAlign:'center' }} />
                      </div>
                    </div>
                  </div>
                ))}

                {/* RPE selector */}
                <div style={{ marginTop:4, marginBottom:16 }}>
                  <WpRirSelector value={(set?.rir ?? null)} onChange={rir => onUpdateSet(exIdx, setIdx, { rir })} />
                </div>
              </>
            ) : (
              <>
                {/* Set info */}
                <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:10, fontWeight:700, color:WP.muted, letterSpacing:0.8, marginBottom:14 }}>
                  SERIE {setIdx + 1} DE {ex.sets.length} · OBJETIVO {ex.repsRange} REPS · RIR {ex.rir}
                </div>

                {/* Kg + Reps inputs */}
                <div style={{ display:'flex', gap:10, marginBottom:14 }}>
                  <div style={{ flex:1 }}>
                    <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:WP.muted, letterSpacing:1, marginBottom:5 }}>PESO (KG)</div>
                    <input
                      type="number"
                      inputMode="decimal"
                      aria-label={`Peso en kilos · serie ${setIdx + 1}`}
                      value={set?.kg || ''}
                      onChange={e => onUpdateSet(exIdx, setIdx, { kg: e.target.value })}
                      placeholder="0"
                      style={{ width:'100%', padding: isMobile ? '13px 12px' : '10px 12px', minHeight: isMobile ? 50 : undefined, borderRadius:10, border:`1px solid ${WP.border}`, background:WP.input, color:WP.text, fontFamily:'ui-monospace,Menlo,monospace', fontSize: isMobile ? 22 : 20, fontWeight:800, textAlign:'center', boxSizing:'border-box' }}
                    />
                  </div>
                  <div style={{ flex:1 }}>
                    <div style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, color:WP.muted, letterSpacing:1, marginBottom:5 }}>REPS</div>
                    <input
                      type="number"
                      inputMode="numeric"
                      aria-label={`Repeticiones · serie ${setIdx + 1}`}
                      value={set?.reps || ''}
                      onChange={e => onUpdateSet(exIdx, setIdx, { reps: e.target.value })}
                      placeholder="0"
                      style={{ width:'100%', padding: isMobile ? '13px 12px' : '10px 12px', minHeight: isMobile ? 50 : undefined, borderRadius:10, border:`1px solid ${WP.border}`, background:WP.input, color:WP.text, fontFamily:'ui-monospace,Menlo,monospace', fontSize: isMobile ? 22 : 20, fontWeight:800, textAlign:'center', boxSizing:'border-box' }}
                    />
                  </div>
                </div>

                {/* RPE selector */}
                <div style={{ marginBottom:12 }}>
                  <WpRirSelector value={(set?.rir ?? null)} onChange={rir => onUpdateSet(exIdx, setIdx, { rir })} />
                </div>

                {/* Drop set (optional advanced technique) */}
                <div style={{ marginBottom:16 }}>
                  <WpDropSets set={set} isMobile={isMobile} disabled={!!set?.done}
                    onChange={drops => onUpdateSet(exIdx, setIdx, { drops })} />
                </div>
              </>
            )}

            {/* Complete set button */}
            <button
              className="wp-press"
              onClick={() => !set?.done && onCompleteSet(exIdx, setIdx)}
              disabled={!!set?.done}
              style={{
                width:'100%', padding: isMobile ? '16px' : '13px 16px', minHeight: isMobile ? 54 : undefined, borderRadius:12, border:'none',
                cursor: set?.done ? 'default' : 'pointer',
                background: set?.done ? 'rgba(34,197,94,0.20)' : WP.green,
                color: set?.done ? WP.green : '#fff',
                fontFamily:'Inter,system-ui', fontSize: isMobile ? 16 : 15, fontWeight:800, letterSpacing:-0.3,
                boxShadow: set?.done ? 'none' : '0 4px 18px -4px rgba(34,197,94,0.45)',
                // Explicit properties (never `all`): scale-on-press via var(--ease-out),
                // plus a gentle settle on the completed-state color/shadow swap.
                transition:'transform 120ms var(--ease-out), background 150ms ease, box-shadow 150ms ease, color 150ms ease',
              }}
            >
              {set?.done ? '✓ Serie completada' : '✓ Completar serie'}
            </button>
          </div>

          {/* Set status grid + añadir/quitar serie (solo esta sesión) */}
          <div style={{ borderRadius:14, border:`1px solid ${WP.border}`, background:WP.card, padding:'14px 16px', marginBottom:14 }}>
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:10 }}>
              <span style={{ fontFamily:'ui-monospace,Menlo,monospace', fontSize:8, fontWeight:700, color:WP.muted, letterSpacing:1.2 }}>SERIES · {ex.sets.filter(s => s.done).length}/{ex.sets.length}</span>
            </div>
            <WpSetGrid sets={ex.sets} currentSetIdx={setIdx} onSelectSet={i => onSelectSet(exIdx, i)} />
            <div style={{ display:'flex', gap:8, marginTop:12 }}>
              <button className="wp-press" onClick={() => onAddSet(exIdx)} aria-label="Añadir una serie al final"
                style={{ flex:1, minHeight:44, borderRadius:10, cursor:'pointer', border:`1px solid rgba(59,130,246,0.30)`, background:'rgba(59,130,246,0.08)', color:WP.blue, fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700 }}>
                + Serie
              </button>
              <button className="wp-press" onClick={() => onRemoveLastSet(exIdx)} disabled={ex.sets.length <= 1} aria-label="Quitar la última serie"
                style={{ flex:1, minHeight:44, borderRadius:10, cursor: ex.sets.length <= 1 ? 'default' : 'pointer', border:`1px solid ${WP.border}`, background:'transparent', color: ex.sets.length <= 1 ? WP.muted : WP.sub, fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700 }}>
                − Quitar serie
              </button>
            </div>
          </div>

          {/* Exercise navigation */}
          <div style={{ display:'flex', gap:8, marginBottom:10 }}>
            <button className="wp-press"
              onClick={() => onNavigateEx(exIdx - 1)}
              disabled={exIdx === 0}
              style={{ flex:1, padding:'10px 12px', borderRadius:10, border:`1px solid ${WP.border}`, background:'transparent', color: exIdx === 0 ? WP.muted : WP.sub, fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor: exIdx === 0 ? 'default' : 'pointer' }}
            >
              ← Ej. anterior
            </button>
            <button className="wp-press"
              onClick={() => onNavigateEx(exIdx + 1)}
              disabled={exIdx >= session.exercises.length - 1}
              style={{ flex:1, padding:'10px 12px', borderRadius:10, border:`1px solid ${WP.border}`, background:'transparent', color: exIdx >= session.exercises.length - 1 ? WP.muted : WP.sub, fontFamily:'Inter,system-ui', fontSize:12, fontWeight:700, cursor: exIdx >= session.exercises.length - 1 ? 'default' : 'pointer' }}
            >
              Siguiente ej. →
            </button>
          </div>

          {/* Add an extra exercise mid-session (today only) — reuses the picker. */}
          <button
            onClick={openAdd}
            className="wp-press"
            style={{ display:'flex', alignItems:'center', justifyContent:'center', gap:8, width:'100%',
              minHeight: isMobile ? 46 : 40, marginBottom:10, borderRadius:10, cursor:'pointer',
              border:`1px dashed rgba(59,130,246,0.45)`, background:'rgba(59,130,246,0.06)', color:WP.blue,
              fontFamily:'Inter,system-ui', fontSize: isMobile ? 14 : 13, fontWeight:700 }}>
            + Añadir ejercicio
          </button>

          <button className="wp-press"
            onClick={handleFinishClick}
            style={{ width:'100%', padding:'11px 16px', borderRadius:10, border:`1px solid rgba(239,68,68,0.30)`, background:'transparent', color:'#F87171', fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700, cursor:'pointer' }}
          >
            Finalizar entrenamiento
          </button>
        </div>
      </div>

      {/* Confirmación al terminar con menos de la mitad de las series hechas */}
      {confirmFinish && typeof ReactDOM !== 'undefined' && ReactDOM.createPortal(
        <div role="dialog" aria-modal="true" onClick={() => setConfirmFinish(false)}
          style={{ position:'fixed', inset:0, zIndex:4000, background:'rgba(6,13,24,0.6)', display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}>
          <div onClick={e => e.stopPropagation()} style={{ background:WP.card, border:`1px solid ${WP.border}`, borderRadius:16, padding:'22px 22px', width:'100%', maxWidth:360 }}>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:16, fontWeight:800, color:WP.text, marginBottom:8 }}>¿Terminar la sesión?</div>
            <p style={{ fontFamily:'Inter,system-ui', fontSize:13.5, color:WP.sub, lineHeight:1.55, margin:'0 0 16px' }}>
              Has completado {doneCount} de {totalCount} {totalCount === 1 ? 'serie' : 'series'}. ¿Terminar igualmente?
            </p>
            <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
              <button className="wp-press" onClick={() => { setConfirmFinish(false); onFinish(); }}
                style={{ width:'100%', minHeight:46, borderRadius:11, border:'none', cursor:'pointer', background:'#2563EB', color:'#fff', fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700 }}>
                Terminar igualmente
              </button>
              <button className="wp-press" onClick={() => setConfirmFinish(false)}
                style={{ width:'100%', minHeight:46, borderRadius:11, border:`1px solid ${WP.border}`, background:'transparent', color:WP.sub, fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700, cursor:'pointer' }}>
                Seguir entrenando
              </button>
            </div>
          </div>
        </div>,
        document.body
      )}

      {/* Confirmación al quitar una serie YA COMPLETADA (no por toque accidental) */}
      {confirmSet && typeof ReactDOM !== 'undefined' && ReactDOM.createPortal(
        <div role="dialog" aria-modal="true" onClick={onCancelConfirm}
          style={{ position:'fixed', inset:0, zIndex:4200, background:'rgba(6,13,24,0.6)', display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}>
          <div onClick={e => e.stopPropagation()} style={{ background:WP.card, border:`1px solid ${WP.border}`, borderRadius:16, padding:'22px', width:'100%', maxWidth:340 }}>
            <div style={{ fontFamily:'Inter,system-ui', fontSize:16, fontWeight:800, color:WP.text, marginBottom:8 }}>¿Quitar una serie completada?</div>
            <p style={{ fontFamily:'Inter,system-ui', fontSize:13.5, color:WP.sub, lineHeight:1.55, margin:'0 0 16px' }}>
              Esta serie ya tiene datos registrados. Si la quitas, no contará en esta sesión.
            </p>
            <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
              <button className="wp-press" onClick={onConfirmRemove}
                style={{ width:'100%', minHeight:46, borderRadius:11, border:'none', cursor:'pointer', background:'#EF4444', color:'#fff', fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700 }}>
                Quitar serie
              </button>
              <button className="wp-press" onClick={onCancelConfirm}
                style={{ width:'100%', minHeight:46, borderRadius:11, border:`1px solid ${WP.border}`, background:'transparent', color:WP.sub, fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700, cursor:'pointer' }}>
                Cancelar
              </button>
            </div>
          </div>
        </div>,
        document.body
      )}

      {/* "Serie quitada · Deshacer" — unos segundos tras quitar una pendiente */}
      {undoSet && typeof ReactDOM !== 'undefined' && ReactDOM.createPortal(
        <div style={{ position:'fixed', left:0, right:0, bottom:'calc(16px + env(safe-area-inset-bottom))', zIndex:4100, display:'flex', justifyContent:'center', padding:'0 16px', pointerEvents:'none' }}>
          <div style={{ pointerEvents:'auto', display:'flex', alignItems:'center', gap:14, maxWidth:420, width:'100%', background:WP.card, border:`1px solid ${WP.border}`, borderRadius:12, padding:'10px 12px 10px 16px', boxShadow:'0 8px 30px rgba(0,0,0,0.45)' }}>
            <span style={{ flex:1, fontFamily:'Inter,system-ui', fontSize:13.5, color:WP.text, fontWeight:600 }}>Serie quitada</span>
            <button className="wp-press" onClick={onUndoRemove}
              style={{ flexShrink:0, minHeight:40, padding:'0 14px', borderRadius:9, border:'none', cursor:'pointer', background:WP.blue, color:'#fff', fontFamily:'Inter,system-ui', fontSize:13, fontWeight:800 }}>
              Deshacer
            </button>
            <button className="wp-press" onClick={onDismissUndo} aria-label="Descartar"
              style={{ flexShrink:0, width:40, height:40, borderRadius:9, border:`1px solid ${WP.border}`, background:'transparent', color:WP.muted, cursor:'pointer', fontSize:15 }}>✕</button>
          </div>
        </div>,
        document.body
      )}

      {/* Exercise picker — one selector, two modes (swap / add). Smart suggestions
          first, then tolerant free search. Portaled to <body> so it isn't clipped. */}
      {sheetEx && typeof ReactDOM !== 'undefined' && ReactDOM.createPortal(
        (() => {
        const isAdd = sheetMode === 'add';
        return (
        <div onClick={closeSheet} style={{ position:'fixed', inset:0, zIndex:4000, background:'rgba(3,7,14,0.72)',
          display:'flex', alignItems: isMobile ? 'flex-end' : 'center', justifyContent:'center', animation:'fadeIn .15s ease' }}>
          <div onClick={e => e.stopPropagation()} style={{
            width: isMobile ? '100%' : 520, maxWidth:'100%', maxHeight: isMobile ? '88vh' : '82vh',
            display:'flex', flexDirection:'column', background:WP.card, border:`1px solid ${WP.border}`,
            borderRadius: isMobile ? '18px 18px 0 0' : 16, overflow:'hidden', boxShadow:'0 -8px 40px rgba(0,0,0,0.5)' }}>

            <div style={{ padding:'14px 18px', borderBottom:`1px solid ${WP.border}`, flexShrink:0, display:'flex', alignItems:'center', gap:12,
              paddingTop: isMobile ? '14px' : '14px' }}>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ fontSize:10, fontWeight:700, color:WP.blue, letterSpacing:1.2, fontFamily:'ui-monospace,Menlo,monospace' }}>{isAdd ? 'AÑADIR EJERCICIO' : 'CAMBIAR EJERCICIO'}</div>
                <div style={{ fontSize:14, fontWeight:700, color:WP.text, marginTop:2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                  {isAdd ? 'Un extra para la sesión de hoy' : sheetEx.name}
                </div>
              </div>
              <button className="wp-press" onClick={closeSheet} aria-label="Cerrar" style={{ flexShrink:0, width:36, height:36, borderRadius:9, border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.04)', color:WP.sub, cursor:'pointer', fontSize:16, fontWeight:700 }}>✕</button>
            </div>

            <div style={{ flex:1, overflowY:'auto', overflowX:'hidden', padding:'14px 18px' }}>
              <div style={{ fontSize:10, fontWeight:700, color:WP.sub, letterSpacing:1.1, fontFamily:'ui-monospace,Menlo,monospace', marginBottom:8 }}>{isAdd ? 'SUGERENCIAS' : 'ALTERNATIVAS INTELIGENTES'}</div>
              {alternatives.length === 0 && (
                <div style={{ fontSize:12, color:WP.muted, marginBottom:8 }}>{isAdd ? 'Busca el ejercicio que quieras añadir.' : 'No hay alternativas directas — busca abajo.'}</div>
              )}
              <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
                {alternatives.map(alt => <WpSwapOption key={alt.id} ex={alt} onPick={() => pickExercise(alt)} />)}
              </div>

              <div style={{ fontSize:10, fontWeight:700, color:WP.sub, letterSpacing:1.1, fontFamily:'ui-monospace,Menlo,monospace', margin:'18px 0 8px' }}>{isAdd ? 'O BUSCA CUALQUIER EJERCICIO' : 'O BUSCA CUALQUIER EJERCICIO'}</div>
              <input value={sheetQuery} onChange={e => setSheetQuery(e.target.value)} aria-label="Buscar ejercicio para añadir o sustituir" placeholder="Nombre, músculo, equipamiento…"
                style={{ width:'100%', boxSizing:'border-box', background:WP.input, border:`1px solid ${WP.border}`,
                  borderRadius:10, color:WP.text, padding: isMobile ? '12px' : '9px 12px', minHeight: isMobile ? 46 : undefined,
                  fontSize: isMobile ? 16 : 13, fontFamily:'Inter,system-ui', outline:'none' }} />
              <div style={{ display:'flex', flexDirection:'column', gap:8, marginTop:10 }}>
                {searchResults.map(r => <WpSwapOption key={r.id} ex={r} onPick={() => pickExercise(r)} />)}
                {sheetQuery.trim().length >= 2 && searchResults.length === 0 && (
                  <div style={{ fontSize:12, color:WP.muted }}>Sin resultados para “{sheetQuery.trim()}”.</div>
                )}
              </div>
            </div>

            <div style={{ padding:'12px 18px', paddingBottom: isMobile ? 'calc(12px + env(safe-area-inset-bottom))' : 12, borderTop:`1px solid ${WP.border}`, flexShrink:0 }}>
              {isAdd ? (
                <div style={{ fontSize:11, color:WP.muted, textAlign:'center' }}>Se añade solo a la sesión de hoy. Al terminar podrás guardarlo en tu rutina.</div>
              ) : swapRoutineName ? (
                <label style={{ display:'flex', alignItems:'center', gap:10, cursor:'pointer', fontSize:12, color:WP.sub }}>
                  <input type="checkbox" checked={applyToRoutine} onChange={e => setApplyToRoutine(e.target.checked)}
                    style={{ width:18, height:18, accentColor:WP.blue, flexShrink:0 }} />
                  Aplicar también a mi rutina guardada <span style={{ color:WP.muted }}>({swapRoutineName})</span>
                </label>
              ) : (
                <div style={{ fontSize:11, color:WP.muted, textAlign:'center' }}>El cambio afecta solo a la sesión de hoy.</div>
              )}
            </div>
          </div>
        </div>
        );
        })(),
        document.body
      )}

      {/* Exit dialog — shown when leaving with unfinished progress. Bottom sheet
          on mobile, coherent with the swap sheet. */}
      {exitOpen && typeof ReactDOM !== 'undefined' && ReactDOM.createPortal(
        <div onClick={() => setExitOpen(false)} style={{ position:'fixed', inset:0, zIndex:4000, background:'rgba(3,7,14,0.72)',
          display:'flex', alignItems: isMobile ? 'flex-end' : 'center', justifyContent:'center', animation:'fadeIn .15s ease' }}>
          <div onClick={e => e.stopPropagation()} style={{
            width: isMobile ? '100%' : 460, maxWidth:'100%',
            display:'flex', flexDirection:'column', background:WP.card, border:`1px solid ${WP.border}`,
            borderRadius: isMobile ? '18px 18px 0 0' : 16, overflow:'hidden', boxShadow:'0 -8px 40px rgba(0,0,0,0.5)' }}>

            <div style={{ padding:'18px 18px 6px' }}>
              <div style={{ fontSize:17, fontWeight:800, color:WP.text, letterSpacing:-0.3 }}>¿Salir del entrenamiento?</div>
              <div style={{ fontSize:12.5, color:WP.sub, marginTop:6, lineHeight:1.5 }}>
                Llevas <b style={{ color:WP.text }}>{doneCount} {doneCount === 1 ? 'serie' : 'series'}</b> registrada{doneCount === 1 ? '' : 's'}. Elige qué hacer con tu progreso.
              </div>
            </div>

            <div style={{ padding:'12px 18px', paddingBottom: isMobile ? 'calc(14px + env(safe-area-inset-bottom))' : 16, display:'flex', flexDirection:'column', gap:10 }}>
              {!confirmDiscard ? (
                <>
                  <button className="wp-press" onClick={() => setExitOpen(false)}
                    style={{ width:'100%', minHeight:50, borderRadius:12, border:'none', cursor:'pointer', background:WP.green, color:'#fff',
                      fontFamily:'Inter,system-ui', fontSize:15, fontWeight:800, letterSpacing:-0.2, boxShadow:'0 4px 18px -4px rgba(34,197,94,0.45)' }}>
                    Continuar entrenando
                  </button>

                  <button className="wp-press" onClick={onExitPause}
                    style={{ width:'100%', minHeight:50, borderRadius:12, cursor:'pointer', textAlign:'left', padding:'8px 14px',
                      border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.04)', color:WP.text, fontFamily:'Inter,system-ui' }}>
                    <div style={{ fontSize:14, fontWeight:700 }}>Pausar y salir</div>
                    <div style={{ fontSize:11, color:WP.sub, marginTop:2 }}>Guarda la sesión activa. Vuelve cuando quieras y sigue donde lo dejaste.</div>
                  </button>

                  <button className="wp-press" onClick={onExitSave}
                    style={{ width:'100%', minHeight:50, borderRadius:12, cursor:'pointer', textAlign:'left', padding:'8px 14px',
                      border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.04)', color:WP.text, fontFamily:'Inter,system-ui' }}>
                    <div style={{ fontSize:14, fontWeight:700 }}>Guardar y finalizar</div>
                    <div style={{ fontSize:11, color:WP.sub, marginTop:2 }}>Registra las series hechas como sesión parcial en tu historial.</div>
                  </button>

                  <button className="wp-press" onClick={() => setConfirmDiscard(true)}
                    style={{ width:'100%', minHeight:44, borderRadius:12, cursor:'pointer', marginTop:4,
                      border:`1px solid rgba(239,68,68,0.35)`, background:'transparent', color:'#F87171', fontFamily:'Inter,system-ui', fontSize:13, fontWeight:700 }}>
                    Descartar entrenamiento
                  </button>
                </>
              ) : (
                <>
                  <div style={{ padding:'10px 12px', borderRadius:12, background:'rgba(239,68,68,0.08)', border:'1px solid rgba(239,68,68,0.22)' }}>
                    <div style={{ fontSize:13.5, fontWeight:800, color:'#F87171' }}>Descartar y perder el progreso</div>
                    <div style={{ fontSize:12, color:WP.sub, marginTop:3, lineHeight:1.5 }}>Se eliminará la sesión activa y las {doneCount} {doneCount === 1 ? 'serie' : 'series'} registrada{doneCount === 1 ? '' : 's'}. Esto no se puede deshacer.</div>
                  </div>
                  <button className="wp-press" onClick={onExitDiscard}
                    style={{ width:'100%', minHeight:50, borderRadius:12, border:'none', cursor:'pointer', background:'#EF4444', color:'#fff',
                      fontFamily:'Inter,system-ui', fontSize:15, fontWeight:800 }}>
                    Sí, descartar
                  </button>
                  <button className="wp-press" onClick={() => setConfirmDiscard(false)}
                    style={{ width:'100%', minHeight:46, borderRadius:12, cursor:'pointer',
                      border:`1px solid ${WP.border}`, background:'rgba(255,255,255,0.04)', color:WP.text, fontFamily:'Inter,system-ui', fontSize:14, fontWeight:700 }}>
                    Cancelar
                  </button>
                </>
              )}
            </div>
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}

// ── Main Component ────────────────────────────────────────────────────────────

function WorkoutPlayerSection() {
  const { state, actions } = useStore();
  const { navigate } = useRoute();

  const [session,   setSession]   = React.useState(null);
  const [exIdx,     setExIdx]     = React.useState(0);
  const [setIdx,    setSetIdx]    = React.useState(0);
  const [elapsed,   setElapsed]   = React.useState(0);
  const [view,      setView]      = React.useState('loading');
  const [finished,  setFinished]  = React.useState(null);
  const [restFlash, setRestFlash] = React.useState(false);
  // Offer at finish: save mid-session-added exercises to the saved routine.
  const [addOffer,  setAddOffer]  = React.useState(null);
  // Quitar serie: "Deshacer" durante unos segundos tras quitar una pendiente,
  // y confirmación si la serie estaba completada.
  const [undoSet,     setUndoSet]     = React.useState(null);   // { ei, si, set }
  const [confirmSet,  setConfirmSet]  = React.useState(null);   // { ei, si }
  const undoTimerRef  = React.useRef(null);

  // The session's start timestamp (Date.now()). Elapsed is ALWAYS derived from
  // this (Date.now() - startTime), never accumulated per tick — so it stays
  // accurate through backgrounding, lock and reload (the browser freezes
  // setInterval in the background, which would otherwise lose time).
  const startTimeRef  = React.useRef(0);
  const elapsedIntRef = React.useRef(null);
  const flashTimerRef = React.useRef(null);

  const keepAwakePref = state.prefs?.keepAwake !== false;

  // Rest-end alert mode ('sound' | 'vibration' | 'none') lives in AtlasSettings
  // (atlas.settings.v1). Kept reactive so changes in Ajustes apply immediately.
  const [restAlert, setRestAlert] = React.useState(
    () => (typeof AtlasSettings !== 'undefined' ? AtlasSettings.get('restAlert') : 'sound')
  );
  React.useEffect(() => {
    const sync = () => { try { setRestAlert(AtlasSettings.get('restAlert')); } catch {} };
    window.addEventListener('atlas:settingschange', sync);
    return () => window.removeEventListener('atlas:settingschange', sync);
  }, []);

  function triggerRestFlash() {
    setRestFlash(true);
    if (flashTimerRef.current) clearTimeout(flashTimerRef.current);
    flashTimerRef.current = setTimeout(() => setRestFlash(false), 1100);
  }

  // Timestamp-based rest timer (survives backgrounding + reload). Persisted so
  // an active rest resumes after an accidental reload / app-close.
  const { restTimer, start: startRest, adjust: adjustRest, skip: skipRest } =
    useRestTimer(restAlert, { persist: true, onDone: triggerRestFlash });

  // ── Mount: load or create session ─────────────────────────────────────────
  React.useEffect(() => {
    const rawExs  = (() => { try { return JSON.parse(localStorage.getItem('atlas.pendingWorkout') || 'null'); } catch { return null; } })();
    const rawMeta = (() => { try { return JSON.parse(localStorage.getItem('atlas.pendingWorkoutMeta') || 'null'); } catch { return null; } })();

    if (rawExs && Array.isArray(rawExs) && rawExs.length > 0) {
      // Clear pending keys
      localStorage.removeItem('atlas.pendingWorkout');
      localStorage.removeItem('atlas.pendingWorkoutMeta');
      const s = WorkoutSessionStore.create(rawExs, rawMeta);
      // Precarga cada serie con la sugerencia de progresión (o el peso de la
      // última vez). Solo en sesiones nuevas; persiste para sobrevivir recargas.
      try { wpPrefillSuggestions(s, state.log); WorkoutSessionStore.save(s); } catch (e) {}
      setSession(s);
      startTimeRef.current = s.startTime || Date.now();
      setElapsed(0);
      setExIdx(0);
      setSetIdx(0);
      setView('active');
    } else {
      const active = WorkoutSessionStore.getActive();
      if (active) {
        setSession(active);
        // Duration is derived from the PERSISTED start timestamp, not from zero —
        // so resuming mid-session (reload / app-close) shows the real elapsed time.
        startTimeRef.current = active.startTime || Date.now();
        setElapsed(Math.max(0, Math.round((Date.now() - startTimeRef.current) / 1000)));
        // Find first incomplete ex+set
        let foundEx = 0, foundSet = 0;
        outer: for (let ei = 0; ei < active.exercises.length; ei++) {
          for (let si = 0; si < active.exercises[ei].sets.length; si++) {
            if (!active.exercises[ei].sets[si].done) { foundEx = ei; foundSet = si; break outer; }
          }
        }
        setExIdx(foundEx);
        setSetIdx(foundSet);
        setView('active');
      } else {
        setView('history');
      }
    }
  }, []);

  // ── Elapsed timer ──────────────────────────────────────────────────────────
  // The interval only REPAINTS; the value is recomputed from the start timestamp
  // each tick, so a frozen background tab never loses time. On returning to the
  // foreground we recompute immediately (visibilitychange) instead of waiting for
  // the next tick, so the jump to the real elapsed time is instant.
  React.useEffect(() => {
    if (view !== 'active') return;
    const tick = () => {
      const st = startTimeRef.current || Date.now();
      setElapsed(Math.max(0, Math.round((Date.now() - st) / 1000)));
    };
    tick();
    elapsedIntRef.current = setInterval(tick, 1000);
    const onVisible = () => { if (document.visibilityState === 'visible') tick(); };
    document.addEventListener('visibilitychange', onVisible);
    return () => {
      clearInterval(elapsedIntRef.current);
      document.removeEventListener('visibilitychange', onVisible);
    };
  }, [view]);

  // Rest timer: warning/done sounds, background-freeze recovery and persistence
  // are all handled inside useRestTimer (timestamp-based). The helpers below just
  // translate the rest string / actions.
  function startRestTimer(restStr) {
    // Parse rest string like '90s', '2 min', '90-120s' → pick first number
    const match = (restStr || '90s').match(/(\d+)/);
    const secs  = match ? parseInt(match[1]) : 90;
    startRest(secs);
  }

  function handleRestAdjust(delta) { adjustRest(delta); }
  function handleRestSkip() { skipRest(); }

  // ── Keep-awake + audio unlock during training ──────────────────────────────
  React.useEffect(() => {
    if (view !== 'active') return;
    if (keepAwakePref) { try { AtlasTimer.requestWake(); } catch (e) {} }
    // iOS Safari needs a user gesture to unlock audio — prime on first touch.
    const unlock = () => { try { AtlasTimer.unlock(); } catch (e) {} };
    window.addEventListener('pointerdown', unlock, { once: true });
    window.addEventListener('touchstart', unlock, { once: true });
    return () => {
      try { AtlasTimer.releaseWake(); } catch (e) {}
      window.removeEventListener('pointerdown', unlock);
      window.removeEventListener('touchstart', unlock);
    };
  }, [view, keepAwakePref]);

  // ── Update a set field ─────────────────────────────────────────────────────
  function updateSet(ei, si, patch) {
    setSession(prev => {
      if (!prev) return prev;
      const exercises = prev.exercises.map((ex, eIdx) => {
        if (eIdx !== ei) return ex;
        return { ...ex, sets: ex.sets.map((s, sIdx) => sIdx === si ? { ...s, ...patch } : s) };
      });
      const next = { ...prev, exercises };
      WorkoutSessionStore.save(next);
      return next;
    });
  }

  // ── Update one side of a unilateral set (nested merge) ─────────────────────
  function updateSetSide(ei, si, side, patch) {
    setSession(prev => {
      if (!prev) return prev;
      const exercises = prev.exercises.map((ex, eIdx) => {
        if (eIdx !== ei) return ex;
        return { ...ex, sets: ex.sets.map((s, sIdx) =>
          sIdx === si ? { ...s, [side]: { ...(s[side] || { kg:'', reps:'' }), ...patch } } : s) };
      });
      const next = { ...prev, exercises };
      WorkoutSessionStore.save(next);
      return next;
    });
  }

  // ── Complete a set ─────────────────────────────────────────────────────────
  function completeSet(ei, si) {
    setSession(prev => {
      if (!prev) return prev;
      const justDone = prev.exercises[ei]?.sets[si] || {};
      const exercises = prev.exercises.map((ex, eIdx) => {
        if (eIdx !== ei) return ex;
        return { ...ex, sets: ex.sets.map((s, sIdx) => {
          if (sIdx === si) return { ...s, done: true, doneAt: Date.now() };
          // La siguiente serie se rellena con el peso/reps de la recién hecha
          // (si está vacía). Son editables.
          if (sIdx === si + 1 && !s.done) {
            const patch = {};
            if ((s.kg == null || s.kg === '') && (justDone.kg != null && justDone.kg !== '')) patch.kg = justDone.kg;
            if ((s.reps == null || s.reps === '') && (justDone.reps != null && justDone.reps !== '')) patch.reps = justDone.reps;
            if ((s.sec == null || s.sec === '') && (justDone.sec != null && justDone.sec !== '')) patch.sec = justDone.sec;
            return Object.keys(patch).length ? { ...s, ...patch } : s;
          }
          return s;
        }) };
      });
      const next = { ...prev, exercises };
      WorkoutSessionStore.save(next);

      // Find next undone set/exercise
      let foundEx = ei, foundSet = si;
      outer: for (let e = ei; e < exercises.length; e++) {
        const startS = e === ei ? si + 1 : 0;
        for (let s = startS; s < exercises[e].sets.length; s++) {
          if (!exercises[e].sets[s].done) { foundEx = e; foundSet = s; break outer; }
        }
      }
      setTimeout(() => { setExIdx(foundEx); setSetIdx(foundSet); }, 50);

      // Start rest timer. The exercise's own rest wins; otherwise fall back to
      // the user's default from Ajustes (atlas.settings.v1), then to 90s.
      const settingsRest = (typeof AtlasSettings !== 'undefined') ? AtlasSettings.get('restDefault') : null;
      const restStr = exercises[ei]?.rest || settingsRest || '90s';
      startRestTimer(restStr);

      return next;
    });
  }

  // ── Añadir una serie al final (solo esta sesión) ───────────────────────────
  // Hereda el objetivo del ejercicio (reps/RIR vienen del propio ejercicio) y
  // precarga el último peso usado. Marca la sesión como modificada para ofrecer
  // guardarla en la rutina al terminar.
  function addSet(ei) {
    setSession(prev => {
      if (!prev) return prev;
      const exercises = prev.exercises.map((ex, eIdx) => {
        if (eIdx !== ei) return ex;
        const sets = ex.sets || [];
        const lastKg = [...sets].reverse().find(s => s.kg != null && s.kg !== '')?.kg || '';
        const base = { idx: sets.length, kg: lastKg, reps: '', rir: null, done: false, doneAt: null };
        if (ex.unilateral) { base.left = { kg: '', reps: '' }; base.right = { kg: '', reps: '' }; }
        return { ...ex, sets: [...sets, base] };
      });
      const next = { ...prev, exercises, structChanged: true };
      WorkoutSessionStore.save(next);
      return next;
    });
  }

  // ── Quitar una serie (mínimo 1 por ejercicio) ──────────────────────────────
  function removeSet(ei, si) {
    setSession(prev => {
      if (!prev) return prev;
      const cur = prev.exercises[ei];
      if (!cur || (cur.sets || []).length <= 1) return prev;   // mínimo 1 serie
      const exercises = prev.exercises.map((ex, eIdx) => {
        if (eIdx !== ei) return ex;
        const sets = ex.sets.filter((_, sIdx) => sIdx !== si).map((s, i) => ({ ...s, idx: i }));
        return { ...ex, sets };
      });
      const next = { ...prev, exercises, structChanged: true };
      WorkoutSessionStore.save(next);
      return next;
    });
  }

  // Reinsertar una serie quitada (para "Deshacer"), en su posición original.
  function restoreSetAt(ei, si, set) {
    setSession(prev => {
      if (!prev) return prev;
      const exercises = prev.exercises.map((ex, eIdx) => {
        if (eIdx !== ei) return ex;
        const sets = [...ex.sets];
        sets.splice(Math.min(si, sets.length), 0, set);
        return { ...ex, sets: sets.map((s, i) => ({ ...s, idx: i })) };
      });
      const next = { ...prev, exercises };
      WorkoutSessionStore.save(next);
      return next;
    });
  }

  // Quita la ÚLTIMA serie: si está completada pide confirmación; si está
  // pendiente, la quita al momento y ofrece "Deshacer" unos segundos.
  function requestRemoveLastSet(ei) {
    const sets = session?.exercises[ei]?.sets || [];
    if (sets.length <= 1) return;
    const si = sets.length - 1;
    if (sets[si]?.done) { setConfirmSet({ ei, si }); return; }
    doRemoveSet(ei, si);
  }
  function doRemoveSet(ei, si) {
    const set = session?.exercises[ei]?.sets[si];
    removeSet(ei, si);
    // Ajusta el foco si la serie actual queda fuera de rango.
    setSetIdx(cur => Math.max(0, Math.min(cur, (session?.exercises[ei]?.sets.length || 1) - 2)));
    if (set) {
      setUndoSet({ ei, si, set });
      if (undoTimerRef.current) clearTimeout(undoTimerRef.current);
      undoTimerRef.current = setTimeout(() => setUndoSet(null), 5000);
    }
    setConfirmSet(null);
  }
  function undoRemoveSet() {
    if (!undoSet) return;
    restoreSetAt(undoSet.ei, undoSet.si, undoSet.set);
    if (undoTimerRef.current) clearTimeout(undoTimerRef.current);
    setUndoSet(null);
  }

  // ── Navigate to set ────────────────────────────────────────────────────────
  function handleSelectSet(ei, si) {
    setExIdx(ei);
    setSetIdx(si);
    handleRestSkip();
  }

  // ── Navigate exercise ──────────────────────────────────────────────────────
  function handleNavigateEx(newEi) {
    if (!session) return;
    const clampedEi = Math.max(0, Math.min(session.exercises.length - 1, newEi));
    setExIdx(clampedEi);
    // Find first undone set in new exercise
    const ex = session.exercises[clampedEi];
    const firstUndone = ex.sets.findIndex(s => !s.done);
    setSetIdx(firstUndone >= 0 ? firstUndone : 0);
    handleRestSkip();
  }

  // ── Swap the active exercise (occupied machine) ────────────────────────────
  const swapRoutineName = React.useMemo(() => {
    if (!session?.routineId) return null;
    const r = (state.savedRoutines || []).find(x => x.id === session.routineId);
    return r ? r.name : null;
  }, [session?.routineId, state.savedRoutines]);

  function applySwapToRoutine(origId, dbEx) {
    const rid = session?.routineId;
    if (!rid || !origId) return;
    const routine = (state.savedRoutines || []).find(r => r.id === rid);
    if (!routine) return;
    const sIdx = session.sessionIndex;
    const matchSession = (s, i) =>
      (sIdx != null ? i === sIdx : (s.exercises || []).some(e => e.id === origId));
    let touched = false;
    const sessions = (routine.sessions || []).map((s, i) => {
      if (touched || !matchSession(s, i)) return s;
      let replaced = false;
      const exercises = (s.exercises || []).map(e => {
        if (!replaced && e.id === origId) {
          replaced = true; touched = true;
          return { ...e, id: dbEx.id, name: dbEx.name, pattern: dbEx.pattern,
                   muscles: dbEx.muscles || { primary: [], secondary: [] }, group: dbEx.group || dbEx.category || '' };
        }
        return e;
      });
      return { ...s, exercises };
    });
    if (touched) { try { actions.saveRoutine({ ...routine, sessions }); } catch (e) {} }
  }

  // Abre un artículo del Aula (desde "¿Por qué?" de la sugerencia).
  function openArticle(id) {
    try { localStorage.setItem('atlas.aula.pending.v1', id); } catch (e) {}
    navigate('/aula');
  }

  function handleSwap(dbEx, applyToRoutine) {
    if (!session || !dbEx) return;
    const ei = exIdx;
    const orig = session.exercises[ei];
    if (!orig) return;
    const doneSets   = orig.sets.filter(s => s.done);
    const undoneSets = orig.sets.filter(s => !s.done);
    const reindex = arr => arr.map((s, i) => ({ ...s, idx: i }));
    const subMeta = {
      id: dbEx.id, name: dbEx.name,
      muscles: dbEx.muscles || { primary: [], secondary: [] },
      group: dbEx.group || dbEx.category || '',
      pattern: dbEx.pattern || null,
      // Carry the substitute's own registration mode so the panel renders the
      // right inputs (a swap can change reps↔time↔per-side).
      unit: dbEx.unit === 'time' ? 'time' : 'reps',
      timeTarget: dbEx.timeTarget || null,
      unilateral: dbEx.unit !== 'time' && dbEx.unilateral === true,
      // Se mantienen las REPS objetivo, el RIR y el descanso del hueco.
      targetSets: orig.targetSets, repsRange: orig.repsRange, rir: orig.rir, rest: orig.rest,
      swappedFrom: orig.swappedFrom || orig.name,
    };
    // El peso NO se hereda del ejercicio original (pueden ser cargas muy
    // distintas: una prensa mueve mucho más que un hack squat). Se precarga con
    // la sugerencia / última vez del NUEVO ejercicio, o se deja vacío si no tiene
    // historial. Las reps objetivo (repsRange), el RIR y el descanso sí se mantienen.
    let subKg = '';
    try {
      const info = wpSuggestFor(subMeta, state.log);
      if (info && !info.isBodyweight) {
        const k = (info.sug && info.sug.kg != null) ? info.sug.kg
                : (info.history[0] ? info.history[0].maxKg : null);
        if (k != null && k > 0) subKg = String(k);
      }
    } catch (e) {}
    const freshSet = s => ({ idx: 0, kg: subKg, reps: s.reps || '', rpe: null, done: false, doneAt: null });

    // Where the substitute lands: in-place (0 hechas) → mismo índice; con series
    // hechas se inserta DESPUÉS del original conservado → ei+1. Se calcula aquí,
    // no dentro del updater de setSession (que corre en el render siguiente), para
    // que el foco salte de verdad al sustituto y no se quede en el original.
    const focusIdx = doneSets.length > 0 ? ei + 1 : ei;
    setSession(prev => {
      if (!prev) return prev;
      const cur = prev.exercises[ei];
      let exercises;
      if (cur.sets.filter(s => s.done).length === 0) {
        // No logged data → clean in-place replacement, full scheme preserved.
        const replaced = { ...subMeta, sets: reindex(cur.sets.map(freshSet)) };
        exercises = prev.exercises.map((e, i) => (i === ei ? replaced : e));
      } else {
        // Sets already logged → keep original (trimmed, partial) + substitute
        // for the remaining sets. Nothing entered is lost.
        const kept = { ...cur, sets: reindex(cur.sets.filter(s => s.done)), partial: true };
        const remaining = cur.sets.filter(s => !s.done);
        const rem = remaining.length ? remaining : [{ kg: '', reps: cur.sets[0]?.reps || '', done: false }];
        const substitute = { ...subMeta, sets: reindex(rem.map(freshSet)) };
        exercises = [];
        prev.exercises.forEach((e, i) => { if (i === ei) { exercises.push(kept, substitute); } else exercises.push(e); });
      }
      const next = { ...prev, exercises, structChanged: true };
      WorkoutSessionStore.save(next);
      return next;
    });

    setSetIdx(0);
    setExIdx(focusIdx);   // enfoca el sustituto
    handleRestSkip();
    if (applyToRoutine) applySwapToRoutine(orig.id, dbEx);
  }

  // ── Add an extra exercise mid-session (today only) ─────────────────────────
  // Inserted right AFTER the current exercise and focused, so the user can do it
  // now and then continue with what was next. Default scheme 3×(8-12); respects
  // isometric (time) / unilateral (per-side) registration. Flagged `added` so we
  // can offer to save it to the routine at the end. Persisted to the active
  // session so it survives lock / reload / close and reaches the history.
  function handleAddExercise(dbEx) {
    if (!session || !dbEx) return;
    const raw = {
      id: dbEx.id, name: dbEx.name,
      muscles: dbEx.muscles || { primary: [], secondary: [] },
      pattern: dbEx.pattern || null,
      group: dbEx.group || dbEx.category || '',
      unit: dbEx.unit, timeTarget: dbEx.timeTarget, unilateral: dbEx.unilateral,
      setsCount: 3, repsRange: '8-12', rir: 2, rest: '90s',
    };
    let norm;
    try { norm = WorkoutSessionStore.normalize([raw])[0]; } catch { norm = null; }
    if (!norm) return;
    norm.added = true;

    const ei = exIdx;
    setSession(prev => {
      if (!prev) return prev;
      const exercises = [...prev.exercises];
      exercises.splice(Math.min(ei + 1, exercises.length), 0, norm);
      const next = { ...prev, exercises, structChanged: true };
      WorkoutSessionStore.save(next);
      return next;
    });
    setSetIdx(0);
    setExIdx(ei + 1);   // focus the newly added exercise
    handleRestSkip();
  }

  // Append exercises to a saved routine's trained session (offered at finish).
  function addExercisesToRoutine(routineId, sessionIndex, routineExs) {
    const routine = (state.savedRoutines || []).find(r => r.id === routineId);
    if (!routine || !routineExs.length) return false;
    const sessions = (routine.sessions || []).map((s, i) => {
      const target = sessionIndex != null ? i === sessionIndex : i === 0;
      return target ? { ...s, exercises: [...(s.exercises || []), ...routineExs] } : s;
    });
    try { actions.saveRoutine({ ...routine, sessions }); return true; } catch { return false; }
  }

  // Reemplaza los ejercicios de la sesión entrenada de la rutina por los de HOY
  // (nº de series, sustituciones y añadidos). Excluye los restos parciales que
  // deja un cambio de ejercicio (el original conservado con sus series hechas).
  function saveSessionChangesToRoutine(routineId, sessionIndex, exercises) {
    const routine = (state.savedRoutines || []).find(r => r.id === routineId);
    if (!routine) return false;
    const toRoutineEx = e => ({
      id: e.id, name: e.name, pattern: e.pattern || null,
      muscles: e.muscles || { primary: [], secondary: [] },
      group: e.group || '',
      setsCount: (e.sets || []).length || e.targetSets || 3,
      repsRange: e.repsRange || '8-12', rir: e.rir ?? 2, rest: e.rest || '90s',
      ...(e.unit === 'time' ? { unit: 'time', timeTarget: e.timeTarget || null } : {}),
      ...(e.unilateral ? { unilateral: true } : {}),
    });
    const routineExs = (exercises || []).filter(e => !e.partial).map(toRoutineEx);
    if (!routineExs.length) return false;
    const sessions = (routine.sessions || []).map((s, i) => {
      const target = sessionIndex != null ? i === sessionIndex : i === 0;
      return target ? { ...s, exercises: routineExs } : s;
    });
    try { actions.saveRoutine({ ...routine, sessions }); return true; } catch { return false; }
  }

  // Guarda la sesión de hoy como una rutina NUEVA del usuario (para cuando venía
  // de una predeterminada, que no se edita). Un solo día con los ejercicios de hoy.
  function saveSessionAsNewRoutine(name, exercises) {
    const toRoutineEx = e => ({
      id: e.id, name: e.name, pattern: e.pattern || null,
      muscles: e.muscles || { primary: [], secondary: [] },
      group: e.group || '',
      setsCount: (e.sets || []).length || e.targetSets || 3,
      repsRange: e.repsRange || '8-12', rir: e.rir ?? 2, rest: e.rest || '90s',
      ...(e.unit === 'time' ? { unit: 'time', timeTarget: e.timeTarget || null } : {}),
      ...(e.unilateral ? { unilateral: true } : {}),
    });
    const routineExs = (exercises || []).filter(e => !e.partial).map(toRoutineEx);
    if (!routineExs.length) return false;
    const routine = {
      id: `routine-${Date.now()}`,
      name: `${name || 'Mi rutina'} (mi versión)`,
      createdAt: Date.now(),
      sessions: [{ name: session?.sessionName || 'Sesión', exercises: routineExs }],
    };
    try { actions.saveRoutine(routine); return true; } catch { return false; }
  }

  // ── Finish workout ─────────────────────────────────────────────────────────
  // Logs whatever sets are done. Sessions with unfinished sets are flagged
  // `partial` so Perfil/Progreso can label them.
  function finishWorkout() {
    if (!session) return;
    clearInterval(elapsedIntRef.current);
    skipRest();
    // ¿Hubo cambios de estructura (series, sustituciones o añadidos)? Al terminar
    // se ofrece UNA vez guardarlos: en la rutina guardada si la hay, o como
    // rutina nueva si venía de una predeterminada (que no se edita).
    const savedRoutine = (state.savedRoutines || []).find(r => r.id === session.routineId);
    if (session.structChanged && savedRoutine) {
      setAddOffer({
        kind: 'changes',
        routineName: savedRoutine.name,
        routineId: session.routineId,
        sessionIndex: session.sessionIndex,
        exercises: (session.exercises || []),
        done: false,
      });
    } else if (session.structChanged && !savedRoutine) {
      // Predeterminada u origen sin rutina guardada → guardar como rutina mía.
      setAddOffer({
        kind: 'newRoutine',
        routineName: session.routineName || 'Mi rutina',
        exercises: (session.exercises || []),
        done: false,
      });
    } else {
      setAddOffer(null);
    }
    const completed = WorkoutSessionStore.complete(session);
    const isPartial = (completed.completedSets || 0) < (completed.totalSets || 0);

    // Effort-tiered session reward: ≥60% of planned sets done AND ≥15 min → 20;
    // 30–60% → 10; under 30% → 0. Capped to one session reward per day (the
    // store enforces the cap via sessions.lastGemDate).
    const ratio = completed.totalSets > 0 ? completed.completedSets / completed.totalSets : 0;
    const minutes = (completed.duration || 0) / 60;
    const gemsTier = ratio >= 0.3 ? ((ratio >= 0.6 && minutes >= 15) ? 20 : 10) : 0;
    const rewardedToday = (state.sessions?.lastGemDate === new Date().toDateString());
    const gemsAwarded = rewardedToday ? 0 : gemsTier;
    // Build exercises payload for logSession (compatible with store format).
    // Guardamos también el RIR de cada serie y los objetivos del ejercicio
    // (rango de reps y RIR objetivo), para el seguimiento de cargas y la
    // sugerencia de progresión. Retrocompatible: las series antiguas sin estos
    // campos siguen siendo válidas (rir ausente = cumplido).
    const tr = ex => (typeof progParseRepRange === 'function') ? progParseRepRange(ex.repsRange) : { min: 8, max: 12 };
    const logExercises = completed.exercises.map(ex => {
      const t = tr(ex);
      return {
        name: ex.name,
        ...(ex.id ? { id: ex.id } : {}),
        group: ex.group || '',
        ...(ex.pattern ? { pattern: ex.pattern } : {}),
        ...(ex.equipment ? { equipment: ex.equipment } : {}),
        ...(ex.unit === 'time' ? { unit: 'time' } : {}),
        ...(ex.unilateral ? { unilateral: true } : {}),
        muscles: (ex.muscles?.primary || []),
        targetRepsMin: t.min,
        targetRepsMax: t.max,
        targetRir: (ex.rir != null ? ex.rir : null),
        repsRange: ex.repsRange || `${t.min}-${t.max}`,
        sets: ex.sets.filter(s => s.done).map(s => {
          const common = { rir: (s.rir != null ? s.rir : null), done: true, targetRepsMin: t.min, targetRepsMax: t.max, targetRir: (ex.rir != null ? ex.rir : null) };
          if (ex.unit === 'time') return { kg: s.kg, sec: s.sec, ...common };
          if (ex.unilateral) {
            const l = s.left || {}, r = s.right || {};
            // Representative top-level kg/reps = the heavier side, so progression/
            // analysis (which read set.kg/reps) keep working; per-side detail kept.
            const kg   = String(Math.max(parseFloat(l.kg)   || 0, parseFloat(r.kg)   || 0) || '');
            const reps = String(Math.max(parseInt(l.reps)   || 0, parseInt(r.reps)   || 0) || '');
            return { kg, reps, left: { kg: l.kg || '', reps: l.reps || '' }, right: { kg: r.kg || '', reps: r.reps || '' }, ...common };
          }
          // Standard set — keep drop-set sub-tramos with real data only, con el
          // kg normalizado (coma→punto) igual que la serie principal.
          const normKg = (typeof WorkoutSessionStore !== 'undefined' && WorkoutSessionStore.normKgStr)
            ? WorkoutSessionStore.normKgStr
            : (v => { const n = parseFloat(String(v == null ? '' : v).replace(',', '.')); return Number.isFinite(n) ? String(n) : ''; });
          const drops = (s.drops || [])
            .filter(d => (d.kg !== '' && d.kg != null) || (d.reps !== '' && d.reps != null))
            .map(d => ({ ...d, kg: normKg(d.kg) }));
          return { kg: s.kg, reps: s.reps, ...(drops.length ? { drops } : {}), ...common };
        }),
      };
    });
    try {
      actions.logSession(logExercises, {
        duration: completed.duration,
        routineName: completed.routineName,
        sessionName: completed.sessionName,
        partial: isPartial,
        gemsEarned: gemsTier,
        allowPR: true,
      });
    } catch (e) { /* store may not support this */ }
    setFinished({ ...completed, gemsAwarded, gemsTier, rewardCapped: rewardedToday && gemsTier > 0, partial: isPartial });
    setView('finish');
  }

  // ── Exit choices (Salir dialog) ────────────────────────────────────────────
  // Pause: keep the session (and rest timer) persisted so it resumes exactly
  // where it was. Inicio surfaces a "Entrenamiento en curso" banner.
  function handleExitPause() {
    clearInterval(elapsedIntRef.current);
    navigate('/');
  }
  // Save & finish: log the completed sets (as partial) and show the summary.
  function handleExitSave() { finishWorkout(); }
  // Discard: drop the active session without saving anything.
  function handleExitDiscard() {
    clearInterval(elapsedIntRef.current);
    skipRest();
    try { WorkoutSessionStore.discard(); } catch (e) {}
    navigate('/');
  }

  // ── Render ─────────────────────────────────────────────────────────────────

  if (view === 'loading') return <WpSpinner />;

  if (view === 'history') {
    return <WpHistoryScreen onNewWorkout={() => navigate('/coach')} />;
  }

  if (view === 'finish') {
    return (
      <WpFinishScreen
        finished={finished}
        onHome={() => navigate('/')}
        onCoach={() => navigate('/coach')}
        onOpenArticle={openArticle}
        addOffer={addOffer}
        onAddToRoutine={() => {
          if (!addOffer) return;
          const ok = addOffer.kind === 'changes'
            ? saveSessionChangesToRoutine(addOffer.routineId, addOffer.sessionIndex, addOffer.exercises)
            : addOffer.kind === 'newRoutine'
            ? saveSessionAsNewRoutine(addOffer.routineName, addOffer.exercises)
            : addExercisesToRoutine(addOffer.routineId, addOffer.sessionIndex, addOffer.exs);
          setAddOffer(prev => prev ? { ...prev, done: ok } : prev);
        }}
        onDismissOffer={() => setAddOffer(null)}
      />
    );
  }

  // 'active'
  if (!session) return <WpSpinner />;

  return (
    <WpActiveView
      session={session}
      log={state.log}
      onOpenArticle={openArticle}
      exIdx={exIdx}
      setIdx={setIdx}
      elapsed={elapsed}
      restTimer={restTimer}
      restFlash={restFlash}
      onCompleteSet={completeSet}
      onUpdateSet={updateSet}
      onUpdateSide={updateSetSide}
      onSelectSet={handleSelectSet}
      onNavigateEx={handleNavigateEx}
      onFinish={finishWorkout}
      onExitPause={handleExitPause}
      onExitSave={handleExitSave}
      onExitDiscard={handleExitDiscard}
      onRestAdjust={handleRestAdjust}
      onRestSkip={handleRestSkip}
      onSwap={handleSwap}
      onAddExercise={handleAddExercise}
      swapRoutineName={swapRoutineName}
      onAddSet={addSet}
      onRemoveLastSet={requestRemoveLastSet}
      undoSet={undoSet}
      onUndoRemove={undoRemoveSet}
      onDismissUndo={() => { if (undoTimerRef.current) clearTimeout(undoTimerRef.current); setUndoSet(null); }}
      confirmSet={confirmSet}
      onConfirmRemove={() => confirmSet && doRemoveSet(confirmSet.ei, confirmSet.si)}
      onCancelConfirm={() => setConfirmSet(null)}
    />
  );
}

Object.assign(window, { WorkoutPlayerSection });
