Analyseur Basket Mi-Temps & Quarts

0
(0)

🎯 Prédictions Analyseur Basket Mi-Temps & Quarts pour chaque match

Vous cherchez des prévisions fiables sur les vainqueurs par mi-temps et par quart-temps ? Avec l’Analyseur Mi-Temps & Quarts de Tedam’s Pronos, accédez à une sélection pointue de pronostics à forte probabilité, pensés pour maximiser vos chances de succès sur les paris partiels.

Notre approche se concentre sur les dynamiques de match : entame de rencontre, domination intermédiaire, ou relâchement en fin de période. Grâce à une analyse complète des statistiques détaillées par quart-temps ou mi-temps, vous saurez quand et sur quelle période miser intelligemment.


<?php


/**
 * ===========================================================
 * ✅ TP NBA — SPOT FUSION ANALYZER + 2 TICKET BUILDERS (V6 + UI)
 * Shortcode : [tp_nba_spot_fusion]
 *
 * ✅ AJOUTS :
 * - Bouton 🔄 Rafraîchir (rebuild cache HTML via nonce)
 * - Filtre Marché (liste déroulante)
 * - Filtre Match (liste déroulante)
 * - Bouton ➕ Charger plus (par match) => AJAX chunking
 * - ✅ TRI colonnes (clic sur entêtes) : Ligne/Cote/Confiance/Niveau/Palier(moy)/Matchs/Min/IA/ΔIA/Palier1/Edge/Value/Score
 * - ✅ Jusqu’à 10 legs (manuel + auto)
 *
 * ✅ FIX :
 * - NAMESPACE complet en tpnbaf6_*
 * - remove_shortcode + add_shortcode
 * - Polyfills MINUTE/HOUR
 * - ✅ TICKET BUILDERS + ✅ AUTO BUILDERS (Main & P1)
 * - ✅ CSS/JS imprimés même quand HTML est servi depuis le cache
 * - ✅ PRP : ajout paliers 44.5 et 49.5 (fallback dash)
 * ===========================================================
 */

if (!defined('ABSPATH')) exit;

/* Polyfills temps */
if (!defined('MINUTE_IN_SECONDS')) define('MINUTE_IN_SECONDS', 60);
if (!defined('HOUR_IN_SECONDS'))   define('HOUR_IN_SECONDS', 3600);

/* Force override shortcode (évite collision ancienne version) */
add_action('init', function(){
  remove_shortcode('tp_nba_spot_fusion');
  add_shortcode('tp_nba_spot_fusion', 'tpnbaf6_render_shortcode');
}, 99);

/* =========================
   AJAX: Load more rows (par match)
   ========================= */
add_action('wp_ajax_tpnbaf6_more', 'tpnbaf6_ajax_more');
add_action('wp_ajax_nopriv_tpnbaf6_more', 'tpnbaf6_ajax_more');

if (!function_exists('tpnbaf6_ajax_more')) {
  function tpnbaf6_ajax_more(){
    if (!defined('DOING_AJAX') || !DOING_AJAX) wp_die();

    $nonce = sanitize_text_field($_POST['nonce'] ?? '');
    if (!wp_verify_nonce($nonce, 'tpnbaf6_more')) {
      wp_send_json(['ok'=>0,'msg'=>'Nonce invalide'], 403);
    }

    if (!function_exists('tpnbap_get_player_props_ou') || !function_exists('tpnbap_player_props_rows')) {
      wp_send_json(['ok'=>0,'msg'=>'tp_nba_props non détecté'], 400);
    }

    $gid    = (int)($_POST['gid'] ?? 0);
    $league = (int)($_POST['league'] ?? 0);
    $season = sanitize_text_field((string)($_POST['season'] ?? ''));
    $bk     = (int)($_POST['bookmaker'] ?? 0);

    $offset = max(0, (int)($_POST['offset'] ?? 0));
    $limit  = (int)($_POST['limit'] ?? 200);
    if ($limit < 50) $limit = 50;
    if ($limit > 500) $limit = 500;

    $minMinutes = (float)($_POST['min_minutes'] ?? 25);
    $minConf    = (int)($_POST['min_conf'] ?? 60);
    $minGames   = (int)($_POST['min_games'] ?? 12);

    $matchLabel = sanitize_text_field((string)($_POST['match'] ?? ''));
    $timeLabel  = sanitize_text_field((string)($_POST['time'] ?? ''));

    if (!$gid || !$season || !$bk) {
      wp_send_json(['ok'=>0,'msg'=>'Paramètres manquants'], 400);
    }

    // Récup props
    $propsOut = tpnbap_get_player_props_ou($gid, $season, $bk);
    if (empty($propsOut)) {
      wp_send_json([
        'ok'=>1,'rows_html'=>[],'next_offset'=>$offset,'done'=>true,'raw_total'=>0,'kept'=>0
      ]);
    }

    // On récupère un gros batch (mais sur 1 match => maîtrisé)
    $allRows = tpnbap_player_props_rows($propsOut, 5000);
    if (empty($allRows) || !is_array($allRows)) {
      wp_send_json([
        'ok'=>1,'rows_html'=>[],'next_offset'=>$offset,'done'=>true,'raw_total'=>0,'kept'=>0
      ]);
    }

    $rawTotal = count($allRows);
    if ($offset >= $rawTotal) {
      wp_send_json([
        'ok'=>1,'rows_html'=>[],'next_offset'=>$offset,'done'=>true,'raw_total'=>$rawTotal,'kept'=>0
      ]);
    }

    $slice = array_slice($allRows, $offset, $limit);

    // IA snapshot joueurs (cache identique au rendu principal)
    $predCacheKey = 'tpnbaf6_playerpred_'.$gid;
    $playerPredMap = get_transient($predCacheKey);
    if (!is_array($playerPredMap)) {
      $snapPlayers = tpnbaf6_get_player_analyzer_snapshot($gid);
      if (is_array($snapPlayers) && !empty($snapPlayers['combined']) && is_array($snapPlayers['combined'])) {
        $playerPredMap = tpnbaf6_build_player_pred_map($snapPlayers['combined']);
        set_transient($predCacheKey, $playerPredMap, 6 * HOUR_IN_SECONDS);
      } else {
        $playerPredMap = [];
        set_transient($predCacheKey, $playerPredMap, 10 * MINUTE_IN_SECONDS);
      }
    }

    $rowsHtml = [];
    $kept = 0;

    foreach ($slice as $j => $r){
      $player = (string)($r['player'] ?? '');
      $market = tpnbaf6_norm_market((string)($r['market'] ?? ''));
      $line   = (float)($r['line'] ?? 0);

      if ($player === '' || $market === '' || $line <= 0) continue;

      $oddOver  = tpnbaf6_parse_odd($r['over'] ?? 0);
      $oddUnder = tpnbaf6_parse_odd($r['under'] ?? 0);

      $st = tpnbaf6_conf_stats($player, $market, $line);
      $co = $st['over'];
      $cu = $st['under'];
      $n  = (int)($st['n'] ?? 0);
      $avg= $st['avg'];

      if ($co === null && $cu === null) continue;

      // side recommandé
      $side = 'Over';
      $conf = $co;
      $odd  = $oddOver;

      if ($co === null) { $side='Under'; $conf=$cu; $odd=$oddUnder; }
      elseif ($cu !== null && $cu > $co) { $side='Under'; $conf=$cu; $odd=$oddUnder; }

      // fallback cote
      if (($odd <= 0) && (($side==='Over' && $oddUnder>0) || ($side==='Under' && $oddOver>0))) {
        $side = ($side==='Over') ? 'Under' : 'Over';
        $conf = ($side==='Over') ? $co : $cu;
        $odd  = ($side==='Over') ? $oddOver : $oddUnder;
      }

      if ($conf === null || $odd <= 0) continue;

      // seuils obligatoires
      if ((int)$conf < $minConf) continue;
      if ($n < $minGames) continue;

      $lvl = tpnbaf6_level((int)$conf);

      // ✅ Palier 1 (Dash) = palier inférieur réel (liste du dashboard)
      $pal1Txt  = '—';
      $pal1Conf = null;
      $pal1Line = null;
      if ($side === 'Over') {
        $lowerLine = tpnbaf6_dash_lower_line($market, $line);
        if ($lowerLine !== null) {
          $st2 = tpnbaf6_conf_stats($player, $market, (float)$lowerLine);
          $pal1Conf = $st2['over'];
          $pal1Line = (float)$lowerLine;
          if ($pal1Conf !== null) $pal1Txt = number_format((float)$lowerLine, 1, '.', '').' • '.$pal1Conf.'%';
        }
      }

      // Minutes: hist sinon snapshot joueurs
      $minHist = tpnbaf6_minutes_avg($player);
      $minPred = (!empty($playerPredMap) ? tpnbaf6_player_minutes_pred($playerPredMap, $player) : null);
      $minUse  = ($minHist !== null) ? $minHist : $minPred;

      if ($minUse === null || (float)$minUse < $minMinutes) continue;

      // IA
      $predIA = null;
      if (is_array($playerPredMap) && !empty($playerPredMap)) {
        $predIA = tpnbaf6_player_pred_value($playerPredMap, $player, $market);
      }
      $deltaIA = null;
      if ($predIA !== null) $deltaIA = (float)$predIA - (float)$line;

      // Edge + EV
      $implied = (100.0 / (float)$odd);
      $edgePP  = ((float)$conf - (float)$implied);
      $evPct   = (((float)$conf/100.0) * (float)$odd - 1.0) * 100.0;

      $hay = strtolower(trim($matchLabel.' '.$timeLabel.' '.$player.' '.$market.' '.$side));

      $row = [
        'gid'=>$gid,'match'=>$matchLabel,'time'=>$timeLabel,'player'=>$player,
        'player_norm'=>tpnbaf6_norm_player($player),
        'market'=>$market,'line'=>$line,'side'=>$side,'odd'=>$odd,'conf'=>(int)$conf,
        'lvl_key'=>$lvl['key'],'lvl_label'=>$lvl['label'],'lvl_cls'=>$lvl['cls'],
        'co'=>$co,'cu'=>$cu,'avg'=>$avg,'n'=>$n,
        'min_hist'=>$minHist,'min_pred'=>$minPred,'min_use'=>$minUse,
        'pred_ia'=>$predIA,'delta_ia'=>$deltaIA,
        'pal1_txt'=>$pal1Txt,'pal1_conf'=>$pal1Conf,'pal1_line'=>$pal1Line,
        'edge_pp'=>$edgePP,'ev_pct'=>$evPct,'hay'=>$hay,
      ];
      $row['score'] = tpnbaf6_compute_score($row);

      // render <tr>
      $id = $gid.'_more_'.$offset.'_'.$j.'_'.substr(md5($row['player'].$row['market'].$row['line'].$row['side']),0,6);

      $mktLabel = $row['market'];
      $labels = ['AST'=>'PAS','PTS+AST'=>'PTS+PAS','RA'=>'RP','PRP'=>'PRP'];
      if (isset($labels[$mktLabel])) $mktLabel = $labels[$mktLabel];

      $oddTxt = ($row['odd']>0) ? number_format((float)$row['odd'], 2, '.', '') : '-';

      $details = [];
      if ($row['co'] !== null) $details[] = 'Over '.$row['co'].'%';
      if ($row['cu'] !== null) $details[] = 'Under '.$row['cu'].'%';

      $avgTxt = ($row['avg'] !== null) ? number_format((float)$row['avg'], 1, '.', '') : '—';
      $nTxt   = ($row['n'] ?? 0) ? (int)$row['n'] : '—';

      $minTxt = '—';
      if ($row['min_hist'] !== null) $minTxt = number_format((float)$row['min_hist'], 1, '.', '');
      elseif ($row['min_pred'] !== null) $minTxt = number_format((float)$row['min_pred'], 1, '.', '').'*';

      $iaTxt = ($row['pred_ia'] !== null) ? number_format((float)$row['pred_ia'], 1, '.', '') : '—';
      $hasIA = ($row['pred_ia'] !== null) ? 1 : 0;

      $dTxt  = '—';
      if ($row['delta_ia'] !== null) {
        $d = (float)$row['delta_ia'];
        $dTxt = ($d >= 0 ? '+' : '').number_format($d, 1, '.', '');
      }

      $pal1Badge = '';
      $pal85 = 0;
      $pal1LineTxt = '';
      $pal1ConfTxt = '';
      if ($row['pal1_conf'] !== null) $pal1ConfTxt = (string)((int)$row['pal1_conf']);
      if ($row['pal1_line'] !== null) $pal1LineTxt = number_format((float)$row['pal1_line'], 1, '.', '');

      if ($row['pal1_conf'] !== null && (int)$row['pal1_conf'] >= 85) {
        $pal1Badge = ' <span class="tpnbaf-badge85">≥85%</span>';
        $pal85 = 1;
      }

      $edgeTxt = '<span class="tpnbaf-chipblue">'.(($row['edge_pp']>=0)?'+':'').number_format((float)$row['edge_pp'], 1, '.', '').'pp</span>';
      $valTxt  = '<span class="tpnbaf-chipnum">'.(($row['ev_pct']>=0)?'+':'').number_format((float)$row['ev_pct'], 1, '.', '').'%</span>';

      // baseline eligibility at 80 (JS applies selected threshold live)
      $p1Eligible = ($pal1ConfTxt !== '' && (int)$pal1ConfTxt >= 80 && $pal1LineTxt !== '');

      ob_start();
      ?>
      <tr
        data-gid="<?php echo esc_attr((string)$row['gid']); ?>"
        data-id="<?php echo esc_attr($id); ?>"
        data-match="<?php echo esc_attr($row['match']); ?>"
        data-player="<?php echo esc_attr($row['player']); ?>"
        data-playernorm="<?php echo esc_attr($row['player_norm']); ?>"
        data-market="<?php echo esc_attr($row['market']); ?>"
        data-side="<?php echo esc_attr($row['side']); ?>"
        data-line="<?php echo esc_attr(number_format((float)$row['line'], 1, '.', '')); ?>"
        data-odd="<?php echo esc_attr($oddTxt); ?>"
        data-conf="<?php echo esc_attr((int)$row['conf']); ?>"
        data-level="<?php echo esc_attr($row['lvl_key']); ?>"
        data-avg="<?php echo esc_attr($row['avg'] !== null ? number_format((float)$row['avg'], 1, '.', '') : ''); ?>"
        data-hasia="<?php echo esc_attr($hasIA); ?>"
        data-score="<?php echo esc_attr(number_format((float)$row['score'], 1, '.', '')); ?>"
        data-n="<?php echo esc_attr((int)$row['n']); ?>"
        data-min="<?php echo esc_attr(number_format((float)$row['min_use'], 1, '.', '')); ?>"
        data-edge="<?php echo esc_attr(number_format((float)$row['edge_pp'], 1, '.', '')); ?>"
        data-ev="<?php echo esc_attr(number_format((float)$row['ev_pct'], 1, '.', '')); ?>"
        data-ia="<?php echo esc_attr($row['pred_ia'] !== null ? number_format((float)$row['pred_ia'], 1, '.', '') : ''); ?>"
        data-delta="<?php echo esc_attr($row['delta_ia'] !== null ? number_format((float)$row['delta_ia'], 1, '.', '') : ''); ?>"
        data-pal85="<?php echo esc_attr($pal85); ?>"
        data-pal1line="<?php echo esc_attr($pal1LineTxt); ?>"
        data-pal1conf="<?php echo esc_attr($pal1ConfTxt); ?>"
        data-hay="<?php echo esc_attr($row['hay']); ?>"
      >
        <td><b><?php echo esc_html($row['match']); ?></b><div class="tpnbaf-muted" style="font-size:11px"><?php echo esc_html($row['time']); ?></div></td>
        <td><?php echo esc_html($row['player']); ?></td>
        <td><?php echo esc_html($mktLabel); ?></td>
        <td><b><?php echo esc_html($row['side']); ?></b><div class="tpnbaf-muted" style="font-size:11px"><?php echo esc_html(implode(' • ', $details)); ?></div></td>
        <td><?php echo esc_html(number_format((float)$row['line'], 1, '.', '')); ?></td>
        <td><b><?php echo $oddTxt; ?></b></td>
        <td><b><?php echo (int)$row['conf']; ?>%</b></td>
        <td><span class="<?php echo esc_attr($row['lvl_cls']); ?>"><?php echo esc_html($row['lvl_label']); ?></span></td>
        <td><span class="tpnbaf-chipgray"><?php echo $avgTxt; ?></span></td>
        <td><span class="tpnbaf-chipgray"><?php echo $nTxt; ?></span></td>
        <td><span class="tpnbaf-chipgray"><?php echo $minTxt; ?></span></td>
        <td><span class="tpnbaf-chipgray"><?php echo $iaTxt; ?></span></td>
        <td><span class="tpnbaf-chipgray"><?php echo esc_html($dTxt); ?></span></td>
        <td><?php echo esc_html($row['pal1_txt']).$pal1Badge; ?></td>
        <td><?php echo $edgeTxt; ?></td>
        <td><?php echo $valTxt; ?></td>
        <td><span class="tpnbaf-chipblue"><?php echo esc_html(number_format((float)$row['score'], 1, '.', '')); ?></span></td>
        <td>
          <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
            <button type="button" class="tpnbaf-btn tpnbaf-btn-add tpnbaf-add-main">➕ Main</button>
            <button type="button" class="tpnbaf-btn tpnbaf-btn-p1 tpnbaf-add-p1" <?php echo ($p1Eligible ? '' : 'disabled'); ?>>🛡️ P1</button>
          </div>
          <div class="tpnbaf-muted" style="font-size:11px;margin-top:4px">
            P1 piloté par le filtre “Palier 1 Dash”
          </div>
        </td>
      </tr>
      <?php
      $rowsHtml[] = trim(ob_get_clean());
      $kept++;
    }

    $nextOffset = min($rawTotal, $offset + $limit);
    $done = ($nextOffset >= $rawTotal);

    wp_send_json([
      'ok'=>1,
      'rows_html'=>$rowsHtml,
      'next_offset'=>$nextOffset,
      'done'=>$done,
      'raw_total'=>$rawTotal,
      'kept'=>$kept
    ]);
  }
}

/* =========================
   CSS (once)
   ========================= */
if (!function_exists('tpnbaf6_css_once')) {
  function tpnbaf6_css_once(){
    static $done = false; if ($done) return; $done = true;

    $css = <<<'CSS'
.tpnbaf-wrap{margin:14px 0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
.tpnbaf-head{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin-bottom:10px}
.tpnbaf-title{font-weight:900;font-size:18px}
.tpnbaf-sub{opacity:.7;font-size:12px}
.tpnbaf-filters{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.tpnbaf-filters input,.tpnbaf-filters select{padding:7px 10px;border-radius:999px;border:1px solid #e5e7eb;font-size:12px;background:#fff}
.tpnbaf-card{background:#fff;border:1px solid #e5e7eb;border-radius:16px;box-shadow:0 8px 18px rgba(0,0,0,.06);padding:12px}
.tpnbaf-tablewrap{width:100%;overflow:auto;border-radius:14px;border:1px solid #e5e7eb}
.tpnbaf-table{width:100%;border-collapse:collapse;min-width:2050px}
.tpnbaf-table th{position:sticky;top:0;z-index:2;background:linear-gradient(90deg,#ff8c00,#ffcc33);color:#111;font-weight:900;font-size:12px;text-transform:uppercase;letter-spacing:.3px;padding:10px;border-bottom:2px solid #ffd37a;text-align:left;white-space:nowrap}
.tpnbaf-table td{padding:9px 10px;border-bottom:1px solid #f1f5f9;font-size:13px;vertical-align:middle;white-space:nowrap}
.tpnbaf-table tr:hover td{background:#fff7ed}

.tpnbaf-chip{display:inline-block;padding:3px 9px;border-radius:999px;font-weight:900;font-size:12px}
.tpnbaf-low{background:#e2e8f0;color:#0f172a}
.tpnbaf-med{background:#16a34a;color:#fff}
.tpnbaf-high{background:linear-gradient(90deg,#8b5cf6,#f97316);color:#fff}
.tpnbaf-muted{opacity:.72}

.tpnbaf-btn{border:none;border-radius:12px;padding:8px 10px;font-weight:900;font-size:12px;cursor:pointer}
.tpnbaf-btn-add{background:#111827;color:#fff}
.tpnbaf-btn-add:hover{filter:brightness(1.08)}
.tpnbaf-btn-copy{background:#2563eb;color:#fff}
.tpnbaf-btn-clear{background:#ef4444;color:#fff}
.tpnbaf-btn-auto{background:linear-gradient(90deg,#2563eb,#22c55e);color:#fff}
.tpnbaf-btn-p1{background:linear-gradient(90deg,#22c55e,#10b981);color:#fff}
.tpnbaf-btn-p1:hover{filter:brightness(1.06)}
.tpnbaf-btn:disabled{opacity:.45;cursor:not-allowed;filter:none}

.tpnbaf-btn-refresh{background:#0f172a;color:#fff}
.tpnbaf-btn-more{background:linear-gradient(90deg,#f59e0b,#ef4444);color:#fff}

.tpnbaf-kpis{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
.tpnbaf-kpi{background:#f8fafc;border:1px solid #e5e7eb;border-radius:14px;padding:10px 12px;font-size:12px;min-width:120px}
.tpnbaf-kpi b{font-weight:900}
.tpnbaf-kpi div{font-weight:900;font-size:16px;margin-top:2px}

.tpnbaf-textarea{width:100%;min-height:110px;border:1px solid #e5e7eb;border-radius:14px;padding:10px;font-size:12px}
.tpnbaf-textarea.small{min-height:130px}

.tpnbaf-badge85{display:inline-block;margin-left:6px;padding:3px 8px;border-radius:999px;font-weight:900;font-size:11px;background:linear-gradient(90deg,#22c55e,#10b981);color:#fff}
.tpnbaf-chipnum{display:inline-block;padding:3px 8px;border-radius:999px;font-weight:900;font-size:12px;background:#111827;color:#fff}
.tpnbaf-chipblue{display:inline-block;padding:3px 8px;border-radius:999px;font-weight:900;font-size:12px;background:#2563eb;color:#fff}
.tpnbaf-chipgray{display:inline-block;padding:3px 8px;border-radius:999px;font-weight:900;font-size:12px;background:#e2e8f0;color:#0f172a}
.tpnbaf-star{display:inline-block;margin-left:6px;padding:3px 8px;border-radius:999px;font-weight:900;font-size:11px;background:linear-gradient(90deg,#2563eb,#22c55e);color:#fff}

.tpnbaf-builders{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start;margin-top:12px}
@media(max-width:1100px){.tpnbaf-builders{grid-template-columns:1fr}}

.tpnbaf-ticketbox{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.tpnbaf-leg{border:1px solid #e5e7eb;border-radius:14px;padding:10px;background:linear-gradient(180deg,#ffffff,#f8fafc)}
.tpnbaf-leg-top{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
.tpnbaf-leg-main{min-width:0}
.tpnbaf-leg-main .player{font-weight:950;font-size:13px;line-height:1.2}
.tpnbaf-leg-main .pick{font-weight:950;font-size:12px;margin-top:4px}
.tpnbaf-leg-main .sub{font-size:11px;opacity:.72;margin-top:3px;white-space:normal}
.tpnbaf-leg-right{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:0 0 auto}
.tpnbaf-pill{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:4px 10px;font-weight:900;font-size:11px;border:1px solid #e5e7eb;background:#fff}
.tpnbaf-pill strong{font-size:11px}
.tpnbaf-x{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:999px;border:1px solid #fecaca;background:#fff1f2;color:#b91c1c;font-weight:950;cursor:pointer}
.tpnbaf-x:hover{filter:brightness(0.97)}

/* ✅ TRI colonnes */
.tpnbaf-sortable{cursor:pointer;user-select:none}
.tpnbaf-sortable:after{content:" ↕";opacity:.45;font-weight:900}
.tpnbaf-sortable[data-dir="asc"]:after{content:" ↑";opacity:.95}
.tpnbaf-sortable[data-dir="desc"]:after{content:" ↓";opacity:.95}
CSS;

    echo "<style>{$css}</style>";
  }
}

/* =========================
   JS (once)
   ========================= */
if (!function_exists('tpnbaf6_js_once')) {
  function tpnbaf6_js_once(){
    static $done = false; if ($done) return; $done = true;

    $js = <<<'JS'
(function(){
  const LS_MAIN = "tpnbaf6_ticket_main_v1";
  const LS_P1   = "tpnbaf6_ticket_p1_v1";

  const AJAX = (window.tpnbaf6_ajax || {});
  const AJAX_URL = AJAX.url || "";
  const AJAX_NONCE = AJAX.nonce || "";
  const CFG = AJAX.cfg || {};
  const MAX_ROWS_GAME = parseInt(CFG.maxRowsGame || "240", 10) || 240;

  // ✅ Jusqu’à 10 legs (manuel + auto)
  const MAX_LEGS = parseInt(CFG.maxLegs || "10", 10) || 10;

  const rawOffsets = {};

  function readTicket(key){
    try { const x = JSON.parse(localStorage.getItem(key) || "[]"); return Array.isArray(x) ? x : []; }
    catch(e){ return []; }
  }
  function saveTicket(key, arr){ localStorage.setItem(key, JSON.stringify(arr || [])); }

  function fmtOdd(x){ const n = parseFloat(x); if (!isFinite(n) || n<=0) return "-"; return n.toFixed(2); }
  function toNum(x){ const n = parseFloat(x); return isFinite(n) ? n : null; }
  function calcTotalOdds(items){ let t = 1; items.forEach(it=>{ const o = parseFloat(it.odd); if (isFinite(o) && o>0) t *= o; }); return t; }
  function fairOddFromConf(conf){ const c = toNum(conf); if (c===null || c<=0) return null; return (100.0 / c); }

  function getP1SelectValue(){ return (document.querySelector("#tpnbafP1Min")?.value || "ALL"); }
  function getP1Threshold(){
    const v = getP1SelectValue();
    const n = parseInt(v, 10);
    if ([80,85,90,95].includes(n)) return n;
    return 85;
  }
  function getSelectedMatchGid(){ return (document.querySelector("#tpnbafMatch")?.value || "ALL"); }

  function updateLoadMoreBtn(){
    const btn = document.querySelector("#tpnbafLoadMore");
    if (!btn) return;
    const gid = getSelectedMatchGid();
    const ok = (gid !== "ALL" && !!AJAX_URL && !!AJAX_NONCE);
    btn.disabled = !ok;
    if (ok) btn.textContent = "➕ Charger plus (ce match)";
    else btn.textContent = "➕ Charger plus (sélectionne un match)";
  }

  function buildWhyForItem(it){
    const parts = [];
    const score = toNum(it.score);
    const conf  = toNum(it.conf);
    const n     = toNum(it.n);
    const min   = toNum(it.min);
    const edge  = toNum(it.edge_pp);
    const ev    = toNum(it.ev_pct);
    const ia    = toNum(it.pred_ia);
    const dIA   = toNum(it.delta_ia);
    const pal85 = (String(it.pal85||"0") === "1");

    if (score!==null) parts.push("Score " + score.toFixed(1));
    if (conf!==null)  parts.push("Confiance " + Math.round(conf) + "%");
    if (min!==null)   parts.push("Minutes " + min.toFixed(1) + "/match");
    if (n!==null)     parts.push("Échantillon " + Math.round(n) + " matchs");

    if (edge!==null) parts.push("Edge " + (edge>=0?"+":"") + edge.toFixed(1) + "pp");
    if (ev!==null)   parts.push("Value " + (ev>=0?"+":"") + ev.toFixed(1) + "%");

    if (ia!==null && dIA!==null) parts.push("IA " + ia.toFixed(1) + " (Δ " + (dIA>=0?"+":"") + dIA.toFixed(1) + ")");
    else if (ia!==null) parts.push("IA " + ia.toFixed(1));

    if (pal85) parts.push("Palier ≥85% (SAFE)");
    return "• " + (it.player||"") + " — " + (it.market||"") + " " + (it.side||"") + " " + (it.line||"") + " : " + parts.join(" • ");
  }

  function renderWhy(textareaId, title, items){
    const why = document.querySelector("#"+textareaId);
    if (!why) return;
    if (!items.length){ why.value = ""; return; }
    const lines = [];
    lines.push(title);
    lines.push("");
    items.forEach(it => lines.push(buildWhyForItem(it)));
    why.value = lines.join("\n");
  }

  function flashBtn(btn, okText, revertText){
    if (!btn) return;
    const old = btn.textContent;
    btn.textContent = okText;
    setTimeout(()=>btn.textContent=(revertText||old), 1200);
  }

  function getVisibleRows(){
    return Array.from(document.querySelectorAll(".tpnbaf-table tbody tr"))
      .filter(tr => tr.style.display !== "none");
  }

  function mainItemFromTr(tr){
    return {
      gid: tr.getAttribute("data-gid") || "",
      id: tr.getAttribute("data-id") || (Date.now()+""),
      match: tr.getAttribute("data-match") || "",
      player: tr.getAttribute("data-player") || "",
      player_norm: tr.getAttribute("data-playernorm") || "",
      market: tr.getAttribute("data-market") || "",
      side: tr.getAttribute("data-side") || "",
      line: tr.getAttribute("data-line") || "",
      odd: tr.getAttribute("data-odd") || "",
      conf: tr.getAttribute("data-conf") || "",
      score: tr.getAttribute("data-score") || "",
      n: tr.getAttribute("data-n") || "",
      min: tr.getAttribute("data-min") || "",
      edge_pp: tr.getAttribute("data-edge") || "",
      ev_pct: tr.getAttribute("data-ev") || "",
      pred_ia: tr.getAttribute("data-ia") || "",
      delta_ia: tr.getAttribute("data-delta") || "",
      pal85: tr.getAttribute("data-pal85") || "0",
      mode: "MAIN"
    };
  }

  function p1ItemFromTr(tr){
    const p1line = tr.getAttribute("data-pal1line") || "";
    const p1conf = tr.getAttribute("data-pal1conf") || "";
    const oddN = fairOddFromConf(p1conf);
    const oddS = (oddN!==null) ? oddN.toFixed(2) : "";
    return {
      gid: tr.getAttribute("data-gid") || "",
      id: (tr.getAttribute("data-id") || (Date.now()+"")) + "_p1",
      match: tr.getAttribute("data-match") || "",
      player: tr.getAttribute("data-player") || "",
      player_norm: tr.getAttribute("data-playernorm") || "",
      market: tr.getAttribute("data-market") || "",
      side: "Over",
      line: p1line,
      odd: oddS,
      conf: p1conf,
      score: tr.getAttribute("data-score") || "",
      n: tr.getAttribute("data-n") || "",
      min: tr.getAttribute("data-min") || "",
      pal85: (toNum(p1conf)!==null && toNum(p1conf) >= 85) ? "1" : "0",
      mode: "P1"
    };
  }

  function renderTicketBox(containerId, key){
    const items = readTicket(key);
    const box = document.querySelector("#"+containerId);
    if (!box) return;

    box.innerHTML = "";
    if (!items.length){
      box.innerHTML = "<div class='tpnbaf-muted'>Aucune sélection. Utilise les boutons du tableau ou l’Auto Builder.</div>";
      return;
    }

    const wrap = document.createElement("div");
    wrap.className = "tpnbaf-ticketbox";

    items.forEach((it, idx)=>{
      const sc = toNum(it.score);
      const conf = toNum(it.conf);
      const min = toNum(it.min);
      const n = toNum(it.n);

      const leg = document.createElement("div");
      leg.className = "tpnbaf-leg";

      const top = document.createElement("div");
      top.className = "tpnbaf-leg-top";

      const main = document.createElement("div");
      main.className = "tpnbaf-leg-main";
      main.innerHTML =
        "<div class='player'>"+(it.player||"-")+"</div>" +
        "<div class='pick'>"+(it.market||"-")+" <span style='font-weight:950'>"+(it.side||"-")+"</span> "+(it.line||"-")+"</div>" +
        "<div class='sub'>"+(it.match||"")+"</div>";

      const right = document.createElement("div");
      right.className = "tpnbaf-leg-right";

      const x = document.createElement("div");
      x.className = "tpnbaf-x";
      x.textContent = "×";
      x.setAttribute("data-x", String(idx));
      x.setAttribute("data-key", key);

      const p1 = document.createElement("div");
      p1.className = "tpnbaf-pill";
      p1.innerHTML = "<strong>@"+fmtOdd(it.odd)+"</strong> • "+(conf!==null?Math.round(conf)+"%":"-");

      const p2 = document.createElement("div");
      p2.className = "tpnbaf-pill";
      p2.innerHTML = "score <strong>"+(sc!==null?sc.toFixed(1):"-")+"</strong>";

      right.appendChild(x);
      right.appendChild(p1);
      right.appendChild(p2);

      if (min!==null || n!==null){
        const p3 = document.createElement("div");
        p3.className = "tpnbaf-pill";
        p3.innerHTML = (min!==null ? ("min <strong>"+min.toFixed(1)+"</strong>") : "min -") + (n!==null ? (" • n <strong>"+Math.round(n)+"</strong>") : "");
        right.appendChild(p3);
      }

      top.appendChild(main);
      top.appendChild(right);

      leg.appendChild(top);
      wrap.appendChild(leg);
    });

    box.appendChild(wrap);
  }

  function renderBuilder(key, cfg){
    const items = readTicket(key);

    renderTicketBox(cfg.boxId, key);

    const total = calcTotalOdds(items);
    const kpiOdds = document.querySelector("#"+cfg.kpiOddsId);
    const kpiN    = document.querySelector("#"+cfg.kpiNId);
    const kpiRet  = document.querySelector("#"+cfg.kpiRetId);
    const stakeEl = document.querySelector("#"+cfg.stakeId);

    if (kpiOdds) kpiOdds.textContent = items.length ? total.toFixed(2) : "-";
    if (kpiN)    kpiN.textContent    = items.length ? String(items.length) : "0";

    const stake = parseFloat(stakeEl ? stakeEl.value : 0) || 0;
    const ret = (stake>0 && items.length) ? (stake * total) : 0;
    if (kpiRet) kpiRet.textContent = (ret>0) ? ret.toFixed(2) + "€" : "-";

    const txt = document.querySelector("#"+cfg.textId);
    if (txt){
      if (!items.length){ txt.value = ""; }
      else {
        const lines = items.map((it,i)=>(
          (i+1)+". " + it.match + " — " + it.player + " — " + it.market + " " + it.side + " " + it.line +
          " @"+fmtOdd(it.odd) + " (" + (it.conf||"-") + "% | score " + (toNum(it.score)!==null?toNum(it.score).toFixed(1):"-") + ")"
        ));
        txt.value = cfg.title + "\n" + lines.join("\n") + "\n\nTotal cotes: " + (items.length ? total.toFixed(2) : "-");
      }
    }

    renderWhy(cfg.whyId, cfg.whyTitle, items);
  }

  function renderAll(){
    const p1Min = getP1Threshold();

    const t1 = document.querySelector("#tpnbafP1Title");
    if (t1) t1.textContent = "🛡️ Ticket SAFE (Palier 1 Dash ≥"+p1Min+"%)";
    const hint = document.querySelector("#tpnbafP1Hint");
    if (hint) hint.textContent =
      "SAFE P1 = uniquement Over Palier 1 (Dash) ≥"+p1Min+"% • cotes = estimation (100/conf) • 1 match = 1 spot • max "+MAX_LEGS+" legs.";

    renderBuilder(LS_MAIN, {
      boxId:"tpnbafTicketBoxMain",
      textId:"tpnbafTicketTextMain",
      whyId:"tpnbafWhyTextMain",
      kpiOddsId:"tpnbafKpiOddsMain",
      kpiNId:"tpnbafKpiNMain",
      kpiRetId:"tpnbafKpiRetMain",
      stakeId:"tpnbafStakeMain",
      title:"🎫 Ticket NBA (Fusion Spots) — max "+MAX_LEGS+" legs",
      whyTitle:"Pourquoi ces joueurs ?"
    });

    renderBuilder(LS_P1, {
      boxId:"tpnbafTicketBoxP1",
      textId:"tpnbafTicketTextP1",
      whyId:"tpnbafWhyTextP1",
      kpiOddsId:"tpnbafKpiOddsP1",
      kpiNId:"tpnbafKpiNP1",
      kpiRetId:"tpnbafKpiRetP1",
      stakeId:"tpnbafStakeP1",
      title:"🛡️ Ticket SAFE (Palier 1 Dash ≥"+p1Min+"%) — cotes théoriques — max "+MAX_LEGS+" legs",
      whyTitle:"Pourquoi SAFE Palier 1 ?"
    });
  }

  // Remove item
  document.addEventListener("click", (e)=>{
    const x = e.target.closest("[data-x][data-key]");
    if (!x) return;
    const key = x.getAttribute("data-key");
    const idx = parseInt(x.getAttribute("data-x"),10);
    if (![LS_MAIN, LS_P1].includes(key)) return;
    const items = readTicket(key);
    if (!isNaN(idx)) items.splice(idx,1);
    saveTicket(key, items);
    renderAll();
  });

  // Add MAIN
  document.addEventListener("click", (e)=>{
    const btn = e.target.closest(".tpnbaf-add-main");
    if (!btn) return;
    const tr = btn.closest("tr");
    if (!tr) return;

    const it = mainItemFromTr(tr);
    const items = readTicket(LS_MAIN);

    if (items.length >= MAX_LEGS){
      flashBtn(btn, "⛔ Max "+MAX_LEGS, "➕ Main");
      return;
    }

    // 1 spot par match
    if (it.gid && items.some(x => String(x.gid||"") === String(it.gid))) {
      flashBtn(btn, "⛔ Même match", "➕ Main");
      return;
    }
    const sig = [it.match,it.player,it.market,it.side,it.line].join("|");
    if (!items.some(x=>[x.match,x.player,x.market,x.side,x.line].join("|")===sig)){
      items.push(it);
      saveTicket(LS_MAIN, items);
      renderAll();
      flashBtn(btn, "✅ Ajouté", "➕ Main");
    } else {
      flashBtn(btn, "⚠️ Déjà", "➕ Main");
    }
  });

  // Add P1
  document.addEventListener("click", (e)=>{
    const btn = e.target.closest(".tpnbaf-add-p1");
    if (!btn || btn.disabled) return;
    const tr = btn.closest("tr");
    if (!tr) return;

    const items = readTicket(LS_P1);
    if (items.length >= MAX_LEGS){
      flashBtn(btn, "⛔ Max "+MAX_LEGS, "🛡️ P1");
      return;
    }

    const p1Min = getP1Threshold();
    const p1conf = toNum(tr.getAttribute("data-pal1conf"));
    const p1line = tr.getAttribute("data-pal1line") || "";
    if (p1conf===null || p1conf < p1Min || !p1line){
      flashBtn(btn, "⛔ P1 <"+p1Min, "🛡️ P1");
      return;
    }

    const it = p1ItemFromTr(tr);

    // 1 spot par match
    if (it.gid && items.some(x => String(x.gid||"") === String(it.gid))) {
      flashBtn(btn, "⛔ Même match", "🛡️ P1");
      return;
    }
    const sig = [it.match,it.player,it.market,it.side,it.line].join("|");
    if (!items.some(x=>[x.match,x.player,x.market,x.side,x.line].join("|")===sig)){
      items.push(it);
      saveTicket(LS_P1, items);
      renderAll();
      flashBtn(btn, "✅ Ajouté", "🛡️ P1");
    } else {
      flashBtn(btn, "⚠️ Déjà", "🛡️ P1");
    }
  });

  // ✅ Auto builders
  function autoBuildMain(btn){
    const rows = getVisibleRows()
      .sort((a,b)=> (parseFloat(b.getAttribute("data-score")||"0") - parseFloat(a.getAttribute("data-score")||"0")));

    const out = [];
    const usedGid = new Set();

    for (const tr of rows){
      if (out.length >= MAX_LEGS) break;
      const gid = tr.getAttribute("data-gid") || "";
      if (!gid || usedGid.has(gid)) continue;

      out.push(mainItemFromTr(tr));
      usedGid.add(gid);
    }

    saveTicket(LS_MAIN, out);
    renderAll();
    flashBtn(btn, "✅ Généré", "🤖 Auto");
  }

  function autoBuildP1(btn){
    const thr = getP1Threshold();
    const rows = getVisibleRows()
      .filter(tr=>{
        const p1c = toNum(tr.getAttribute("data-pal1conf"));
        const p1l = tr.getAttribute("data-pal1line") || "";
        return (p1c !== null && p1c >= thr && !!p1l);
      })
      .sort((a,b)=> (toNum(b.getAttribute("data-pal1conf"))||0) - (toNum(a.getAttribute("data-pal1conf"))||0));

    const out = [];
    const usedGid = new Set();

    for (const tr of rows){
      if (out.length >= MAX_LEGS) break;
      const gid = tr.getAttribute("data-gid") || "";
      if (!gid || usedGid.has(gid)) continue;

      out.push(p1ItemFromTr(tr));
      usedGid.add(gid);
    }

    saveTicket(LS_P1, out);
    renderAll();
    flashBtn(btn, "✅ Généré", "🤖 Auto");
  }

  // Copy / Clear / Auto
  document.addEventListener("click", async (e)=>{
    if (e.target.closest("#tpnbafClearMain")){ saveTicket(LS_MAIN, []); renderAll(); return; }
    if (e.target.closest("#tpnbafClearP1")){ saveTicket(LS_P1, []); renderAll(); return; }

    const aM = e.target.closest("#tpnbafAutoMain");
    if (aM){ autoBuildMain(aM); return; }

    const aP = e.target.closest("#tpnbafAutoP1");
    if (aP){ autoBuildP1(aP); return; }

    const cpM = e.target.closest("#tpnbafCopyMain");
    if (cpM){
      const txt = document.querySelector("#tpnbafTicketTextMain");
      try{ await navigator.clipboard.writeText(txt?.value || ""); cpM.textContent="✅ Copié"; }
      catch(err){ cpM.textContent="❌"; }
      setTimeout(()=>cpM.textContent="📋 Copier ticket", 1200);
      return;
    }
    const cpP = e.target.closest("#tpnbafCopyP1");
    if (cpP){
      const txt = document.querySelector("#tpnbafTicketTextP1");
      try{ await navigator.clipboard.writeText(txt?.value || ""); cpP.textContent="✅ Copié"; }
      catch(err){ cpP.textContent="❌"; }
      setTimeout(()=>cpP.textContent="📋 Copier ticket", 1200);
      return;
    }

    const cpWM = e.target.closest("#tpnbafCopyWhyMain");
    if (cpWM){
      const why = document.querySelector("#tpnbafWhyTextMain");
      try{ await navigator.clipboard.writeText(why?.value || ""); cpWM.textContent="✅ Copié"; }
      catch(err){ cpWM.textContent="❌"; }
      setTimeout(()=>cpWM.textContent="📋 Copier pourquoi", 1200);
      return;
    }
    const cpWP = e.target.closest("#tpnbafCopyWhyP1");
    if (cpWP){
      const why = document.querySelector("#tpnbafWhyTextP1");
      try{ await navigator.clipboard.writeText(why?.value || ""); cpWP.textContent="✅ Copié"; }
      catch(err){ cpWP.textContent="❌"; }
      setTimeout(()=>cpWP.textContent="📋 Copier pourquoi", 1200);
      return;
    }
  });

  document.addEventListener("input",(e)=>{
    if (e.target && (e.target.id==="tpnbafStakeMain" || e.target.id==="tpnbafStakeP1")) renderAll();
  });

  // ==========================
  // ✅ TRI TABLE (asc/desc)
  // ==========================
  let currentSort = { key: null, dir: null }; // dir: "asc" | "desc"

  function levelRank(v){
    const x = String(v || "").toUpperCase();
    if (x === "HIGH") return 3;
    if (x === "MED")  return 2;
    return 1; // LOW ou vide
  }

  function numOrNull(v){
    if (v === null || v === undefined) return null;
    const s = String(v).trim();
    if (!s || s === "-" || s === "—") return null;
    const n = parseFloat(s.replace(",", "."));
    return Number.isFinite(n) ? n : null;
  }

  function getSortVal(tr, key){
    switch(key){
      case "line":  return numOrNull(tr.getAttribute("data-line"));
      case "odd":   return numOrNull(tr.getAttribute("data-odd"));
      case "conf":  return numOrNull(tr.getAttribute("data-conf"));
      case "level": return levelRank(tr.getAttribute("data-level"));
      case "avg":   return numOrNull(tr.getAttribute("data-avg"));
      case "n":     return numOrNull(tr.getAttribute("data-n"));
      case "min":   return numOrNull(tr.getAttribute("data-min"));
      case "ia":    return numOrNull(tr.getAttribute("data-ia"));
      case "delta": return numOrNull(tr.getAttribute("data-delta"));
      case "edge":  return numOrNull(tr.getAttribute("data-edge"));
      case "ev":    return numOrNull(tr.getAttribute("data-ev"));
      case "score": return numOrNull(tr.getAttribute("data-score"));
      case "pal1": {
        const c = numOrNull(tr.getAttribute("data-pal1conf"));
        const l = numOrNull(tr.getAttribute("data-pal1line"));
        return (c === null ? null : (c * 1000 + (l || 0)));
      }
      default:
        return null;
    }
  }

  function applySort(){
    const tbody = document.querySelector(".tpnbaf-table tbody");
    if (!tbody) return;
    if (!currentSort.key || !currentSort.dir) return;

    const rows = Array.from(tbody.querySelectorAll("tr"));
    const dirMul = (currentSort.dir === "asc") ? 1 : -1;

    const withIdx = rows.map((tr, idx)=>({tr, idx}));

    withIdx.sort((a,b)=>{
      const va = getSortVal(a.tr, currentSort.key);
      const vb = getSortVal(b.tr, currentSort.key);

      const aNull = (va === null);
      const bNull = (vb === null);
      if (aNull && bNull) return a.idx - b.idx;
      if (aNull) return 1;
      if (bNull) return -1;

      if (va < vb) return -1 * dirMul;
      if (va > vb) return  1 * dirMul;
      return a.idx - b.idx;
    });

    withIdx.forEach(o => tbody.appendChild(o.tr));
  }

  document.addEventListener("click", (e)=>{
    const th = e.target.closest("th.tpnbaf-sortable[data-sort]");
    if (!th) return;

    const key = th.getAttribute("data-sort");

    document.querySelectorAll("th.tpnbaf-sortable[data-sort]").forEach(x=>{
      if (x !== th) x.removeAttribute("data-dir");
    });

    if (currentSort.key !== key){
      currentSort.key = key;
      currentSort.dir = "desc"; // 1er clic => décroissant
    } else {
      currentSort.dir = (currentSort.dir === "desc") ? "asc" : "desc";
    }

    th.setAttribute("data-dir", currentSort.dir);
    applySort();
  });

  function applyFilters(){
    const q = (document.querySelector("#tpnbafQ")?.value || "").toLowerCase().trim();
    const lvl = (document.querySelector("#tpnbafLvl")?.value || "ALL");
    const side = (document.querySelector("#tpnbafSide")?.value || "ALL");
    const iaf  = (document.querySelector("#tpnbafIA")?.value || "ALL");
    const mktF = (document.querySelector("#tpnbafMarket")?.value || "ALL");
    const gidF = (document.querySelector("#tpnbafMatch")?.value || "ALL");

    const p1Sel = getP1SelectValue();
    const p1Min = getP1Threshold();

    document.querySelectorAll(".tpnbaf-table tbody tr").forEach(tr=>{
      const hay = (tr.getAttribute("data-hay") || "").toLowerCase();
      const tl  = tr.getAttribute("data-level") || "";
      const sd  = tr.getAttribute("data-side") || "";
      const hasIA = tr.getAttribute("data-hasia") || "0";
      const mk  = tr.getAttribute("data-market") || "";
      const gid = tr.getAttribute("data-gid") || "";

      const p1c = toNum(tr.getAttribute("data-pal1conf"));
      const p1l = (tr.getAttribute("data-pal1line") || "");

      let ok = true;
      if (q && hay.indexOf(q) === -1) ok = false;
      if (lvl !== "ALL" && tl !== lvl) ok = false;
      if (side !== "ALL" && sd !== side) ok = false;
      if (iaf === "YES" && hasIA !== "1") ok = false;
      if (iaf === "NO"  && hasIA !== "0") ok = false;
      if (mktF !== "ALL" && mk !== mktF) ok = false;
      if (gidF !== "ALL" && gid !== gidF) ok = false;

      if (p1Sel !== "ALL"){
        if (p1c===null || p1c < p1Min || !p1l) ok = false;
      }

      tr.style.display = ok ? "" : "none";

      const p1btn = tr.querySelector(".tpnbaf-add-p1");
      if (p1btn){
        const eligibleNow = (p1c!==null && p1c >= p1Min && !!p1l);
        p1btn.disabled = !eligibleNow;
      }
    });

    updateLoadMoreBtn();
    renderAll();
    applySort(); // ✅ conserve le tri après filtre / load more
  }

  document.addEventListener("input",(e)=>{ if (e.target && (e.target.id==="tpnbafQ")) applyFilters(); });
  document.addEventListener("change",(e)=>{
    if (e.target && (
      e.target.id==="tpnbafLvl" || e.target.id==="tpnbafSide" || e.target.id==="tpnbafIA" ||
      e.target.id==="tpnbafP1Min" || e.target.id==="tpnbafMarket" || e.target.id==="tpnbafMatch"
    )) applyFilters();
  });

  document.addEventListener("click",(e)=>{
    const b = e.target.closest("#tpnbafRefresh");
    if (!b) return;
    const url = b.getAttribute("data-url") || "";
    if (url) window.location.href = url;
    else window.location.reload();
  });

  document.addEventListener("click", async (e)=>{
    const b = e.target.closest("#tpnbafLoadMore");
    if (!b || b.disabled) return;

    const gid = getSelectedMatchGid();
    if (gid === "ALL") return;

    if (rawOffsets[gid] === undefined) rawOffsets[gid] = MAX_ROWS_GAME;

    const offset = rawOffsets[gid];
    const limit = parseInt(b.getAttribute("data-limit") || "200", 10) || 200;

    const sel = document.querySelector("#tpnbafMatch");
    const opt = sel?.selectedOptions?.[0];
    const matchText = opt ? (opt.textContent || "") : "";
    const time = opt ? (opt.getAttribute("data-time") || "") : "";

    b.disabled = true;
    const old = b.textContent;
    b.textContent = "⏳ Chargement…";

    try{
      const params = new URLSearchParams();
      params.set("action","tpnbaf6_more");
      params.set("nonce", AJAX_NONCE);
      params.set("gid", gid);
      params.set("league", String(CFG.league||""));
      params.set("season", String(CFG.season||""));
      params.set("bookmaker", String(CFG.bookmaker||""));
      params.set("offset", String(offset));
      params.set("limit", String(limit));
      params.set("min_minutes", String(CFG.minMinutes||""));
      params.set("min_conf", String(CFG.minConf||""));
      params.set("min_games", String(CFG.minGames||""));
      params.set("match", matchText);
      params.set("time", time);

      const res = await fetch(AJAX_URL, {
        method: "POST",
        headers: {"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},
        body: params.toString()
      });

      const data = await res.json();
      if (!data || !data.ok) throw new Error((data && data.msg) ? data.msg : "Erreur AJAX");

      const tbody = document.querySelector(".tpnbaf-table tbody");
      const rows = (data.rows_html || []);

      if (tbody && rows.length){
        rows.forEach(html=>{
          const tmp = document.createElement("tbody");
          tmp.innerHTML = html.trim();
          const tr = tmp.querySelector("tr");
          if (tr) tbody.appendChild(tr);

          const mk = tr ? (tr.getAttribute("data-market")||"") : "";
          if (mk){
            const msel = document.querySelector("#tpnbafMarket");
            if (msel && !Array.from(msel.options).some(o=>o.value===mk)){
              const o = document.createElement("option");
              o.value = mk;
              o.textContent = mk;
              msel.appendChild(o);
            }
          }
        });
      }

      rawOffsets[gid] = (data.next_offset !== undefined) ? data.next_offset : (offset + limit);

      if (data.done){
        b.textContent = "✅ Plus rien à charger";
        b.disabled = true;
      } else {
        b.textContent = old;
        b.disabled = false;
      }

      applyFilters();

    } catch(err){
      b.textContent = "❌ Erreur";
      setTimeout(()=>{
        b.textContent = old;
        b.disabled = false;
      }, 1200);
    }
  });

  window.addEventListener("DOMContentLoaded", ()=>{
    renderAll();
    applyFilters();
    updateLoadMoreBtn();
  });
})();
JS;

    echo "<script>{$js}</script>";
  }
}

/* =========================
   Helpers: normalisation
   ========================= */
if (!function_exists('tpnbaf6_lower')) {
  function tpnbaf6_lower(string $s): string {
    $s = trim($s);
    if (function_exists('mb_strtolower')) return mb_strtolower($s, 'UTF-8');
    return strtolower($s);
  }
}

if (!function_exists('tpnbaf6_norm_player')) {
  function tpnbaf6_norm_player(string $name): string {
    $n = tpnbaf6_lower($name);
    $n = str_replace(['’','ʼ','`'], "'", $n);
    $n = str_replace('.', '', $n);
    $n = preg_replace('~\s+~',' ', $n);
    return trim((string)$n);
  }
}

if (!function_exists('tpnbaf6_norm_market')) {
  function tpnbaf6_norm_market(string $m): string {
    $x = strtoupper(trim($m));
    $x = str_replace(['PTS+PAS','PTS+AST'], 'PTS+AST', $x);
    if ($x === 'PAS') $x = 'AST';
    if ($x === 'RP')  $x = 'RA';
    if ($x === 'PRA') $x = 'PRP';
    return $x;
  }
}

/* =========================
   ✅ Dash lines map + palier inférieur réel (PRP 44.5/49.5)
   ========================= */
if (!function_exists('tpnbaf6_dash_lines_map')) {
  function tpnbaf6_dash_lines_map(): array {
    // 1) depuis dashboard si dispo
    $map = [];
    if (function_exists('tp_nba_get_lines_map')) {
      $m = tp_nba_get_lines_map();
      if (is_array($m)) $map = $m;
    }

    // 2) fallback interne (copie “dashboard” + PRP 44.5/49.5)
    if (empty($map)) {
      $map = [
        'PTS' => [4.5,9.5,14.5,19.5,24.5,29.5],
        'REB' => [0.5,2.5,4.5,6.5,8.5,10.5,12.5],
        'AST' => [0.5,2.5,4.5,6.5,8.5,10.5,12.5],
        '3PM' => [0.5,1.5,2.5,3.5,4.5],
        'PRP' => [9.5,14.5,19.5,24.5,29.5,34.5,39.5,44.5,49.5],
        'PR'  => [9.5,14.5,19.5,24.5,29.5,34.5],
        'PA'  => [9.5,14.5,19.5,24.5,29.5],
        'RA'  => [4.5,6.5,8.5,10.5,12.5,14.5],
        'STL' => [0.5,1.5,2.5,3.5,4.5],
        'BLK' => [0.5,1.5,2.5,3.5,4.5],
      ];
    }

    // Aliases utiles
    if (!empty($map['PA']) && empty($map['PTS+AST'])) $map['PTS+AST'] = $map['PA'];
    if (!empty($map['PTS+AST']) && empty($map['PA'])) $map['PA'] = $map['PTS+AST'];

    return $map;
  }
}

if (!function_exists('tpnbaf6_dash_lower_line')) {
  function tpnbaf6_dash_lower_line(string $market, float $line): ?float {
    $m = tpnbaf6_norm_market($market);
    $map = tpnbaf6_dash_lines_map();
    $list = $map[$m] ?? null;

    if (!is_array($list) || empty($list)) return null;

    $vals = array_map('floatval', $list);
    sort($vals, SORT_NUMERIC);

    $best = null;
    foreach ($vals as $v){
      if ($v < $line) $best = $v;
      else break;
    }
    return ($best !== null && $best > 0) ? round((float)$best, 1) : null;
  }
}

if (!function_exists('tpnbaf6_level')) {
  function tpnbaf6_level(?int $pct): array {
    if ($pct === null) return ['key'=>'LOW','label'=>'Faible','cls'=>'tpnbaf-chip tpnbaf-low'];
    if ($pct >= 75)     return ['key'=>'HIGH','label'=>'Forte','cls'=>'tpnbaf-chip tpnbaf-high'];
    if ($pct >= 60)     return ['key'=>'MED','label'=>'Moyenne','cls'=>'tpnbaf-chip tpnbaf-med'];
    return ['key'=>'LOW','label'=>'Faible','cls'=>'tpnbaf-chip tpnbaf-low'];
  }
}

/* =========================
   Dashboard hist global (souple)
   ========================= */
if (!function_exists('tpnbaf6_get_hist_global')) {
  function tpnbaf6_get_hist_global(): array {
    if (function_exists('tp_nba_get_hist_global')) {
      $h = tp_nba_get_hist_global();
      return is_array($h) ? $h : [];
    }
    $h = get_transient('tp_hist_global_cache_2h');
    if ($h !== false && is_array($h)) return $h;
    $o = get_option('tp_hist_global_cache_2h');
    return is_array($o) ? $o : [];
  }
}

if (!function_exists('tpnbaf6_find_hist_key')) {
  function tpnbaf6_find_hist_key(array $hist, string $player, string $market): ?string {
    $key = tpnbaf6_norm_player($player);
    $candidates = [];
    if ($key !== '') $candidates[] = $key;

    $parts = preg_split('/\s+/', $key);
    if (count($parts) >= 2) {
      $rev = trim(implode(' ', array_reverse($parts)));
      if ($rev && $rev !== $key) $candidates[] = $rev;
    }

    foreach ($candidates as $k) {
      if (!empty($hist[$k][$market]) && is_array($hist[$k][$market])) return $k;
    }

    if (count($parts) >= 2) {
      $first = $parts[0];
      $last  = $parts[count($parts)-1];
      foreach ($hist as $k => $per) {
        if (empty($per[$market]) || !is_array($per[$market])) continue;
        $kp = preg_split('/\s+/', (string)$k);
        if (in_array($first,$kp,true) && in_array($last,$kp,true)) return $k;
      }
    }

    return null;
  }
}

if (!function_exists('tpnbaf6_get_series')) {
  function tpnbaf6_get_series(string $player, string $market): array {
    $hist = tpnbaf6_get_hist_global();
    if (empty($hist)) return [];

    $mkt = tpnbaf6_norm_market($market);

    $k = tpnbaf6_find_hist_key($hist, $player, $mkt);
    if ($k && !empty($hist[$k][$mkt]) && is_array($hist[$k][$mkt])) {
      return array_map('floatval', array_values($hist[$k][$mkt]));
    }

    $kPts = tpnbaf6_find_hist_key($hist, $player, 'PTS');
    $kReb = tpnbaf6_find_hist_key($hist, $player, 'REB');
    $kAst = tpnbaf6_find_hist_key($hist, $player, 'AST');

    if ($mkt === 'PTS+AST' && $kPts && $kAst) {
      $a = array_values($hist[$kPts]['PTS'] ?? []);
      $b = array_values($hist[$kAst]['AST'] ?? []);
      $n = min(count($a), count($b));
      if ($n >= 3){
        $series = [];
        for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i];
        return $series;
      }
    }

    if ($mkt === 'PRP' && $kPts && $kReb && $kAst) {
      $a = array_values($hist[$kPts]['PTS'] ?? []);
      $b = array_values($hist[$kReb]['REB'] ?? []);
      $c = array_values($hist[$kAst]['AST'] ?? []);
      $n = min(count($a), count($b), count($c));
      if ($n >= 3){
        $series = [];
        for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i] + (float)$c[$i];
        return $series;
      }
    }

    if ($mkt === 'PR' && $kPts && $kReb) {
      $a = array_values($hist[$kPts]['PTS'] ?? []);
      $b = array_values($hist[$kReb]['REB'] ?? []);
      $n = min(count($a), count($b));
      if ($n >= 3){
        $series = [];
        for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i];
        return $series;
      }
    }

    if ($mkt === 'RA' && $kReb && $kAst) {
      $a = array_values($hist[$kReb]['REB'] ?? []);
      $b = array_values($hist[$kAst]['AST'] ?? []);
      $n = min(count($a), count($b));
      if ($n >= 3){
        $series = [];
        for($i=0;$i<$n;$i++) $series[] = (float)$a[$i] + (float)$b[$i];
        return $series;
      }
    }

    return [];
  }
}

if (!function_exists('tpnbaf6_conf_stats')) {
  function tpnbaf6_conf_stats(string $player, string $market, float $line): array {
    $series = tpnbaf6_get_series($player, $market);
    if (!is_array($series) || count($series) < 3) {
      return ['over'=>null,'under'=>null,'n'=>0,'avg'=>null,'ho'=>0,'hu'=>0];
    }

    $n = count($series);
    $sum = 0.0; $ho = 0; $hu = 0;

    foreach ($series as $v){
      $v = (float)$v;
      $sum += $v;
      if ($v > $line) $ho++;
      if ($v < $line) $hu++;
    }

    $avg  = $n ? round($sum / $n, 1) : null;
    $over = $n ? (int)round(100 * $ho / $n) : null;
    $under= $n ? (int)round(100 * $hu / $n) : null;

    return ['over'=>$over,'under'=>$under,'n'=>$n,'avg'=>$avg,'ho'=>$ho,'hu'=>$hu];
  }
}

if (!function_exists('tpnbaf6_minutes_avg')) {
  function tpnbaf6_minutes_avg(string $player): ?float {
    $series = tpnbaf6_get_series($player, 'MIN');
    if (!is_array($series) || count($series) < 3) {
      $series = tpnbaf6_get_series($player, 'MINUTES');
    }
    if (!is_array($series) || count($series) < 3) return null;

    $n= count($series); $sum=0.0;
    foreach ($series as $v) $sum += (float)$v;
    return $n ? round($sum/$n, 1) : null;
  }
}

if (!function_exists('tpnbaf6_parse_odd')) {
  function tpnbaf6_parse_odd($s): float {
    if (is_float($s) || is_int($s)) return (float)$s;
    $x = (string)$s;
    $x = preg_replace('~[^0-9,\.]~','', $x);
    $x = str_replace(',', '.', $x);
    return (float)$x;
  }
}

/* Snapshots joueurs IA */
if (!function_exists('tpnbaf6_get_any_cache_array')) {
  function tpnbaf6_get_any_cache_array(string $key): ?array {
    $t = get_transient($key);
    if (is_array($t)) return $t;
    $o = get_option($key);
    return is_array($o) ? $o : null;
  }
}

if (!function_exists('tpnbaf6_get_player_analyzer_snapshot')) {
  function tpnbaf6_get_player_analyzer_snapshot(int $game_id): ?array {
    if (!$game_id) return null;

    $arr = null;

    if (function_exists('tp_pre_cache_get')) {
      $s = tp_pre_cache_get($game_id);
      if (is_array($s) && isset($s['combined']) && is_array($s['combined'])) $arr = $s;
    }
    if (!$arr) {
      $arr = tpnbaf6_get_any_cache_array('tp_pre_'.$game_id);
      if (!(is_array($arr) && isset($arr['combined']) && is_array($arr['combined']))) $arr = null;
    }

    return $arr;
  }
}

if (!function_exists('tpnbaf6_build_player_pred_map')) {
  function tpnbaf6_build_player_pred_map(array $combined): array {
    $map = [
      '_by_last' => [],
      '_by_init_last' => [],
    ];

    foreach ($combined as $k => $pl){
      if (!is_array($pl)) continue;
      $name = (string)($pl['name'] ?? $k);
      $norm = tpnbaf6_norm_player($name);
      if ($norm === '') continue;

      $g = (int)($pl['games'] ?? 0);
      if ($g <= 0) continue;

      $tp = (float)($pl['total_points'] ?? 0);
      $tr = (float)($pl['total_rebounds'] ?? 0);
      $ta = (float)($pl['total_assists'] ?? 0);

      $pts = (int)floor($tp / $g);
      $reb = (int)floor($tr / $g);
      $ast = (int)floor($ta / $g);
      $prp = $pts + $reb + $ast;

      $a3 = null;
      if (!empty($pl['avg_3pts'])) {
        $a3 = (float)$pl['avg_3pts'];
      } elseif (!empty($pl['avg_3pts_count']) && !empty($pl['total_avg_3pts']) && (int)$pl['avg_3pts_count'] > 0) {
        $a3 = round(((float)$pl['total_avg_3pts']) / (int)$pl['avg_3pts_count'], 1);
      }

      $min = null;
      if (isset($pl['avg_minutes']) && is_numeric($pl['avg_minutes'])) {
        $min = (float)$pl['avg_minutes'];
      }

      $map[$norm] = [
        'name' => $name,
        'PTS'  => $pts,
        'REB'  => $reb,
        'AST'  => $ast,
        'PRP'  => $prp,
        '3PM'  => $a3,
        'MIN'  => $min,
        'G'    => $g,
      ];

      $parts = preg_split('/\s+/', $norm);
      $first = $parts[0] ?? '';
      $last  = $parts[count($parts)-1] ?? '';
      if ($last){
        $map['_by_last'][$last][] = $norm;
        if ($first){
          $init = substr($first, 0, 1);
          $key  = $init.' '.$last;
          $map['_by_init_last'][$key][] = $norm;
        }
      }
    }

    return $map;
  }
}

if (!function_exists('tpnbaf6_find_player_pred_key')) {
  function tpnbaf6_find_player_pred_key(array $predMap, string $player): ?string {
    $p = tpnbaf6_norm_player($player);
    if ($p && isset($predMap[$p])) return $p;

    $parts = preg_split('/\s+/', $p);
    $first = $parts[0] ?? '';
    $last  = $parts[count($parts)-1] ?? '';

    if ($first && $last){
      $key = substr($first,0,1).' '.$last;
      $list = $predMap['_by_init_last'][$key] ?? [];
      if (count($list) === 1) return $list[0];
    }
    if ($last){
      $list = $predMap['_by_last'][$last] ?? [];
      if (count($list) === 1) return $list[0];
    }

    if ($first && $last){
      foreach ($predMap as $k => $_){
        if ($k === '_by_last' || $k === '_by_init_last') continue;
        if (strpos($k, $first) !== false && strpos($k, $last) !== false) return $k;
      }
    }
    return null;
  }
}

if (!function_exists('tpnbaf6_player_pred_value')) {
  function tpnbaf6_player_pred_value(array $predMap, string $player, string $market): ?float {
    $k = tpnbaf6_find_player_pred_key($predMap, $player);
    if (!$k || empty($predMap[$k]) || !is_array($predMap[$k])) return null;

    $m = tpnbaf6_norm_market($market);

    $pts = $predMap[$k]['PTS'] ?? null;
    $reb = $predMap[$k]['REB'] ?? null;
    $ast = $predMap[$k]['AST'] ?? null;

    switch ($m){
      case 'PTS': return is_numeric($pts) ? (float)$pts : null;
      case 'REB': return is_numeric($reb) ? (float)$reb : null;
      case 'AST': return is_numeric($ast) ? (float)$ast : null;
      case '3PM': return isset($predMap[$k]['3PM']) && $predMap[$k]['3PM'] !== null ? (float)$predMap[$k]['3PM'] : null;
      case 'PRP':
        if (is_numeric($predMap[$k]['PRP'] ?? null)) return (float)$predMap[$k]['PRP'];
        if (is_numeric($pts) && is_numeric($reb) && is_numeric($ast)) return (float)$pts + (float)$reb + (float)$ast;
        return null;
      case 'PTS+AST':
        if (is_numeric($pts) && is_numeric($ast)) return (float)$pts + (float)$ast;
        return null;
      case 'PR':
        if (is_numeric($pts) && is_numeric($reb)) return (float)$pts + (float)$reb;
        return null;
      case 'RA':
        if (is_numeric($reb) && is_numeric($ast)) return (float)$reb + (float)$ast;
        return null;
      default:
        return null;
    }
  }
}

if (!function_exists('tpnbaf6_player_minutes_pred')) {
  function tpnbaf6_player_minutes_pred(array $predMap, string $player): ?float {
    $k = tpnbaf6_find_player_pred_key($predMap, $player);
    if (!$k || empty($predMap[$k]) || !is_array($predMap[$k])) return null;
    $min = $predMap[$k]['MIN'] ?? null;
    return (is_numeric($min) ? (float)$min : null);
  }
}

/* Scoring */
if (!function_exists('tpnbaf6_market_weight')) {
  function tpnbaf6_market_weight(string $market): float {
    $m = tpnbaf6_norm_market($market);
    switch ($m){
      case 'PTS':
      case 'REB':
      case 'AST':
        return 1.00;
      case 'PR':
      case 'RA':
      case 'PTS+AST':
        return 0.92;
      case 'PRP':
        return 0.85;
      case '3PM':
        return 0.75;
      default:
        return 0.90;
    }
  }
}

if (!function_exists('tpnbaf6_align_bonus')) {
  function tpnbaf6_align_bonus(string $side, ?float $deltaIA): float {
    if ($deltaIA === null) return 0.0;

    $d = (float)$deltaIA;
    if ($side === 'Over'){
      if ($d >= 0.0) return 8.0;
      if ($d >= -1.0) return 4.0;
      return -8.0;
    } else {
      if ($d <= 0.0) return 8.0;
      if ($d <= 1.0) return 4.0;
      return -8.0;
    }
  }
}

if (!function_exists('tpnbaf6_compute_score')) {
  function tpnbaf6_compute_score(array $r): float {
    $conf = (float)($r['conf'] ?? 0);
    $edge = ($r['edge_pp'] !== null) ? (float)$r['edge_pp'] : 0.0;
    $ev   = ($r['ev_pct'] !== null)  ? (float)$r['ev_pct']  : 0.0;
    $n    = (int)($r['n'] ?? 0);
    $min  = ($r['min_use'] !== null) ? (float)$r['min_use'] : 0.0;

    $edge = max(-10.0, min(20.0, $edge));
    $ev   = max(-15.0, min(60.0, $ev));

    $sampleBonus = 0.0;
    if ($n >= 12) $sampleBonus = min(6.0, ($n - 12) * 0.30);

    $minBonus = 0.0;
    if ($min >= 25) $minBonus = min(4.0, ($min - 25) * 0.25);

    $align = tpnbaf6_align_bonus((string)($r['side'] ?? ''), $r['delta_ia'] ?? null);
    $palBonus = (!empty($r['pal1_conf']) && (int)$r['pal1_conf'] >= 85) ? 3.0 : 0.0;

    $base = $conf
      + (1.2 * $edge)
      + (0.35 * $ev)
      + $align
      + $sampleBonus
      + $minBonus
      + $palBonus;

    $w = tpnbaf6_market_weight((string)($r['market'] ?? ''));
    return round($base * $w, 1);
  }
}

/* =========================
   Render Shortcode
   ========================= */
if (!function_exists('tpnbaf6_render_shortcode')) {
  function tpnbaf6_render_shortcode($atts = []){
    $atts = shortcode_atts([
      'season' => defined('TP_BASK_SEASON') ? TP_BASK_SEASON : '2025-2026',
      'league' => defined('TP_BASK_LEAGUE') ? TP_BASK_LEAGUE : 12,
      'bookmaker' => 4,
      'ttl' => 600,
      'max_rows_per_game' => 240,
      'player_pred_ttl' => 6 * HOUR_IN_SECONDS,
      'min_minutes' => 25,
      'min_conf'    => 10,
      'min_games'   => 12,
      'max_legs'    => 10, // ✅ jusqu’à 10 legs
    ], $atts, 'tp_nba_spot_fusion');

    $league = (int)$atts['league'];
    $season = (string)$atts['season'];
    $bk     = (int)$atts['bookmaker'];
    $ttl    = max(60, (int)$atts['ttl']);
    $maxRowsGame   = max(60, (int)$atts['max_rows_per_game']);
    $playerPredTtl = max(300, (int)$atts['player_pred_ttl']);

    $minMinutes = (float)$atts['min_minutes'];
    $minConf    = (int)$atts['min_conf'];
    $minGames   = (int)$atts['min_games'];
    $maxLegs    = max(1, (int)$atts['max_legs']);
    if ($maxLegs > 20) $maxLegs = 20; // garde-fou

    if (!function_exists('tpnbap_get_player_props_ou') || !function_exists('tpnbap_player_props_rows')) {
      return '<div class="tpnbaf-wrap"><div class="tpnbaf-card"><b>⚠️ tp_nba_props non détecté.</b><br>Il faut que ton analyseur [tp_nba_props] soit actif.</div></div>';
    }

    // matches
    if (function_exists('tpnbap_get_matches_next_days')) {
      $matches = tpnbap_get_matches_next_days($league, $season);
    } elseif (function_exists('get_nba_matches_on_next_2_days')) {
      $matches = get_nba_matches_on_next_2_days((string)$league, $season);
      $matches = array_map(function($m){
        return [
          'id' => (int)($m['id'] ?? 0),
          'ts' => (int)($m['date_ts'] ?? 0),
          'teams'  => $m['teams'] ?? [],
          'status' => $m['status'] ?? [],
        ];
      }, $matches);
    } else {
      return '<div class="tpnbaf-wrap"><div class="tpnbaf-card"><b>⚠️ Impossible de récupérer les matchs.</b></div></div>';
    }

    if (empty($matches)) {
      return '<div class="tpnbaf-wrap"><div class="tpnbaf-card">Aucun match NBA à venir trouvé.</div></div>';
    }

    $ck = 'tpnbaf6_html_'.md5(wp_json_encode([$league,$season,$bk,$minMinutes,$minConf,$minGames,$maxLegs,date_i18n('Ymd')]));
    $force = (!empty($_GET['tpnbaf6_refresh']) && !empty($_GET['_tpnbaf6_nonce']) && wp_verify_nonce((string)$_GET['_tpnbaf6_nonce'], 'tpnbaf6_refresh'));
    if ($force) delete_transient($ck);

    // ✅ IMPORTANT: CSS/JS doivent être imprimés même si HTML vient du cache
    tpnbaf6_css_once();
    tpnbaf6_js_once();

    $cached = get_transient($ck);
    if (!$force && $cached !== false) return $cached;

    $rowsAll = [];
    $matchMap = [];   // gid => ['label'=>..., 'time'=>...]
    $marketSet = [];  // market => true

    foreach ($matches as $m){
      $gid = (int)($m['id'] ?? 0);
      if (!$gid) continue;

      $status = strtoupper($m['status']['short'] ?? '');
      if ($status && $status !== 'NS') continue;

      $home = $m['teams']['home'] ?? [];
      $away = $m['teams']['away'] ?? [];
      $homeN = (string)($home['name'] ?? 'Home');
      $awayN = (string)($away['name'] ?? 'Away');

      $ts = (int)($m['ts'] ?? 0);
      $timeLabel = '';
      if ($ts > 0 && function_exists('tpnbap_format_fr_datetime')) {
        $fr = tpnbap_format_fr_datetime($ts);
        $timeLabel = trim(($fr['time'] ?? '').' '.($fr['date'] ?? ''));
      }

      $matchLabel = $homeN.' vs '.$awayN;
      $matchMap[(string)$gid] = ['label'=>$matchLabel, 'time'=>$timeLabel];

      $propsOut = tpnbap_get_player_props_ou($gid, $season, $bk);
      if (empty($propsOut)) continue;

      $rows = tpnbap_player_props_rows($propsOut, (int)$maxRowsGame);
      if (empty($rows)) continue;

      $predCacheKey = 'tpnbaf6_playerpred_'.$gid;
      $playerPredMap = get_transient($predCacheKey);

      if (!is_array($playerPredMap)) {
        $snapPlayers = tpnbaf6_get_player_analyzer_snapshot($gid);
        if (is_array($snapPlayers) && !empty($snapPlayers['combined']) && is_array($snapPlayers['combined'])) {
          $playerPredMap = tpnbaf6_build_player_pred_map($snapPlayers['combined']);
          set_transient($predCacheKey, $playerPredMap, $playerPredTtl);
        } else {
          $playerPredMap = [];
          set_transient($predCacheKey, $playerPredMap, 10 * MINUTE_IN_SECONDS);
        }
      }

      foreach ($rows as $r){
        $player = (string)($r['player'] ?? '');
        $market = tpnbaf6_norm_market((string)($r['market'] ?? ''));
        $line   = (float)($r['line'] ?? 0);

        if ($player === '' || $market === '' || $line <= 0) continue;

        $oddOver  = tpnbaf6_parse_odd($r['over'] ?? 0);
        $oddUnder = tpnbaf6_parse_odd($r['under'] ?? 0);

        $st = tpnbaf6_conf_stats($player, $market, $line);
        $co = $st['over'];
        $cu = $st['under'];
        $n  = (int)($st['n'] ?? 0);
        $avg= $st['avg'];

        if ($co === null && $cu === null) continue;

        $side = 'Over';
        $conf = $co;
        $odd  = $oddOver;

        if ($co === null) { $side='Under'; $conf=$cu; $odd=$oddUnder; }
        elseif ($cu !== null && $cu > $co) { $side='Under'; $conf=$cu; $odd=$oddUnder; }

        if (($odd <= 0) && (($side==='Over' && $oddUnder>0) || ($side==='Under' && $oddOver>0))) {
          $side = ($side==='Over') ? 'Under' : 'Over';
          $conf = ($side==='Over') ? $co : $cu;
          $odd  = ($side==='Over') ? $oddOver : $oddUnder;
        }

        if ($conf === null || $odd <= 0) continue;

        if ((int)$conf < $minConf) continue;
        if ($n < $minGames) continue;

        $lvl = tpnbaf6_level((int)$conf);

        // ✅ Palier 1 (Dash) = palier inférieur réel
        $pal1Txt  = '—';
        $pal1Conf = null;
        $pal1Line = null;
        if ($side === 'Over') {
          $lowerLine = tpnbaf6_dash_lower_line($market, $line);
          if ($lowerLine !== null) {
            $st2 = tpnbaf6_conf_stats($player, $market, (float)$lowerLine);
            $pal1Conf = $st2['over'];
            $pal1Line = (float)$lowerLine;
            if ($pal1Conf !== null) $pal1Txt = number_format((float)$lowerLine, 1, '.', '').' • '.$pal1Conf.'%';
          }
        }

        $minHist = tpnbaf6_minutes_avg($player);
        $minPred = (!empty($playerPredMap) ? tpnbaf6_player_minutes_pred($playerPredMap, $player) : null);
        $minUse  = ($minHist !== null) ? $minHist : $minPred;
        if ($minUse === null || (float)$minUse < $minMinutes) continue;

        $predIA = null;
        if (is_array($playerPredMap) && !empty($playerPredMap)) {
          $predIA = tpnbaf6_player_pred_value($playerPredMap, $player, $market);
        }
        $deltaIA = null;
        if ($predIA !== null) $deltaIA = (float)$predIA - (float)$line;

        $implied = (100.0 / (float)$odd);
        $edgePP  = ((float)$conf - (float)$implied);
        $evPct   = (((float)$conf/100.0) * (float)$odd - 1.0) * 100.0;

        $hay = strtolower($matchLabel.' '.$timeLabel.' '.$player.' '.$market.' '.$side);

        $row = [
          'gid'=>$gid,'match'=>$matchLabel,'time'=>$timeLabel,'player'=>$player,
          'player_norm'=>tpnbaf6_norm_player($player),
          'market'=>$market,'line'=>$line,'side'=>$side,'odd'=>$odd,'conf'=>(int)$conf,
          'lvl_key'=>$lvl['key'],'lvl_label'=>$lvl['label'],'lvl_cls'=>$lvl['cls'],
          'co'=>$co,'cu'=>$cu,'avg'=>$avg,'n'=>$n,
          'min_hist'=>$minHist,'min_pred'=>$minPred,'min_use'=>$minUse,
          'pred_ia'=>$predIA,'delta_ia'=>$deltaIA,
          'pal1_txt'=>$pal1Txt,'pal1_conf'=>$pal1Conf,'pal1_line'=>$pal1Line,
          'edge_pp'=>$edgePP,'ev_pct'=>$evPct,'hay'=>$hay,
        ];
        $row['score'] = tpnbaf6_compute_score($row);

        $rowsAll[] = $row;
        $marketSet[$market] = true;
      }
    }

    if (empty($rowsAll)) {
      $html = '<div class="tpnbaf-wrap"><div class="tpnbaf-card">Aucun spot éligible (min '.$minMinutes.' min, conf ≥ '.$minConf.'%, matchs ≥ '.$minGames.').</div></div>';
      set_transient($ck, $html, $ttl);
      return $html;
    }

    usort($rowsAll, function($a,$b){
      if ($b['score'] !== $a['score']) return $b['score'] <=> $a['score'];
      if ($b['conf'] !== $a['conf']) return $b['conf'] <=> $a['conf'];
      return strcmp($a['match'], $b['match']);
    });

    $marketOptions = array_keys($marketSet);
    sort($marketOptions);

    $matchOptions = $matchMap;

    $current_url = (is_ssl() ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? '') . ($_SERVER['REQUEST_URI'] ?? '');
    $base_url = remove_query_arg(['tpnbaf6_refresh','_tpnbaf6_nonce'], $current_url);
    $refresh_url = add_query_arg([
      'tpnbaf6_refresh' => 1,
      '_tpnbaf6_nonce'  => wp_create_nonce('tpnbaf6_refresh'),
    ], $base_url);

    $ajaxData = [
      'url' => admin_url('admin-ajax.php'),
      'nonce' => wp_create_nonce('tpnbaf6_more'),
      'cfg' => [
        'league' => $league,
        'season' => $season,
        'bookmaker' => $bk,
        'maxRowsGame' => $maxRowsGame,
        'minMinutes' => $minMinutes,
        'minConf' => $minConf,
        'minGames' => $minGames,
        'maxLegs' => $maxLegs, // ✅
      ]
    ];

    ob_start();
    echo '<div class="tpnbaf-wrap">';
    echo '<script>window.tpnbaf6_ajax = '.wp_json_encode($ajaxData).';</script>';

    echo '<div class="tpnbaf-head">';
      echo '<div>';
        echo '<div class="tpnbaf-title">🏀 Fusion Spots NBA</div>';
        echo '<div class="tpnbaf-sub">'.count($rowsAll).' spots éligibles (min '.$minMinutes.'m • conf ≥ '.$minConf.'% • matchs ≥ '.$minGames.' • max '.$maxLegs.' legs)</div>';
      echo '</div>';

      echo '<div class="tpnbaf-filters">';
        echo '<button id="tpnbafRefresh" class="tpnbaf-btn tpnbaf-btn-refresh" type="button" data-url="'.esc_url($refresh_url).'">🔄 Rafraîchir</button>';
        echo '<input id="tpnbafQ" type="search" placeholder="Recherche (joueur, match, marché...)">';

        echo '<select id="tpnbafMatch">';
          echo '<option value="ALL">Tous les matchs</option>';
          foreach ($matchOptions as $gidStr => $mm){
            $label = (string)($mm['label'] ?? '');
            $t = (string)($mm['time'] ?? '');
            $optLabel = trim($label . ($t ? (' • '.$t) : ''));
            echo '<option value="'.esc_attr((string)$gidStr).'" data-time="'.esc_attr($t).'">'.esc_html($optLabel).'</option>';
          }
        echo '</select>';

        echo '<button id="tpnbafLoadMore" class="tpnbaf-btn tpnbaf-btn-more" type="button" data-limit="200" disabled>➕ Charger plus (sélectionne un match)</button>';

        echo '<select id="tpnbafMarket">';
          echo '<option value="ALL">Tous marchés</option>';
          foreach ($marketOptions as $mk){
            echo '<option value="'.esc_attr($mk).'">'.esc_html($mk).'</option>';
          }
        echo '</select>';

        echo '<select id="tpnbafLvl">
                <option value="ALL">Tous niveaux</option>
                <option value="HIGH">Confiance forte (≥75%)</option>
                <option value="MED">Confiance moyenne (60–74%)</option>
                <option value="LOW">Confiance faible (≤59%)</option>
              </select>';

        echo '<select id="tpnbafSide">
                <option value="ALL">Over+Under</option>
                <option value="Over">Over</option>
                <option value="Under">Under</option>
              </select>';

        echo '<select id="tpnbafIA">
                <option value="ALL">Prédiction IA : Tout</option>
                <option value="YES">Prédiction IA : Oui</option>
                <option value="NO">Prédiction IA : Non</option>
              </select>';

        echo '<select id="tpnbafP1Min">
                <option value="ALL">Palier 1 Dash : Tous</option>
                <option value="80">Palier 1 Dash ≥80%</option>
                <option value="85" selected>Palier 1 Dash ≥85%</option>
                <option value="90">Palier 1 Dash ≥90%</option>
                <option value="95">Palier 1 Dash ≥95%</option>
              </select>';

      echo '</div>';
    echo '</div>';

    echo '<div class="tpnbaf-card">';
      echo '<div class="tpnbaf-tablewrap">';
        echo '<table class="tpnbaf-table">';
          echo '<thead><tr>
            <th>Match</th>
            <th>Joueur</th>
            <th>Marché</th>
            <th>Pick</th>
            <th class="tpnbaf-sortable" data-sort="line">Ligne</th>
            <th class="tpnbaf-sortable" data-sort="odd">Cote</th>
            <th class="tpnbaf-sortable" data-sort="conf">Confiance</th>
            <th class="tpnbaf-sortable" data-sort="level">Niveau</th>
            <th class="tpnbaf-sortable" data-sort="avg">Palier (moy)</th>
            <th class="tpnbaf-sortable" data-sort="n">Matchs</th>
            <th class="tpnbaf-sortable" data-sort="min">Min/m</th>
            <th class="tpnbaf-sortable" data-sort="ia">Prédiction IA</th>
            <th class="tpnbaf-sortable" data-sort="delta">Δ IA</th>
            <th class="tpnbaf-sortable" data-sort="pal1">Palier 1 (Dash)</th>
            <th class="tpnbaf-sortable" data-sort="edge">Edge</th>
            <th class="tpnbaf-sortable" data-sort="ev">Value</th>
            <th class="tpnbaf-sortable" data-sort="score">Score</th>
            <th>Ticket</th>
          </tr></thead><tbody>';

          foreach ($rowsAll as $i => $r){
            $id = $r['gid'].'_'.$i.'_'.substr(md5($r['player'].$r['market'].$r['line'].$r['side']),0,6);

            $mktLabel = $r['market'];
            $labels = ['AST'=>'PAS','PTS+AST'=>'PTS+PAS','RA'=>'RP','PRP'=>'PRP'];
            if (isset($labels[$mktLabel])) $mktLabel = $labels[$mktLabel];

            $oddTxt = ($r['odd']>0) ? number_format((float)$r['odd'], 2, '.', '') : '-';

            $details = [];
            if ($r['co'] !== null) $details[] = 'Over '.$r['co'].'%';
            if ($r['cu'] !== null) $details[] = 'Under '.$r['cu'].'%';

            $avgTxt = ($r['avg'] !== null) ? number_format((float)$r['avg'], 1, '.', '') : '—';
            $nTxt   = ($r['n'] ?? 0) ? (int)$r['n'] : '—';

            $minTxt = '—';
            if ($r['min_hist'] !== null) $minTxt = number_format((float)$r['min_hist'], 1, '.', '');
            elseif ($r['min_pred'] !== null) $minTxt = number_format((float)$r['min_pred'], 1, '.', '').'*';

            $iaTxt = ($r['pred_ia'] !== null) ? number_format((float)$r['pred_ia'], 1, '.', '') : '—';
            $hasIA = ($r['pred_ia'] !== null) ? 1 : 0;

            $dTxt  = '—';
            if ($r['delta_ia'] !== null) {
              $d = (float)$r['delta_ia'];
              $dTxt = ($d >= 0 ? '+' : '').number_format($d, 1, '.', '');
            }

            $pal1Badge = '';
            $pal85 = 0;
            $pal1LineTxt = '';
            $pal1ConfTxt = '';
            if ($r['pal1_conf'] !== null) $pal1ConfTxt = (string)((int)$r['pal1_conf']);
            if ($r['pal1_line'] !== null) $pal1LineTxt = number_format((float)$r['pal1_line'], 1, '.', '');

            if ($r['pal1_conf'] !== null && (int)$r['pal1_conf'] >= 85) {
              $pal1Badge = ' <span class="tpnbaf-badge85">≥85%</span>';
              $pal85 = 1;
            }

            $edgeTxt = '<span class="tpnbaf-chipblue">'.(($r['edge_pp']>=0)?'+':'').number_format((float)$r['edge_pp'], 1, '.', '').'pp</span>';
            $valTxt  = '<span class="tpnbaf-chipnum">'.(($r['ev_pct']>=0)?'+':'').number_format((float)$r['ev_pct'], 1, '.', '').'%</span>';

            $bestBadge = ($i === 0) ? ' <span class="tpnbaf-star">BEST</span>' : '';

            $p1Eligible = ($pal1ConfTxt !== '' && (int)$pal1ConfTxt >= 80 && $pal1LineTxt !== '');

            echo '<tr
              data-gid="'.esc_attr((string)$r['gid']).'"
              data-id="'.esc_attr($id).'"
              data-match="'.esc_attr($r['match']).'"
              data-player="'.esc_attr($r['player']).'"
              data-playernorm="'.esc_attr($r['player_norm']).'"
              data-market="'.esc_attr($r['market']).'"
              data-side="'.esc_attr($r['side']).'"
              data-line="'.esc_attr(number_format((float)$r['line'], 1, '.', '')).'"
              data-odd="'.esc_attr($oddTxt).'"
              data-conf="'.esc_attr((int)$r['conf']).'"
              data-level="'.esc_attr($r['lvl_key']).'"
              data-avg="'.esc_attr($r['avg'] !== null ? number_format((float)$r['avg'], 1, '.', '') : '').'"
              data-hasia="'.esc_attr($hasIA).'"
              data-score="'.esc_attr(number_format((float)$r['score'], 1, '.', '')).'"
              data-n="'.esc_attr((int)$r['n']).'"
              data-min="'.esc_attr(number_format((float)$r['min_use'], 1, '.', '')).'"
              data-edge="'.esc_attr(number_format((float)$r['edge_pp'], 1, '.', '')).'"
              data-ev="'.esc_attr(number_format((float)$r['ev_pct'], 1, '.', '')).'"
              data-ia="'.esc_attr($r['pred_ia'] !== null ? number_format((float)$r['pred_ia'], 1, '.', '') : '').'"
              data-delta="'.esc_attr($r['delta_ia'] !== null ? number_format((float)$r['delta_ia'], 1, '.', '') : '').'"
              data-pal85="'.esc_attr($pal85).'"
              data-pal1line="'.esc_attr($pal1LineTxt).'"
              data-pal1conf="'.esc_attr($pal1ConfTxt).'"
              data-hay="'.esc_attr($r['hay']).'"
            >';

              echo '<td><b>'.esc_html($r['match']).'</b><div class="tpnbaf-muted" style="font-size:11px">'.esc_html($r['time']).'</div></td>';
              echo '<td>'.esc_html($r['player']).$bestBadge.'</td>';
              echo '<td>'.esc_html($mktLabel).'</td>';

              echo '<td><b>'.esc_html($r['side']).'</b><div class="tpnbaf-muted" style="font-size:11px">'.esc_html(implode(' • ', $details)).'</div></td>';
              echo '<td>'.esc_html(number_format((float)$r['line'], 1, '.', '')).'</td>';
              echo '<td><b>'.$oddTxt.'</b></td>';
              echo '<td><b>'.(int)$r['conf'].'%</b></td>';
              echo '<td><span class="'.esc_attr($r['lvl_cls']).'">'.esc_html($r['lvl_label']).'</span></td>';

              echo '<td><span class="tpnbaf-chipgray">'.$avgTxt.'</span></td>';
              echo '<td><span class="tpnbaf-chipgray">'.$nTxt.'</span></td>';
              echo '<td><span class="tpnbaf-chipgray">'.$minTxt.'</span></td>';

              echo '<td><span class="tpnbaf-chipgray">'.$iaTxt.'</span></td>';
              echo '<td><span class="tpnbaf-chipgray">'.esc_html($dTxt).'</span></td>';

              echo '<td>'.esc_html($r['pal1_txt']).$pal1Badge.'</td>';
              echo '<td>'.$edgeTxt.'</td>';
              echo '<td>'.$valTxt.'</td>';

              echo '<td><span class="tpnbaf-chipblue">'.esc_html(number_format((float)$r['score'], 1, '.', '')).'</span></td>';

              echo '<td>
                <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
                  <button type="button" class="tpnbaf-btn tpnbaf-btn-add tpnbaf-add-main">➕ Main</button>
                  <button type="button" class="tpnbaf-btn tpnbaf-btn-p1 tpnbaf-add-p1" '.($p1Eligible ? '' : 'disabled').'>🛡️ P1</button>
                </div>
                <div class="tpnbaf-muted" style="font-size:11px;margin-top:4px">
                  P1 piloté par le filtre “Palier 1 Dash”
                </div>
              </td>';

            echo '</tr>';
          }

          echo '</tbody></table>';
      echo '</div>';

      echo '<div class="tpnbaf-muted" style="font-size:11px;margin-top:10px">
              * minutes avec astérisque = minutes issues du snapshot joueurs (si hist minutes indisponible).
            </div>';
    echo '</div>'; // card table

    /* =========================
       ✅ TICKET BUILDERS (UI)
       ========================= */
    echo '<div class="tpnbaf-builders">';

      // MAIN
      echo '<div class="tpnbaf-card">';
        echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">';
          echo '<div>';
            echo '<div style="font-weight:950;font-size:14px">🎫 Ticket MAIN</div>';
            echo '<div class="tpnbaf-muted" style="font-size:11px">1 sélection par match max • max '.$maxLegs.' legs</div>';
          echo '</div>';
          echo '<div style="display:flex;gap:8px;flex-wrap:wrap">';
            echo '<button id="tpnbafAutoMain" class="tpnbaf-btn tpnbaf-btn-auto" type="button">🤖 Auto</button>';
            echo '<button id="tpnbafCopyMain" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier ticket</button>';
            echo '<button id="tpnbafClearMain" class="tpnbaf-btn tpnbaf-btn-clear" type="button">🗑️ Vider</button>';
          echo '</div>';
        echo '</div>';

        echo '<div class="tpnbaf-kpis">';
          echo '<div class="tpnbaf-kpi"><b>Sélections</b><div id="tpnbafKpiNMain">0</div></div>';
          echo '<div class="tpnbaf-kpi"><b>Cote totale</b><div id="tpnbafKpiOddsMain">-</div></div>';
          echo '<div class="tpnbaf-kpi"><b>Mise</b><div><input id="tpnbafStakeMain" type="number" min="0" step="0.5" value="10" style="width:90px;padding:6px;border:1px solid #e5e7eb;border-radius:10px"></div></div>';
          echo '<div class="tpnbaf-kpi"><b>Retour</b><div id="tpnbafKpiRetMain">-</div></div>';
        echo '</div>';

        echo '<div id="tpnbafTicketBoxMain"></div>';
        echo '<textarea id="tpnbafTicketTextMain" class="tpnbaf-textarea" readonly></textarea>';

        echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px">';
          echo '<div style="font-weight:900">Pourquoi (explications)</div>';
          echo '<button id="tpnbafCopyWhyMain" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier pourquoi</button>';
        echo '</div>';
        echo '<textarea id="tpnbafWhyTextMain" class="tpnbaf-textarea small" readonly></textarea>';
      echo '</div>';

      // SAFE P1
      echo '<div class="tpnbaf-card">';
        echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">';
          echo '<div>';
            echo '<div id="tpnbafP1Title" style="font-weight:950;font-size:14px">🛡️ Ticket SAFE (Palier 1 Dash)</div>';
            echo '<div id="tpnbafP1Hint" class="tpnbaf-muted" style="font-size:11px"></div>';
          echo '</div>';
          echo '<div style="display:flex;gap:8px;flex-wrap:wrap">';
            echo '<button id="tpnbafAutoP1" class="tpnbaf-btn tpnbaf-btn-auto" type="button">🤖 Auto</button>';
            echo '<button id="tpnbafCopyP1" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier ticket</button>';
            echo '<button id="tpnbafClearP1" class="tpnbaf-btn tpnbaf-btn-clear" type="button">🗑️ Vider</button>';
          echo '</div>';
        echo '</div>';

        echo '<div class="tpnbaf-kpis">';
          echo '<div class="tpnbaf-kpi"><b>Sélections</b><div id="tpnbafKpiNP1">0</div></div>';
          echo '<div class="tpnbaf-kpi"><b>Cote totale</b><div id="tpnbafKpiOddsP1">-</div></div>';
          echo '<div class="tpnbaf-kpi"><b>Mise</b><div><input id="tpnbafStakeP1" type="number" min="0" step="0.5" value="10" style="width:90px;padding:6px;border:1px solid #e5e7eb;border-radius:10px"></div></div>';
          echo '<div class="tpnbaf-kpi"><b>Retour</b><div id="tpnbafKpiRetP1">-</div></div>';
        echo '</div>';

        echo '<div id="tpnbafTicketBoxP1"></div>';
        echo '<textarea id="tpnbafTicketTextP1" class="tpnbaf-textarea" readonly></textarea>';

        echo '<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px">';
          echo '<div style="font-weight:900">Pourquoi (SAFE)</div>';
          echo '<button id="tpnbafCopyWhyP1" class="tpnbaf-btn tpnbaf-btn-copy" type="button">📋 Copier pourquoi</button>';
        echo '</div>';
        echo '<textarea id="tpnbafWhyTextP1" class="tpnbaf-textarea small" readonly></textarea>';
      echo '</div>';

    echo '</div>'; // builders

    echo '</div>'; // wrap

    $html = ob_get_clean();
    set_transient($ck, $html, $ttl);
    return $html;
  }
}

✅ Pourquoi choisir l’Analyseur Basket Mi-Temps & Quarts ?

  • 🎯 Prédictions ciblées et sécurisées : seulement les suggestions avec une probabilité élevée de succès.
  • 📈 Stratégie fine : idéal pour ceux qui veulent profiter des variations de cotes par période.
  • 🧩 Lisibilité totale : vous savez qui domine quand, sans devoir analyser vous-même.
  • 🔄 Actualisations régulières : nos analyses sont mises à jour avec les dernières données disponibles.

Êtes-vous satisfait des analyses ?

Cliquez sur une étoile pour la noter !

Note Moyenne 0 / 5. Vote 0

Aucun vote pour l'instant ! Soyez le premier à noter cet article.