Fédérale 2 – Demi-finales en direct

Résultats des 16es de finale Aller de Fédérale 2

Ce dimanche 11 mai marque une nouvelle étape cruciale dans la saison de Fédérale 2. Place aux 16es de finale aller, point de départ d’une phase finale qui mènera les meilleurs jusqu’à la montée en Fédérale 1. Sur l’ensemble du territoire, les clubs encore en lice entament cette confrontation en deux manches avec l’ambition d’aller le plus loin possible. Avantage au match retour pour les mieux classés, pression immédiate pour ceux qui recevaient aujourd’hui. Retrouvez sur Vibrez Rugby toutes les affiches, suivez les scores en direct et ne manquez aucun temps fort de cette journée décisive. Les matchs retours se dérouleront le 18 mai et détermineront les qualifiés pour le tour suivant au score cumulé des deux manches. Tous les matchs seront à suivre en direct sur Vibrez Rugby.


.comp-wrap { /* Brand tokens */ –navy: #003f7c; –navy-dark: #002a5d; –red: #d62242; –red-dark: #b91c3a; –gold: #f59e0b; –gray: #676767; –gray-light: #e9ecef; –gray-lighter: #f8f9fa; –white: #ffffff; –text: #0f141a; –radius: 8px; –radius-lg: 12px; –radius-pill: 9999px; –shadow-sm: 0 1px 3px rgba(0,0,0,.05); –shadow-md: 0 4px 12px rgba(0, 63, 124, 0.12); –shadow-lg: 0 10px 30px rgba(0, 42, 93, 0.18); –shadow-red: 0 4px 16px rgba(214, 34, 66, 0.22); –font-display: ‘Barlow Semi Condensed’, ‘Poppins’, -apple-system, sans-serif; –font-body: ‘Poppins’, -apple-system, BlinkMacSystemFont, ‘Segoe UI’, sans-serif; –ease: cubic-bezier(.4, 0, .2, 1); }/* Scoped reset */ .comp-wrap *, .comp-wrap *::before, .comp-wrap *::after { box-sizing: border-box; }.comp-wrap { max-width: 1240px; margin: 0 auto; padding: 20px; font-family: var(–font-body); background: var(–gray-lighter); color: var(–text); }/* ============================================== HEADER COMPÉTITION ============================================== */ .comp-title { position: relative; background: linear-gradient(135deg, var(–navy) 0%, var(–navy-dark) 100%); color: white; padding: 18px 24px 16px; border-radius: var(–radius-lg); margin-bottom: 18px; overflow: hidden; box-shadow: var(–shadow-lg); } .comp-title::before { content:  »; position: absolute; inset: 0; background: repeating-linear-gradient( -45deg, transparent 0 20px, rgba(255,255,255,0.04) 20px 21px ); pointer-events: none; } .comp-title::after { content:  »; position: absolute; right: -60px; top: 50%; transform: translateY(-50%); width: 220px; height: 220px; background: radial-gradient(circle, var(–red) 0%, transparent 65%); opacity: 0.28; pointer-events: none; } .comp-title-inner { position: relative; } .comp-title h1 { font-family: var(–font-display); font-size: 1.55rem; line-height: 1.05; margin: 0; font-weight: 700; color: white; text-transform: uppercase; letter-spacing: 0.02em; } .comp-kicker { display: inline-block; font-family: var(–font-body); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.14em; color: rgba(255,255,255,0.7); margin-bottom: 5px; } .comp-meta { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; } .comp-live-count { display: inline-flex; align-items: center; gap: 7px; background: var(–red); padding: 5px 12px; border-radius: var(–radius-pill); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; box-shadow: 0 2px 10px rgba(214,34,66,0.4); } .comp-live-count::before { content:  »; width: 7px; height: 7px; background: white; border-radius: 50%; animation: blink 1.5s infinite; } .comp-year-pill { display: inline-block; padding: 4px 10px; background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.18); border-radius: var(–radius-pill); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; } @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }/* ============================================== SECTION LABELS ============================================== */ .comp-section-label { font-family: var(–font-display); margin: 22px 0 10px; font-size: 0.85rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(–gray); display: flex; align-items: center; gap: 12px; } .comp-section-label::before { content:  »; display: inline-block; width: 3px; height: 16px; background: var(–red); border-radius: var(–radius-pill); } .comp-section-label::after { content:  »; flex: 1; height: 1px; background: linear-gradient(to right, var(–gray-light), transparent); } .comp-section-label:first-of-type { margin-top: 14px; }/* ============================================== POULE SELECTOR (sticky) ============================================== */ .poule-selector { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; padding: 10px 12px; background: rgba(255,255,255,0.95); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border: 1px solid var(–gray-light); border-radius: var(–radius); box-shadow: var(–shadow-sm); position: sticky; top: var(–comp-sticky-top, 0px); z-index: 20; } .poule-btn { padding: 8px 16px; background: white; border: 1.5px solid var(–gray-light); border-radius: var(–radius-pill); font-family: var(–font-display); font-size: 13px; font-weight: 600; letter-spacing: 0.05em; color: var(–navy); cursor: pointer; transition: all 0.2s var(–ease); text-transform: uppercase; font-family: inherit; } .poule-btn { font-family: var(–font-display); } .poule-btn:hover { border-color: var(–red); color: var(–red); transform: translateY(-1px); } .poule-btn.active { background: var(–navy); color: white; border-color: var(–navy); box-shadow: 0 2px 10px rgba(0,63,124,0.3); }/* ============================================== PHASE CONTAINER ============================================== */ .phase-container { margin-bottom: 28px; } .phase-container.group { display: none; } .phase-container.group.active { display: block; animation: fadeIn 0.3s var(–ease); } @keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }/* Phase header (standalone card) */ .phase-header { position: relative; color: white; padding: 12px 20px; border-radius: var(–radius-lg); margin-bottom: 10px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; overflow: hidden; box-shadow: 0 4px 16px rgba(0, 42, 93, 0.1); } .phase-container.group .phase-header { background: linear-gradient(135deg, var(–navy) 0%, var(–navy-dark) 100%); } .phase-container.knockout .phase-header { background: linear-gradient(135deg, var(–red) 0%, var(–red-dark) 100%); } .phase-header::before { content:  »; position: absolute; inset: 0; background: repeating-linear-gradient( -45deg, transparent 0 18px, rgba(255,255,255,0.04) 18px 19px ); pointer-events: none; } .phase-title { position: relative; font-family: var(–font-display); font-size: 1rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; } .phase-badge { position: relative; margin-left: auto; display: inline-flex; align-items: center; gap: 6px; background: rgba(0,0,0,0.25); padding: 4px 10px; border-radius: var(–radius-pill); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; } .phase-badge.live { background: rgba(255,255,255,0.2); } .phase-badge.live::before { content:  »; width: 6px; height: 6px; background: white; border-radius: 50%; animation: blink 1.5s infinite; }/* Phase subtabs (journées) — standalone card */ .phase-subtabs { display: flex; flex-wrap: wrap; gap: 6px; padding: 12px 14px; background: white; border: 1px solid var(–gray-light); border-radius: var(–radius); margin-bottom: 10px; box-shadow: var(–shadow-sm); } .phase-subtab { position: relative; padding: 6px 13px; background: white; border: 1.5px solid var(–gray-light); border-radius: var(–radius-pill); font-family: var(–font-body); font-size: 12px; font-weight: 600; color: var(–navy); cursor: pointer; transition: all 0.2s var(–ease); white-space: nowrap; } .phase-subtab:hover { border-color: var(–navy); transform: translateY(-1px); } .phase-subtab.active { background: var(–navy); color: white; border-color: var(–navy); } .phase-subtab.has-live::after { content:  »; position: absolute; top: -3px; right: -3px; width: 9px; height: 9px; background: var(–red); border: 2px solid white; border-radius: 50%; animation: blink 1.5s infinite; } .phase-subtab.current .dot-current { display: inline-block; width: 5px; height: 5px; background: var(–red); border-radius: 50%; margin-right: 5px; vertical-align: middle; } .phase-subtab.active .dot-current { background: white; }/* Phase content — standalone card */ .phase-content { background: white; border: 1px solid var(–gray-light); border-radius: var(–radius); padding: 16px; box-shadow: var(–shadow-sm); }/* Phase body: sidebar (standings) + main (matches) */ .phase-body { display: block; } .phase-main { min-width: 0; } .phase-sidebar { min-width: 0; }/* Phases finales : grille 2 colonnes sur desktop */ .knockouts-grid { display: flex; flex-direction: column; gap: 16px; } .knockouts-grid .phase-container { margin-bottom: 0; min-width: 0; } .phase-journee-panel { display: none; } .phase-journee-panel.active { display: block; animation: fadeIn 0.2s var(–ease); }/* ============================================== KNOCKOUT — onglets Aller/Retour + rangées compactes ============================================== */ .ko-leg-tabs { display: flex; gap: 8px; margin-bottom: 12px; padding: 4px; background: var(–gray-lighter); border-radius: var(–radius); } .ko-leg-tab { flex: 1; position: relative; padding: 8px 14px; background: transparent; border: none; border-radius: 6px; font-family: var(–font-display); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(–gray); cursor: pointer; transition: all 0.18s var(–ease); display: inline-flex; align-items: center; justify-content: center; gap: 6px; } .ko-leg-tab:hover { color: var(–navy); } .ko-leg-tab.active { background: white; color: var(–navy-dark); box-shadow: var(–shadow-sm); } .ko-leg-tab .ko-leg-count { display: inline-block; padding: 1px 7px; background: var(–gray-light); color: var(–navy); border-radius: var(–radius-pill); font-size: 10px; font-weight: 700; line-height: 1.4; } .ko-leg-tab.active .ko-leg-count { background: var(–navy); color: white; } .ko-leg-tab .ko-leg-live { display: inline-block; width: 7px; height: 7px; background: var(–red); border-radius: 50%; animation: blink 1.5s infinite; } .ko-leg-panel { display: none; } .ko-leg-panel.active { display: block; animation: fadeIn 0.18s var(–ease); }/* Rangées compactes — une ligne par match */ .match-compact { display: grid; grid-template-columns: 22px minmax(0, 1fr) auto minmax(0, 1fr) 22px; column-gap: 10px; align-items: center; padding: 6px 12px; background: white; border: 1px solid var(–gray-light); border-radius: 6px; margin-bottom: 4px; text-decoration: none; color: var(–navy); font-size: 12.5px; line-height: 1.3; position: relative; transition: border-color 0.15s var(–ease), background 0.15s var(–ease), transform 0.15s var(–ease); } .match-compact:last-child { margin-bottom: 0; } .match-compact:hover { border-color: var(–red); background: white; transform: translateX(2px); text-decoration: none; color: var(–navy); } .match-compact.live { border-color: rgba(214,34,66,0.4); background: rgba(214,34,66,0.04); } .match-compact.live::before { content:  »; position: absolute; left: -1px; top: -1px; bottom: -1px; width: 3px; background: var(–red); border-radius: 6px 0 0 6px; } .match-compact.finished { background: var(–gray-lighter); } .match-compact .team-logo-mini { width: 22px; height: 22px; flex-shrink: 0; } .match-compact .team-logo-mini img { width: 100%; height: 100%; object-fit: contain; display: block; } .match-compact .team-name-mini { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .match-compact .team-name-mini.visitor { text-align: right; } .match-compact.finished.local-loser .team-name-mini.local, .match-compact.finished.visitor-loser .team-name-mini.visitor { color: var(–gray); font-weight: 500; } .match-compact.finished.local-winner .team-name-mini.local, .match-compact.finished.visitor-winner .team-name-mini.visitor { color: var(–navy-dark); font-weight: 700; } .match-compact .match-mid { display: flex; align-items: center; justify-content: center; min-width: 90px; } .match-compact .score-pill { display: inline-flex; align-items: baseline; gap: 3px; padding: 4px 9px; background: var(–navy); color: white; border-radius: 4px; font-family: var(–font-display); font-weight: 700; font-size: 13px; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; } .match-compact .score-pill.live { background: var(–red); } .match-compact .score-pill .score-dash { font-weight: 400; opacity: 0.5; } .match-compact .live-mini { margin-left: 6px; padding: 2px 6px; background: var(–red); color: white; border-radius: var(–radius-pill); font-family: var(–font-display); font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; animation: blink 1.5s infinite; } .match-compact .match-time-compact { font-size: 11.5px; color: var(–gray); font-variant-numeric: tabular-nums; white-space: nowrap; } .match-compact .match-time-compact strong { color: var(–navy); font-weight: 700; margin-left: 4px; } /* Badge « Cumul aller-retour » sur le retour quand les 2 manches sont jouées */ .match-compact .cumul-badge { position: absolute; top: -7px; right: 10px; padding: 2px 7px; background: var(–gold); color: white; border-radius: var(–radius-pill); font-family: var(–font-display); font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; box-shadow: 0 2px 4px rgba(245,158,11,0.35); pointer-events: none; white-space: nowrap; } .match-compact .cumul-badge .cumul-dash { opacity: 0.7; margin: 0 2px; } .match-compact .cumul-badge.draw { background: var(–gray); }/* ============================================== MATCH ROW ============================================== */ .match-row { display: grid; grid-template-columns: 1fr auto 1fr; gap: 16px; padding: 14px 18px 14px 21px; background: white; border: 1.5px solid var(–gray-light); border-radius: var(–radius); margin-bottom: 8px; cursor: pointer; transition: all 0.25s var(–ease); align-items: center; text-decoration: none; color: inherit; position: relative; overflow: hidden; } .match-row:last-child { margin-bottom: 0; } .match-row:hover { border-color: var(–red); transform: translateY(-2px); box-shadow: var(–shadow-red); text-decoration: none; color: inherit; } .match-row.live { background: linear-gradient(90deg, rgba(214,34,66,0.05) 0%, transparent 60%); border-color: rgba(214,34,66,0.35); } .match-row::after { content:  »; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: transparent; transition: background 0.25s var(–ease); } .match-row.live::after { background: var(–red); } .match-row.finished::after { background: var(–navy); }/* Today pill */ .today-pill { display: inline-block; padding: 2px 7px; background: var(–gold); color: white; border-radius: var(–radius-pill); font-family: var(–font-display); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; }.team-info { display: flex; align-items: center; gap: 12px; min-width: 0; } .team-info.away { flex-direction: row-reverse; text-align: right; } .team-name { font-family: var(–font-body); font-size: 14px; font-weight: 600; color: var(–navy); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .team-info.away .team-name { color: var(–gray); } .team-logo { width: 40px; height: 40px; object-fit: contain; flex-shrink: 0; transition: transform 0.25s var(–ease); } .match-row:hover .team-logo { transform: scale(1.08); }.match-center { text-align: center; min-width: 84px; } .score-box { display: inline-flex; align-items: center; justify-content: center; gap: 4px; background: var(–navy); color: white; padding: 7px 12px; border-radius: var(–radius); font-family: var(–font-display); font-weight: 700; font-size: 1.15rem; line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; min-width: 68px; } .score-box .score-dash { font-weight: 400; opacity: 0.5; margin: 0 2px; } .score-box.live { background: var(–red); animation: pulse 2s infinite; } @keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(214,34,66,.4); } 50% { box-shadow: 0 0 0 10px rgba(214,34,66,0); } } .live-pill { display: block; font-family: var(–font-body); font-size: 9px; font-weight: 700; letter-spacing: 0.12em; color: var(–red); margin-top: 4px; text-transform: uppercase; } .live-pill::before { content:  »; display: inline-block; width: 5px; height: 5px; background: var(–red); border-radius: 50%; margin-right: 4px; animation: blink 1.5s infinite; vertical-align: middle; } .match-datetime { font-size: 12px; color: var(–gray); line-height: 1.3; } .match-datetime .match-date { display: block; font-weight: 500; } .match-time { display: block; font-family: var(–font-display); font-weight: 700; color: var(–navy); font-size: 1.05rem; margin-top: 3px; letter-spacing: 0.02em; font-variant-numeric: tabular-nums; }/* ============================================== STANDINGS ============================================== */ .standings-wrap { background: white; border: 1px solid var(–gray-light); border-radius: var(–radius); overflow: hidden; margin: 0 0 16px; } .comp-standings { width: 100%; border-collapse: collapse; font-size: 13px; } .comp-standings thead tr { background: var(–navy); color: white; } .comp-standings th { padding: 13px 8px; text-align: center; font-family: var(–font-display); font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; } .comp-standings th:nth-child(2) { text-align: left; padding-left: 16px; } .comp-standings td { padding: 11px 8px; text-align: center; border-bottom: 1px solid var(–gray-light); font-variant-numeric: tabular-nums; } .comp-standings td:nth-child(2) { text-align: left; padding-left: 16px; } .comp-standings tbody tr:last-child td { border-bottom: 0; } .comp-standings tbody tr { transition: background 0.15s ease; } .comp-standings tbody tr:hover { background: #fafbfd; }.rank { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; font-family: var(–font-display); font-weight: 700; font-size: 14px; color: var(–navy); background: var(–gray-lighter); border-radius: 6px; } .comp-standings tbody tr:first-child .rank { background: linear-gradient(135deg, var(–gold), #d97706); color: white; box-shadow: 0 2px 6px rgba(245,158,11,0.4); }.team-cell { display: flex; align-items: center; gap: 10px; } .team-cell img { width: 24px; height: 24px; object-fit: contain; flex-shrink: 0; } .team-cell span { font-family: var(–font-body); font-weight: 600; color: var(–navy); } .comp-standings td.pts strong { display: inline-block; background: var(–navy); color: white; padding: 4px 11px; border-radius: var(–radius); font-family: var(–font-display); font-size: 14px; font-weight: 700; min-width: 36px; font-variant-numeric: tabular-nums; }/* Forme column */ .forme-cell { display: inline-flex; gap: 3px; justify-content: center; } .forme-dot { width: 18px; height: 18px; border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-family: var(–font-display); font-size: 10px; font-weight: 700; color: white; } .forme-dot.V { background: #16a34a; } .forme-dot.N { background: var(–gray); } .forme-dot.D { background: var(–red); }/* ============================================== TEAM SEARCH ============================================== */ .team-search { position: relative; margin-bottom: 14px; } .team-search input { width: 100%; padding: 11px 38px 11px 40px; background: white; border: 1.5px solid var(–gray-light); border-radius: var(–radius); font-family: var(–font-body); font-size: 14px; color: var(–text); outline: none; transition: all 0.2s var(–ease); } .team-search input::placeholder { color: #9aa3af; font-family: var(–font-body); } .team-search input:focus { border-color: var(–navy); box-shadow: 0 0 0 3px rgba(0, 63, 124, 0.12); } .team-search .ts-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(–gray); pointer-events: none; width: 16px; height: 16px; } .team-search .ts-clear { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 22px; height: 22px; border: 0; background: var(–gray-light); border-radius: 50%; color: var(–gray); cursor: pointer; display: none; align-items: center; justify-content: center; font-family: inherit; font-size: 14px; line-height: 1; } .team-search .ts-clear:hover { background: var(–red); color: white; } .team-search.has-value .ts-clear { display: inline-flex; } .ts-hint { display: none; margin-top: 6px; font-size: 12px; color: var(–gray); padding: 0 4px; } .team-search.has-value + .ts-hint { display: block; }/* Highlight matching rows/matches */ .comp-standings tbody tr.is-match { background: rgba(245, 158, 11, 0.12); box-shadow: inset 3px 0 0 var(–gold); } .match-row.is-dim { opacity: 0.35; filter: saturate(0.4); } .match-row.is-match { border-color: var(–gold); box-shadow: 0 2px 10px rgba(245, 158, 11, 0.2); } .poule-btn.is-match { border-color: var(–gold); color: var(–gold); } .poule-btn.is-match.active { background: var(–gold); color: white; border-color: var(–gold); box-shadow: 0 2px 10px rgba(245, 158, 11, 0.35); }/* ============================================== DAY GROUPING (inside a journée) ============================================== */ .day-group + .day-group { margin-top: 14px; } .day-header { display: flex; align-items: center; gap: 10px; margin: 4px 0 10px; font-family: var(–font-display); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.12em; color: var(–gray); } .day-header::before { content:  »; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(–navy); } .day-header.is-today::before { background: var(–red); animation: blink 1.5s infinite; } .day-header.is-today { color: var(–red); } .day-header::after { content:  »; flex: 1; height: 1px; background: var(–gray-light); }/* ============================================== EMPTY / LOADING / ERROR ============================================== */ .empty-state { text-align: center; padding: 36px 20px; color: var(–gray); font-size: 14px; } .empty-state::before { content:  »; display: block; width: 44px; height: 44px; margin: 0 auto 12px; background: var(–gray-lighter); border-radius: 50%; background-image: radial-gradient(circle, var(–gray-light) 30%, var(–gray-lighter) 31%); }.comp-loading { padding: 8px 0; } .skel { background: linear-gradient(90deg, #eef1f5 0%, #f7f9fb 50%, #eef1f5 100%); background-size: 200% 100%; animation: shimmer 1.4s infinite; border-radius: var(–radius); } .skel-title { height: 110px; margin-bottom: 20px; border-radius: var(–radius-lg); } .skel-row { height: 66px; margin-bottom: 8px; } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }.comp-error { padding: 20px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; border-radius: var(–radius); }/* ============================================== RESPONSIVE ============================================== */ @media (max-width: 768px) { .comp-wrap { padding: 12px; } .comp-title { padding: 14px 16px; } .comp-title h1 { font-size: 1.25rem; } .comp-kicker { font-size: 9px; } .poule-selector { padding: 8px 10px; gap: 6px; } .poule-btn { padding: 6px 12px; font-size: 12px; } .match-row { padding: 12px 12px 12px 16px; gap: 10px; } .team-info { gap: 8px; } .team-name { font-size: 13px; } .team-logo { width: 32px; height: 32px; } .match-center { min-width: 88px; } .score-box { font-size: 1rem; padding: 6px 11px; min-width: 62px; } .match-time { font-size: 0.95rem; } .match-datetime { font-size: 11px; } .comp-standings { font-size: 12px; } .comp-standings th, .comp-standings td { padding: 9px 4px; } .comp-standings th:nth-child(2), .comp-standings td:nth-child(2) { padding-left: 10px; } .rank { width: 22px; height: 22px; font-size: 12px; } .team-cell { gap: 6px; } .team-cell img { width: 18px; height: 18px; } .comp-standings td.pts strong { padding: 3px 9px; font-size: 12px; min-width: 30px; } .phase-header { padding: 10px 14px; } .phase-title { font-size: 0.95rem; } .phase-content { padding: 12px; } .phase-subtabs { padding: 10px 12px; } .phase-subtab { font-size: 11px; padding: 5px 10px; } .forme-dot { width: 16px; height: 16px; font-size: 9px; } .hide-mobile { display: none; } } @media (max-width: 480px) { .comp-standings th, .comp-standings td { padding: 7px 3px; font-size: 11px; } .team-cell span { font-size: 11px; } }/* ============================================== DESKTOP (≥1024px) — 2-column split ============================================== */ @media (min-width: 1024px) { .comp-title { padding: 24px 28px 22px; } .comp-title h1 { font-size: 1.75rem; }.phase-container.group .phase-body:has(.phase-sidebar) { display: grid; grid-template-columns: minmax(460px, 560px) 1fr; gap: 20px; align-items: start; } .phase-container.group .phase-sidebar { position: sticky; top: calc(var(–comp-sticky-top, 0px) + 80px); z-index: 5; } .phase-container.group .phase-sidebar .standings-wrap { margin: 0; max-height: calc(100vh – 110px); overflow-y: auto; scrollbar-width: thin; } /* Sidebar standings : on masque seulement PM/PE, on garde N, +/-, Forme */ .phase-container.group .phase-sidebar .comp-standings .col-hide-narrow { display: none; } .phase-container.group .phase-sidebar .comp-standings th, .phase-container.group .phase-sidebar .comp-standings td { padding: 9px 6px; } .phase-container.group .phase-sidebar .comp-standings th:nth-child(2), .phase-container.group .phase-sidebar .comp-standings td:nth-child(2) { padding-left: 12px; } .phase-container.group .phase-sidebar .team-cell span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; display: inline-block; vertical-align: middle; } .phase-container.group .phase-sidebar .forme-dot { width: 16px; height: 16px; font-size: 9px; }/* More generous match row on desktop */ .match-row { padding: 16px 20px 16px 24px; } .team-logo { width: 44px; height: 44px; } .team-name { font-size: 15px; } .score-box { font-size: 1.25rem; padding: 8px 14px; min-width: 72px; } .match-time { font-size: 1.15rem; }/* Phases finales : 2 colonnes */ .knockouts-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } /* Match rows en phase finale (single-leg) repassent compacts */ .knockouts-grid .match-row { padding: 14px 16px 14px 18px; } .knockouts-grid .team-logo { width: 36px; height: 36px; } .knockouts-grid .team-name { font-size: 13.5px; } .knockouts-grid .score-box { font-size: 1.05rem; padding: 6px 11px; min-width: 62px; } .knockouts-grid .match-time { font-size: 1rem; } /* Finale plein écran (D) */ .knockouts-grid .phase-container.is-final { grid-column: 1 / -1; } .knockouts-grid .phase-container.is-final .phase-title { font-size: 1.4rem; letter-spacing: 0.06em; } .knockouts-grid .phase-container.is-final .match-compact { padding: 12px 18px; font-size: 14.5px; } .knockouts-grid .phase-container.is-final .match-compact .team-logo-mini { width: 32px; height: 32px; } .knockouts-grid .phase-container.is-final .match-compact .score-pill { font-size: 1.15rem; padding: 7px 14px; } }/* Reduced motion — respect user preference */ @media (prefers-reduced-motion: reduce) { .comp-wrap *, .comp-wrap *::before, .comp-wrap *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }
(function () { const API_BASE = ‘https://rugby-app-4ebacff10fda.herokuapp.com’; const root = document.getElementById(‘comp-root’);// Slug URL → slug API quand ils diffèrent const SLUG_ALIASES = { ‘rugby-nationale’: ‘nationale’, ‘six-nations’: ‘tournoi-des-6-nations’, ‘six-nations-u20’: ‘tournoi-des-6-nations-u20’, ‘six-nations-feminin’: ‘tournoi-des-6-nations-f’, ‘champions-cup’: ‘coupe-des-champions’, ‘challenge-cup’: ‘challenge’, ‘rugby-feminine’: ‘coupe-du-monde-f’, ‘rugby-u20’: ‘coupe-du-monde-u20’, ‘coupe-du-monde-de-rugby-feminine’: ‘coupe-du-monde-f’, ‘coupe-du-monde-de-rugby-u20’: ‘coupe-du-monde-u20’, ‘tournee-dete’: ‘test-matches-automne’, // Élite féminines : aliases de secours si la page WP utilise elite-1/elite-2 ‘elite-1’: ‘elite-1-feminine’, ‘elite-2’: ‘elite-2-feminine’, ‘elite-1-f’: ‘elite-1-feminine’, ‘elite-2-f’: ‘elite-2-feminine’ };// Ordre : ?competition=, data-competition, URL path const slugFromQuery = new URLSearchParams(window.location.search).get(‘competition’); const slugFromUrl = (() => { const path = window.location.pathname.replace(/\/+$/,  »); const m = path.match(/\/(?:resultats-|competitions?\/)([a-z0-9-]+)/i); return m ? m[1] : null; })(); let slug = slugFromQuery || root.dataset.competition || slugFromUrl; if (slug && SLUG_ALIASES[slug]) slug = SLUG_ALIASES[slug];if (!slug) { root.innerHTML = ‘
Compétition non spécifiée. Ajoute data-competition="federale-3" sur #comp-root.
‘; return; }// — Utils — const esc = (s) => String(s ??  »).replace(/[& »‘]/g, (c) => ({‘&’:’&’, »:’>’,’ »‘:’"’, »‘ »:’'’}[c])); const MATCH_URL = (id) => `/live-en-direct/?id=${id}`;const isToday = (iso) => { if (!iso) return false; const d = new Date(iso); const n = new Date(); return d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate(); }; const formatDate = (iso) => { if (!iso) return  »; if (isToday(iso)) return « Aujourd’hui »; return new Date(iso).toLocaleDateString(‘fr-FR’, { day: ‘numeric’, month: ‘short’ }); }; const formatDayHeader = (iso) => { if (!iso) return  »; if (isToday(iso)) return « Aujourd’hui »; return new Date(iso).toLocaleDateString(‘fr-FR’, { weekday: ‘long’, day: ‘numeric’, month: ‘long’ }); }; const dayKey = (iso) => iso ? new Date(iso).toDateString() : ‘__none__’; const groupMatchesByDay = (matches) => { const order = []; const map = new Map(); (matches || []).forEach((m) => { const k = dayKey(m.date); if (!map.has(k)) { map.set(k, []); order.push(k); } map.get(k).push(m); }); return order.map((k) => ({ key: k, iso: (map.get(k)[0] || {}).date, matches: map.get(k) })); }; const formatTime = (iso) => { if (!iso) return  »; return new Date(iso).toLocaleTimeString(‘fr-FR’, { hour: ‘2-digit’, minute: ‘2-digit’ }); }; const isLiveState = (s) => s === ‘endirect’ || s === ‘mi-temps’;// — Rendu d’un match — const renderMatch = (m) => { const live = isLiveState(m.etat); const finished = m.etat === ‘termine’; const hasScore = live || finished; const stateClass = live ? ‘live’ : (finished ? ‘finished’ :  »);let center; if (hasScore) { const score = `
${esc(m.score_locale ?? 0)} ${esc(m.score_visiteuse ?? 0)}
`; center = live ? `${score}En direct` : score; } else { const dateLabel = formatDate(m.date); const dateHtml = isToday(m.date) ? `${esc(dateLabel)}` : `${esc(dateLabel)}`; center = `
${dateHtml}${esc(formatTime(m.date))}
`; }return `
${m.local_team.img_url ? `` :  »} ${esc(m.local_team.name)}
${center}
${m.visitor_team.img_url ? `` :  »} ${esc(m.visitor_team.name)}
`; };// — Rendu du classement — const renderStandings = (standings) => { if (!standings || !standings.length) return  »; const hasForme = standings.some((t) => Array.isArray(t.forme) && t.forme.length); return `
${hasForme ? ‘‘ :  »} ${standings.map((t, i) => ` ${hasForme ? `` :  »} `).join( »)}
PosÉquipeJGNPPMPE+/-FormePts
${esc(t.rank ?? i + 1)}
${t.team_img_url ? `Résultats Fédérale 2` :  »} ${esc(t.team_name)}
${esc(t.matchs_joues ?? 0)}${esc(t.matchs_gagnes ?? 0)}${esc(t.matchs_nuls ?? 0)}${esc(t.matchs_perdus ?? 0)}${esc(t.buts_marques ?? 0)}${esc(t.buts_encaisses ?? 0)}${esc(t.diff_buts ?? 0)}${ (Array.isArray(t.forme) ? t.forme.slice(-5) : []).map((f) => { const c = String(f).toUpperCase(); return `${esc(c)}`; }).join( ») }${esc(t.points ?? 0)}
`; };// — Détection du round en cours — const findCurrentRoundIndex = (rounds) => { const now = Date.now(); let lastWithFinished = -1; let firstUpcoming = -1; rounds.forEach((r, i) => { const matches = r.matches || []; if (matches.some((m) => [‘termine’, ‘endirect’, ‘mi-temps’].includes(m.etat))) { lastWithFinished = i; } if (firstUpcoming === -1 && matches.some((m) => m.date && new Date(m.date).getTime() >= now)) { firstUpcoming = i; } }); if (lastWithFinished !== -1) return lastWithFinished; if (firstUpcoming !== -1) return firstUpcoming; return 0; };const roundHasLive = (r) => (r.matches || []).some((m) => isLiveState(m.etat)); const groupLiveCount = (g) => (g.rounds || []).reduce((acc, r) => acc + (r.matches || []).filter((m) => isLiveState(m.etat)).length, 0);// — Rendu d’une poule / groupe — const renderGroupPhase = (group, isActive, customTitle) => { const rounds = (group.rounds || []).slice().sort((a, b) => { const na = parseInt((a.name ||  »).match(/\d+/)?.[0] || 0); const nb = parseInt((b.name ||  »).match(/\d+/)?.[0] || 0); return na – nb; });const currentIdx = findCurrentRoundIndex(rounds); const groupKey = esc(group.name); const title = customTitle ? esc(customTitle) : `Poule ${groupKey}`; const liveCount = groupLiveCount(group);const subtabs = rounds.map((r, i) => { const classes = [ ‘phase-subtab’, i === currentIdx ? ‘active current’ :  », roundHasLive(r) ? ‘has-live’ :  » ].filter(Boolean).join(‘ ‘); return ``; }).join( »);const renderMatchesBlock = (matches) => { if (!matches || !matches.length) return ‘
Aucun match pour cette journée
‘; const days = groupMatchesByDay(matches); if (days.length `
${esc(formatDayHeader(d.iso) || ‘Date à définir’)}
${d.matches.map(renderMatch).join( »)}
`).join( »); };const panels = rounds.map((r, i) => `
${renderMatchesBlock(r.matches)}
`).join( »);const standingsHtml = renderStandings(group.standings); const subtabsHtml = rounds.length ? `
${subtabs}
` :  »; const contentHtml = `
${panels || ‘
Aucun match
‘}
`;return `
${title} ${liveCount ? `${liveCount} en direct` :  »}
${standingsHtml ? `` :  »}
${subtabsHtml} ${contentHtml}
`; };const renderGroups = (groups, compName) => { if (!groups || !groups.length) return  »; const sorted = […groups].sort((a, b) => { const na = parseInt(a.name), nb = parseInt(b.name); if (!isNaN(na) && !isNaN(nb)) return na – nb; return String(a.name).localeCompare(String(b.name)); });const singleGroup = sorted.length === 1; const phases = sorted.map((g, i) => renderGroupPhase(g, i === 0, singleGroup ? compName : null)).join( »);if (singleGroup) { return `${phases}`; }const selectorBtns = sorted.map((g, i) => { const live = groupLiveCount(g); return ``; }).join( »);return `
${selectorBtns}
${phases} `; };// — Phases finales (knockout) —// (B) Mappage nom verbeux → label court const SHORT_ROUND_LABELS = [ [/^trente.deuxi[èe]me\s+de\s+finale$/i, ’32es de finale’], [/^seizi[èe]me\s+de\s+finale$/i, ’16es de finale’], [/^huiti[èe]me\s+de\s+finale$/i, ‘8es de finale’], [/^quart\s+de\s+finale$/i, ‘Quarts de finale’], [/^quarts?\s+de\s+finale$/i, ‘Quarts de finale’], [/^demi[\s-]?finales?$/i, ‘Demi-finales’], [/^finale$/i, ‘Finale’], [/^barrages?$/i, ‘Barrages’], [/^match\s+pour\s+la\s+3e\s+place$/i, ‘3e place’], ]; const shortRoundLabel = (name) => { if (!name) return name; const trimmed = String(name).trim(); for (const [re, label] of SHORT_ROUND_LABELS) { if (re.test(trimmed)) return label; } return trimmed; };// (C) Ordre canonique des tours de phase finale const KNOCKOUT_RANK_PATTERNS = [ /classement|consolante/i, // 0 — matches de classement /^barrages?$/i, // 1 /trente.deuxi/i, // 2 — 32es /seizi/i, // 3 — 16es /huiti/i, // 4 — 8es /^quarts?\s+de\s+finale$/i, // 5 /^demi[\s-]?finales?$/i, // 6 /3e\s*place|troisi[èe]me\s+place/i, // 7 /^finale$/i, // 8 (exact, doit passer après demi/3e place) ]; const knockoutRank = (name) => { const n = String(name ||  »).trim(); for (let i = 0; i { const m = String(name ||  »).match(/^(.+?)\s*[-–—]\s*(aller|retour)\s*$/i); if (!m) return { base: String(name ||  »).trim(), leg: null }; return { base: m[1].trim(), leg: m[2].toLowerCase() }; };// Apparie les matchs aller/retour par paire d’équipes (clé = ids triés) const pairLegMatches = (allerMatches, retourMatches) => { const idOf = (t) => t?.id ?? t?.team_id ?? ‘?’; const keyOf = (m) => [idOf(m?.local_team), idOf(m?.visitor_team)].sort().join(‘|’); const retourByKey = new Map(); (retourMatches || []).forEach((m) => retourByKey.set(keyOf(m), m)); const ties = []; const used = new Set(); (allerMatches || []).forEach((aller) => { const k = keyOf(aller); const retour = retourByKey.get(k) || null; if (retour) used.add(k); ties.push({ aller, retour }); }); (retourMatches || []).forEach((retour) => { const k = keyOf(retour); if (!used.has(k)) ties.push({ aller: null, retour }); }); return ties; };// Calcule le score cumulé de l’aller/retour du point de vue de teamA (= local de l’aller). const aggregateTie = (tie) => { let teamA = null, teamB = null; if (tie.aller) { teamA = tie.aller.local_team; teamB = tie.aller.visitor_team; } else if (tie.retour) { teamA = tie.retour.visitor_team; teamB = tie.retour.local_team; } if (!teamA || !teamB) return null; const aId = teamA.id ?? teamA.team_id;let scoreA = 0, scoreB = 0; let aLegFinished = false, rLegFinished = false; let aLive = false, rLive = false;const addLeg = (m, isAller) => { if (!m) return; const finished = m.etat === ‘termine’; const live = isLiveState(m.etat); if (!finished && !live) return; const localId = m.local_team?.id ?? m.local_team?.team_id; const localScore = Number(m.score_locale ?? 0); const visitorScore = Number(m.score_visiteuse ?? 0); if (localId === aId) { scoreA += localScore; scoreB += visitorScore; } else { scoreA += visitorScore; scoreB += localScore; } if (isAller) { aLegFinished = finished; aLive = live; } else { rLegFinished = finished; rLive = live; } }; addLeg(tie.aller, true); addLeg(tie.retour, false);const anyPlayed = aLegFinished || rLegFinished || aLive || rLive; const bothDone = aLegFinished && rLegFinished; const winner = bothDone ? (scoreA > scoreB ? ‘A’ : (scoreB > scoreA ? ‘B’ : null)) : null; return { teamA, teamB, scoreA, scoreB, anyPlayed, bothDone, winner, aLive, rLive }; };// Rendu d’une confrontation aller/retour (ou aller seul / retour seul). // Construit la map des scores cumulés par paire d’équipes — utilisée pour afficher // un badge « Cumul » sur la rangée du retour quand les 2 manches sont jouées. const buildAggregateMap = (allerMatches, retourMatches) => { const map = new Map(); if (!allerMatches?.length || !retourMatches?.length) return map; const ties = pairLegMatches(allerMatches, retourMatches); ties.forEach((t) => { const agg = aggregateTie(t); if (!agg || !agg.bothDone) return; const aId = agg.teamA?.id ?? agg.teamA?.team_id; const bId = agg.teamB?.id ?? agg.teamB?.team_id; if (aId == null || bId == null) return; const key = [aId, bId].sort((x, y) => Number(x) – Number(y)).join(‘|’); map.set(key, agg); }); return map; };// Rangée compacte (mono-ligne) pour les phases finales const renderCompactMatch = (m, aggMap) => { const live = isLiveState(m.etat); const finished = m.etat === ‘termine’; const hasScore = live || finished; const localScore = Number(m.score_locale ?? 0); const visitorScore = Number(m.score_visiteuse ?? 0); const localWon = finished && localScore > visitorScore; const visitorWon = finished && visitorScore > localScore;const stateClasses = []; if (live) stateClasses.push(‘live’); else if (finished) stateClasses.push(‘finished’); if (localWon) stateClasses.push(‘local-winner’, ‘visitor-loser’); else if (visitorWon) stateClasses.push(‘visitor-winner’, ‘local-loser’);let mid; if (hasScore) { mid = `${esc(localScore)}${esc(visitorScore)}${live ? ‘live‘ :  »}`; } else { const date = m.date ? formatDate(m.date) :  »; const time = m.date ? formatTime(m.date) :  »; mid = `${esc(date)}${time ? ` ${esc(time)}` :  »}`; }let cumulBadge =  »; if (aggMap && aggMap.size) { const aId = m.local_team?.id ?? m.local_team?.team_id; const bId = m.visitor_team?.id ?? m.visitor_team?.team_id; if (aId != null && bId != null) { const key = [aId, bId].sort((x, y) => Number(x) – Number(y)).join(‘|’); const agg = aggMap.get(key); if (agg) { // Affiche le cumul du point de vue local du match courant. const matchLocalIsA = String(aId) === String(agg.teamA?.id ?? agg.teamA?.team_id); const cumulLocal = matchLocalIsA ? agg.scoreA : agg.scoreB; const cumulVisitor = matchLocalIsA ? agg.scoreB : agg.scoreA; const drawClass = agg.winner ?  » : ‘draw’; cumulBadge = `★ Cumul ${cumulLocal}${cumulVisitor}`; } } }const localLogo = m.local_team?.img_url ? `Résultats Fédérale 2` :  »; const visitorLogo = m.visitor_team?.img_url ? `Résultats Fédérale 2` :  »;return ` ${localLogo} ${esc(m.local_team?.name || ‘?’)} ${mid} ${esc(m.visitor_team?.name || ‘?’)} ${visitorLogo} ${cumulBadge} `; };const renderKnockouts = (phases) => { if (!phases || !phases.length) return  »; const ko = phases.filter((p) => Array.isArray(p.matches) && p.matches.length); if (!ko.length) return  »;// (A) Regroupement aller/retour : on fusionne les phases dont le nom ne diffère // que par le suffixe  » – aller » /  » – retour ». const groups = new Map(); ko.forEach((p) => { const { base, leg } = parseLegName(p.name); if (!groups.has(base)) { groups.set(base, { name: base, aller: null, retour: null, single: null, anyNumber: p.number ?? 999 }); } const g = groups.get(base); if (leg === ‘aller’) g.aller = p; else if (leg === ‘retour’) g.retour = p; else g.single = p; g.anyNumber = Math.min(g.anyNumber, p.number ?? 999); });// (C) Tri canonique : Barrage → 32es → 16es → 8es → 1/4 → 1/2 → 3e place → Finale const sortedGroups = Array.from(groups.values()).sort((a, b) => { const ra = knockoutRank(a.name); const rb = knockoutRank(b.name); if (ra !== rb) return ra – rb; return (a.anyNumber || 0) – (b.anyNumber || 0); });const blocks = sortedGroups.map((g, gi) => { const allMatches = [ …(g.aller?.matches || []), …(g.retour?.matches || []), …(g.single?.matches || []), ]; const liveCount = allMatches.filter((m) => isLiveState(m.etat)).length; const isFinal = /^finale$/i.test(g.name.trim()); const hasBothLegs = g.aller && g.retour;let body; if (hasBothLegs) { // (A) Onglets Aller / Retour, badge « Cumul » sur les retours bouclés const aggMap = buildAggregateMap(g.aller.matches, g.retour.matches); const allerMatches = (g.aller.matches || []).slice().sort((a, b) => new Date(a.date || 0) – new Date(b.date || 0)); const retourMatches = (g.retour.matches || []).slice().sort((a, b) => new Date(a.date || 0) – new Date(b.date || 0)); const allerLive = allerMatches.filter((m) => isLiveState(m.etat)).length; const retourLive = retourMatches.filter((m) => isLiveState(m.etat)).length; const allerDone = allerMatches.length && allerMatches.every((m) => m.etat === ‘termine’); // Onglet actif par défaut : retour si live, sinon le tour le plus récent (aller pas fini → aller, sinon retour). const defaultLeg = retourLive ? ‘retour’ : (allerDone ? ‘retour’ : ‘aller’); const tabsId = `ko-${gi}`; const allerBody = allerMatches.map((m) => renderCompactMatch(m, null)).join( ») || ‘
Aucun match
‘; const retourBody = retourMatches.map((m) => renderCompactMatch(m, aggMap)).join( ») || ‘
Aucun match
‘; body = `
${allerBody}
${retourBody}
`; } else if (g.aller || g.retour || g.single) { const phase = g.single || g.aller || g.retour; const matches = (phase.matches || []).slice().sort((a, b) => new Date(a.date || 0) – new Date(b.date || 0)); body = matches.length ? matches.map((m) => renderCompactMatch(m, null)).join( ») : ‘
Aucun match
‘; } else { body = ‘
Aucun match
‘; }return `
${esc(shortRoundLabel(g.name))} ${liveCount ? `${liveCount} en direct` :  »}
${body}
`; }).join( »);return `
${blocks}
`; };// Bind des onglets aller/retour (ajouté en plus du bindTabs principal) const bindKnockoutLegTabs = () => { root.querySelectorAll(‘.ko-leg-tab’).forEach((btn) => { btn.addEventListener(‘click’, () => { const id = btn.dataset.tabs; const leg = btn.dataset.leg; if (!id || !leg) return; root.querySelectorAll(`.ko-leg-tab[data-tabs= »${id} »]`).forEach((b) => { const on = b === btn; b.classList.toggle(‘active’, on); b.setAttribute(‘aria-selected’, on ? ‘true’ : ‘false’); }); root.querySelectorAll(`.ko-leg-panel[data-tabs= »${id} »]`).forEach((p) => { p.classList.toggle(‘active’, p.dataset.leg === leg); }); }); }); };// — URL state — const updateUrl = () => { const url = new URL(window.location.href); const activePoule = root.querySelector(‘.poule-btn.active’)?.dataset.poule; const activePanel = root.querySelector(‘.phase-container.group.active’); const activeJournee = activePanel?.querySelector(‘.phase-subtab.active’)?.dataset.journee; if (activePoule != null) url.searchParams.set(‘poule’, activePoule); else url.searchParams.delete(‘poule’); if (activeJournee != null) url.searchParams.set(‘journee’, activeJournee); else url.searchParams.delete(‘journee’); history.replaceState(null,  », url.toString()); };const setActivePoule = (p, { scroll = true } = {}) => { if (!p) return; let found = false; root.querySelectorAll(‘.poule-btn’).forEach((b) => { const match = b.dataset.poule === p; b.classList.toggle(‘active’, match); if (match) found = true; }); if (!found) return; root.querySelectorAll(‘.phase-container.group’).forEach((panel) => panel.classList.toggle(‘active’, panel.dataset.group === p)); if (scroll) { const btn = root.querySelector(‘.poule-btn.active’); btn?.scrollIntoView({ behavior: ‘smooth’, block: ‘nearest’, inline: ‘center’ }); } };const setActiveJournee = (groupKey, journeeIdx) => { const key = String(journeeIdx); root.querySelectorAll(`.phase-subtab[data-group= »${groupKey} »]`).forEach((b) => b.classList.toggle(‘active’, b.dataset.journee === key)); root.querySelectorAll(`.phase-journee-panel[data-group= »${groupKey} »]`).forEach((p) => p.classList.toggle(‘active’, p.dataset.journeePanel === key)); };// — Bindings tabs — const bindTabs = () => { root.querySelectorAll(‘.poule-btn’).forEach((btn) => { btn.addEventListener(‘click’, () => { setActivePoule(btn.dataset.poule, { scroll: true }); updateUrl(); }); });root.querySelectorAll(‘.phase-subtab’).forEach((btn) => { btn.addEventListener(‘click’, () => { setActiveJournee(btn.dataset.group, btn.dataset.journee); updateUrl(); }); }); };const applyUrlState = () => { const params = new URLSearchParams(window.location.search); const p = params.get(‘poule’); const j = params.get(‘journee’); if (p) setActivePoule(p, { scroll: false }); if (j != null) { const groupKey = root.querySelector(‘.phase-container.group.active’)?.dataset.group; if (groupKey) setActiveJournee(groupKey, j); } };// — Team search — let teamIndex = new Map(); // team_name (lowercased) → Set const buildTeamIndex = (groups) => { teamIndex = new Map(); (groups || []).forEach((g) => { (g.standings || []).forEach((t) => { const k = (t.team_name ||  »).toLowerCase().trim(); if (!k) return; if (!teamIndex.has(k)) teamIndex.set(k, new Set()); teamIndex.get(k).add(g.name); }); }); };const resetSearch = () => { root.querySelectorAll(‘.poule-btn’).forEach((b) => { b.style.display =  »; b.classList.remove(‘is-match’); }); root.querySelectorAll(‘.comp-standings tbody tr’).forEach((r) => r.classList.remove(‘is-match’)); root.querySelectorAll(‘.match-row’).forEach((r) => r.classList.remove(‘is-match’, ‘is-dim’)); };const bindSearch = () => { const wrap = root.querySelector(‘.team-search’); const input = wrap?.querySelector(‘input’); const clear = wrap?.querySelector(‘.ts-clear’); const hint = root.querySelector(‘.ts-hint’); if (!input) return;const run = () => { const q = input.value.trim().toLowerCase(); wrap.classList.toggle(‘has-value’, !!q); if (!q) { resetSearch(); if (hint) hint.textContent =  »; return; }const matchingPools = new Set(); teamIndex.forEach((pools, name) => { if (name.includes(q)) pools.forEach((p) => matchingPools.add(p)); });// Filter poule buttons + mark matches let firstMatch = null; root.querySelectorAll(‘.poule-btn’).forEach((b) => { const hit = matchingPools.has(b.dataset.poule); b.style.display = matchingPools.size ? (hit ?  » : ‘none’) :  »; b.classList.toggle(‘is-match’, hit); if (hit && !firstMatch) firstMatch = b.dataset.poule; });// Highlight standings rows root.querySelectorAll(‘.comp-standings tbody tr’).forEach((r) => { const n = r.querySelector(‘.team-cell span’)?.textContent.toLowerCase() ||  »; r.classList.toggle(‘is-match’, n.includes(q)); });// Dim non-matching matches, highlight matching ones root.querySelectorAll(‘.match-row’).forEach((r) => { const names = Array.from(r.querySelectorAll(‘.team-name’)).map((t) => t.textContent.toLowerCase()); const hit = names.some((n) => n.includes(q)); r.classList.toggle(‘is-match’, hit); r.classList.toggle(‘is-dim’, !hit); });// Auto-switch to first matching poule if current isn’t matching if (firstMatch) { const current = root.querySelector(‘.poule-btn.active’)?.dataset.poule; if (!matchingPools.has(current)) setActivePoule(firstMatch, { scroll: true }); }if (hint) { const n = matchingPools.size; hint.textContent = n ? `${n} poule${n > 1 ? ‘s’ :  »} contient${n > 1 ? ‘nent’ :  »} « ${input.value.trim()} »` : `Aucune équipe trouvée pour « ${input.value.trim()} »`; } };let t; input.addEventListener(‘input’, () => { clearTimeout(t); t = setTimeout(run, 120); }); clear?.addEventListener(‘click’, () => { input.value =  »; input.focus(); run(); }); };// — Compte le total de matchs live sur la compétition — const countLive = (data) => { let n = 0; const walk = (arr) => (arr || []).forEach((m) => { if (isLiveState(m.etat)) n++; }); (data.groups || []).forEach((g) => (g.rounds || []).forEach((r) => walk(r.matches))); (data.phases || []).forEach((p) => { walk(p.matches); (p.rounds || []).forEach((r) => walk(r.matches)); }); (data.knockout_phases || []).forEach((p) => walk(p.matches)); return n; };// — Fetch + rendu — fetch(`${API_BASE}/competitions/${encodeURIComponent(slug)}/adaptive`) .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then((data) => { const title = esc(data.competition?.display_name || slug); const year = data.competition?.years ? esc(data.competition.years) :  »; const liveTotal = countLive(data);// Normalisation : data.groups (championship) — sinon on reconstruit depuis data.phases // – cup : phases type=’group’ avec `journees` + `matches` à plat // – tournament: phases type=’group’ avec `rounds` déjà structurés // – knockouts : phases type=’knockout’ avec `matches` let groupsToRender = data.groups || null; let knockoutsToRender = data.knockout_phases || null;// Détecte un nom de round de phase finale (vs une journée régulière) const isKnockoutRoundName = (n) => /finale|demi|quart|barrage|classement|match\s+pour|(?:huiti|seizi|trente)/i.test(n ||  »);if (!groupsToRender && Array.isArray(data.phases)) { const normGroups = []; const koByName = new Map(); // dédup phase finale entre poules data.phases.forEach((p) => { const hasGroupShape = Array.isArray(p.rounds) || Array.isArray(p.journees); if (hasGroupShape) { let rounds = Array.isArray(p.rounds) ? p.rounds : null; // Cup : journees liste + matches plats → on regroupe par phase_name if (!rounds && Array.isArray(p.journees) && Array.isArray(p.matches)) { const byRound = {}; p.matches.forEach((m) => { const key = m.phase_name ||  »; (byRound[key] = byRound[key] || []).push(m); }); rounds = []; p.journees.forEach((j) => { const matches = byRound[j.name] || []; if (isKnockoutRoundName(j.name)) { // Extrait vers la section « Phases finales », dédupliqué par nom entre poules const existing = koByName.get(j.name); if (existing) { const seen = new Set(existing.matches.map((m) => m.id)); matches.forEach((m) => { if (!seen.has(m.id)) { existing.matches.push(m); seen.add(m.id); } }); } else { koByName.set(j.name, { id: j.id, name: j.name, number: j.journee_number || j.number || 999, matches: matches.slice() }); } } else { rounds.push({ id: j.id, name: j.name, number: j.journee_number || j.number, matches }); } }); } // Nom normalisé : strip « Poule  » pour réutiliser le prefix côté rendu const cleanName = String(p.name || p.id ||  »).replace(/^Poule\s+/i,  »).trim() || String(p.id ||  »); normGroups.push({ name: cleanName, rounds: rounds || [], standings: p.standings }); } else if (Array.isArray(p.matches) && p.matches.length) { koByName.set(p.name || p.id, p); } }); if (normGroups.length) groupsToRender = normGroups; if (koByName.size && !knockoutsToRender) knockoutsToRender = Array.from(koByName.values()); }// Phases finales « actuelles » = au moins un match live, à venir, ou terminé // dans les 7 derniers jours. Si oui, on les rend AVANT la saison régulière // pour que l’utilisateur n’ait pas à scroller. const knockoutsAreCurrent = (knockouts) => { if (!Array.isArray(knockouts) || !knockouts.length) return false; const now = Date.now(); const RECENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; return knockouts.some((p) => (p.matches || []).some((m) => { if (isLiveState(m.etat)) return true; if (!m.date) return false; const t = new Date(m.date).getTime(); if (Number.isNaN(t)) return false; return t >= now – RECENT_WINDOW_MS; })); }; const knockoutsFirst = knockoutsAreCurrent(knockoutsToRender);const showSearch = (groupsToRender || []).length > 1 && (groupsToRender || []).some((g) => (g.standings || []).length);const searchHtml = showSearch ? `
` :  »;root.innerHTML = `
Compétition

${title}

${year ? `Saison ${year}` :  »} ${liveTotal ? `${liveTotal} match${liveTotal > 1 ? ‘s’ :  »} en direct` :  »}
${searchHtml} ${knockoutsFirst ? renderKnockouts(knockoutsToRender) + renderGroups(groupsToRender, data.competition?.display_name || slug) : renderGroups(groupsToRender, data.competition?.display_name || slug) + renderKnockouts(knockoutsToRender)} `; buildTeamIndex(groupsToRender); bindTabs(); bindKnockoutLegTabs(); bindSearch(); applyUrlState(); }) .catch((err) => { root.innerHTML = `
Erreur de chargement : ${esc(err.message)}
`; }); })();



Résultats Fédérale 2

Résultats Fédérale 2 : Cliquez sur le club ou le match de votre choix pour suivre le direct en direct/live, revoir les actions du match, les joueurs qui ont marqué, ainsi que la composition des équipes. Pour tous les amateurs de rugby, nous partagerons les Résultats à la fin de chaque journée.

Comme chaque journée, nous proposons une couverture en direct des matchs du Résultats Fédérale 2 de rugby à travers toutes les compétitions. Suivez les résultats en écoutant une couverture radio détaillée ou en suivant les scores en temps réel, nous avons tout dont vous avez besoin pour ne rien manquer de l’action sur le terrain.

Ce service couvre toutes les compétitions majeures, y compris le prestigieux Tournoi des Six Nations, la coupe du monde de rugby , le Top 14, la Pro D2, la Nationale et la Fédérale. Peu importe votre ligue préférée ou votre équipe favorite, vous trouverez ici une mine d’informations sur les Résultats Fédérale 2 pour vivre chaque instant du rugby en direct.

À lire aussi | Saint-Raphaël / Fréjus champion de France après un sacre historique en Fédérale 3

Pour accéder à la couverture en direct du Résultats Fédérale 2, il suffit de naviguer dans le menu convivial. Une fois là-bas, vous trouverez un onglet dédié intitulé « 🔴 EN DIRECT ». Cliquez simplement sur cet onglet, et vous serez transporté virtuellement au stade de votre choix, prêt à suivre le match de rugby en temps réel.

Grâce à l’équipe de commentateurs ou de scoreurs passionnés et experts du Résultats Fédérale 2, soyez plongé au cœur de l’action. Les commentaires détaillés vous tiendront informé de tous les événements marquants, des essais spectaculaires aux pénalités cruciales, en passant par les transformations réussies. Nous donnerons également des analyses approfondies, des statistiques clés, et des anecdotes intéressantes sur les joueurs et l’équipe pour enrichir votre expérience .

Si vous préférez suivre les résultats plutôt qu’écouter les commentaires en direct, le service de score en temps réel est là pour. Vous pouvez suivre les résultats pour chaque match, avec des mises à jour régulières pour tenir au courant des développements sur le terrain. Vous aurez ainsi un aperçu rapide des performances des équipes et de l’évolution des résultats tout au long des matchs de Résultats Fédérale 2.

Écoutez le direct sur une des radios dédiées. Suivez les commentaires et les résumés des matchs grâce à nos commentateurs passionnés qui vous tiendront informé de chaque action sur le terrain. Au travail, en déplacement, ou à la maison, la radio permet de rester connecté partout aux Résultats Fédérale 2.

À lire aussi | Demi-finales Fédérale 3 : le dernier carré est désormais connu

Pour ne manquer aucune action, suivez les matchs en direct sur la plateforme des radios. Les radios sont disponibles sur tous les appareils, que ce soit sur votre ordinateur, votre tablette ou votre téléphone avec notamment notre application rugby qui inclut les Résultats Fédérale 2.

Grace à la couverture en direct soyez sûr de ne rien manquer. Alors, rejoignez-nous dès maintenant pour suivre en direct dans les moindres détails, que ce soit sur la radio, en suivant les résultats. Vivez pleinement votre passion pour le rugby avec nous et plongez au cœur de l’action de Résultats Rugby Fédérale 2 !

Résultats Fédérale 2
Partager :