'use strict';

const VERSION = '1.0.0-rc.1+distribution';
const SUPPORT_URL = 'https://www.delta-sierra.com/soutenir.html';
const SITE_URL = 'https://www.delta-sierra.com';
const STORAGE_KEY = 'ds-calisthenie-v06';
const ACTIVE_SESSION_KEY = 'ds-calisthenie-active-session-v06';

const ICONS = {
  menu:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>',
  download:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 5-5m-5 5-5-5M5 21h14"/></svg>',
  home:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 11.5 12 4l9 7.5V21h-6v-6H9v6H3z"/></svg>',
  search:'<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/></svg>',
  play:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>',
  chart:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20V10h4v10H4Zm6 0V4h4v16h-4Zm6 0v-7h4v7h-4Z"/></svg>',
  user:'<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="4"/><path d="M4 21c.7-4.5 3.5-7 8-7s7.3 2.5 8 7"/></svg>',
  heart:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21S3 16 3 9.5C3 6.5 5 5 7.5 5c1.8 0 3.3 1 4.5 2.6C13.2 6 14.7 5 16.5 5 19 5 21 6.5 21 9.5 21 16 12 21 12 21Z"/></svg>',
  star:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 3 2.7 5.5 6.1.9-4.4 4.3 1 6.1-5.4-2.9-5.4 2.9 1-6.1-4.4-4.3 6.1-.9L12 3Z"/></svg>',
  chevron:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 5 7 7-7 7"/></svg>',
  plus:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>',
  check:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"/></svg>',
  back:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 5-7 7 7 7"/></svg>',
  pause:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14M16 5v14"/></svg>',
  skip:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 5 9 7-9 7V5Zm11 0h3v14h-3z"/></svg>',
  timer:'<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="13" r="8"/><path d="M9 2h6M12 5V2m0 11 3-3"/></svg>',
  trash:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3m-9 0 1 14h10l1-14M10 11v6m4-6v6"/></svg>',
  up:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 15 6-6 6 6"/></svg>',
  down:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>',
  share:'<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.7 10.7 6.6-4.4m-6.6 7 6.6 4.4"/></svg>',
  offline:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 18h10a4 4 0 0 0 .5-8 6 6 0 0 0-11.2-1.8A5 5 0 0 0 7 18Z"/><path d="M12 11v6m0 0 3-3m-3 3-3-3"/></svg>',
  filter:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M7 12h10M10 18h4"/></svg>'
};

function injectIcons(root=document){
  root.querySelectorAll('[data-icon]').forEach(el=>{
    const name=el.dataset.icon;
    if(ICONS[name]) el.innerHTML=ICONS[name];
  });
}
function icon(name, cls=''){ return `<span class="svg-icon ${cls}" aria-hidden="true">${ICONS[name]||''}</span>`; }

function ex(slug,name,cue,sets,reps,level,meta={}){
  return {slug,name,cue,sets,reps,level,
    equipment:'Sans matériel', type:'Gainage', duration:false, perSide:false, hybridHold:false, repsPerSet:0, holdPerRepSeconds:0,
    technique:`Exécutez ${name.toLowerCase()} lentement, sans à-coup, en conservant une posture stable.`,
    breathing:'Respirez régulièrement. Expirez pendant la phase d’effort et inspirez pendant le retour.',
    controls:[cue,'Mouvement lent et contrôlé','Arrêter si une douleur vive apparaît'],
    errors:['Accélérer pour terminer la série','Perdre l’alignement du tronc','Forcer une amplitude inconfortable'],
    easier:'Réduire l’amplitude ou choisir une variante plus assistée.',
    harder:'Augmenter progressivement les répétitions, la durée ou l’amplitude sans sacrifier la technique.',
    primary:[], secondary:[],
    ...meta};
}

const groups={
  'pectoraux':{name:'Pectoraux',subtitle:'Poussée contrôlée et respect des poignets', exercises:[
    ex('pompes-murales','Pompes murales','corps gainé','2 à 3 séries','12 à 15 reps','Débutant',{type:'Poussée',primary:['Pectoraux'],secondary:['Triceps','Épaules'],technique:'Placez les mains à hauteur de poitrine sur un mur. Gardez le corps aligné, fléchissez les coudes pour rapprocher la poitrine du mur puis repoussez sans décoller les talons.'}),
    ex('pompes-inclinees','Pompes inclinées sur table','plus l’appui est haut, plus c’est facile','2 à 3 séries','8 à 12 reps','Débutant',{equipment:'Table',type:'Poussée',primary:['Pectoraux'],secondary:['Triceps','Épaules'],technique:'Posez les mains sur un support très stable. Gardez tête, tronc et jambes alignés. Descendez la poitrine vers le support puis repoussez en conservant les coudes sous contrôle.'}),
    ex('serrage-paumes','Serrage de paumes isométrique','poitrine haute, coudes à l’horizontale','3 séries','20 à 30 sec','Doux',{type:'Isométrique',duration:true,primary:['Pectoraux'],secondary:['Avant-bras'],technique:'Joignez les paumes devant la poitrine et pressez-les l’une contre l’autre de façon progressive. Gardez les épaules basses et maintenez une tension continue.'}),
    ex('pompes-amplitude-courte','Pompes inclinées amplitude courte','descendre sans douleur','2 séries','6 à 10 reps','Doux',{equipment:'Table',type:'Poussée',primary:['Pectoraux'],secondary:['Triceps','Épaules'],easier:'Augmenter la hauteur du support et réduire encore l’amplitude.',harder:'Descendre progressivement davantage tout en gardant les poignets confortables.'})
  ]},
  'epaules':{name:'Épaules',subtitle:'Contrôle, stabilité et mobilité utile', exercises:[
    ex('anges-mur','Anges contre le mur','dos et bras au mur','2 séries','8 à 10 reps','Doux',{type:'Mobilité',primary:['Épaules'],secondary:['Haut du dos'],technique:'Dos contre le mur, faites glisser lentement les bras vers le haut puis vers le bas sans hausser les épaules.'}),
    ex('pompes-scapulaires','Pompes scapulaires au mur','bras tendus, bouger les omoplates','2 à 3 séries','10 à 15 reps','Débutant',{type:'Stabilité',primary:['Stabilisateurs de l’omoplate'],secondary:['Épaules'],technique:'Mains au mur et bras tendus, rapprochez puis écartez les omoplates sans plier les coudes.'}),
    ex('ytw-sol','Y–T–W au sol','lever léger, nuque longue','2 séries','6 à 8 cycles','Doux',{type:'Mobilité',primary:['Épaules','Haut du dos'],secondary:['Coiffe des rotateurs'],technique:'Allongé sur le ventre, formez successivement un Y, un T puis un W avec les bras. Levez très légèrement les mains et gardez la nuque dans l’axe.'}),
    ex('pike-hold','Pike hold incliné','mains sur table, épaules poussées','2 séries','15 à 25 sec','Intermédiaire',{equipment:'Table',type:'Isométrique',duration:true,primary:['Épaules'],secondary:['Triceps','Gainage'],technique:'Mains sur un support stable, reculez les hanches et poussez activement dans les épaules. Maintenez une position stable sans forcer la flexion des épaules.'})
  ]},
  'grand-dorsal':{name:'Grand dorsal',subtitle:'Renforcer le dos et apprendre à tirer', exercises:[
    ex('tirage-sous-table','Tirage sous table','table très stable uniquement','2 à 3 séries','6 à 10 reps','Intermédiaire',{equipment:'Table',type:'Tirage',primary:['Grand dorsal','Haut du dos'],secondary:['Biceps'],technique:'Sous une table extrêmement stable, saisissez le bord et tirez la poitrine vers le support en gardant le corps gainé. Redescendez lentement.'}),
    ex('superman-tirage','Superman tirage','tirer les coudes vers les côtes','2 séries','8 à 12 reps','Doux',{type:'Tirage',primary:['Grand dorsal'],secondary:['Haut du dos','Lombaires'],technique:'Allongé sur le ventre, bras devant, ramenez lentement les coudes vers les côtes puis réallongez les bras. Gardez le mouvement petit et contrôlé.'}),
    ex('nageur-sol','Nageur au sol','petite amplitude, poitrine légère','2 séries','8 à 12 reps','Doux',{type:'Mobilité',primary:['Grand dorsal','Haut du dos'],secondary:['Épaules'],technique:'Allongé sur le ventre, faites parcourir aux bras un arc contrôlé de l’avant vers les côtés. Ne cherchez pas une grande hauteur.'}),
    ex('tirage-serviette','Tirage isométrique serviette','tirer sans à-coup','3 séries','20 sec','Débutant',{equipment:'Serviette',type:'Isométrique',duration:true,primary:['Grand dorsal','Biceps'],secondary:['Avant-bras'],technique:'Tenez une serviette sous tension et créez une résistance volontaire en tirant sans mouvement brusque. Maintenez la tension sans bloquer la respiration.'})
  ]},
  'quadriceps':{name:'Quadriceps',subtitle:'Force utile des cuisses et des genoux', exercises:[
    ex('squat-chaise','Squat vers chaise','hanches en arrière, poitrine haute','2 à 3 séries','8 à 12 reps','Débutant',{equipment:'Chaise',type:'Jambes',primary:['Quadriceps'],secondary:['Fessiers'],technique:'Reculez les hanches vers une chaise, fléchissez les genoux dans une amplitude confortable, effleurez l’assise puis remontez en poussant dans les pieds.'}),
    ex('fente-arriere','Fente arrière assistée','tenir un support, buste droit','2 séries','6 à 8 reps / côté','Débutant',{perSide:true,equipment:'Support stable',type:'Jambes',primary:['Quadriceps'],secondary:['Fessiers','Équilibre'],technique:'Tenez un support stable. Reculez un pied, descendez verticalement dans une amplitude confortable puis revenez en poussant dans la jambe avant.'}),
    ex('montee-marche','Montée sur marche','pousser par la jambe d’appui','2 à 3 séries','8 reps / côté','Débutant',{perSide:true,equipment:'Marche',type:'Jambes',primary:['Quadriceps'],secondary:['Fessiers','Mollets'],technique:'Posez un pied sur une marche basse et stable. Montez en poussant surtout avec la jambe placée sur la marche puis redescendez lentement.'}),
    ex('chaise-mur','Chaise au mur','genoux confortables, dos plaqué','2 à 3 séries','20 à 40 sec','Débutant',{type:'Isométrique',duration:true,primary:['Quadriceps'],secondary:['Fessiers'],technique:'Dos contre le mur, glissez jusqu’à une position confortable. Maintenez les genoux alignés avec les pieds et le dos en contact avec le mur.'})
  ]},
  'ischio-jambiers':{name:'Ischio-jambiers',subtitle:'Arrière des cuisses et chaîne postérieure', exercises:[
    ex('charniere-hanches','Charnière de hanches','dos neutre, mains sur cuisses','2 à 3 séries','10 à 12 reps','Débutant',{type:'Jambes',primary:['Ischio-jambiers'],secondary:['Fessiers','Lombaires'],technique:'Poussez les hanches vers l’arrière en gardant le dos neutre et les genoux légèrement fléchis, puis revenez en contractant les fessiers.'}),
    ex('pont-fessier-ischio','Pont fessier','pousser dans les talons','2 à 3 séries','10 à 15 reps','Débutant',{type:'Jambes',primary:['Fessiers','Ischio-jambiers'],secondary:['Gainage'],technique:'Allongé sur le dos, pieds au sol, poussez dans les talons pour élever le bassin sans creuser exagérément le bas du dos puis redescendez.'}),
    ex('walkout-pont','Walkout du pont','petits pas, bassin haut','2 séries','4 à 8 reps','Intermédiaire',{type:'Jambes',primary:['Ischio-jambiers'],secondary:['Fessiers','Gainage'],technique:'Depuis un pont fessier, avancez les pieds par petits pas sans laisser tomber brutalement le bassin, puis revenez sous contrôle.'}),
    ex('leg-curl-serviette','Leg curl glissé serviette','sur sol glissant','2 séries','6 à 10 reps','Intermédiaire',{equipment:'Serviette',type:'Jambes',primary:['Ischio-jambiers'],secondary:['Fessiers'],technique:'Talons posés sur des serviettes sur sol glissant, allongez progressivement les jambes puis ramenez les talons sous contrôle en gardant le bassin stable.'})
  ]},
  'mollets':{name:'Mollets',subtitle:'Chevilles solides, appuis stables', exercises:[
    ex('elevation-mollets','Élévation mollets debout','monter haut, descendre lentement','2 à 3 séries','12 à 20 reps','Débutant',{type:'Jambes',primary:['Mollets'],secondary:['Chevilles'],technique:'Montez progressivement sur la pointe des pieds, marquez une courte pause en haut puis redescendez lentement jusqu’au sol.'}),
    ex('elevation-unilaterale','Élévation unilatérale assistée','tenir un support','2 séries','8 à 12 reps / côté','Intermédiaire',{perSide:true,equipment:'Support stable',type:'Jambes',primary:['Mollets'],secondary:['Équilibre'],technique:'Tenez un support, placez le poids sur une jambe et montez sur la pointe du pied sans basculer la cheville.'}),
    ex('mollets-genoux-flechis','Mollets genoux fléchis','viser aussi le soléaire','2 séries','12 à 20 reps','Débutant',{type:'Jambes',primary:['Mollets'],secondary:['Chevilles'],technique:'Fléchissez légèrement les genoux puis effectuez des élévations de talons en gardant les genoux dans la même position.'}),
    ex('pause-marche','Pause en haut sur marche','marche stable uniquement','2 séries','20 à 30 sec','Débutant',{equipment:'Marche',type:'Isométrique',duration:true,primary:['Mollets'],secondary:['Équilibre'],technique:'Sur une marche stable, montez sur la pointe des pieds et maintenez la position sans laisser les chevilles s’effondrer vers l’intérieur.'})
  ]},
  'lombaires':{name:'Lombaires',subtitle:'Renforcer et protéger le bas du dos', exercises:[
    ex('bird-dog','Bird-dog','allonger bras et jambe opposés','2 séries','6 à 8 reps / côté','Débutant',{perSide:true,type:'Gainage',primary:['Gainage','Lombaires'],secondary:['Fessiers','Épaules'],technique:'À quatre pattes, allongez lentement un bras et la jambe opposée sans tourner le bassin. Revenez au centre puis changez de côté.',breathing:'Expirez pendant l’allongement, inspirez au retour. Gardez la respiration calme et continue.'}),
    ex('curl-up-lomb','Curl-up modifié','une jambe pliée, lever légèrement tête et épaules','5 répétitions','10 sec','Doux',{type:'Gainage',duration:true,hybridHold:true,repsPerSet:5,holdPerRepSeconds:10,primary:['Abdominaux'],secondary:['Stabilité lombaire'],technique:'Allongé sur le dos avec une jambe pliée, soulevez légèrement la tête et les épaules sans arrondir fortement le bas du dos. Maintenez brièvement puis reposez.'}),
    ex('planche-laterale-lomb','Planche latérale sur genoux','tête–tronc–genoux alignés','2 séries','15 à 25 sec / côté','Doux',{perSide:true,type:'Gainage',duration:true,primary:['Obliques','Gainage'],secondary:['Épaules','Fessiers'],technique:'Appui sur l’avant-bras et les genoux, soulevez le bassin jusqu’à aligner tête, tronc et genoux. Maintenez sans laisser l’épaule s’affaisser.'}),
    ex('charniere-mur-lomb','Charnière de hanches au mur','envoyer les fesses vers l’arrière','2 séries','8 à 12 reps','Débutant',{type:'Mobilité',primary:['Ischio-jambiers','Fessiers'],secondary:['Lombaires'],technique:'Placez-vous à courte distance d’un mur, dos au mur. Poussez les hanches en arrière jusqu’à toucher le mur sans arrondir le dos puis revenez.'}),
    ex('pont-fessier-lomb','Pont fessier','pousser dans les talons','2 à 3 séries','10 à 15 reps','Débutant',{type:'Gainage',primary:['Fessiers'],secondary:['Ischio-jambiers','Stabilité lombaire'],technique:'Allongé sur le dos, poussez dans les talons pour soulever le bassin jusqu’à une position confortable. Redescendez lentement sans hyperextension.'})
  ]},
  'abdos':{name:'Abdos',subtitle:'Renforcer la sangle abdominale sans tirer sur le bas du dos', exercises:[
    ex('dead-bug','Dead bug','bassin plaqué','2 séries','6 à 8 reps / côté','Doux',{perSide:true,type:'Gainage',primary:['Abdominaux'],secondary:['Fléchisseurs de hanche'],technique:'Allongé sur le dos, hanches et genoux fléchis, allongez lentement un bras et la jambe opposée sans laisser le bas du dos se creuser.'}),
    ex('curl-up-abdos','Curl-up modifié','lever légèrement tête et épaules','5 répétitions','10 sec','Doux',{type:'Gainage',duration:true,hybridHold:true,repsPerSet:5,holdPerRepSeconds:10,primary:['Abdominaux'],secondary:['Stabilité lombaire'],technique:'Soulevez légèrement la tête et les épaules, sans chercher un grand enroulement du tronc, puis maintenez avant de redescendre.'}),
    ex('planche-avant-bras-genoux','Planche avant-bras sur genoux','tête–tronc–genoux alignés','2 séries','15 à 25 sec','Doux',{type:'Gainage',duration:true,primary:['Abdominaux'],secondary:['Épaules','Fessiers'],technique:'Appui sur les avant-bras et les genoux, alignez tête, tronc et genoux. Serrez légèrement le ventre sans bloquer la respiration.'}),
    ex('reverse-crunch','Reverse crunch léger','petite amplitude, enrouler le bassin','2 séries','8 à 10 reps','Intermédiaire',{type:'Gainage',primary:['Abdominaux'],secondary:['Fléchisseurs de hanche'],technique:'Genoux au-dessus des hanches, enroulez légèrement le bassin pour rapprocher les genoux du tronc, sans élan ni grand balancement.'})
  ]},
  'biceps':{name:'Biceps',subtitle:'Avant-bras forts, tirage et flexion du coude', exercises:[
    ex('tirage-table-supination','Tirage sous table supination','table très stable, paumes vers soi','2 à 3 séries','6 à 10 reps','Intermédiaire',{equipment:'Table',type:'Tirage',primary:['Biceps','Grand dorsal'],secondary:['Avant-bras'],technique:'Sous une table extrêmement stable, saisissez le bord paumes vers vous. Tirez la poitrine vers le support puis redescendez lentement.'}),
    ex('curl-serviette','Curl serviette sous pied','serviette bien tendue, coude près du corps','2 à 3 séries','8 à 12 reps','Débutant',{equipment:'Serviette',type:'Tirage',primary:['Biceps'],secondary:['Avant-bras'],technique:'Bloquez une serviette sous le pied et tenez les extrémités. Fléchissez le coude contre la résistance créée par la jambe et la serviette.'}),
    ex('curl-auto-resiste','Curl auto-résisté','l’autre main freine','2 séries','8 à 10 reps / côté','Débutant',{perSide:true,type:'Tirage',primary:['Biceps'],secondary:['Avant-bras'],technique:'Une main effectue un curl tandis que l’autre oppose une résistance progressive. Gardez le coude près du corps et la résistance régulière.'}),
    ex('maintien-90-serviette','Maintien 90° serviette','tirer sans à-coup','3 séries','20 sec','Débutant',{equipment:'Serviette',type:'Isométrique',duration:true,primary:['Biceps'],secondary:['Avant-bras'],technique:'Assis, serviette autour des pieds, tirez jusqu’à environ 90° de flexion du coude et maintenez une tension stable.'})
  ]},
  'triceps':{name:'Triceps',subtitle:'Travail de poussée et extension du coude', exercises:[
    ex('extensions-triceps-mur','Extensions triceps au mur','coudes près du corps','2 à 3 séries','10 à 15 reps','Doux',{type:'Poussée',primary:['Triceps'],secondary:['Épaules'],technique:'Face au mur, mains posées, gardez les coudes dirigés vers le bas. Fléchissez les coudes pour rapprocher le front du mur puis repoussez.'}),
    ex('pompes-serrees','Pompes serrées inclinées','mains rapprochées, descendre sans douleur','2 à 3 séries','8 à 12 reps','Débutant',{equipment:'Table',type:'Poussée',primary:['Triceps'],secondary:['Pectoraux','Épaules'],technique:'Mains rapprochées sur un support stable, descendez en gardant les coudes proches du tronc puis repoussez sans verrouiller brutalement.'}),
    ex('extensions-triceps-table','Extensions triceps sur table','épaules basses, coudes fixes','2 séries','8 à 10 reps','Intermédiaire',{equipment:'Table',type:'Poussée',primary:['Triceps'],secondary:['Épaules'],technique:'Mains sur un support stable, corps incliné. Fléchissez seulement les coudes pour rapprocher la tête du support puis étendez les bras.'}),
    ex('demi-dips','Demi-dips sur chaise','petite amplitude','2 séries','6 à 10 reps','Intermédiaire',{equipment:'Chaise',type:'Poussée',primary:['Triceps'],secondary:['Épaules'],technique:'Mains sur une chaise très stable, descendez seulement de quelques centimètres en gardant les épaules basses puis repoussez. Arrêtez si l’avant de l’épaule devient douloureux.'})
  ]}
};

const lessonOverrides = {
  "pompes-murales": {
    "breathing": "Inspirez en rapprochant la poitrine du mur, puis expirez en repoussant sans bloquer la respiration.",
    "errors": [
      "Plier les hanches au lieu de déplacer tout le corps",
      "Écarter les coudes à angle droit",
      "Rentrer la tête vers le mur avant la poitrine"
    ],
    "easier": "Placez-vous plus près du mur et réduisez légèrement l’amplitude.",
    "harder": "Éloignez progressivement les pieds du mur avant de passer aux pompes inclinées."
  },
  "pompes-inclinees": {
    "breathing": "Inspirez pendant la descente vers le support et expirez pendant la poussée.",
    "errors": [
      "Laisser le bassin s’affaisser",
      "Poser les mains sur un support instable",
      "Descendre les épaules jusqu’aux oreilles"
    ],
    "easier": "Choisissez un support plus haut et gardez une amplitude confortable.",
    "harder": "Utilisez progressivement un support plus bas, sans perdre l’alignement."
  },
  "serrage-paumes": {
    "breathing": "Respirez lentement pendant toute la contraction ; ne retenez pas l’air pendant l’effort isométrique.",
    "errors": [
      "Hausser les épaules",
      "Relâcher la pression entre les paumes",
      "Pousser surtout avec les doigts au lieu des paumes"
    ],
    "easier": "Réduisez volontairement la pression entre les mains et raccourcissez le maintien.",
    "harder": "Augmentez progressivement le temps de maintien sans crisper le cou."
  },
  "pompes-amplitude-courte": {
    "breathing": "Inspirez sur la petite descente et expirez en revenant à la position de départ.",
    "errors": [
      "Transformer la petite amplitude en mouvement brusque",
      "Laisser les poignets s’effondrer vers l’intérieur",
      "Chercher une profondeur qui réveille la douleur"
    ],
    "easier": "Augmentez la hauteur du support et diminuez encore la descente.",
    "harder": "Augmentez l’amplitude par petits paliers avant de réduire la hauteur du support."
  },
  "anges-mur": {
    "breathing": "Inspirez en montant les bras, puis expirez en les ramenant lentement vers le bas.",
    "errors": [
      "Cambrer fortement le bas du dos",
      "Hausser les épaules pendant la montée",
      "Forcer les mains à rester au mur au prix d’une douleur"
    ],
    "easier": "Éloignez légèrement les mains du mur ou réduisez la hauteur atteinte.",
    "harder": "Ralentissez la montée et ajoutez une pause de deux secondes en haut."
  },
  "pompes-scapulaires": {
    "breathing": "Inspirez en laissant les omoplates se rapprocher, puis expirez en repoussant le mur avec les bras toujours tendus.",
    "errors": [
      "Plier les coudes",
      "Avancer la tête",
      "Confondre le mouvement des omoplates avec une pompe complète"
    ],
    "easier": "Tenez-vous plus près du mur et réduisez le déplacement des omoplates.",
    "harder": "Éloignez légèrement les pieds du mur tout en gardant les coudes verrouillés sans force excessive."
  },
  "ytw-sol": {
    "breathing": "Expirez lors de chaque légère élévation des bras et inspirez en reposant les mains.",
    "errors": [
      "Lever les bras trop haut",
      "Casser la nuque pour regarder devant soi",
      "Faire les trois positions trop vite"
    ],
    "easier": "Gardez les mains au sol et travaillez seulement le placement des bras.",
    "harder": "Ajoutez une courte pause dans chaque position Y, T et W."
  },
  "pike-hold": {
    "breathing": "Respirez calmement et régulièrement pendant tout le maintien ; évitez toute apnée.",
    "errors": [
      "Enfoncer les épaules entre les bras",
      "Arrondir fortement le dos",
      "Choisir un appui trop bas pour votre mobilité"
    ],
    "easier": "Utilisez un support plus haut et raccourcissez le maintien.",
    "harder": "Abaissez progressivement le support ou allongez le maintien par tranches de cinq secondes."
  },
  "tirage-sous-table": {
    "breathing": "Inspirez bras tendus, expirez en tirant la poitrine vers la table, puis inspirez en redescendant.",
    "errors": [
      "Utiliser une table légère ou mobile",
      "Tirer le menton avant la poitrine",
      "Laisser le bassin tomber pendant le tirage"
    ],
    "easier": "Pliez davantage les genoux et rapprochez les pieds du bassin pour réduire la charge.",
    "harder": "Éloignez les pieds et rapprochez le corps de l’horizontale si la table reste parfaitement stable."
  },
  "superman-tirage": {
    "breathing": "Expirez en ramenant les coudes vers les côtes et inspirez en réallongeant les bras.",
    "errors": [
      "Lever fortement les jambes et la poitrine",
      "Creuser le bas du dos pour gagner de l’amplitude",
      "Tirer les coudes avec un à-coup"
    ],
    "easier": "Gardez la poitrine au sol et concentrez-vous seulement sur le trajet des bras.",
    "harder": "Ajoutez une pause d’une seconde lorsque les coudes sont près des côtes."
  },
  "nageur-sol": {
    "breathing": "Gardez une respiration fluide ; expirez pendant le passage des bras vers l’arrière et inspirez au retour.",
    "errors": [
      "Chercher une grande élévation des bras",
      "Accélérer le balayage",
      "Lever le menton et comprimer la nuque"
    ],
    "easier": "Faites un arc plus court avec les mains presque en contact avec le sol.",
    "harder": "Ralentissez le balayage et marquez une courte pause bras près du corps."
  },
  "tirage-serviette": {
    "breathing": "Expirez en créant la tension dans la serviette, puis continuez à respirer normalement pendant le maintien.",
    "errors": [
      "Donner un coup sec sur la serviette",
      "Crisper les épaules vers les oreilles",
      "Bloquer la respiration pendant l’isométrie"
    ],
    "easier": "Réduisez la tension volontaire et le temps de maintien.",
    "harder": "Augmentez progressivement la tension et la durée sans changer la posture."
  },
  "squat-chaise": {
    "breathing": "Inspirez en descendant vers la chaise et expirez en remontant en poussant dans les pieds.",
    "errors": [
      "Laisser les genoux s’effondrer vers l’intérieur",
      "S’asseoir lourdement sur la chaise",
      "Arrondir le dos pour atteindre l’assise"
    ],
    "easier": "Utilisez une chaise plus haute ou ajoutez un coussin ferme sur l’assise.",
    "harder": "Effleurez la chaise sans vous asseoir puis remontez, ou utilisez une assise légèrement plus basse."
  },
  "fente-arriere": {
    "breathing": "Inspirez pendant la descente, puis expirez en revenant debout grâce à la jambe avant.",
    "errors": [
      "Faire un pas arrière trop court",
      "Basculer le buste vers l’avant",
      "Pousser excessivement sur le support avec les bras"
    ],
    "easier": "Réduisez l’amplitude et utilisez deux points d’appui stables.",
    "harder": "Diminuez progressivement l’aide des mains puis augmentez légèrement l’amplitude."
  },
  "montee-marche": {
    "breathing": "Expirez en montant sur la marche et inspirez pendant la descente contrôlée.",
    "errors": [
      "Prendre de l’élan avec la jambe au sol",
      "Choisir une marche trop haute",
      "Laisser le genou partir vers l’intérieur"
    ],
    "easier": "Utilisez une marche plus basse et un support pour l’équilibre.",
    "harder": "Augmentez légèrement la hauteur de marche ou ralentissez la descente."
  },
  "chaise-mur": {
    "breathing": "Respirez profondément et régulièrement pendant le maintien, sans bloquer l’air.",
    "errors": [
      "Descendre plus bas que la mobilité ne le permet",
      "Décoller le dos du mur",
      "Rapprocher les genoux l’un de l’autre"
    ],
    "easier": "Remontez légèrement la position et réduisez le temps de maintien.",
    "harder": "Allongez progressivement le maintien ou descendez de quelques centimètres si les genoux restent confortables."
  },
  "charniere-hanches": {
    "breathing": "Inspirez en envoyant les hanches vers l’arrière et expirez en revenant debout en serrant les fessiers.",
    "errors": [
      "Arrondir le dos",
      "Transformer le mouvement en squat profond",
      "Avancer les genoux au lieu de reculer les hanches"
    ],
    "easier": "Réduisez l’amplitude et entraînez-vous face à un miroir ou avec les mains sur les cuisses.",
    "harder": "Ralentissez la descente et marquez une pause lorsque les ischio-jambiers sont en tension confortable."
  },
  "pont-fessier-ischio": {
    "breathing": "Expirez en montant le bassin, inspirez en redescendant sans relâcher brutalement.",
    "errors": [
      "Pousser surtout sur l’avant des pieds",
      "Creuser exagérément les lombaires en haut",
      "Écarter ou rapprocher les genoux pendant la montée"
    ],
    "easier": "Montez moins haut et rapprochez légèrement les pieds du bassin.",
    "harder": "Éloignez légèrement les pieds ou ajoutez une pause de deux secondes en haut."
  },
  "walkout-pont": {
    "breathing": "Respirez régulièrement ; expirez légèrement à chaque petit pas d’éloignement et gardez le souffle fluide au retour.",
    "errors": [
      "Faire de grands pas qui font tomber le bassin",
      "Perdre l’alignement des genoux",
      "Aller trop loin jusqu’à tirer dans le bas du dos"
    ],
    "easier": "Faites seulement un ou deux petits pas avant de revenir.",
    "harder": "Ajoutez progressivement un pas supplémentaire tout en gardant le bassin stable."
  },
  "leg-curl-serviette": {
    "breathing": "Inspirez en éloignant les talons et expirez en les ramenant sous le bassin.",
    "errors": [
      "Laisser le bassin tomber pendant le retour",
      "Faire glisser les talons de façon saccadée",
      "Choisir un sol où les serviettes accrochent ou partent brusquement"
    ],
    "easier": "Faites glisser un seul talon à la fois ou réduisez la distance.",
    "harder": "Allongez davantage les jambes avant de ramener les talons, sans perdre la hauteur de bassin."
  },
  "elevation-mollets": {
    "breathing": "Expirez en montant sur la pointe des pieds et inspirez en redescendant lentement.",
    "errors": [
      "Rebondir en bas du mouvement",
      "Laisser les chevilles partir vers l’extérieur",
      "Redescendre sans contrôle"
    ],
    "easier": "Tenez un support stable et réduisez l’amplitude.",
    "harder": "Ajoutez une pause de deux secondes en haut ou ralentissez fortement la descente."
  },
  "elevation-unilaterale": {
    "breathing": "Expirez pendant la montée sur une jambe et inspirez pendant la descente.",
    "errors": [
      "S’aider fortement du bras",
      "Laisser la cheville s’incliner",
      "Monter en donnant un à-coup avec le genou"
    ],
    "easier": "Gardez une partie du poids sur l’autre pied et utilisez un support.",
    "harder": "Réduisez progressivement l’aide de l’autre pied et de la main."
  },
  "mollets-genoux-flechis": {
    "breathing": "Expirez en levant les talons tout en gardant les genoux fléchis, inspirez en redescendant.",
    "errors": [
      "Tendre les genoux pendant la montée",
      "Basculer le buste",
      "Faire des répétitions rapides et rebondies"
    ],
    "easier": "Fléchissez moins les genoux et tenez un support.",
    "harder": "Augmentez le temps sous tension avec une montée et une descente plus lentes."
  },
  "pause-marche": {
    "breathing": "Respirez normalement pendant le maintien en haut ; ne bloquez pas le souffle pour chercher l’équilibre.",
    "errors": [
      "Laisser les talons descendre sans contrôle",
      "S’appuyer sur le bord d’une marche instable",
      "Verrouiller les genoux avec force"
    ],
    "easier": "Faites le maintien au sol avec les deux pieds et un appui de main.",
    "harder": "Allongez progressivement le maintien ou passez à une jambe avec assistance."
  },
  "bird-dog": {
    "breathing": "Expirez pendant l’allongement du bras et de la jambe opposée, puis inspirez au retour au centre.",
    "errors": [
      "Tourner le bassin vers le côté de la jambe levée",
      "Lever le pied plus haut que le bassin",
      "Creuser le bas du dos pour gagner de l’amplitude"
    ],
    "easier": "N’allongez qu’un bras ou qu’une jambe à la fois avant d’associer les deux.",
    "harder": "Ajoutez une pause de deux à trois secondes en extension sans augmenter la hauteur des membres."
  },
  "curl-up-lomb": {
    "breathing": "Expirez doucement en soulevant légèrement la tête et les épaules, puis respirez normalement pendant le maintien.",
    "errors": [
      "Transformer le mouvement en crunch complet",
      "Tirer la tête avec les mains",
      "Écraser volontairement le bas du dos contre le sol"
    ],
    "easier": "Réduisez encore la hauteur du soulèvement et raccourcissez le maintien.",
    "harder": "Allongez le maintien de quelques secondes sans augmenter l’amplitude."
  },
  "planche-laterale-lomb": {
    "breathing": "Respirez lentement pendant le maintien, en expirant sans laisser le bassin redescendre.",
    "errors": [
      "Laisser l’épaule s’effondrer vers l’oreille",
      "Faire pivoter le bassin vers le sol",
      "Pousser la tête en avant"
    ],
    "easier": "Gardez le bassin plus bas ou raccourcissez le maintien.",
    "harder": "Allongez progressivement la durée avant d’envisager une variante jambes tendues."
  },
  "charniere-mur-lomb": {
    "breathing": "Inspirez en reculant les hanches vers le mur et expirez en revenant debout.",
    "errors": [
      "Chercher le mur en arrondissant le dos",
      "Plier fortement les genoux",
      "Se placer trop loin du mur dès le départ"
    ],
    "easier": "Rapprochez-vous du mur pour réduire l’amplitude de la charnière.",
    "harder": "Éloignez-vous progressivement de quelques centimètres tout en gardant le dos neutre."
  },
  "pont-fessier-lomb": {
    "breathing": "Expirez en soulevant le bassin et inspirez pendant la descente contrôlée.",
    "errors": [
      "Monter jusqu’à sentir une compression lombaire",
      "Pousser la tête dans le sol",
      "Perdre l’alignement des genoux"
    ],
    "easier": "Réduisez la hauteur de bassin et rapprochez les pieds.",
    "harder": "Ajoutez une pause en haut ou alternez de petits transferts de poids sans tourner le bassin."
  },
  "dead-bug": {
    "breathing": "Expirez lentement pendant l’allongement du bras et de la jambe, puis inspirez en revenant au centre.",
    "errors": [
      "Laisser le bas du dos se creuser",
      "Descendre la jambe trop bas",
      "Accélérer l’alternance des côtés"
    ],
    "easier": "Ne bougez que les bras ou faites glisser un talon au sol.",
    "harder": "Allongez davantage le membre opposé tout en maintenant le bassin parfaitement stable."
  },
  "curl-up-abdos": {
    "breathing": "Expirez pendant le léger soulèvement, puis inspirez en revenant sans laisser tomber la tête.",
    "errors": [
      "Chercher à toucher les genoux",
      "Tirer sur la nuque",
      "Faire plusieurs répétitions rapides sans maintien"
    ],
    "easier": "Soulevez seulement la tête et le haut des épaules pendant quelques secondes.",
    "harder": "Allongez le temps de maintien ou ajoutez une répétition sans augmenter la hauteur."
  },
  "planche-avant-bras-genoux": {
    "breathing": "Respirez normalement pendant la planche ; expirez longuement pour maintenir la sangle abdominale active.",
    "errors": [
      "Creuser le bas du dos",
      "Monter les fesses très haut",
      "S’effondrer entre les épaules"
    ],
    "easier": "Rapprochez les genoux des coudes et réduisez la durée.",
    "harder": "Éloignez légèrement les genoux ou augmentez le maintien par paliers de cinq secondes."
  },
  "reverse-crunch": {
    "breathing": "Expirez en enroulant légèrement le bassin et inspirez en reposant le bassin au sol.",
    "errors": [
      "Lancer les jambes pour créer de l’élan",
      "Ramener les genoux très loin vers la tête",
      "Redescendre brutalement le bassin"
    ],
    "easier": "Réduisez l’enroulement et gardez les pieds plus proches du sol.",
    "harder": "Ralentissez la descente et marquez une pause lorsque le bassin est légèrement décollé."
  },
  "tirage-table-supination": {
    "breathing": "Expirez en tirant la poitrine vers la table et inspirez pendant la redescente.",
    "errors": [
      "Utiliser un meuble non conçu pour supporter la traction",
      "Casser les poignets en supination",
      "Laisser le bassin descendre avant le torse"
    ],
    "easier": "Pliez les genoux et rapprochez les pieds afin de diminuer la charge.",
    "harder": "Éloignez les pieds pour rendre le corps plus horizontal uniquement si la table est totalement stable."
  },
  "curl-serviette": {
    "breathing": "Expirez pendant la flexion du coude et inspirez en revenant lentement vers le bas.",
    "errors": [
      "Décoller le coude du flanc",
      "Tirer avec l’épaule",
      "Varier brutalement la résistance donnée par le pied"
    ],
    "easier": "Diminuez la pression du pied sur la serviette.",
    "harder": "Augmentez progressivement la résistance du pied ou ralentissez la phase de descente."
  },
  "curl-auto-resiste": {
    "breathing": "Expirez en montant l’avant-bras contre la main opposée et inspirez pendant la descente résistée.",
    "errors": [
      "Créer une résistance irrégulière",
      "Tourner le tronc pour aider le bras",
      "Éloigner le coude du corps"
    ],
    "easier": "Réduisez la force appliquée par la main opposée.",
    "harder": "Augmentez progressivement la résistance tout en gardant la même vitesse de mouvement."
  },
  "maintien-90-serviette": {
    "breathing": "Respirez normalement pendant le maintien à 90 degrés ; évitez de bloquer le souffle.",
    "errors": [
      "Arrondir fortement le dos",
      "Remonter les épaules",
      "Tirer par à-coups sur la serviette"
    ],
    "easier": "Réduisez la tension ou rapprochez davantage les pieds.",
    "harder": "Augmentez légèrement la tension ou le temps de maintien sans modifier l’angle des coudes."
  },
  "extensions-triceps-mur": {
    "breathing": "Inspirez en fléchissant les coudes vers le mur et expirez en les réétendant.",
    "errors": [
      "Écarter les coudes sur les côtés",
      "Casser l’alignement du corps",
      "Descendre la tête sans déplacer les épaules"
    ],
    "easier": "Rapprochez les pieds du mur et réduisez la flexion des coudes.",
    "harder": "Éloignez progressivement les pieds du mur tout en gardant les coudes proches."
  },
  "pompes-serrees": {
    "breathing": "Inspirez pendant la descente inclinée et expirez en repoussant le support.",
    "errors": [
      "Placer les mains si serrées que les poignets deviennent inconfortables",
      "Écarter les coudes",
      "Laisser le bassin s’affaisser"
    ],
    "easier": "Utilisez un support plus haut et espacez légèrement les mains.",
    "harder": "Abaissez progressivement le support ou augmentez l’amplitude sans douleur."
  },
  "extensions-triceps-table": {
    "breathing": "Inspirez en fléchissant les coudes vers le support et expirez en tendant les bras.",
    "errors": [
      "Laisser les coudes s’ouvrir",
      "Transformer le mouvement en pompe complète",
      "Utiliser une table qui bouge"
    ],
    "easier": "Choisissez un support plus haut et réduisez la flexion.",
    "harder": "Éloignez les pieds ou utilisez un support légèrement plus bas si les épaules restent confortables."
  },
  "demi-dips": {
    "breathing": "Inspirez pendant la courte descente et expirez en repoussant pour revenir en haut.",
    "errors": [
      "Descendre profondément derrière la chaise",
      "Laisser les épaules rouler vers l’avant",
      "Utiliser une chaise légère ou glissante"
    ],
    "easier": "Réduisez fortement l’amplitude ou revenez aux extensions triceps au mur.",
    "harder": "Augmentez très progressivement l’amplitude uniquement si les épaules restent parfaitement confortables."
  }
};

const controlOverrides = {
  "pompes-murales": [
    "Tête, bassin et talons restent sur une même ligne",
    "Mains à hauteur de poitrine, légèrement plus larges que les épaules",
    "Coudes orientés en diagonale, sans partir à 90° du buste"
  ],
  "pompes-inclinees": [
    "Support parfaitement stable avant de prendre appui",
    "Corps aligné de la tête aux talons pendant toute la répétition",
    "Poitrine descend vers le support sans affaissement du bassin"
  ],
  "serrage-paumes": [
    "Paumes entièrement en contact au centre de la poitrine",
    "Coudes restent à hauteur confortable, sans hausser les épaules",
    "Pression progressive et symétrique entre les deux mains"
  ],
  "pompes-amplitude-courte": [
    "Amplitude choisie avant l’apparition d’une gêne au poignet ou à l’épaule",
    "Tronc gainé malgré la faible descente",
    "Mains stables et pression répartie sur toute la paume"
  ],
  "anges-mur": [
    "Bassin et cage thoracique restent neutres contre le mur",
    "Épaules restent basses lorsque les bras montent",
    "Amplitude limitée à celle qui permet de garder une trajectoire contrôlée"
  ],
  "pompes-scapulaires": [
    "Coudes restent tendus : seul le mouvement des omoplates change",
    "Tête dans le prolongement du tronc",
    "Omoplates se rapprochent puis s’écartent sans mouvement du bassin"
  ],
  "ytw-sol": [
    "Nuque longue et regard dirigé vers le sol",
    "Élévation des bras faible : priorité au placement des omoplates",
    "Passage Y, T puis W effectué sans élan"
  ],
  "pike-hold": [
    "Support stable et mains solidement posées",
    "Épaules activement repoussées loin des oreilles",
    "Bassin reculé sans arrondir brutalement la colonne"
  ],
  "tirage-sous-table": [
    "Table lourde et vérifiée avant de passer dessous",
    "Corps reste gainé du genou ou du pied jusqu’aux épaules",
    "Poitrine se dirige vers la table avant le menton"
  ],
  "superman-tirage": [
    "Bassin et jambes restent lourds au sol",
    "Coudes reviennent vers les côtes sans hausser les épaules",
    "Bas du dos reste neutre : ne pas chercher à lever haut la poitrine"
  ],
  "nageur-sol": [
    "Mains décrivent un arc proche du sol",
    "Épaules restent éloignées des oreilles",
    "Menton légèrement rentré pour protéger la nuque"
  ],
  "tirage-serviette": [
    "Serviette intacte et prise symétrique",
    "Coudes tirent vers le bas sans mouvement brusque",
    "Tension répartie sans cambrer le dos"
  ],
  "squat-chaise": [
    "Genoux suivent la direction des orteils",
    "Hanches reculent avant de descendre vers la chaise",
    "Poids réparti sur tout le pied, talons restant au sol"
  ],
  "fente-arriere": [
    "Pied avant reste entièrement posé",
    "Genou avant suit la ligne du pied sans rentrer vers l’intérieur",
    "Support utilisé pour l’équilibre, pas pour tirer le corps vers le haut"
  ],
  "montee-marche": [
    "Pied d’appui entièrement posé sur la marche",
    "Genou de la jambe d’appui reste aligné avec les orteils",
    "Descente contrôlée sans se laisser tomber sur la jambe au sol"
  ],
  "chaise-mur": [
    "Dos conserve un contact confortable avec le mur",
    "Pieds assez avancés pour éviter un angle de genou inconfortable",
    "Genoux restent parallèles et alignés avec les pieds"
  ],
  "charniere-hanches": [
    "Colonne reste neutre pendant que les hanches reculent",
    "Genoux légèrement fléchis sans transformer le geste en squat",
    "Tension ressentie derrière les cuisses, pas dans le bas du dos"
  ],
  "pont-fessier-ischio": [
    "Talons restent lourds et stables au sol",
    "Bassin monte par contraction des fessiers sans hyperextension lombaire",
    "Genoux conservent la même largeur pendant toute la montée"
  ],
  "walkout-pont": [
    "Bassin reste suffisamment haut pendant les petits pas",
    "Pas courts et symétriques, sans éloignement brutal des pieds",
    "Arrêter l’éloignement avant de perdre la position du bassin"
  ],
  "leg-curl-serviette": [
    "Surface choisie permet un glissement régulier et prévisible",
    "Bassin reste contrôlé pendant l’allongement et le retour",
    "Talons glissent dans l’axe sans ouvrir les genoux"
  ],
  "elevation-mollets": [
    "Chevilles montent verticalement sans basculer vers l’extérieur",
    "Montée terminée par une courte stabilisation en haut",
    "Descente lente jusqu’au contact complet du talon"
  ],
  "elevation-unilaterale": [
    "Main sur le support uniquement pour l’équilibre",
    "Bassin reste horizontal pendant l’appui sur une jambe",
    "Cheville monte dans l’axe du deuxième orteil"
  ],
  "mollets-genoux-flechis": [
    "Angle des genoux reste presque constant pendant la série",
    "Talons montent ensemble sans déplacement du bassin",
    "Pression maintenue sous l’avant-pied pendant la montée"
  ],
  "pause-marche": [
    "Marche stable et suffisamment large pour l’avant-pied",
    "Chevilles restent alignées en haut du maintien",
    "Descente effectuée lentement après la pause"
  ],
  "bird-dog": [
    "Bassin reste parallèle au sol pendant l’extension croisée",
    "Bras et jambe s’allongent sans chercher à monter au-dessus du tronc",
    "Main d’appui pousse dans le sol pour stabiliser l’épaule"
  ],
  "curl-up-lomb": [
    "Bas du dos conserve sa courbure naturelle sans écrasement forcé",
    "Tête et épaules se décollent seulement de quelques centimètres",
    "Menton reste légèrement rentré, sans tirer sur la nuque"
  ],
  "planche-laterale-lomb": [
    "Coude d’appui placé sous l’épaule",
    "Tête, bassin et genoux forment une ligne continue",
    "Bassin reste empilé sans rouler vers l’avant ou l’arrière"
  ],
  "charniere-mur-lomb": [
    "Distance au mur permet de toucher avec les fesses sans tomber en arrière",
    "Tibia reste presque vertical pendant le recul des hanches",
    "Retour debout initié par les hanches, pas par une extension lombaire"
  ],
  "pont-fessier-lomb": [
    "Côtes restent abaissées pendant la montée du bassin",
    "Poussée répartie dans les deux talons",
    "Sommet du mouvement arrêté avant de creuser le bas du dos"
  ],
  "dead-bug": [
    "Bas du dos conserve un contact doux et stable avec le sol",
    "Bras et jambe opposés s’éloignent seulement jusqu’à garder le bassin immobile",
    "Genou de la jambe restée en haut demeure au-dessus de la hanche"
  ],
  "curl-up-abdos": [
    "Mouvement court initié par la sangle abdominale, pas par la tête",
    "Regard dirigé vers le plafond pour conserver la nuque neutre",
    "Épaules reposées complètement entre les répétitions si la technique se dégrade"
  ],
  "planche-avant-bras-genoux": [
    "Coudes placés sous les épaules",
    "Bassin ni trop haut ni affaissé entre épaules et genoux",
    "Avant-bras poussent légèrement dans le sol pour stabiliser les omoplates"
  ],
  "reverse-crunch": [
    "Genoux restent au-dessus des hanches au départ",
    "Bassin s’enroule de quelques centimètres sans balancer les jambes",
    "Retour lent jusqu’au contact du bassin avec le sol"
  ],
  "tirage-table-supination": [
    "Table vérifiée et prise ferme paumes vers soi",
    "Coudes restent proches du buste pendant la traction",
    "Corps reste en bloc sans pousser le bassin vers le plafond"
  ],
  "curl-serviette": [
    "Pied bloque fermement le centre de la serviette",
    "Coude reste près du flanc pendant la flexion",
    "Résistance de la jambe reste progressive et contrôlable"
  ],
  "curl-auto-resiste": [
    "Coude de travail reste fixe près du tronc",
    "Main opposée fournit une résistance continue sans à-coup",
    "Poignet reste dans le prolongement de l’avant-bras"
  ],
  "maintien-90-serviette": [
    "Coudes restent proches du corps autour de 90°",
    "Serviette tendue sans tirer les épaules vers l’avant",
    "Buste vertical et poignets neutres pendant le maintien"
  ],
  "extensions-triceps-mur": [
    "Coudes restent orientés vers le bas et proches l’un de l’autre",
    "Corps reste aligné pendant que seuls les coudes se fléchissent",
    "Mains restent fixes sur le mur pendant toute la répétition"
  ],
  "pompes-serrees": [
    "Support stable et mains rapprochées sans contrainte du poignet",
    "Coudes longent le tronc pendant la descente",
    "Corps reste gainé jusqu’au retour bras tendus"
  ],
  "extensions-triceps-table": [
    "Coudes restent fixes et dirigés vers le support",
    "Tête se déplace vers le bord sans transformer le geste en pompe",
    "Épaules restent basses et stables pendant l’extension"
  ],
  "demi-dips": [
    "Chaise immobilisée contre un mur ou support non glissant",
    "Descente volontairement courte, épaules restant loin des oreilles",
    "Coudes se fléchissent vers l’arrière sans laisser les épaules rouler vers l’avant"
  ]
};

Object.values(groups).forEach(group=>group.exercises.forEach(exercise=>{
  const override=lessonOverrides[exercise.slug];
  if(override) Object.assign(exercise,override);
}));
Object.values(groups).forEach(group=>group.exercises.forEach(exercise=>{
  if(controlOverrides[exercise.slug]) exercise.controls=controlOverrides[exercise.slug].slice();
}));

const videoSlugs=new Set([
  'pompes-murales','pompes-inclinees','serrage-paumes','pompes-amplitude-courte','anges-mur','pompes-scapulaires','ytw-sol','pike-hold','tirage-sous-table','superman-tirage','nageur-sol','tirage-serviette','squat-chaise','fente-arriere','montee-marche','chaise-mur','charniere-hanches','pont-fessier-ischio','walkout-pont','leg-curl-serviette','elevation-mollets','elevation-unilaterale','mollets-genoux-flechis','pause-marche','bird-dog','curl-up-lomb','planche-laterale-lomb','charniere-mur-lomb','pont-fessier-lomb','dead-bug','curl-up-abdos','planche-avant-bras-genoux','reverse-crunch','tirage-table-supination','curl-serviette','curl-auto-resiste','maintien-90-serviette'
]);

const muscleZones=[
  {g:'epaules',label:'ÉPAULES',hits:[{x:19.5,y:18.2,w:5.2,h:7.4},{x:40.6,y:18.2,w:5.2,h:7.4}],labels:[{x:2.6,y:11.3,w:14.1,h:5.7}]},
  {g:'pectoraux',label:'PECTORAUX',hits:[{x:24.2,y:20.5,w:9,h:9},{x:34,y:20.5,w:9,h:9}],labels:[{x:1.6,y:20.8,w:14.8,h:5.5}]},
  {g:'biceps',label:'BICEPS',hits:[{x:19.2,y:27,w:4.6,h:9.1},{x:43.7,y:27,w:4.6,h:9.1}],labels:[{x:2.8,y:29.8,w:11.6,h:5.4}]},
  {g:'abdos',label:'ABDOS',hits:[{x:28.8,y:30.5,w:10.3,h:14.2}],labels:[{x:2.7,y:40.5,w:11.4,h:5.4}]},
  {g:'quadriceps',label:'QUADRICEPS',hits:[{x:21.8,y:58.2,w:7.2,h:14.4},{x:38.4,y:58.2,w:7.2,h:14.4}],labels:[{x:1.5,y:65.1,w:16.4,h:6}]},
  {g:'mollets',label:'MOLLETS',hits:[{x:21,y:76.5,w:5.4,h:16.4},{x:39.7,y:76.5,w:5.4,h:16.4}],labels:[{x:2.4,y:80.4,w:13.2,h:5.9}]},
  {g:'triceps',label:'TRICEPS',hits:[{x:55.3,y:27,w:4.7,h:9.4},{x:79.8,y:27,w:4.7,h:9.4}],labels:[{x:86.7,y:23.8,w:12.9,h:5.8}]},
  {g:'grand-dorsal',label:'GRAND DORSAL',hits:[{x:58.6,y:23.5,w:8.2,h:14.2},{x:68.6,y:23.5,w:8.2,h:14.2}],labels:[{x:86.5,y:33.8,w:13,h:8.8}]},
  {g:'lombaires',label:'LOMBAIRES',hits:[{x:63.3,y:38.4,w:11.4,h:6.7}],labels:[{x:85.5,y:44.8,w:14.2,h:5.8}]},
  {g:'ischio-jambiers',label:'ISCHIO-JAMBIERS',hits:[{x:57.6,y:59,w:7.2,h:15.7},{x:74.1,y:59,w:7.2,h:15.7}],labels:[{x:85.4,y:64.1,w:14.3,h:9.2}]}
];

const programs={
  'reprise-douce':{name:'Reprise douce',desc:'Une séance progressive, sans impact, pour reprendre sans chercher l’épuisement.',minutes:24,level:'Débutant',items:[
    p('quadriceps','squat-chaise',2,'8 reps',45),p('pectoraux','pompes-murales',2,'10 reps',45),p('epaules','anges-mur',2,'8 reps',30),p('lombaires','bird-dog',2,'6 reps / côté',45),p('lombaires','pont-fessier-lomb',2,'10 reps',45),p('mollets','elevation-mollets',2,'12 reps',30)
  ]},
  'corps-complet':{name:'Corps complet',desc:'Un circuit équilibré jambes, poussée, tirage et gainage.',minutes:32,level:'Débutant',items:[
    p('quadriceps','squat-chaise',3,'10 reps',60),p('pectoraux','pompes-inclinees',3,'8 reps',60),p('grand-dorsal','tirage-serviette',3,null,45,20),p('ischio-jambiers','pont-fessier-ischio',3,'12 reps',45),p('lombaires','bird-dog',2,'6 reps / côté',45),p('abdos','dead-bug',2,'6 reps / côté',45),p('mollets','elevation-mollets',2,'15 reps',30)
  ]},
  'dos-lombaires':{name:'Dos & lombaires',desc:'Stabilité du tronc, contrôle du bassin et chaîne postérieure.',minutes:20,level:'Doux',items:[
    p('lombaires','bird-dog',2,'6 reps / côté',45),p('lombaires','curl-up-lomb',2,null,30,0),p('lombaires','planche-laterale-lomb',2,null,45,20),p('lombaires','charniere-mur-lomb',2,'10 reps',45),p('lombaires','pont-fessier-lomb',2,'12 reps',45)
  ]},
  'haut-du-corps':{name:'Haut du corps',desc:'Pectoraux, dos, épaules et bras avec variantes contrôlées.',minutes:28,level:'Débutant',items:[
    p('pectoraux','pompes-inclinees',3,'8 reps',60),p('grand-dorsal','tirage-serviette',3,null,45,20),p('epaules','anges-mur',2,'10 reps',30),p('biceps','curl-serviette',2,'10 reps',45),p('triceps','extensions-triceps-mur',2,'10 reps',45)
  ]},
  'jambes':{name:'Jambes',desc:'Cuisses, chaîne postérieure et mollets sans saut.',minutes:27,level:'Débutant',items:[
    p('quadriceps','squat-chaise',3,'10 reps',60),p('quadriceps','montee-marche',2,'8 reps / côté',60),p('ischio-jambiers','charniere-hanches',3,'10 reps',45),p('ischio-jambiers','pont-fessier-ischio',3,'12 reps',45),p('mollets','elevation-mollets',3,'15 reps',30)
  ]},
  'express-15':{name:'Express 15 minutes',desc:'Un format court pour maintenir une routine les jours chargés.',minutes:15,level:'Débutant',items:[
    p('quadriceps','squat-chaise',2,'8 reps',30),p('pectoraux','pompes-murales',2,'10 reps',30),p('lombaires','bird-dog',2,'5 reps / côté',30),p('abdos','planche-avant-bras-genoux',2,null,30,20)
  ]}
};
function p(group,slug,sets,target,restSec,durationSec=0){return {group,slug,sets,target:target||null,restSec,durationSec};}

function findExercise(group,slug){return groups[group]?.exercises.find(e=>e.slug===slug)||null;}
function allExercises(){return Object.entries(groups).flatMap(([group,g])=>g.exercises.map(e=>({...e,group,groupName:g.name})));}
function hasVideo(e){return videoSlugs.has(e.slug);}
function videoFor(e){return `assets/videos/${e.slug}.mp4`;}
function imgFor(e,n=1){return `assets/exercises/${e.slug}-${n}.jpg`;}
function thumbFor(e){return `assets/thumbs/${e.slug}.jpg`;}
function heroFor(e){
  const special={'bird-dog':'bird-dog-hero.jpg','curl-up-lomb':'curl-up-modifie-hero.jpg','planche-laterale-lomb':'planche-laterale-genoux-hero.jpg','pont-fessier-lomb':'pont-fessier-hero.jpg'};
  return special[e.slug]?`assets/exercises/${special[e.slug]}`:imgFor(e,1);
}

function defaultData(){return {favorites:[],selection:[],sessions:[],offlinePrograms:[],settings:{reducedMotion:false},firstUse:new Date().toISOString()};}
let data=loadData();
function loadData(){
  try{
    const current=JSON.parse(localStorage.getItem(STORAGE_KEY)||'null');
    if(current)return normalizeStoredData(current);
    const legacyFavorites=JSON.parse(localStorage.getItem('ds-favorites')||'[]');
    const migrated={...defaultData(),favorites:Array.isArray(legacyFavorites)?legacyFavorites:[]};
    localStorage.setItem(STORAGE_KEY,JSON.stringify(migrated));
    return migrated;
  }catch{return defaultData();}
}
function saveData(){localStorage.setItem(STORAGE_KEY,JSON.stringify(data));}
function normalizeStoredData(current){
  const base=defaultData();if(!current||typeof current!=='object'||Array.isArray(current))return base;
  const favorites=Array.isArray(current.favorites)?current.favorites.filter(k=>!!validExerciseKey(k)).slice(0,41):[];
  const selectionRaw=Array.isArray(current.selection)?current.selection.map(sanitizeSelectionItem).filter(Boolean).slice(0,41):[];
  const seenSelectionKeys=new Set(),selection=selectionRaw.filter(it=>!seenSelectionKeys.has(it.key)&&(seenSelectionKeys.add(it.key),true));
  const sessions=Array.isArray(current.sessions)?current.sessions.map(sanitizeImportedSession).filter(Boolean).slice(-2000):[];
  const offlinePrograms=Array.isArray(current.offlinePrograms)?[...new Set(current.offlinePrograms.filter(slug=>typeof slug==='string'&&programs[slug]))]:[];
  const settings=current.settings&&typeof current.settings==='object'&&!Array.isArray(current.settings)?{reducedMotion:!!current.settings.reducedMotion}:{reducedMotion:false};
  const firstUse=validDateString(current.firstUse)?current.firstUse:base.firstUse;
  return {favorites:[...new Set(favorites)],selection,sessions,offlinePrograms,settings,firstUse};
}
const LIMITS={
  sets:{min:1,max:8},restSec:{min:10,max:180},durationSec:{min:5,max:180},
  actualReps:{min:0,max:999},actualSeconds:{min:0,max:3600},hybridReps:{min:1,max:999},holdPerRepSeconds:{min:1,max:3600}
};
function clampInt(value,min,max,fallback){
  const n=Number(value);
  if(!Number.isFinite(n))return fallback;
  return Math.max(min,Math.min(max,Math.round(n)));
}
function strictInteger(value,min,max){
  if(value===null||value===undefined||typeof value==='boolean'||(typeof value==='string'&&value.trim()===''))return {ok:false,value:null};
  const n=Number(value);
  return Number.isFinite(n)&&Number.isInteger(n)&&n>=min&&n<=max?{ok:true,value:n}:{ok:false,value:null};
}
function validatePerformanceValue(kind,value){
  const lim=kind==='seconds'?LIMITS.actualSeconds:LIMITS.actualReps;
  return strictInteger(value,lim.min,lim.max);
}
function exerciseMode(e){return e?.hybridHold?'hybrid':e?.duration?'duration':'reps';}
function validExerciseKey(key){
  if(typeof key!=='string'||key.length>100)return null;
  const parts=key.split('/');if(parts.length!==2)return null;
  const [group,slug]=parts,e=findExercise(group,slug);return e?{group,slug,e}:null;
}
function sanitizeSessionItem(raw){
  if(!raw||typeof raw!=='object')return null;
  const ref=raw.key?validExerciseKey(raw.key):(findExercise(raw.group,raw.slug)?{group:raw.group,slug:raw.slug,e:findExercise(raw.group,raw.slug)}:null);
  if(!ref)return null;
  const mode=exerciseMode(ref.e),perSide=!!ref.e.perSide;
  const sets=clampInt(raw.sets,LIMITS.sets.min,LIMITS.sets.max,2),restSec=clampInt(raw.restSec,LIMITS.restSec.min,LIMITS.restSec.max,45);
  if(mode==='hybrid'){
    const repsTarget=clampInt(raw.repsTarget,LIMITS.hybridReps.min,LIMITS.hybridReps.max,ref.e.repsPerSet||5);
    // V0.7: preserve a legacy custom Curl-up duration stored as durationSec before hybrid mode existed.
    const legacyHold=(raw.holdPerRepSeconds!==undefined&&raw.holdPerRepSeconds!==null)?raw.holdPerRepSeconds:((raw.durationSec!==undefined&&raw.durationSec!==null&&Number(raw.durationSec)>0)?raw.durationSec:ref.e.holdPerRepSeconds);
    const holdPerRepSeconds=clampInt(legacyHold,LIMITS.holdPerRepSeconds.min,LIMITS.holdPerRepSeconds.max,ref.e.holdPerRepSeconds||10);
    return {group:ref.group,slug:ref.slug,sets,target:null,restSec,durationSec:0,mode,perSide,repsTarget,holdPerRepSeconds};
  }
  if(mode==='duration'){
    const durationSec=clampInt(raw.durationSec,LIMITS.durationSec.min,LIMITS.durationSec.max,parseDuration(ref.e.reps));
    return {group:ref.group,slug:ref.slug,sets,target:null,restSec,durationSec,mode,perSide,repsTarget:0,holdPerRepSeconds:0};
  }
  const target=String(raw.target||ref.e.reps||'').slice(0,80);
  return {group:ref.group,slug:ref.slug,sets,target,restSec,durationSec:0,mode,perSide,repsTarget:0,holdPerRepSeconds:0};
}
function sanitizeSelectionItem(raw){
  const ref=validExerciseKey(raw?.key);if(!ref)return null;
  const item=sanitizeSessionItem({...raw,group:ref.group,slug:ref.slug});
  return item?{key:`${ref.group}/${ref.slug}`,sets:item.sets,restSec:item.restSec,target:item.target||null,durationSec:item.durationSec,mode:item.mode,perSide:item.perSide,repsTarget:item.repsTarget,holdPerRepSeconds:item.holdPerRepSeconds}:null;
}
const RUNTIME_ID=`rt-${Date.now()}-${Math.random().toString(36).slice(2)}`;
function syncResultTotals(result){
  if(!Array.isArray(result.series)) result.series=[];
  result.setsDone=result.series.filter(x=>x.completed).length;
  result.reps=result.series.reduce((sum,x)=>sum+(Number(x.reps)||0),0);
  result.seconds=result.series.reduce((sum,x)=>sum+(Number(x.seconds)||0),0);
  return result;
}
function makeSeriesRecord(resultKey,index){return {id:`${resultKey}::${index+1}`,index:index+1,completed:false,reps:0,enteredReps:null,seconds:0,enteredSeconds:null,completedAt:null};}
function normalizeSession(s){
  if(!s||!Array.isArray(s.items)) return s;
  const sanitizedItems=s.items.slice(0,41).map(sanitizeSessionItem).filter(Boolean);
  if(!sanitizedItems.length)return null;
  s.items=sanitizedItems;
  if(!Number.isFinite(s.activeElapsedMs)) s.activeElapsedMs=Math.max(0,Number(s.elapsedBeforePause||0)*1000);
  if(typeof s.sessionPaused!=='boolean') s.sessionPaused=false;
  if(!Array.isArray(s.results)) s.results=[];
  s.results=s.items.map((it,i)=>{
    const key=`${it.group}/${it.slug}`;
    const old=s.results[i]||{};
    const name=findExercise(it.group,it.slug)?.name||it.slug;
    let series=Array.isArray(old.series)?old.series.slice(0,it.sets):[];
    while(series.length<it.sets) series.push(makeSeriesRecord(key,series.length));
    if(!old.series&&old.setsDone){
      const approxPerSet=Math.max(0,Math.round((Number(old.reps)||0)/Math.max(1,old.setsDone)));
      for(let k=0;k<Math.min(old.setsDone,series.length);k++){
        series[k]={...series[k],id:`${key}::${k+1}`,index:k+1,completed:true,reps:approxPerSet,enteredReps:approxPerSet,seconds:Math.round((Number(old.seconds)||0)/Math.max(1,old.setsDone)),enteredSeconds:null,completedAt:new Date().toISOString(),migrated:true};
      }
    }
    series=series.map((sr,k)=>({...makeSeriesRecord(key,k),...sr,id:`${key}::${k+1}`,index:k+1}));
    return syncResultTotals({...old,key,name,series});
  });
  s.exerciseIndex=clampInt(s.exerciseIndex,0,s.items.length-1,0);
  s.setIndex=clampInt(s.setIndex,0,s.items[s.exerciseIndex].sets-1,0);
  s.workSide=clampInt(s.workSide,1,2,1);
  if(!['work','rest'].includes(s.phase)) s.phase='work';
  if(s.runtimeId!==RUNTIME_ID){
    s.runtimeId=RUNTIME_ID;
    // A new runtime never resumes the training clock implicitly. The clock restarts only on /session.
    s.activeSince=null;
    try{localStorage.setItem(ACTIVE_SESSION_KEY,JSON.stringify(s));}catch{}
  }
  return s;
}
function saveActiveSession(s){
  if(s){s.runtimeId=RUNTIME_ID;localStorage.setItem(ACTIVE_SESSION_KEY,JSON.stringify(s));}
  else localStorage.removeItem(ACTIVE_SESSION_KEY);
}
function loadActiveSession(){
  try{
    const raw=JSON.parse(localStorage.getItem(ACTIVE_SESSION_KEY)||'null');
    const normalized=normalizeSession(raw);
    if(raw&&!normalized)localStorage.removeItem(ACTIVE_SESSION_KEY);
    return normalized;
  }catch{localStorage.removeItem(ACTIVE_SESSION_KEY);return null;}
}

let deferredPrompt=null;
let pendingSessionStart=null;
let sessionRuntime={timer:null,tick:null};
let currentFilters={query:'',group:'all',level:'all',equipment:'all',type:'all'};
let lastRenderedRoute=null;

function esc(s=''){return String(s).replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));}
function fmtTime(sec){sec=Math.max(0,Math.round(sec||0));const m=Math.floor(sec/60),s=sec%60;return `${m}:${String(s).padStart(2,'0')}`;}
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');clearTimeout(t._x);t._x=setTimeout(()=>t.classList.remove('show'),2600);}
function setHash(path){location.hash=path.startsWith('#')?path:`#${path}`;}
function parseRoute(){
  const raw=(location.hash||'#/home').replace(/^#\/?/,'');
  const parts=raw.split('/').filter(Boolean);
  return {name:parts[0]||'home',parts};
}
function pageTitle(title,sub=''){return `<section class="page-head"><h1>${esc(title)}</h1>${sub?`<p>${esc(sub)}</p>`:''}</section>`;}
function supportMini(){return `<a class="support-mini" href="${SUPPORT_URL}" target="_blank" rel="noopener">${icon('heart')}<span><strong>Cette application est gratuite.</strong><small>Soutenir librement Delta-Sierra</small></span>${icon('chevron')}</a>`;}
function distributionNotice(){return `<a class="distribution-notice" href="${SITE_URL}" target="_blank" rel="noopener"><span><strong>DISTRIBUTION GRATUITE</strong><small>https://www.delta-sierra.com · outil partagé gratuitement, sans fonction payante</small></span>${icon('chevron')}</a>`;}

function render(){
  clearSessionTimers();
  const view=document.getElementById('view');
  const r=parseRoute();
  if(lastRenderedRoute==='session'&&r.name!=='session'){ pauseWorkTimerForRoute(); pauseActiveClockForRoute(); }
  lastRenderedRoute=r.name;
  document.querySelectorAll('.nav-item').forEach(a=>a.classList.toggle('active',a.dataset.nav===navForRoute(r.name)));
  let html='';
  switch(r.name){
    case 'home':html=homeHTML();break;
    case 'explore':html=exploreHTML();break;
    case 'muscles':html=musclesHTML();break;
    case 'group':html=groupHTML(r.parts[1]);break;
    case 'exercise':html=exerciseHTML(r.parts[1],r.parts[2]);break;
    case 'train':html=trainHTML();break;
    case 'program':html=programHTML(r.parts[1]);break;
    case 'builder':html=builderHTML();break;
    case 'session':html=sessionHTML();break;
    case 'summary':html=summaryHTML(r.parts[1]);break;
    case 'progress':html=progressHTML();break;
    case 'profile':html=profileHTML();break;
    default:html=notFoundHTML();
  }
  view.innerHTML=html;
  injectIcons(view);
  bindViewEvents(r);
  if(r.name==='session'){resumeActiveClockForRoute();resumeSessionUI();}
}
function navForRoute(name){if(['explore','muscles','group','exercise'].includes(name))return'explore';if(['train','program','builder','session','summary'].includes(name))return'train';return name;}

function homeHTML(){
  const last=data.sessions.filter(s=>s.status==='completed').slice(-1)[0];
  return `${pageTitle('QUE VOULEZ-VOUS FAIRE AUJOURD’HUI ?','Choisissez un exercice, un programme ou construisez votre propre séance.')}
  <section class="hero-clean" aria-label="Delta-Sierra Calisthénie"><img src="assets/hero-duo-clean.jpg" alt="Un homme et une femme sportifs" loading="eager"></section>
  <section class="home-grid">
    ${homeCard('#/explore','search','EXPLORER','Rechercher et filtrer les 41 exercices.')}
    ${homeCard('#/train','play','S’ENTRAÎNER','Programmes guidés et séance personnalisée.')}
    ${homeCard('#/muscles','chart','CHOIX PAR MUSCLES','Touchez une zone anatomique ou un muscle.')}
    ${homeCard('#/progress','chart','MES PROGRÈS',last?`Dernière séance : ${new Date(last.endedAt).toLocaleDateString('fr-FR')}`:'Votre activité apparaîtra ici.')}
  </section>
  ${loadActiveSession()?`<section class="resume-card"><div><strong>Séance en cours</strong><p>Une séance interrompue peut être reprise.</p></div><a class="primary compact" href="#/session">Reprendre</a></section>`:''}
  ${supportMini()}
  ${distributionNotice()}`;
}
function homeCard(href,ic,title,text){return `<a class="action-card" href="${href}">${icon(ic)}<span><strong>${esc(title)}</strong><small>${esc(text)}</small></span>${icon('chevron')}</a>`;}

function musclesHTML(){
  const legend=Object.entries(groups).map(([k,g])=>`<a class="muscle-legend-btn" href="#/group/${k}">${esc(g.name)}</a>`).join('');
  const hits=muscleZones.map(z=>[
    ...z.hits.map(h=>`<a class="muscle-hit" href="#/group/${z.g}" aria-label="${esc(z.label)}" style="left:${h.x}%;top:${h.y}%;width:${h.w}%;height:${h.h}%;"></a>`),
    ...(z.labels||[]).map(h=>`<a class="muscle-hit label-hit" href="#/group/${z.g}" aria-label="Ouvrir ${esc(z.label)}" style="left:${h.x}%;top:${h.y}%;width:${h.w}%;height:${h.h}%;"></a>`)
  ].join('')).join('');
  return `${pageTitle('CHOIX PAR MUSCLES','Touchez une zone du corps ou choisissez directement un muscle dans la liste.')}
    <div class="muscle-page-layout">
      <div class="muscle-map-wrap"><img class="muscle-map" src="assets/muscle-map-male.jpg" alt="Carte musculaire masculine de face et de dos"><div class="muscle-hitlayer">${hits}</div></div>
      <aside class="muscle-side"><h2>Muscles</h2><div class="muscle-legend">${legend}</div><a class="secondary full" href="#/explore">Voir tous les exercices</a></aside>
    </div>`;
}

function exploreHTML(){
  const list=filteredExercises();
  const groupOptions=Object.entries(groups).map(([k,g])=>`<option value="${k}" ${currentFilters.group===k?'selected':''}>${esc(g.name)}</option>`).join('');
  const levels=[...new Set(allExercises().map(e=>e.level))];
  const equipments=[...new Set(allExercises().map(e=>e.equipment))];
  const types=[...new Set(allExercises().map(e=>e.type))];
  return `${pageTitle('EXPLORER','Recherchez puis combinez les filtres selon vos besoins.')}
    <p class="results-status" id="resultsStatus" role="status" aria-live="polite">${list.length} exercice${list.length>1?'s':''} affiché${list.length>1?'s':''} sur 41.</p>
    <section class="filters" aria-label="Filtres des exercices">
      <label class="search-field">${icon('search')}<input id="filterQuery" type="search" placeholder="Rechercher un exercice" value="${esc(currentFilters.query)}"></label>
      <div class="filter-grid">
        ${selectFilter('filterGroup','Muscle','group','all',groupOptions)}
        ${selectFilter('filterLevel','Difficulté','level','all',levels.map(v=>`<option value="${esc(v)}" ${currentFilters.level===v?'selected':''}>${esc(v)}</option>`).join(''))}
        ${selectFilter('filterEquipment','Matériel','equipment','all',equipments.map(v=>`<option value="${esc(v)}" ${currentFilters.equipment===v?'selected':''}>${esc(v)}</option>`).join(''))}
        ${selectFilter('filterType','Type','type','all',types.map(v=>`<option value="${esc(v)}" ${currentFilters.type===v?'selected':''}>${esc(v)}</option>`).join(''))}
      </div>
      <button id="clearFilters" class="text-btn">Réinitialiser les filtres</button>
    </section>
    ${selectionDock()}
    <section class="exercise-grid" id="exerciseGrid">${list.length?list.map(e=>exerciseCard(e)).join(''):'<div class="empty wide">Aucun exercice ne correspond à ces filtres.</div>'}</section>`;
}
function selectFilter(id,label,key,all,opt){return `<label><span>${label}</span><select id="${id}" data-filter="${key}"><option value="${all}">Tous</option>${opt}</select></label>`;}
function filteredExercises(){return allExercises().filter(e=>{
  const q=currentFilters.query.trim().toLowerCase();
  return (!q||e.name.toLowerCase().includes(q)||e.groupName.toLowerCase().includes(q)) &&
    (currentFilters.group==='all'||e.group===currentFilters.group) &&
    (currentFilters.level==='all'||e.level===currentFilters.level) &&
    (currentFilters.equipment==='all'||e.equipment===currentFilters.equipment) &&
    (currentFilters.type==='all'||e.type===currentFilters.type);
});}
function groupHTML(group){
  const g=groups[group]; if(!g)return notFoundHTML();
  return `${pageTitle(g.name.toUpperCase(),g.subtitle)}<div class="group-toolbar"><a class="secondary" href="#/explore">${icon('filter')} Filtrer dans Explorer</a><a class="secondary" href="#/muscles">Carte musculaire</a></div>${selectionDock()}<section class="exercise-grid">${g.exercises.map(e=>exerciseCard({...e,group,groupName:g.name})).join('')}</section>`;
}
function exerciseCard(e){
  const key=`${e.group}/${e.slug}`,selected=data.selection.some(x=>x.key===key),fav=data.favorites.includes(key);
  return `<article class="exercise-card clickable">
    <a class="card-stretched-link" href="#/exercise/${key}" aria-label="Ouvrir ${esc(e.name)}"></a>
    <img class="exercise-thumb" src="${thumbFor(e)}" alt="${esc(e.name)}" loading="lazy">
    <div class="exercise-card-body"><div class="exercise-card-top"><div><span class="eyebrow">${esc(e.groupName)}</span><h3>${esc(e.name)}</h3></div><button class="icon-action favorite-btn ${fav?'selected':''}" data-favorite="${key}" aria-label="${fav?'Retirer des favoris':'Ajouter aux favoris'}">${icon('star')}</button></div>
    <div class="badges"><span>${esc(e.level)}</span><span>${esc(e.equipment)}</span><span>${esc(e.type)}</span>${hasVideo(e)?'<span>Vidéo</span>':'<span>Séquence illustrée</span>'}</div>
    <p>${esc(e.cue)}</p><div class="meta-line"><strong>${esc(e.sets)}</strong><span>${esc(e.reps)}</span></div>
    <div class="card-actions"><button class="select-btn ${selected?'selected':''}" data-select="${key}">${selected?icon('check')+ ' Sélectionné':icon('plus')+' Ajouter'}</button><a class="mini-btn" href="#/exercise/${key}">Voir l’exercice ${icon('chevron')}</a></div>
    </div></article>`;
}
function selectionDock(){
  if(!data.selection.length)return `<div class="selection-dock muted"><span>Aucun exercice sélectionné.</span><a href="#/builder">Créer une séance</a></div>`;
  return `<div class="selection-dock"><strong>${data.selection.length} exercice${data.selection.length>1?'s':''} sélectionné${data.selection.length>1?'s':''}</strong><a class="primary compact" href="#/builder">Organiser et lancer</a></div>`;
}

function exerciseHTML(group,slug){
  const e=findExercise(group,slug); if(!e)return notFoundHTML();
  const key=`${group}/${slug}`,fav=data.favorites.includes(key),selected=data.selection.some(x=>x.key===key);
  const media=hasVideo(e)?`<video class="exercise-video" src="${videoFor(e)}" poster="${heroFor(e)}" autoplay loop muted playsinline controls preload="metadata"></video>`:`<div class="phase-images">${[1,2,3].map(n=>`<img src="${imgFor(e,n)}" alt="${esc(e.name)} — étape ${n}">`).join('')}</div>`;
  return `<a class="back-link" href="#/group/${group}">${icon('back')} ${esc(groups[group].name)}</a>
  <section class="exercise-detail"><div class="exercise-media">${media}</div><div class="exercise-overview"><span class="eyebrow">${esc(groups[group].name)}</span><h1>${esc(e.name)}</h1><p class="lead">${esc(e.cue)}</p><div class="badges"><span>${esc(e.level)}</span><span>${esc(e.equipment)}</span><span>${esc(e.type)}</span></div>${e.hybridHold?`<div class="target-box"><div><small>Répétitions</small><strong>${e.repsPerSet}</strong></div><div><small>Maintien / répétition</small><strong>${e.holdPerRepSeconds} sec</strong></div></div>`:`<div class="target-box"><div><small>Séries</small><strong>${esc(e.sets)}</strong></div><div><small>Objectif</small><strong>${esc(e.reps)}</strong></div></div>`}<div class="detail-actions"><button class="secondary" data-favorite="${key}">${fav?icon('star')+' Favori':icon('star')+' Ajouter aux favoris'}</button><button class="secondary" data-select="${key}">${selected?icon('check')+' Sélectionné':icon('plus')+' Ajouter à ma séance'}</button><button class="secondary" id="shareExercise">${icon('share')} Partager</button></div></div></section>
  <section class="lesson-grid">
    ${lesson('Comment faire',e.technique)}${lesson('Respiration',e.breathing)}${lessonList('Points de contrôle',e.controls)}${lessonList('Erreurs fréquentes',e.errors)}${lesson('Plus facile',e.easier)}${lesson('Plus difficile',e.harder)}${lesson('Muscles principaux',e.primary.join(' · ')||groups[group].name)}${lesson('Muscles secondaires',e.secondary.join(' · ')||'Stabilisateurs du tronc')}
  </section>
  <div class="safety-note"><strong>Prudence.</strong> Ces démonstrations sont sportives et ne remplacent pas un avis médical. En cas de douleur aiguë, irradiation, engourdissement ou faiblesse inhabituelle, interrompez l’exercice et demandez un avis professionnel.</div>`;
}
function lesson(title,text){return `<article class="lesson"><h2>${esc(title)}</h2><p>${esc(text)}</p></article>`;}
function lessonList(title,items){return `<article class="lesson"><h2>${esc(title)}</h2><ul>${items.map(x=>`<li>${esc(x)}</li>`).join('')}</ul></article>`;}

function trainHTML(){
  const active=loadActiveSession();
  return `${pageTitle('S’ENTRAÎNER','Choisissez un programme structuré ou composez votre propre séance.')}
    ${active?`<section class="resume-card"><div><strong>Une séance est en cours</strong><p>${esc(active.name||'Séance personnalisée')} — exercice ${active.exerciseIndex+1}/${active.items.length}</p></div><a class="primary compact" href="#/session">Reprendre</a></section>`:''}
    <section class="program-grid">${Object.entries(programs).map(([slug,p])=>programCard(slug,p)).join('')}</section>
    <section class="builder-callout"><div><span class="eyebrow">SUR MESURE</span><h2>Créer ma séance</h2><p>Sélectionnez plusieurs exercices, changez leur ordre, leurs séries et leurs récupérations.</p></div><a class="primary" href="#/explore">Choisir mes exercices</a>${data.selection.length?`<a class="secondary" href="#/builder">Modifier ma sélection (${data.selection.length})</a>`:''}</section>`;
}
function programCard(slug,p){return `<article class="program-card"><div class="program-visual"><span>${p.minutes}</span><small>MIN</small></div><div><span class="eyebrow">${esc(p.level)}</span><h2>${esc(p.name)}</h2><p>${esc(p.desc)}</p><div class="meta-line"><span>${p.items.length} exercices</span><span>≈ ${p.minutes} min</span></div><a class="mini-btn" href="#/program/${slug}">Voir le programme ${icon('chevron')}</a></div></article>`;}
function programHTML(slug){
  const p=programs[slug]; if(!p)return notFoundHTML();
  const offline=data.offlinePrograms.includes(slug);
  return `<a class="back-link" href="#/train">${icon('back')} S’entraîner</a>${pageTitle(p.name,p.desc)}
    <div class="program-summary"><div><small>Durée estimée</small><strong>${p.minutes} min</strong></div><div><small>Niveau</small><strong>${esc(p.level)}</strong></div><div><small>Exercices</small><strong>${p.items.length}</strong></div></div>
    <ol class="program-list">${p.items.map((it,i)=>programItemHTML(it,i)).join('')}</ol>
    <div class="program-actions"><button class="primary" data-start-program="${slug}">${icon('play')} Démarrer le programme</button><button class="secondary" data-offline-program="${slug}" data-offline-state="${offline?'pending':'none'}">${offline?icon('offline')+' Vérification du hors connexion…':icon('offline')+' Télécharger pour hors connexion'}</button></div>`;
}
function programItemHTML(raw,i){const it=sanitizeSessionItem(raw),e=findExercise(it.group,it.slug);return `<li><span class="program-num">${i+1}</span><img src="${thumbFor(e)}" alt=""><div><strong>${esc(e.name)}</strong><small>${it.sets} série${it.sets>1?'s':''} · ${esc(itemGoalText(it,e))} · repos ${it.restSec} sec</small></div><a href="#/exercise/${it.group}/${it.slug}" aria-label="Voir ${esc(e.name)}">${icon('chevron')}</a></li>`;}

function builderHTML(){
  const items=data.selection.map(sanitizeSelectionItem).filter(Boolean);
  if(items.length!==data.selection.length||JSON.stringify(items)!==JSON.stringify(data.selection)){data.selection=items;saveData();}
  return `<a class="back-link" href="#/explore">${icon('back')} Explorer</a>${pageTitle('MA SÉANCE',items.length?'Réorganisez les exercices avant de démarrer.':'Ajoutez d’abord plusieurs exercices depuis Explorer.')}
  ${items.length?`<div class="builder-list">${items.map((it,i)=>builderItem(it,i)).join('')}</div><div class="builder-actions"><button class="text-btn danger" id="clearSelection">Vider la sélection</button><button class="primary" id="startCustom">${icon('play')} Lancer ma sélection</button></div>`:'<div class="empty"><p>Aucun exercice sélectionné.</p><a class="primary" href="#/explore">Explorer les exercices</a></div>'}`;
}
function builderItem(raw,i){
  const it=sanitizeSelectionItem(raw);if(!it)return'';
  const [group,slug]=it.key.split('/'),e=findExercise(group,slug);
  const mode=it.mode||exerciseMode(e);
  const goalFields=mode==='hybrid'
    ?`<label>Répétitions<input type="number" min="1" max="999" step="1" value="${it.repsTarget}" data-build-field="repsTarget"></label><label>Maintien / rep<input type="number" min="1" max="3600" step="1" value="${it.holdPerRepSeconds}" data-build-field="holdPerRepSeconds"><span>sec</span></label>`
    :mode==='duration'
      ?`<label>Durée<input type="number" min="5" max="180" step="5" value="${it.durationSec}" data-build-field="durationSec"><span>sec${it.perSide?' / côté':''}</span></label>`
      :`<label>Objectif<input type="text" maxlength="80" value="${esc(it.target||e.reps)}" data-build-field="target"></label>`;
  return `<article class="builder-item" data-builder-key="${it.key}"><span class="builder-order">${i+1}</span><img src="${thumbFor(e)}" alt=""><div class="builder-main"><strong>${esc(e.name)}</strong><small>${esc(groups[group].name)} · ${esc(e.level)}</small><div class="builder-fields"><label>Séries<input type="number" min="1" max="8" value="${it.sets}" data-build-field="sets"></label><label>Repos<input type="number" min="10" max="180" step="5" value="${it.restSec}" data-build-field="restSec"><span>sec</span></label>${goalFields}</div></div><div class="builder-controls"><button data-move="up" aria-label="Monter">${icon('up')}</button><button data-move="down" aria-label="Descendre">${icon('down')}</button><button data-remove="${it.key}" aria-label="Supprimer">${icon('trash')}</button></div></article>`;
}

function sessionHTML(){
  const s=loadActiveSession(); if(!s)return `<div class="empty"><h1>Aucune séance en cours</h1><p>Choisissez un programme ou créez une séance.</p><a class="primary" href="#/train">S’entraîner</a></div>`;
  if(s.phase==='rest') return restHTML(s);
  const it=s.items[s.exerciseIndex],e=findExercise(it.group,it.slug),rec=currentSeriesRecord(s);
  if(!e||!rec)return `<div class="empty">Exercice introuvable.</div>`;
  const mode=it.mode||exerciseMode(e);
  const media=hasVideo(e)?`<video class="session-video" src="${videoFor(e)}" autoplay loop muted playsinline preload="metadata"></video>`:`<div class="phase-images session-fallback">${[1,2,3].map(n=>`<img src="${imgFor(e,n)}" alt="Étape ${n}">`).join('')}</div>`;
  const seriesNav=`<div class="series-pills" aria-label="Séries de cet exercice">${s.results[s.exerciseIndex].series.map((sr,i)=>`<button type="button" data-series-index="${i}" class="${i===s.setIndex?'current':''} ${sr.completed?'done':''}" aria-label="Série ${i+1}${sr.completed?', enregistrée':''}">${sr.completed?icon('check'):''}${i+1}</button>`).join('')}</div>`;
  const remaining=workRemainingNow(s);
  const timerBlock=mode==='duration'?`<div class="exercise-timer"><span id="workSideLabel">Minuteur de travail${it.perSide?` — côté ${s.workSide||1}/2`:''}</span><strong id="workTimer">${fmtTime(remaining)}</strong><button class="secondary compact" id="toggleWorkTimer">${s.workRunning?icon('pause')+' Pause':icon('play')+' Démarrer'}</button><small class="timer-note">Le chrono se met automatiquement en pause si vous quittez le lecteur.</small></div>`:'';
  const elapsedTimed=mode==='duration'?Math.max(0,it.durationSec-remaining):0;
  let entry='';
  if(mode==='duration'){
    const entered=rec.enteredSeconds??(rec.completed?(rec.seconds/(it.perSide?2:1)):(elapsedTimed>0?elapsedTimed:it.durationSec));
    entry=`<label class="session-entry"><span>Durée réellement effectuée${it.perSide?' par côté':''}</span><div><input id="actualSeconds" type="number" min="${LIMITS.actualSeconds.min}" max="${LIMITS.actualSeconds.max}" step="1" value="${Math.round(entered)}"><b>sec</b></div><small>${it.perSide?'Saisissez la durée réalisée pour un côté ; le volume total comptabilisera les deux côtés.':'Confirmez ou corrigez la durée avant d’enregistrer la série.'}</small></label>`;
  }else if(mode==='hybrid'){
    const reps=rec.enteredReps??it.repsTarget,hold=rec.enteredSeconds??it.holdPerRepSeconds;
    entry=`<div class="hybrid-entry"><label class="session-entry"><span>Répétitions réellement effectuées${it.perSide?' par côté':''}</span><div><input id="actualReps" type="number" min="${LIMITS.actualReps.min}" max="${LIMITS.actualReps.max}" step="1" value="${reps}"><b>reps</b></div></label><label class="session-entry"><span>Maintien moyen réellement effectué par répétition</span><div><input id="actualHoldSeconds" type="number" min="${LIMITS.actualSeconds.min}" max="${LIMITS.actualSeconds.max}" step="1" value="${hold}"><b>sec</b></div></label><small>Ce mouvement combine répétitions et maintien. Les deux valeurs sont enregistrées séparément ; aucune prescription supplémentaire n’est inventée par l’application.</small></div>`;
  }else{
    const reps=rec.enteredReps??suggestedReps(it.target);
    entry=`<label class="session-entry"><span>Répétitions réellement effectuées${it.perSide?' par côté':''}</span><div><input id="actualReps" type="number" min="${LIMITS.actualReps.min}" max="${LIMITS.actualReps.max}" step="1" value="${reps}"><b>rep${reps>1?'s':''}</b></div><small>La valeur est modifiable : les statistiques utilisent ce nombre confirmé, pas une estimation automatique.${it.perSide?' Elle est saisie par côté et totalisée sur les deux côtés.':''}</small></label>`;
  }
  return `<section class="session-head"><span>Exercice ${s.exerciseIndex+1} / ${s.items.length}</span><span>Série ${s.setIndex+1} / ${it.sets}</span></section><section class="session-layout"><div class="session-media">${media}</div><div class="session-panel"><span class="eyebrow">${esc(s.name)}</span><h1>${esc(e.name)}</h1><p>${esc(e.cue)}</p><div class="session-target"><small>Objectif prévu</small><strong>${esc(itemGoalText(it,e))}</strong></div>${seriesNav}${timerBlock}${entry}${rec.completed?'<p class="series-status">Cette série est déjà enregistrée. Vous pouvez corriger sa valeur sans la compter une seconde fois.</p>':''}<div class="session-controls"><button class="primary" id="completeSet">${icon('check')} ${rec.completed?'Mettre à jour la série':'Série terminée'}</button><button class="secondary" id="prevExercise">${icon('back')} Exercice précédent</button><button class="secondary" id="nextExercise">Exercice suivant ${icon('skip')}</button><button class="text-btn danger" id="abandonSession">Abandonner la séance</button></div></div></section>`;
}
function restHTML(s){
  const next=s.items[s.exerciseIndex],e=findExercise(next.group,next.slug),rem=restRemainingNow(s),total=Math.max(1,Number(s.restTotalSec)||1),pct=Math.max(0,Math.min(100,(rem/total)*100));
  return `<section class="rest-screen"><span class="eyebrow">RÉCUPÉRATION</span><h1 id="restTimer">${fmtTime(rem)}</h1><p>Prochaine étape : <strong>${esc(e.name)}</strong> — série ${s.setIndex+1}/${next.sets}</p><div class="rest-ring" aria-label="Progression de la récupération"><span style="width:${pct}%"></span></div><div class="rest-actions"><button class="secondary" id="toggleRest">${s.restPaused?icon('play')+' Reprendre':icon('pause')+' Pause'}</button><button class="primary" id="skipRest">Écourter la récupération</button></div></section>`;
}
function summaryHTML(id){
  let wanted='';try{wanted=decodeURIComponent(String(id||''));}catch{wanted=String(id||'');}
  const s=data.sessions.find(x=>x.id===wanted); if(!s)return `<div class="empty"><h1>Bilan introuvable</h1><p>La séance demandée n’existe pas ou n’est plus disponible.</p><a class="primary" href="#/progress">Retour aux progrès</a></div>`;
  return `${pageTitle(s.status==='completed'?'SÉANCE TERMINÉE':'SÉANCE INTERROMPUE',s.name)}<section class="summary-grid"><div><small>Durée</small><strong>${fmtTime(s.durationSec)}</strong></div><div><small>Exercices</small><strong>${s.exerciseCount}</strong></div><div><small>Séries</small><strong>${s.totalSets}</strong></div><div><small>Répétitions enregistrées</small><strong>${s.totalReps}</strong></div></section><section class="soft-card"><h2>Détail</h2>${s.results.map(r=>`<div class="summary-row"><span>${esc(r.name)}</span><strong>${r.setsDone} série${r.setsDone>1?'s':''}${r.reps?` · ${r.reps} reps`:''}${r.seconds?` · ${r.seconds} sec`:''}</strong></div>`).join('')}</section><div class="summary-actions"><a class="primary" href="#/progress">Voir mes progrès</a><a class="secondary" href="#/train">Nouvelle séance</a></div>`;
}

function progressHTML(){
  const done=data.sessions.filter(s=>s.status==='completed');
  const history=data.sessions.slice().reverse().slice(0,20);
  const totalSec=done.reduce((a,s)=>a+(s.durationSec||0),0),sets=done.reduce((a,s)=>a+(s.totalSets||0),0),reps=done.reduce((a,s)=>a+(s.totalReps||0),0);
  const practiced=new Set(done.flatMap(s=>s.results?.map(r=>r.key)||[])).size;
  const days=activity7Days(done);
  return `${pageTitle('PROGRÈS','La progression mesure votre activité réelle, pas le simple fait d’avoir ouvert des exercices.')}
  <section class="metric-grid"><div><small>Séances terminées</small><strong>${done.length}</strong></div><div><small>Temps total</small><strong>${Math.round(totalSec/60)} min</strong></div><div><small>Séries</small><strong>${sets}</strong></div><div><small>Répétitions enregistrées</small><strong>${reps}</strong></div><div><small>Exercices pratiqués</small><strong>${practiced}</strong></div><div><small>7 derniers jours</small><strong>${days.filter(d=>d.count>0).length} jour${days.filter(d=>d.count>0).length>1?'s':''}</strong></div></section>
  <section class="soft-card activity-card"><h2>Activité des 7 derniers jours</h2><div class="activity-bars">${days.map(d=>`<div><span style="height:${Math.max(6,Math.min(100,d.minutes*4))}%" title="${d.minutes} min"></span><small>${d.label}</small></div>`).join('')}</div></section>
  <section class="soft-card"><h2>Historique</h2>${history.length?history.map(s=>`<a class="history-row" href="#/summary/${encodeURIComponent(s.id)}"><div><strong>${esc(s.name)}</strong><small>${new Date(s.endedAt).toLocaleString('fr-FR')} · ${fmtTime(s.durationSec)}</small></div><span class="history-tail">${s.status==='abandoned'?'<b class="history-badge">Interrompue</b>':''}${icon('chevron')}</span></a>`).join(''):'<p>Aucune séance enregistrée. Votre première séance apparaîtra ici.</p>'}</section>`;
}
function activity7Days(sessions){const out=[];for(let i=6;i>=0;i--){const d=new Date();d.setHours(0,0,0,0);d.setDate(d.getDate()-i);const next=new Date(d);next.setDate(d.getDate()+1);const arr=sessions.filter(s=>new Date(s.endedAt)>=d&&new Date(s.endedAt)<next);out.push({label:d.toLocaleDateString('fr-FR',{weekday:'short'}).slice(0,2),minutes:Math.round(arr.reduce((a,s)=>a+(s.durationSec||0),0)/60),count:arr.length});}return out;}

function profileHTML(){
  return `${pageTitle('PROFIL','Vos données restent locales sur cet appareil. Aucun compte n’est nécessaire.')}
  <section class="profile-grid"><article class="soft-card"><h2>Données locales</h2><p>${data.sessions.length} séance${data.sessions.length>1?'s':''} enregistrée${data.sessions.length>1?'s':''}, ${data.favorites.length} favori${data.favorites.length>1?'s':''}.</p><div class="stack-actions"><button class="secondary" id="exportData">Exporter mes données</button><label class="secondary file-label">Importer mes données<input id="importData" type="file" accept="application/json"></label></div></article>
  <article class="soft-card"><h2>Hors connexion</h2><p>Le cœur de l’application est disponible hors connexion après la première visite. Les vidéos ne sont garanties hors ligne que pour les programmes explicitement téléchargés.</p><p><strong id="offlineProgramCount">Vérification…</strong><span id="offlineProgramLabel"></span></p></article>
  <article class="soft-card"><h2>Confidentialité</h2><p>Vos données d’entraînement restent sur cet appareil et ne sont pas transmises à Delta-Sierra.</p></article>
  <article class="soft-card"><h2>Distribution</h2><p><strong>Distribution gratuite.</strong> Cette application est partagée gratuitement, dans un esprit de partage, depuis <a href="${SITE_URL}" target="_blank" rel="noopener">https://www.delta-sierra.com</a>.</p></article></section>
  ${distributionNotice()}
  <a class="support-card" href="${SUPPORT_URL}" target="_blank" rel="noopener">${icon('heart')}<span><strong>REMERCIEMENT</strong><small>Cette application est gratuite et le restera. Soutien libre, sans fonction bloquée.</small></span>${icon('chevron')}</a>`;
}
function notFoundHTML(){return `<div class="empty"><h1>Page introuvable</h1><a class="primary" href="#/home">Retour à l’accueil</a></div>`;}

function bindViewEvents(route){
  const v=document.getElementById('view');
  v.querySelectorAll('[data-favorite]').forEach(btn=>btn.addEventListener('click',ev=>{ev.stopPropagation();toggleFavorite(btn.dataset.favorite);}));
  v.querySelectorAll('[data-select]').forEach(btn=>btn.addEventListener('click',ev=>{ev.stopPropagation();toggleSelection(btn.dataset.select);}));
  if(route.name==='explore') bindFilters();
  if(route.name==='exercise') document.getElementById('shareExercise')?.addEventListener('click',shareCurrent);
  v.querySelectorAll('[data-start-program]').forEach(b=>b.addEventListener('click',()=>startProgram(b.dataset.startProgram)));
  v.querySelectorAll('[data-offline-program]').forEach(b=>b.addEventListener('click',()=>downloadProgram(b.dataset.offlineProgram,b)));
  if(route.name==='program') verifyProgramOffline(route.parts[1],false);
  if(route.name==='builder') bindBuilder();
  if(route.name==='session') bindSessionControls();
  if(route.name==='profile'){bindProfile();verifyProfileOfflinePrograms();}
}
function bindFilters(){
  const q=document.getElementById('filterQuery'); q?.addEventListener('input',()=>{currentFilters.query=q.value;render();queueMicrotask(()=>{const nq=document.getElementById('filterQuery');nq?.focus();nq?.setSelectionRange(nq.value.length,nq.value.length);});});
  document.querySelectorAll('[data-filter]').forEach(s=>s.addEventListener('change',()=>{currentFilters[s.dataset.filter]=s.value;render();}));
  document.getElementById('clearFilters')?.addEventListener('click',()=>{currentFilters={query:'',group:'all',level:'all',equipment:'all',type:'all'};render();});
}
function toggleFavorite(key){data.favorites=data.favorites.includes(key)?data.favorites.filter(x=>x!==key):[...data.favorites,key];saveData();render();toast(data.favorites.includes(key)?'Ajouté aux favoris.':'Retiré des favoris.');}
function toggleSelection(key){
  const i=data.selection.findIndex(x=>x.key===key);if(i>=0)data.selection.splice(i,1);else{const [g,s]=key.split('/'),e=findExercise(g,s);data.selection.push(sanitizeSelectionItem({key,sets:2,restSec:45,target:e.reps,durationSec:exerciseMode(e)==='duration'?parseDuration(e.reps):0,repsTarget:e.repsPerSet||0,holdPerRepSeconds:e.holdPerRepSeconds||0}));}saveData();render();
}
function parseDuration(s){const m=String(s).match(/(\d+)/);return m?Number(m[1]):20;}
function shareCurrent(){const url=location.href;if(navigator.share)navigator.share({title:document.title,url}).catch(()=>{});else navigator.clipboard?.writeText(url).then(()=>toast('Lien copié.'));}

function bindBuilder(){
  document.querySelectorAll('.builder-item').forEach(row=>{
    row.querySelectorAll('[data-build-field]').forEach(inp=>inp.addEventListener('change',()=>{
      const it=data.selection.find(x=>x.key===row.dataset.builderKey);if(!it)return;
      const ref=validExerciseKey(it.key);if(!ref)return;
      const k=inp.dataset.buildField;
      if(k==='sets')it.sets=clampInt(inp.value,1,8,2);
      else if(k==='restSec')it.restSec=clampInt(inp.value,10,180,45);
      else if(k==='durationSec')it.durationSec=exerciseMode(ref.e)==='duration'?clampInt(inp.value,LIMITS.durationSec.min,LIMITS.durationSec.max,parseDuration(ref.e.reps)):0;
      else if(k==='repsTarget')it.repsTarget=exerciseMode(ref.e)==='hybrid'?clampInt(inp.value,LIMITS.hybridReps.min,LIMITS.hybridReps.max,ref.e.repsPerSet||5):0;
      else if(k==='holdPerRepSeconds')it.holdPerRepSeconds=exerciseMode(ref.e)==='hybrid'?clampInt(inp.value,LIMITS.holdPerRepSeconds.min,LIMITS.holdPerRepSeconds.max,ref.e.holdPerRepSeconds||10):0;
      else if(k==='target')it.target=exerciseMode(ref.e)==='reps'?String(inp.value||ref.e.reps).slice(0,80):null;
      const safe=sanitizeSelectionItem(it);if(!safe)return;
      Object.assign(it,safe);inp.value=String(it[k]??'');saveData();
    }));
    row.querySelector('[data-move="up"]')?.addEventListener('click',()=>moveSelection(row.dataset.builderKey,-1));
    row.querySelector('[data-move="down"]')?.addEventListener('click',()=>moveSelection(row.dataset.builderKey,1));
    row.querySelector('[data-remove]')?.addEventListener('click',()=>{data.selection=data.selection.filter(x=>x.key!==row.dataset.builderKey);saveData();render();});
  });
  document.getElementById('clearSelection')?.addEventListener('click',()=>{data.selection=[];saveData();render();});
  document.getElementById('startCustom')?.addEventListener('click',startCustomSession);
}
function moveSelection(key,delta){const i=data.selection.findIndex(x=>x.key===key),j=i+delta;if(i<0||j<0||j>=data.selection.length)return;[data.selection[i],data.selection[j]]=[data.selection[j],data.selection[i]];saveData();render();}

function startCustomSession(){
  if(!data.selection.length)return;
  const safe=data.selection.map(sanitizeSelectionItem).filter(Boolean);
  if(!safe.length){toast('La sélection ne contient aucun exercice valide.');return;}
  data.selection=safe;saveData();
  const items=safe.map(it=>sanitizeSessionItem({...it,group:it.key.split('/')[0],slug:it.key.split('/')[1]})).filter(Boolean);
  if(!items.length){toast('Impossible de créer cette séance. Vérifiez les paramètres.');return;}
  requestStartSession({name:'Séance personnalisée',source:'custom',items});
}
function startProgram(slug){const p=programs[slug];if(!p)return;requestStartSession({name:p.name,source:`program:${slug}`,items:p.items.map(x=>({...x}))});}
function requestStartSession(spec){
  const active=loadActiveSession();
  if(active){
    pendingSessionStart=spec;
    const d=document.getElementById('sessionConflictDialog');
    const txt=document.getElementById('sessionConflictText');
    if(txt)txt.textContent=`« ${active.name} » est déjà en cours. Que souhaitez-vous faire ?`;
    d?.showModal();
    return;
  }
  actuallyStartSession(spec);
}
function buildSession({name,source,items}){
  const safeItems=(Array.isArray(items)?items:[]).slice(0,41).map(sanitizeSessionItem).filter(Boolean);
  if(!safeItems.length)return null;
  items=safeItems;
  const now=Date.now();
  return normalizeSession({
    id:`s-${now}`,name,source,items,exerciseIndex:0,setIndex:0,phase:'work',
    startedAt:new Date(now).toISOString(),activeElapsedMs:0,activeSince:now,sessionPaused:false,
    workRemaining:(items[0]?.mode==='duration'?items[0]?.durationSec:0)||0,workEndsAt:null,workRunning:false,workSide:1,
    restRemaining:0,restTotalSec:0,restEndsAt:null,restPaused:false,
    results:items.map(it=>{
      const key=`${it.group}/${it.slug}`,name=findExercise(it.group,it.slug)?.name||it.slug;
      return syncResultTotals({key,name,series:Array.from({length:it.sets},(_,i)=>makeSeriesRecord(key,i))});
    })
  });
}
function actuallyStartSession(spec){const s=buildSession(spec);if(!s){toast('Impossible de démarrer cette séance : paramètres invalides.');return;}saveActiveSession(s);setHash('/session');}
function abandonActiveForReplacement(){
  const active=loadActiveSession(),spec=pendingSessionStart;pendingSessionStart=null;
  if(active) finalizeSessionRecord('abandoned',active);
  document.getElementById('sessionConflictDialog')?.close();
  if(spec) actuallyStartSession(spec);
}
function replaceActiveSession(){
  const spec=pendingSessionStart;pendingSessionStart=null;
  saveActiveSession(null);clearSessionTimers();
  document.getElementById('sessionConflictDialog')?.close();
  if(spec) actuallyStartSession(spec);
}
function resumeExistingSession(){
  pendingSessionStart=null;document.getElementById('sessionConflictDialog')?.close();setHash('/session');
}
function currentSeriesRecord(s){return s?.results?.[s.exerciseIndex]?.series?.[s.setIndex]||null;}
function firstIncompleteSetIndex(s,exerciseIndex){
  const arr=s.results?.[exerciseIndex]?.series||[];
  const idx=arr.findIndex(x=>!x.completed);
  return idx>=0?idx:Math.max(0,arr.length-1);
}
function suggestedReps(target){
  const nums=(String(target||'').match(/\d+/g)||[]).map(Number);
  if(!nums.length)return 0;
  return nums.length>=2?Math.round((nums[0]+nums[1])/2):nums[0];
}
function targetTextAlreadyMentionsSide(target){return /c[oô]t[ée]/i.test(String(target||''));} // display formatting only.
function itemGoalText(it,e=findExercise(it.group,it.slug)){
  const mode=it.mode||exerciseMode(e);
  if(mode==='hybrid')return `${it.repsTarget} répétition${it.repsTarget>1?'s':''} × ${it.holdPerRepSeconds} sec de maintien par répétition${it.perSide?' et par côté':''}`;
  if(mode==='duration')return `${it.durationSec} seconde${it.durationSec>1?'s':''}${it.perSide?' par côté':''}`;
  const base=String(it.target||e?.reps||'').trim();
  if(it.perSide&&!targetTextAlreadyMentionsSide(base))return `${base} par côté`;
  return base;
}
function workRemainingNow(s){
  if(!s?.workRunning||!s.workEndsAt)return Math.max(0,Number(s?.workRemaining)||0);
  return Math.max(0,Math.ceil((Number(s.workEndsAt)-Date.now())/1000));
}
function restRemainingNow(s){
  if(!s||s.restPaused||!s.restEndsAt)return Math.max(0,Number(s?.restRemaining)||0);
  return Math.max(0,Math.ceil((Number(s.restEndsAt)-Date.now())/1000));
}
function recordSeries(s,value,holdValue=null){
  const it=s.items[s.exerciseIndex],res=s.results[s.exerciseIndex],rec=res.series[s.setIndex];
  if(!rec)return false;
  const mode=it.mode||exerciseMode(findExercise(it.group,it.slug)),sideFactor=it.perSide?2:1;
  if(mode==='duration'){
    const v=validatePerformanceValue('seconds',value);if(!v.ok)return false;
    rec.enteredSeconds=v.value;rec.seconds=v.value*sideFactor;rec.reps=0;rec.enteredReps=null;
  }else if(mode==='hybrid'){
    const vr=validatePerformanceValue('reps',value),vs=validatePerformanceValue('seconds',holdValue);if(!vr.ok||!vs.ok)return false;
    rec.enteredReps=vr.value;rec.enteredSeconds=vs.value;
    rec.reps=vr.value*sideFactor;rec.seconds=vr.value*vs.value*sideFactor;
  }else{
    const v=validatePerformanceValue('reps',value);if(!v.ok)return false;
    rec.enteredReps=v.value;rec.enteredSeconds=null;rec.reps=v.value*sideFactor;rec.seconds=0;
  }
  if(!Number.isSafeInteger(rec.reps)||!Number.isSafeInteger(rec.seconds))return false;
  rec.completed=true;rec.completedAt=new Date().toISOString();
  syncResultTotals(res);
  return true;
}
function startRest(s,seconds){
  const sec=Math.max(0,Number(seconds)||0);
  if(sec<=0){s.phase='work';s.restRemaining=0;s.restTotalSec=0;s.restEndsAt=null;s.restPaused=false;return;}
  s.phase='rest';s.restTotalSec=sec;s.restRemaining=sec;s.restEndsAt=Date.now()+sec*1000;s.restPaused=false;
}
function resetWorkState(s){
  const it=s.items[s.exerciseIndex];
  s.workRemaining=it?.mode==='duration'?(it.durationSec||0):0;s.workEndsAt=null;s.workRunning=false;s.workSide=1;
}
function bindSessionControls(){
  document.getElementById('completeSet')?.addEventListener('click',completeCurrentSet);
  document.getElementById('prevExercise')?.addEventListener('click',()=>jumpExercise(-1));
  document.getElementById('nextExercise')?.addEventListener('click',()=>jumpExercise(1));
  document.getElementById('abandonSession')?.addEventListener('click',abandonSession);
  document.getElementById('skipRest')?.addEventListener('click',finishRest);
  document.getElementById('toggleRest')?.addEventListener('click',toggleRest);
  document.getElementById('toggleWorkTimer')?.addEventListener('click',toggleWorkTimer);
  document.querySelectorAll('[data-series-index]').forEach(btn=>btn.addEventListener('click',()=>{
    const s=loadActiveSession();if(!s)return;
    s.setIndex=Math.max(0,Math.min(s.items[s.exerciseIndex].sets-1,Number(btn.dataset.seriesIndex)||0));
    s.phase='work';s.restEndsAt=null;s.restPaused=false;resetWorkState(s);saveActiveSession(s);render();
  }));
}
function completeCurrentSet(){
  const s=loadActiveSession();if(!s)return;
  const it=s.items[s.exerciseIndex],rec=currentSeriesRecord(s);if(!it||!rec)return;
  const mode=it.mode||exerciseMode(findExercise(it.group,it.slug)),wasCompleted=!!rec.completed;
  let ok=false,focusEl=null;
  if(mode==='duration'){
    const input=document.getElementById('actualSeconds');focusEl=input;
    if(!input||!validatePerformanceValue('seconds',input.value).ok){toast('La durée doit être un nombre entier compris entre 0 et 3 600 secondes.');input?.focus();return;}
    ok=recordSeries(s,input.value);
  }else if(mode==='hybrid'){
    const reps=document.getElementById('actualReps'),hold=document.getElementById('actualHoldSeconds');focusEl=reps;
    if(!reps||!validatePerformanceValue('reps',reps.value).ok){toast('Les répétitions doivent être un entier compris entre 0 et 999.');reps?.focus();return;}
    if(!hold||!validatePerformanceValue('seconds',hold.value).ok){toast('Le maintien doit être un entier compris entre 0 et 3 600 secondes.');hold?.focus();return;}
    ok=recordSeries(s,reps.value,hold.value);
  }else{
    const input=document.getElementById('actualReps');focusEl=input;
    if(!input||!validatePerformanceValue('reps',input.value).ok){toast('Les répétitions doivent être un entier compris entre 0 et 999.');input?.focus();return;}
    ok=recordSeries(s,input.value);
  }
  if(!ok){toast('Valeur invalide : la série n’a pas été enregistrée.');focusEl?.focus();return;}
  if(wasCompleted){saveActiveSession(s);toast('Série mise à jour sans double comptage.');render();return;}
  if(s.setIndex+1<it.sets){
    s.setIndex++;resetWorkState(s);startRest(s,it.restSec);
  }else if(s.exerciseIndex+1<s.items.length){
    s.exerciseIndex++;s.setIndex=firstIncompleteSetIndex(s,s.exerciseIndex);resetWorkState(s);startRest(s,it.restSec);
  }else{
    const pending=s.results.findIndex(r=>Array.isArray(r.series)&&r.series.some(sr=>!sr.completed));
    if(pending>=0){
      s.exerciseIndex=pending;s.setIndex=firstIncompleteSetIndex(s,pending);s.phase='work';s.restEndsAt=null;s.restPaused=false;resetWorkState(s);
      saveActiveSession(s);toast('Il reste des séries non enregistrées avant de terminer la séance.');render();return;
    }
    saveActiveSession(s);return finishSession('completed',s);
  }
  saveActiveSession(s);render();
}
function jumpExercise(delta){
  const s=loadActiveSession();if(!s)return;
  const n=Math.max(0,Math.min(s.items.length-1,s.exerciseIndex+delta));
  s.exerciseIndex=n;s.setIndex=firstIncompleteSetIndex(s,n);s.phase='work';s.restEndsAt=null;s.restPaused=false;resetWorkState(s);
  saveActiveSession(s);render();
}
function abandonSession(){if(!confirm('Abandonner cette séance ? Le bilan partiel sera conservé.'))return;const s=loadActiveSession();if(s)finishSession('abandoned',s);}
function activeDurationSec(s){
  let ms=Math.max(0,Number(s?.activeElapsedMs)||0);
  if(s?.activeSince&&!s.sessionPaused)ms+=Math.max(0,Date.now()-Number(s.activeSince));
  return Math.round(ms/1000);
}
function finalizeSessionRecord(status,s=loadActiveSession()){
  if(!s)return null;
  s=normalizeSession(s);
  const ended=new Date();
  s.results.forEach(syncResultTotals);
  const record={...s,status,endedAt:ended.toISOString(),durationSec:activeDurationSec(s),
    exerciseCount:s.results.filter(r=>r.setsDone>0).length,
    totalSets:s.results.reduce((a,r)=>a+r.setsDone,0),
    totalReps:s.results.reduce((a,r)=>a+r.reps,0),
    totalHoldSeconds:s.results.reduce((a,r)=>a+r.seconds,0)};
  delete record.phase;delete record.restRemaining;delete record.restEndsAt;delete record.workRemaining;delete record.workEndsAt;delete record.workSide;delete record.activeSince;delete record.runtimeId;
  data.sessions.push(record);saveData();saveActiveSession(null);clearSessionTimers();return record;
}
function finishSession(status,s=loadActiveSession()){
  const record=finalizeSessionRecord(status,s);if(record)setHash(`/summary/${record.id}`);
}
function toggleRest(){
  const s=loadActiveSession();if(!s)return;
  if(s.restPaused){s.restPaused=false;s.restEndsAt=Date.now()+Math.max(0,s.restRemaining)*1000;}
  else{s.restRemaining=restRemainingNow(s);s.restPaused=true;s.restEndsAt=null;}
  saveActiveSession(s);render();
}
function finishRest(){const s=loadActiveSession();if(!s)return;s.phase='work';s.restRemaining=0;s.restEndsAt=null;s.restPaused=false;saveActiveSession(s);render();}
function toggleWorkTimer(){
  const s=loadActiveSession();if(!s)return;
  const it=s.items[s.exerciseIndex];if(it?.mode!=='duration')return;
  if(s.workRunning){
    s.workRemaining=workRemainingNow(s);s.workRunning=false;s.workEndsAt=null;
  }else{
    const remaining=Math.max(0,Number(s.workRemaining??it.durationSec));
    s.workRemaining=remaining;s.workRunning=true;s.workEndsAt=Date.now()+remaining*1000;
  }
  saveActiveSession(s);render();
}
function updateRestUI(cur){
  const rem=restRemainingNow(cur),timer=document.getElementById('restTimer'),bar=document.querySelector('.rest-ring span');
  if(timer)timer.textContent=fmtTime(rem);
  if(bar){const total=Math.max(1,Number(cur.restTotalSec)||1);bar.style.width=`${Math.max(0,Math.min(100,(rem/total)*100))}%`;}
  if(rem<=0&&!cur.restPaused){cur.restRemaining=0;cur.restEndsAt=null;saveActiveSession(cur);finishRest();return true;}
  return false;
}
function updateWorkUI(cur){
  const rem=workRemainingNow(cur),el=document.getElementById('workTimer'),it=cur.items[cur.exerciseIndex];
  if(el)el.textContent=fmtTime(rem);
  const sideLabel=document.getElementById('workSideLabel');if(sideLabel&&it?.perSide)sideLabel.textContent=`Minuteur de travail — côté ${cur.workSide||1}/2`;
  if(rem<=0&&cur.workRunning){
    if(it?.mode==='duration'&&it.perSide&&(cur.workSide||1)===1){
      cur.workSide=2;cur.workRemaining=it.durationSec;cur.workEndsAt=Date.now()+it.durationSec*1000;cur.workRunning=true;saveActiveSession(cur);toast('Premier côté terminé. Changez de côté.');render();return true;
    }
    cur.workRunning=false;cur.workRemaining=0;cur.workEndsAt=null;saveActiveSession(cur);toast(it?.perSide?'Deux côtés terminés. Confirmez la durée réellement effectuée par côté.':'Temps atteint. Confirmez la durée réellement effectuée.');return true;
  }
  return false;
}
function resumeSessionUI(){
  const s=loadActiveSession();if(!s)return;
  if(s.phase==='rest'&&!s.restPaused){
    updateRestUI(s);
    sessionRuntime.tick=setInterval(()=>{const cur=loadActiveSession();if(!cur||cur.phase!=='rest'||cur.restPaused)return clearInterval(sessionRuntime.tick);updateRestUI(cur);},250);
  }
  if(s.phase==='work'&&s.workRunning&&s.items[s.exerciseIndex].mode==='duration'){
    updateWorkUI(s);
    sessionRuntime.timer=setInterval(()=>{const cur=loadActiveSession();if(!cur||cur.phase!=='work'||!cur.workRunning)return clearInterval(sessionRuntime.timer);updateWorkUI(cur);},250);
  }
}
function clearSessionTimers(){
  if(sessionRuntime.timer)clearInterval(sessionRuntime.timer);
  if(sessionRuntime.tick)clearInterval(sessionRuntime.tick);
  sessionRuntime={timer:null,tick:null};
}
function isSessionRouteActive(){return parseRoute().name==='session'&&!document.hidden;}
function pauseActiveClockForRoute(){
  const s=loadActiveSession();if(!s||s.sessionPaused)return;
  const now=Date.now();
  if(s.activeSince)s.activeElapsedMs=Math.max(0,Number(s.activeElapsedMs)||0)+Math.max(0,now-Number(s.activeSince));
  s.activeSince=null;saveActiveSession(s);
}
function pauseWorkTimerForRoute(){
  const s=loadActiveSession();if(!s||s.phase!=='work'||!s.workRunning)return;
  s.workRemaining=workRemainingNow(s);
  s.workRunning=false;
  s.workEndsAt=null;
  saveActiveSession(s);
}
function flushActiveClock(background=false){
  const s=loadActiveSession();if(!s||s.sessionPaused)return;
  if(!isSessionRouteActive()){if(s.activeSince)pauseActiveClockForRoute();return;}
  const now=Date.now();
  if(s.activeSince)s.activeElapsedMs=Math.max(0,Number(s.activeElapsedMs)||0)+Math.max(0,now-Number(s.activeSince));
  s.activeSince=background?null:now;saveActiveSession(s);
}
function resumeActiveClockForRoute(){
  const s=loadActiveSession();if(!s||s.sessionPaused||!isSessionRouteActive())return;
  if(!s.activeSince){s.activeSince=Date.now();saveActiveSession(s);}
}
const activeClockHeartbeat=setInterval(()=>{if(isSessionRouteActive())flushActiveClock(false);},5000);


function programAssetURLs(slug){
  const p=programs[slug];if(!p)return[];
  const urls=['./','index.html','styles-v100rc1.css?v=100rc1','app-v100rc1.js?v=100rc1','manifest.webmanifest','assets/muscle-map-male.jpg','assets/hero-duo-clean.jpg'];
  p.items.forEach(it=>{
    const e=findExercise(it.group,it.slug);if(!e)return;
    urls.push(thumbFor(e),heroFor(e));
    if(hasVideo(e))urls.push(videoFor(e));else [1,2,3].forEach(n=>urls.push(imgFor(e,n)));
  });
  return [...new Set(urls)];
}
async function downloadProgram(slug,button){
  const p=programs[slug];if(!p)return;
  if(!('serviceWorker'in navigator)){toast('Service worker indisponible.');return;}
  button.disabled=true;button.textContent='Téléchargement…';
  try{
    const reg=await navigator.serviceWorker.ready,target=reg.active||navigator.serviceWorker.controller;
    if(!target)throw new Error('Service worker non actif');
    await messageSW(target,{type:'CACHE_URLS',urls:programAssetURLs(slug)});
    await verifyProgramOffline(slug,true);
  }catch(e){console.error(e);toast('Téléchargement hors connexion impossible pour le moment.');button.disabled=false;button.textContent='Réessayer le téléchargement';}
}
async function verifyProgramOffline(slug,announce=true){
  const button=document.querySelector(`[data-offline-program="${slug}"]`);
  if(!('serviceWorker'in navigator)){if(button){button.disabled=true;button.textContent='Hors connexion non disponible';}return false;}
  try{
    const reg=await navigator.serviceWorker.ready,target=reg.active||navigator.serviceWorker.controller;
    if(!target)throw new Error('Service worker non actif');
    const res=await messageSW(target,{type:'CHECK_URLS',urls:programAssetURLs(slug)});
    const ok=!!res.complete;
    if(ok){
      if(!data.offlinePrograms.includes(slug)){data.offlinePrograms.push(slug);saveData();}
      if(button){button.disabled=false;button.dataset.offlineState='ready';button.innerHTML=`${icon('check')} Disponible hors connexion · vérifier`;}
      if(announce)toast('Programme vérifié : disponible hors connexion.');
    }else{
      if(data.offlinePrograms.includes(slug)){data.offlinePrograms=data.offlinePrograms.filter(x=>x!==slug);saveData();}
      if(button){button.disabled=false;button.dataset.offlineState='missing';button.innerHTML=`${icon('offline')} Télécharger / réparer hors connexion`;}
      if(announce)toast(`${res.missing?.length||0} ressource(s) à télécharger ou réparer.`);
    }
    return ok;
  }catch(e){
    console.error(e);
    if(button){button.disabled=false;button.dataset.offlineState='unknown';button.innerHTML=`${icon('offline')} Vérifier / réparer hors connexion`;}
    return false;
  }
}
function messageSW(sw,msg){return new Promise((resolve,reject)=>{const ch=new MessageChannel();const timeout=setTimeout(()=>reject(new Error('timeout')),30000);ch.port1.onmessage=e=>{clearTimeout(timeout);e.data?.ok?resolve(e.data):reject(new Error(e.data?.error||'Erreur service worker'));};sw.postMessage(msg,[ch.port2]);});}
function bindProfile(){
  document.getElementById('exportData')?.addEventListener('click',()=>{const blob=new Blob([JSON.stringify({version:VERSION,exportedAt:new Date().toISOString(),data},null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`delta-sierra-calisthenie-${new Date().toISOString().slice(0,10)}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000);});
  document.getElementById('importData')?.addEventListener('change',async ev=>{
    const f=ev.target.files?.[0];if(!f)return;
    try{
      if(f.size>5_000_000)throw new Error('fichier trop volumineux');
      const obj=JSON.parse(await f.text()),incoming=obj.data||obj,result=validateAndSanitizeImportedData(incoming);
      if(!result.ok)throw new Error(result.error||'structure invalide');
      if(!confirm('Remplacer les données locales actuelles par ce fichier ?'))return;
      data=result.data;saveData();
      await verifyAllOfflinePrograms(false);
      toast('Données restaurées et vérifiées.');render();
    }catch(e){console.error(e);toast('Fichier de sauvegarde invalide ou incompatible.');}
    finally{ev.target.value='';}
  });
}
function isPlainObject(v){return !!v&&typeof v==='object'&&!Array.isArray(v);}
function validDateString(v){return typeof v==='string'&&v.length<=40&&Number.isFinite(Date.parse(v));}
function intIn(v,min,max){return Number.isInteger(Number(v))&&Number(v)>=min&&Number(v)<=max;}
function sanitizeImportedSession(raw){
  if(!isPlainObject(raw))return null;
  if(typeof raw.id!=='string'||!/^[A-Za-z0-9_-]{1,64}$/.test(raw.id))return null;
  if(!['completed','abandoned'].includes(raw.status))return null;
  if(typeof raw.name!=='string'||!raw.name.trim()||raw.name.length>120)return null;
  if(!validDateString(raw.startedAt)||!validDateString(raw.endedAt)||Date.parse(raw.endedAt)<Date.parse(raw.startedAt))return null;
  if(!intIn(raw.durationSec,0,86400))return null;
  if(!Array.isArray(raw.items)||raw.items.length<1||raw.items.length>41||!Array.isArray(raw.results)||raw.results.length!==raw.items.length)return null;
  const items=[],results=[];
  for(let i=0;i<raw.items.length;i++){
    const original=raw.items[i];if(!isPlainObject(original))return null;
    const ref=original.key?validExerciseKey(original.key):(findExercise(original.group,original.slug)?{group:original.group,slug:original.slug,e:findExercise(original.group,original.slug)}:null);
    if(!ref||!intIn(original.sets,LIMITS.sets.min,LIMITS.sets.max)||!intIn(original.restSec,LIMITS.restSec.min,LIMITS.restSec.max))return null;
    const mode=exerciseMode(ref.e);
    if(mode==='duration'&&!intIn(original.durationSec,LIMITS.durationSec.min,LIMITS.durationSec.max))return null;
    if(mode==='reps'&&Number(original.durationSec||0)!==0)return null;
    if(mode==='hybrid'){
      const legacyHold=Number(original.durationSec||0);
      if(legacyHold!==0&&!intIn(legacyHold,LIMITS.durationSec.min,LIMITS.durationSec.max))return null;
      if(original.repsTarget!==undefined&&!intIn(original.repsTarget,LIMITS.hybridReps.min,LIMITS.hybridReps.max))return null;
      if(original.holdPerRepSeconds!==undefined&&!intIn(original.holdPerRepSeconds,LIMITS.holdPerRepSeconds.min,LIMITS.holdPerRepSeconds.max))return null;
    }
    const item=sanitizeSessionItem({group:ref.group,slug:ref.slug,sets:Number(original.sets),restSec:Number(original.restSec),durationSec:mode==='duration'?Number(original.durationSec||0):0,target:original.target,repsTarget:original.repsTarget,holdPerRepSeconds:original.holdPerRepSeconds??(mode==='hybrid'&&Number(original.durationSec||0)>0?Number(original.durationSec):undefined)});
    if(!item)return null;items.push(item);
    const rr=raw.results[i];if(!isPlainObject(rr)||rr.key!==`${ref.group}/${ref.slug}`||!Array.isArray(rr.series)||rr.series.length!==item.sets)return null;
    const series=[];
    for(let k=0;k<rr.series.length;k++){
      const sr=rr.series[k];if(!isPlainObject(sr)||typeof sr.completed!=='boolean')return null;
      const rec=makeSeriesRecord(rr.key,k);rec.completed=sr.completed;
      if(!sr.completed){
        if(Number(sr.reps||0)!==0||Number(sr.seconds||0)!==0||(sr.enteredReps!==null&&sr.enteredReps!==undefined&&Number(sr.enteredReps)!==0)||(sr.enteredSeconds!==null&&sr.enteredSeconds!==undefined&&Number(sr.enteredSeconds)!==0))return null;
        rec.reps=0;rec.seconds=0;rec.enteredReps=null;rec.enteredSeconds=null;rec.completedAt=null;
      }else if(item.mode==='duration'){
        const legacyEntered=sr.enteredSeconds!==undefined&&sr.enteredSeconds!==null?sr.enteredSeconds:(Number(sr.seconds||0)/(item.perSide?2:1));
        const v=validatePerformanceValue('seconds',legacyEntered);if(!v.ok)return null;
        rec.enteredSeconds=v.value;rec.seconds=v.value*(item.perSide?2:1);rec.reps=0;rec.enteredReps=null;rec.completedAt=validDateString(sr.completedAt)?sr.completedAt:raw.endedAt;
      }else if(item.mode==='hybrid'){
        // New V0.6.3 records both fields. A legacy timed curl-up is preserved without inventing repetitions.
        if(sr.enteredReps===undefined||sr.enteredReps===null){
          const legacy=validatePerformanceValue('seconds',sr.enteredSeconds??sr.seconds);if(!legacy.ok)return null;
          rec.enteredReps=0;rec.enteredSeconds=legacy.value;rec.reps=0;rec.seconds=legacy.value;rec.legacyHybrid=true;
        }else{
          const vr=validatePerformanceValue('reps',sr.enteredReps),vs=validatePerformanceValue('seconds',sr.enteredSeconds);if(!vr.ok||!vs.ok)return null;
          rec.enteredReps=vr.value;rec.enteredSeconds=vs.value;rec.reps=vr.value*(item.perSide?2:1);rec.seconds=vr.value*vs.value*(item.perSide?2:1);
        }
        rec.completedAt=validDateString(sr.completedAt)?sr.completedAt:raw.endedAt;
      }else{
        const v=validatePerformanceValue('reps',sr.enteredReps);if(!v.ok)return null;
        rec.enteredReps=v.value;rec.enteredSeconds=null;rec.reps=v.value*(item.perSide?2:1);rec.seconds=0;rec.completedAt=validDateString(sr.completedAt)?sr.completedAt:raw.endedAt;
      }
      if(!Number.isSafeInteger(rec.reps)||!Number.isSafeInteger(rec.seconds))return null;
      series.push(rec);
    }
    results.push(syncResultTotals({key:rr.key,name:ref.e.name,series}));
  }
  if(raw.status==='completed'&&results.some(r=>r.series.some(sr=>!sr.completed)))return null;
  const exerciseCount=results.filter(r=>r.setsDone>0).length,totalSets=results.reduce((a,r)=>a+r.setsDone,0),totalReps=results.reduce((a,r)=>a+r.reps,0),totalHoldSeconds=results.reduce((a,r)=>a+r.seconds,0);
  return {id:raw.id,name:raw.name.trim(),source:typeof raw.source==='string'?raw.source.slice(0,120):'import',items,startedAt:raw.startedAt,endedAt:raw.endedAt,status:raw.status,durationSec:Number(raw.durationSec),results,exerciseCount,totalSets,totalReps,totalHoldSeconds};
}
function validateAndSanitizeImportedData(incoming){
  if(!isPlainObject(incoming))return {ok:false,error:'racine invalide'};
  if(!Array.isArray(incoming.sessions)||!Array.isArray(incoming.favorites)||!Array.isArray(incoming.selection)||!Array.isArray(incoming.offlinePrograms))return {ok:false,error:'tableaux obligatoires invalides'};
  if(incoming.settings!==undefined&&!isPlainObject(incoming.settings))return {ok:false,error:'paramètres invalides'};
  if(incoming.firstUse!==undefined&&!validDateString(incoming.firstUse))return {ok:false,error:'date initiale invalide'};
  const favorites=incoming.favorites,selection=incoming.selection,sessions=incoming.sessions,offlinePrograms=incoming.offlinePrograms;
  if(sessions.length>2000||favorites.length>41||selection.length>41||offlinePrograms.length>Object.keys(programs).length)return {ok:false,error:'tableaux invalides'};
  if(favorites.some(k=>!validExerciseKey(k)))return {ok:false,error:'favori invalide'};
  const cleanSelection=[],selectionKeys=new Set();
  for(const raw of selection){
    if(!isPlainObject(raw)||!validExerciseKey(raw.key)||!intIn(raw.sets,LIMITS.sets.min,LIMITS.sets.max)||!intIn(raw.restSec,LIMITS.restSec.min,LIMITS.restSec.max))return {ok:false,error:'sélection invalide'};
    if(selectionKeys.has(raw.key))return {ok:false,error:'doublon dans la sélection'};selectionKeys.add(raw.key);
    const ref=validExerciseKey(raw.key),mode=exerciseMode(ref.e);
    if(mode==='duration'&&!intIn(raw.durationSec,LIMITS.durationSec.min,LIMITS.durationSec.max))return {ok:false,error:'durée de sélection invalide'};
    if(mode==='reps'&&Number(raw.durationSec||0)!==0)return {ok:false,error:'type de sélection invalide'};
    let candidate=raw;
    if(mode==='hybrid'){
      const legacyHold=Number(raw.durationSec||0);
      if(legacyHold!==0&&!intIn(legacyHold,LIMITS.durationSec.min,LIMITS.durationSec.max))return {ok:false,error:'ancienne durée hybride invalide'};
      if(raw.repsTarget!==undefined&&!intIn(raw.repsTarget,LIMITS.hybridReps.min,LIMITS.hybridReps.max))return {ok:false,error:'répétitions hybrides invalides'};
      if(raw.holdPerRepSeconds!==undefined&&!intIn(raw.holdPerRepSeconds,LIMITS.holdPerRepSeconds.min,LIMITS.holdPerRepSeconds.max))return {ok:false,error:'maintien hybride invalide'};
      candidate={...raw,durationSec:0,holdPerRepSeconds:raw.holdPerRepSeconds??(legacyHold>0?legacyHold:undefined)};
    }
    const clean=sanitizeSelectionItem(candidate);if(!clean)return {ok:false,error:'sélection invalide'};cleanSelection.push(clean);
  }
  if(offlinePrograms.some(slug=>typeof slug!=='string'||!programs[slug]))return {ok:false,error:'programme hors connexion invalide'};
  const cleanSessions=[];const ids=new Set();
  for(const raw of sessions){const clean=sanitizeImportedSession(raw);if(!clean||ids.has(clean.id))return {ok:false,error:'séance invalide'};ids.add(clean.id);cleanSessions.push(clean);}
  const settings=isPlainObject(incoming.settings)?{reducedMotion:!!incoming.settings.reducedMotion}:{reducedMotion:false};
  const firstUse=validDateString(incoming.firstUse)?incoming.firstUse:new Date().toISOString();
  return {ok:true,data:{favorites:[...new Set(favorites)],selection:cleanSelection,sessions:cleanSessions,offlinePrograms:[...new Set(offlinePrograms)],settings,firstUse}};
}
function validateImportedData(incoming){return validateAndSanitizeImportedData(incoming).ok;}
async function checkProgramOfflineSilent(slug){
  if(!programs[slug]||!('serviceWorker'in navigator))return false;
  try{const reg=await navigator.serviceWorker.ready,target=reg.active||navigator.serviceWorker.controller;if(!target)return false;const res=await messageSW(target,{type:'CHECK_URLS',urls:programAssetURLs(slug)});return !!res.complete;}catch{return false;}
}
async function verifyAllOfflinePrograms(updateUI=true){
  if(!('serviceWorker'in navigator)){
    if(updateUI){const count=document.getElementById('offlineProgramCount'),label=document.getElementById('offlineProgramLabel');if(count)count.textContent='—';if(label)label.textContent=' Vérification indisponible dans ce navigateur.';}
    return [...data.offlinePrograms];
  }
  const verified=[];
  for(const slug of [...new Set(data.offlinePrograms)].filter(s=>programs[s])) if(await checkProgramOfflineSilent(slug))verified.push(slug);
  if(JSON.stringify(verified)!==JSON.stringify(data.offlinePrograms)){data.offlinePrograms=verified;saveData();}
  if(updateUI){
    const count=document.getElementById('offlineProgramCount'),label=document.getElementById('offlineProgramLabel');
    if(count)count.textContent=String(verified.length);
    if(label)label.textContent=` programme${verified.length>1?'s':''} réellement disponible${verified.length>1?'s':''} hors connexion.`;
  }
  return verified;
}
function verifyProfileOfflinePrograms(){verifyAllOfflinePrograms(true).catch(()=>{const c=document.getElementById('offlineProgramCount'),l=document.getElementById('offlineProgramLabel');if(c)c.textContent='—';if(l)l.textContent=' Vérification indisponible.';});}

function isIOS(){return /iphone|ipad|ipod/i.test(navigator.userAgent);}
function isStandalone(){return window.matchMedia?.('(display-mode: standalone)').matches||navigator.standalone===true;}
function isSafariIOS(){return isIOS()&&/safari/i.test(navigator.userAgent)&&!/crios|fxios|edgios/i.test(navigator.userAgent);}
function refreshInstallButton(){
  const b=document.getElementById('installBtn');if(!b)return;
  const show=!isStandalone()&&(!!deferredPrompt||isSafariIOS());
  b.hidden=!show;b.classList.toggle('available',!!deferredPrompt);
}
window.addEventListener('beforeinstallprompt',e=>{e.preventDefault();deferredPrompt=e;refreshInstallButton();});
window.addEventListener('appinstalled',()=>{deferredPrompt=null;refreshInstallButton();toast('Application installée.');});
document.getElementById('installBtn')?.addEventListener('click',async()=>{
  if(isStandalone()){refreshInstallButton();return;}
  if(deferredPrompt){deferredPrompt.prompt();await deferredPrompt.userChoice;deferredPrompt=null;refreshInstallButton();return;}
  if(isSafariIOS()){toast('Sur iPhone/iPad : Safari → Partager → Sur l’écran d’accueil.');return;}
  refreshInstallButton();
});
document.getElementById('menuBtn')?.addEventListener('click',()=>document.getElementById('menuDialog').showModal());
document.getElementById('dialogClose')?.addEventListener('click',()=>document.getElementById('menuDialog').close());
document.getElementById('menuDialog')?.addEventListener('click',e=>{if(e.target.closest('a'))document.getElementById('menuDialog').close();});
document.getElementById('conflictResume')?.addEventListener('click',resumeExistingSession);
document.getElementById('conflictAbandon')?.addEventListener('click',abandonActiveForReplacement);
document.getElementById('conflictReplace')?.addEventListener('click',replaceActiveSession);
document.getElementById('conflictCancel')?.addEventListener('click',()=>{pendingSessionStart=null;document.getElementById('sessionConflictDialog')?.close();});
window.addEventListener('hashchange',render);
document.addEventListener('visibilitychange',()=>{if(document.hidden){if(parseRoute().name==='session'){pauseWorkTimerForRoute();flushActiveClock(true);}}else if(parseRoute().name==='session')resumeActiveClockForRoute();});
window.addEventListener('pagehide',()=>{if(parseRoute().name==='session'){pauseWorkTimerForRoute();flushActiveClock(true);}});
window.addEventListener('beforeunload',()=>{if(parseRoute().name==='session'){pauseWorkTimerForRoute();flushActiveClock(true);}clearSessionTimers();});

if('serviceWorker'in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('sw-v100rc1.js',{updateViaCache:'none'}).catch(console.error));}
injectIcons();refreshInstallButton();render();
