import streamlit as st
import pandas as pd
import numpy as np
import unicodedata
import os
import base64
import io

# ─────────────────────────────────────────────────────────────────────────────
# PAGE CONFIG
# ─────────────────────────────────────────────────────────────────────────────
st.set_page_config(
    page_title="SportData et territoires — Décideurs du sport par Patrick Bayeux",
    page_icon="🏅",
    layout="wide",
)

# ─────────────────────────────────────────────────────────────────────────────
# PATHS
# ─────────────────────────────────────────────────────────────────────────────
# Détection automatique : local (dossier parent) ou Streamlit Cloud (data/)
_APP_DIR  = os.path.dirname(os.path.abspath(__file__))
_LOCAL    = os.path.normpath(os.path.join(_APP_DIR, ".."))   # ../  en local
_CLOUD    = os.path.join(_APP_DIR, "data")                   # data/  sur Cloud

if os.path.isdir(os.path.join(_LOCAL, "Equipements licenciés")):
    BASE_DIR = _LOCAL + os.sep       # mode local (chemin existant)
elif os.path.isdir(os.path.join(_CLOUD, "Equipements licenciés")):
    BASE_DIR = _CLOUD + os.sep       # mode Streamlit Cloud
else:
    # Fallback : chemin absolu historique
    BASE_DIR = "/Users/patrickbayeux/Documents/2 - Observatoire/DATA/2026/"

SCOLAIRES_DIR = BASE_DIR + "scolaires/"
SPORTS_DIR    = BASE_DIR + "Equipements licenciés/"
CLAUDE_DIR    = BASE_DIR + "claude/"


def find_file(filename, dirs=None):
    dirs = dirs or [SPORTS_DIR, CLAUDE_DIR, BASE_DIR]
    for d in dirs:
        p = os.path.join(d, filename)
        if os.path.exists(p):
            return p
    return None


# ─────────────────────────────────────────────────────────────────────────────
# MODÈLE ÉQUIPEMENTS LICENCIÉS
# ─────────────────────────────────────────────────────────────────────────────
MODELE_EQUIP = [
    {
        "equipement": "Courts de tennis",
        "equip_col": "equip_Court de tennis",
        "coefficient": 1.15,
        "disciplines": [
            {"federation": "FF de Tennis", "effectif": 4, "entrainements": 2, "duree": 1.5, "dispo": 35},
        ],
    },
    {
        "equipement": "Salles multisports / Gymnases",
        "equip_col": "equip_Salle multisports",
        "coefficient": 1.15,
        "disciplines": [
            {"federation": "FF de Basketball",  "effectif": 10, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF de Handball",    "effectif": 14, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF de Volley",      "effectif": 12, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF de Badminton",   "effectif": 15, "entrainements": 3, "duree": 1.5, "dispo": 35},
        ],
    },
    {
        "equipement": "Terrains de grands jeux",
        "equip_col": "equip_Terrain de grands jeux",
        "coefficient": 1.2,
        "disciplines": [
            {"federation": "FF de Football", "effectif": 30, "entrainements": 3, "duree": 1.5, "dispo": 15},
            {"federation": "FF de Rugby",    "effectif": 40, "entrainements": 3, "duree": 1.5, "dispo": 15},
        ],
    },
    {
        "equipement": "Salles de combat",
        "equip_col": "equip_Salle de combat",
        "coefficient": 1.2,
        "disciplines": [
            {"federation": "FF de Judo, Jujitsu, Kendo et DA",        "effectif": 20, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF de Taekwondo et DA",                    "effectif": 20, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF d'Aïkido, d'Aïkibudo et Affinitaires",  "effectif": 20, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF d'Aïkido et de Budo",                   "effectif": 20, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF de Karaté et DA",                       "effectif": 20, "entrainements": 3, "duree": 1.5, "dispo": 35},
            {"federation": "FF de Boxe",                               "effectif": 12, "entrainements": 3, "duree": 1.5, "dispo": 35},
        ],
    },
]

# Catégories de fédérations selon le code national
FED_CATEGORIES = {
    "🏅 Fédérations olympiques":                 (100, 199),
    "🎯 Fédérations délégataires non olympiques": (200, 299),
    "🤝 Fédérations multisports affinitaires":    (400, 499),
    "♿ Fédérations handi & para":                (500, 599),
    "🎓 Fédérations scolaires & universitaires":  (600, 699),
}

# Fédérations aquatiques (noms exacts issus des données 2023)
FEDS_AQUATIQUES = [
    "FF de Natation",
    "FF de Triathlon et Disciplines Enchainées",
    "FF de Sauvetage et de Secourisme",
]

# Ratios m² plan d'eau par habitant
RATIOS_M2 = [
    ("Hypothèse basse",    0.016),
    ("Hypothèse moyenne",  0.018),
    ("Hypothèse haute",    0.020),
]

# ─────────────────────────────────────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────────────────────────────────────
def safe_int(val):
    try:
        return int(val) if pd.notna(val) else 0
    except Exception:
        return 0


def fmt(n):
    try:
        return f"{int(n):,}".replace(",", "\u202f")
    except Exception:
        return "—"


def dep_normalize(val):
    s = str(val).strip()
    if "." in s:
        try:
            s = str(int(float(s)))
        except Exception:
            pass
    return s.zfill(2) if s.isdigit() else s


def norm_code(val):
    s = str(val).strip().split(".")[0]
    return s.zfill(5) if s.isdigit() else s


def rate_str(val):
    if val is None or (isinstance(val, float) and np.isnan(val)):
        return "—"
    return f"{val:.1f}"


# ─────────────────────────────────────────────────────────────────────────────
# DATA LOADING
# ─────────────────────────────────────────────────────────────────────────────

@st.cache_data(show_spinner="Chargement des données scolaires…")
def load_scolaires():
    ecoles   = pd.read_parquet(SCOLAIRES_DIR + "ecoles_2024.parquet")
    colleges = pd.read_parquet(SCOLAIRES_DIR + "colleges_2024.parquet")
    lycees   = pd.read_parquet(SCOLAIRES_DIR + "lycees_2024.parquet")

    if "Code département pays" in lycees.columns and "Code département" not in lycees.columns:
        lycees = lycees.rename(columns={"Code département pays": "Code département"})

    for df in [ecoles, colleges, lycees]:
        df["_commune_norm"] = df["Commune"].str.upper().str.strip()
        df["_dep_str"]      = df["Code département"].apply(dep_normalize)
        df["_dep_code"]     = pd.to_numeric(df["Code département"], errors="coerce").astype("Int64")

    return ecoles, colleges, lycees


@st.cache_data(show_spinner="Chargement du référentiel communes…")
def load_communes_ref():
    df    = pd.read_excel(SCOLAIRES_DIR + "communes-france-2025.xlsx", header=1)
    avail = df.columns.tolist()
    reg_cols = [c for c in avail if any(k in str(c).lower() for k in
                ["reg_code", "reg_nom", "region", "région"])]
    base = ["code_insee", "nom_standard", "dep_code", "dep_nom",
            "epci_nom", "epci_code", "population"]
    keep = [c for c in (base + reg_cols) if c in avail]
    df   = df[keep].copy()

    df["nom_norm"]   = df["nom_standard"].apply(
        lambda s: unicodedata.normalize("NFD", str(s).upper().strip()).encode("ascii", "ignore").decode()
    )
    df["dep_code"]   = pd.to_numeric(df["dep_code"], errors="coerce").astype("Int64")
    df["dep_str"]    = df["dep_code"].apply(lambda x: dep_normalize(x) if pd.notna(x) else "")
    df["population"] = pd.to_numeric(df["population"], errors="coerce").fillna(0).astype(int)
    if "epci_nom" in df.columns:
        df["epci_nom"] = df["epci_nom"].fillna("").astype(str).str.strip()
    return df


@st.cache_data(show_spinner="Chargement des données sportives…")
def load_sports():
    path = find_file("synthese_sports.xlsx")
    if path is None:
        st.error("❌ Fichier synthese_sports.xlsx introuvable.")
        st.stop()

    comm      = pd.read_excel(path, sheet_name="Communes")
    epci      = pd.read_excel(path, sheet_name="EPCI")
    equip_fam = pd.read_excel(path, sheet_name="Equip_par_famille")

    # ── Supprimer les lignes TOTAL / non-communes ─────────────────────────────
    # Le fichier Excel contient :
    #   • une ligne TOTAL (code_commune = NaN, population ≈ 67 M) en bas
    #   • une ligne "NR - Non réparti" sans population utile
    # Ces deux lignes feraient doubler le total national si incluses.
    # Codes valides :
    #   • 5 chiffres       → métropole  (ex. 75056)
    #   • 2A/2B + 3 chiffres → Corse   (ex. 2A004)
    #   • 97x + 3 chiffres → DOM-TOM   (ex. 971101 Guadeloupe)
    _VALID_CODE = r"^(\d{5}|2[AB]\d{3}|97[0-9]\d{3})$"
    def _keep_code(df):
        mask = (
            df["code_commune"].notna() &
            df["code_commune"].astype(str).str.strip()
                              .str.replace(r"\.0$", "", regex=True)
                              .str.match(_VALID_CODE)
        )
        return df[mask].copy()
    comm      = _keep_code(comm)
    equip_fam = _keep_code(equip_fam)

    comm["code_commune"] = comm["code_commune"].apply(norm_code)
    comm["dep"]          = comm["dep"].apply(dep_normalize)
    comm["epci_nom"]     = comm["epci_nom"].fillna("").astype(str).str.strip()
    comm["population"]   = pd.to_numeric(comm["population"], errors="coerce").fillna(0).astype(int)

    # Résolution des noms de région via le code département (dep_code → reg_nom)
    # ⚠️ synthese_sports utilise les ANCIENS codes région INSEE (pre-2016 : 72=Aquitaine,
    #    82=Rhône-Alpes…) qui ne coïncident pas avec les nouveaux (75=Nouvelle-Aquitaine,
    #    84=Auvergne-Rhône-Alpes…). On utilise donc le code département comme clé de
    #    jointure — les codes dep n'ont pas changé lors de la réforme territoriale.
    try:
        ref_dep = pd.read_excel(
            SCOLAIRES_DIR + "communes-france-2025.xlsx", header=1,
            usecols=["dep_code", "reg_nom"]
        )
        # ⚠️ dep_code peut être numérique (float) OU alphanumérique (2A, 2B Corse)
        def _to_dep_str(v):
            s = str(v).strip().upper()
            if s in ("2A", "2B"):          # Corse-du-Sud / Haute-Corse
                return s
            try:
                return dep_normalize(int(float(s)))
            except (ValueError, TypeError):
                return dep_normalize(s) if s and s not in ("NAN", "") else ""
        ref_dep["dep_str_tmp"] = ref_dep["dep_code"].apply(_to_dep_str)
        dep_to_reg = (
            ref_dep[ref_dep["dep_str_tmp"] != ""]
            .groupby("dep_str_tmp")["reg_nom"]
            .first()
            .to_dict()
        )
        comm["region"] = comm["dep"].map(dep_to_reg).fillna("")
    except Exception:
        comm["region"] = ""

    comm["region"] = comm["region"].fillna("").astype(str).str.strip()

    for col in ["nb_clubs", "nb_licencies", "nb_equipements",
                "licencies_pour_1000_hab", "clubs_pour_1000_hab", "equip_pour_1000_hab"]:
        if col in comm.columns:
            comm[col] = pd.to_numeric(comm[col], errors="coerce").fillna(0)

    epci["epci_nom"]   = epci["epci_nom"].fillna("").astype(str).str.strip()
    epci["population"] = pd.to_numeric(epci["population"], errors="coerce").fillna(0).astype(int)
    for col in ["nb_clubs", "nb_licencies", "nb_equipements",
                "licencies_pour_1000_hab", "clubs_pour_1000_hab", "equip_pour_1000_hab"]:
        if col in epci.columns:
            epci[col] = pd.to_numeric(epci[col], errors="coerce").fillna(0)

    equip_fam["code_commune"] = equip_fam["code_commune"].apply(norm_code)
    return comm, epci, equip_fam


@st.cache_data(show_spinner="Chargement des licenciés (patientez…)")
def load_licencies():
    # Préférer le parquet (10× plus petit) ; fallback sur xlsx
    path = find_file("lic-data-2023.parquet") or find_file("lic-data-2023.xlsx")
    if path is None:
        return pd.DataFrame(columns=["code", "federation", "total"])
    lic = pd.read_parquet(path) if path.endswith(".parquet") else pd.read_excel(path)
    lic["code"]  = lic["Code Commune"].apply(norm_code)
    lic["total"] = pd.to_numeric(lic["Total"], errors="coerce").fillna(0).astype(int)
    return lic[["code", "Fédération", "total"]].rename(columns={"Fédération": "federation"})


@st.cache_data(show_spinner="Chargement des codes fédérations…")
def load_fed_code_map():
    """Charge le mapping {federation_name: code_int} depuis lic-data-2023 (parquet ou xlsx)."""
    path = find_file("lic-data-2023.parquet") or find_file("lic-data-2023.xlsx")
    if path is None:
        return {}, {}
    df = pd.read_parquet(path) if path.endswith(".parquet") else pd.read_excel(path)
    # Colonnes attendues : 'Code' et 'Fédération'
    if "Code" not in df.columns or "Fédération" not in df.columns:
        return {}, {}
    df2 = df[["Fédération", "Code"]].drop_duplicates(subset=["Fédération"]).dropna(subset=["Code"])
    df2["Code"] = pd.to_numeric(df2["Code"], errors="coerce")
    df2 = df2.dropna(subset=["Code"])
    code_map = {row["Fédération"]: int(row["Code"]) for _, row in df2.iterrows()}
    # Construire fed_cat_map
    cat_map = {}
    for fed, code in code_map.items():
        for cat_label, (lo, hi) in FED_CATEGORIES.items():
            if lo <= code <= hi:
                cat_map[fed] = cat_label
                break
    return code_map, cat_map


@st.cache_data(show_spinner="Chargement des clubs…")
def load_clubs():
    path = find_file("clubs-data-2023.xlsx")
    if path is None:
        return pd.DataFrame(columns=["code", "federation", "total"])
    clubs = pd.read_excel(path)
    clubs["code"]  = clubs["Code Commune"].apply(norm_code)
    clubs["total"] = pd.to_numeric(clubs["Total_actifs"], errors="coerce").fillna(0).astype(int)
    return clubs[["code", "Fédération", "total"]].rename(columns={"Fédération": "federation"})


@st.cache_data(show_spinner="Chargement des données piscines…")
def load_piscines():
    """Charge data-es (parquet ou xlsx) — bassins de natation filtrés.
    Types retenus : sportif, ludique, mixte, exercice.
    """
    path = find_file("data-es.parquet") or find_file("data-es.xlsx")
    if path is None:
        return pd.DataFrame(columns=["code", "type_bassin", "nature_cat", "equip_bassin_surf"])
    if path.endswith(".parquet"):
        df = pd.read_parquet(path,
                             columns=["equip_type_name", "equip_nature", "equip_bassin_surf", "new_code"])
    else:
        df = pd.read_excel(
            path,
            usecols=["equip_type_name", "equip_nature", "equip_bassin_surf", "new_code"],
        )
    # ── Filtre : uniquement les 4 types de bassins de natation ────────────────
    TYPES_BASSIN = {
        "Bassin sportif de natation":    "Sportif",
        "Bassin ludique de natation":    "Ludique",
        "Bassin mixte de natation":      "Mixte",
        "Bassin d'exercices aquatiques": "Exercice",
    }
    df = df[df["equip_type_name"].isin(TYPES_BASSIN.keys())].copy()
    df["type_bassin"]       = df["equip_type_name"].map(TYPES_BASSIN)
    df["code"]              = df["new_code"].astype(str).str.strip().str.zfill(5)
    # equip_bassin_surf = surface spécifique du plan d'eau (≠ equip_surf générique)
    df["equip_bassin_surf"] = pd.to_numeric(df["equip_bassin_surf"], errors="coerce").fillna(0)
    # ── Nature du plan d'eau ──────────────────────────────────────────────────
    NATURE_MAP = {
        "Intérieur":         "Intérieur",
        "Découvert":         "Découvert",
        "Découvrable":       "Découvrable",
        "Extérieur couvert": "Extérieur couvert",
    }
    df["nature_cat"] = df["equip_nature"].map(NATURE_MAP).fillna("Autres")
    return df[["code", "type_bassin", "nature_cat", "equip_bassin_surf"]]


def get_piscines_m2(codes):
    """Agrège les m² de plan d'eau pour un ensemble de codes commune.
    Retourne (df_nature, df_type, total_m2) :
      - df_nature : ventilation par nature (intérieur / découvert…)
      - df_type   : ventilation par type de bassin (sportif / ludique / mixte / exercice)
    """
    zone = piscines_df[piscines_df["code"].isin(codes)]
    if zone.empty:
        empty_nat  = pd.DataFrame(columns=["Nature du plan d'eau", "Bassins", "m² total"])
        empty_type = pd.DataFrame(columns=["Type de bassin", "Bassins", "m² total"])
        return empty_nat, empty_type, 0

    S = "equip_bassin_surf"   # colonne surface spécifique bassins de natation

    # — Par nature de plan d'eau —
    ORDRE_NAT = ["Intérieur", "Découvert", "Découvrable", "Extérieur couvert", "Autres"]
    grp_nat = (zone.groupby("nature_cat")
                   .agg(bassins=(S, "count"), m2=(S, "sum"))
                   .reindex(ORDRE_NAT).dropna(how="all").reset_index())
    grp_nat.columns = ["Nature du plan d'eau", "Bassins", "m² total"]
    grp_nat["m² total"] = grp_nat["m² total"].round(0).astype(int)
    total_m2 = int(grp_nat["m² total"].sum())
    tot_nat = pd.DataFrame([{"Nature du plan d'eau": "**TOTAL**",
                              "Bassins": int(grp_nat["Bassins"].sum()),
                              "m² total": total_m2}])
    grp_nat = pd.concat([grp_nat, tot_nat], ignore_index=True)

    # — Par type de bassin —
    ORDRE_TYPE = ["Sportif", "Ludique", "Mixte", "Exercice"]
    grp_type = (zone.groupby("type_bassin")
                    .agg(bassins=(S, "count"), m2=(S, "sum"))
                    .reindex(ORDRE_TYPE).dropna(how="all").reset_index())
    grp_type.columns = ["Type de bassin", "Bassins", "m² total"]
    grp_type["m² total"] = grp_type["m² total"].round(0).astype(int)
    tot_type = pd.DataFrame([{"Type de bassin": "**TOTAL**",
                               "Bassins": int(grp_type["Bassins"].sum()),
                               "m² total": total_m2}])
    grp_type = pd.concat([grp_type, tot_type], ignore_index=True)

    return grp_nat, grp_type, total_m2


# ─────────────────────────────────────────────────────────────────────────────
# CHARGEMENT GLOBAL
# ─────────────────────────────────────────────────────────────────────────────
ecoles_df, colleges_df, lycees_df = load_scolaires()
communes_ref                       = load_communes_ref()
comm_df, epci_df, equip_fam_df    = load_sports()
lic_df                             = load_licencies()
clubs_df                           = load_clubs()
piscines_df                        = load_piscines()
fed_code_map, fed_cat_map          = load_fed_code_map()

FAM_COLS = [c for c in equip_fam_df.columns if c.startswith("equip_")]

# ── Mapping dep → région ──────────────────────────────────────────────────────
dep_region_map = {}
if "region" in comm_df.columns and comm_df["region"].ne("").any():
    dep_region_map = (
        comm_df[comm_df["region"] != ""]
        .groupby("dep")["region"].first()
        .to_dict()
    )

# ── Liste régions ─────────────────────────────────────────────────────────────
agg_cols = {col: (col, "sum") for col in
            ["population", "nb_clubs", "nb_licencies", "nb_equipements"]
            if col in comm_df.columns}
if "region" in comm_df.columns and comm_df["region"].ne("").any() and agg_cols:
    regions_agg = (
        comm_df[comm_df["region"] != ""]
        .groupby("region")
        .agg(**agg_cols)
        .reset_index()
        .sort_values("region")
    )
else:
    regions_agg = pd.DataFrame()

# ── Liste départements ────────────────────────────────────────────────────────
deps_agg = (
    comm_df.groupby("dep")
    .agg(**agg_cols)
    .reset_index()
    .sort_values("dep")
)
dep_names = communes_ref.groupby("dep_str")["dep_nom"].first().to_dict()
deps_agg["dep_nom"] = deps_agg["dep"].map(dep_names).fillna(deps_agg["dep"])
deps_agg["label"]   = deps_agg["dep"] + " — " + deps_agg["dep_nom"]
deps_agg["region"]  = deps_agg["dep"].map(dep_region_map).fillna("")

# ── Liste EPCI ────────────────────────────────────────────────────────────────
epci_list = epci_df[epci_df["epci_nom"] != ""].copy().sort_values("epci_nom")

# ── Liste communes ────────────────────────────────────────────────────────────
communes_list = comm_df[comm_df["commune"].notna() & (comm_df["commune"] != "")].copy()
_pop_str = communes_list["population"].apply(lambda p: f"  {fmt(p)} hab." if p > 0 else "")
communes_list["label"] = (communes_list["commune"].str.title()
                          + "  (" + communes_list["dep"] + ")" + _pop_str)
communes_list = communes_list.sort_values("label").reset_index(drop=True)


# ─────────────────────────────────────────────────────────────────────────────
# STRATES DE RÉFÉRENCE — communes et EPCI
# ─────────────────────────────────────────────────────────────────────────────
STRATES_COMM = [
    (0,        3_500,   "< 3 500 hab."),
    (3_500,    10_000,  "3 500 à 10 000 hab."),
    (10_000,   30_000,  "10 000 à 30 000 hab."),
    (30_000,   100_000, "30 000 à 100 000 hab."),
    (100_000,  None,    "> 100 000 hab."),
]
STRATES_EPCI = [
    (0,        15_000,  "< 15 000 hab."),
    (15_000,   50_000,  "15 000 à 50 000 hab."),
    (50_000,   250_000, "50 000 à 250 000 hab."),
    (250_000,  None,    "> 250 000 hab."),
]


def assign_strate(pop, strates):
    """Retourne le libellé de la strate correspondant à une population."""
    for lo, hi, label in strates:
        if hi is None:
            if pop >= lo:
                return label
        elif lo <= pop < hi:
            return label
    return "—"


def compute_strate_avgs(df, strates):
    """Taux pondérés par la population : sum(total) / sum(pop) × 1000 par strate."""
    avgs = {}
    if df.empty or "population" not in df.columns:
        return avgs
    d = df[df["population"] > 0].copy()
    d["strate"] = d["population"].apply(lambda p: assign_strate(p, strates))
    for nb_col, key in [
        ("nb_licencies",    "lic"),
        ("nb_clubs",        "clubs"),
        ("nb_equipements",  "equip"),
    ]:
        if nb_col not in d.columns:
            continue
        grp = d.groupby("strate").agg(total=(nb_col, "sum"), pop=("population", "sum"))
        grp["rate"] = (grp["total"] / grp["pop"] * 1000).round(1)
        avgs[key] = grp["rate"].to_dict()
    return avgs


comm_strates_avgs = compute_strate_avgs(comm_df,  STRATES_COMM)
epci_strates_avgs = compute_strate_avgs(epci_df,  STRATES_EPCI)

# ── Taux par département/région/France par fédération ─────────────────────────
# Population par département (depuis comm_df, une fois)
_dep_pop_total  = comm_df[comm_df["population"] > 0].groupby("dep")["population"].sum().to_dict()
_reg_pop_total  = (comm_df[(comm_df["population"] > 0) & (comm_df["region"] != "")]
                   .groupby("region")["population"].sum().to_dict()
                   if "region" in comm_df.columns else {})
_nat_pop_total  = sum(_dep_pop_total.values())

# Licenciés par dep et fédération
_lic_dep_merge = lic_df.merge(
    comm_df[["code_commune", "dep", "region"]], left_on="code", right_on="code_commune", how="inner"
)

# Taux dep par fédération: {dep: {fed: rate}}
dep_lic_rates = {}
for dep, grp in _lic_dep_merge.groupby("dep"):
    pop = _dep_pop_total.get(dep, 0)
    if pop > 0:
        fed_t = grp.groupby("federation")["total"].sum()
        dep_lic_rates[dep] = (fed_t / pop * 1000).round(2).to_dict()

# Taux région par fédération: {region: {fed: rate}}
_lic_reg_merge = _lic_dep_merge[_lic_dep_merge["region"] != ""] if "region" in _lic_dep_merge.columns else pd.DataFrame()
reg_lic_rates = {}
if not _lic_reg_merge.empty:
    for reg, grp in _lic_reg_merge.groupby("region"):
        pop = _reg_pop_total.get(reg, 0)
        if pop > 0:
            fed_t = grp.groupby("federation")["total"].sum()
            reg_lic_rates[reg] = (fed_t / pop * 1000).round(2).to_dict()

# Taux national par fédération: {fed: rate}
nat_lic_rates = (
    (lic_df.groupby("federation")["total"].sum() / _nat_pop_total * 1000).round(2).to_dict()
    if _nat_pop_total > 0 else {}
)

# ── Taux équipements par département/région/France par famille ────────────────
# equip_fam_df possède déjà la colonne "dep" — on ajoute juste "region"
# via dep_region_map pour éviter un conflit de colonnes lors d'un merge.
_dep_equip_df = equip_fam_df.copy()
_dep_equip_df["region"] = _dep_equip_df["dep"].map(dep_region_map).fillna("")

dep_equip_rates = {}
for dep, grp in _dep_equip_df.groupby("dep"):
    pop = _dep_pop_total.get(dep, 0)
    if pop > 0:
        fam_row = {}
        for col in FAM_COLS:
            fam_name = col.replace("equip_", "")
            fam_row[fam_name] = round(grp[col].sum() / pop * 1000, 2)
        dep_equip_rates[dep] = fam_row

_reg_equip_df = _dep_equip_df[_dep_equip_df["region"] != ""]
reg_equip_rates = {}
for reg, grp in _reg_equip_df.groupby("region"):
    pop = _reg_pop_total.get(reg, 0)
    if pop > 0:
        fam_row = {}
        for col in FAM_COLS:
            fam_name = col.replace("equip_", "")
            fam_row[fam_name] = round(grp[col].sum() / pop * 1000, 2)
        reg_equip_rates[reg] = fam_row

nat_equip_rates = {}
if _nat_pop_total > 0 and not equip_fam_df.empty:
    for col in FAM_COLS:
        fam_name = col.replace("equip_", "")
        nat_equip_rates[fam_name] = round(equip_fam_df[col].sum() / _nat_pop_total * 1000, 2)

# ── Taux clubs par département/région/France par fédération ──────────────────
_clubs_dep_merge = clubs_df.merge(
    comm_df[["code_commune", "dep", "region"]], left_on="code", right_on="code_commune", how="inner"
)

dep_clubs_rates = {}
for dep, grp in _clubs_dep_merge.groupby("dep"):
    pop = _dep_pop_total.get(dep, 0)
    if pop > 0:
        fed_t = grp.groupby("federation")["total"].sum()
        dep_clubs_rates[dep] = (fed_t / pop * 1000).round(2).to_dict()

_clubs_reg_merge = _clubs_dep_merge[_clubs_dep_merge["region"] != ""] if "region" in _clubs_dep_merge.columns else pd.DataFrame()
reg_clubs_rates = {}
if not _clubs_reg_merge.empty:
    for reg, grp in _clubs_reg_merge.groupby("region"):
        pop = _reg_pop_total.get(reg, 0)
        if pop > 0:
            fed_t = grp.groupby("federation")["total"].sum()
            reg_clubs_rates[reg] = (fed_t / pop * 1000).round(2).to_dict()

nat_clubs_rates = (
    (clubs_df.groupby("federation")["total"].sum() / _nat_pop_total * 1000).round(2).to_dict()
    if _nat_pop_total > 0 else {}
)

# ── Helpers pour taux de catégorie fédération ─────────────────────────────────
def get_cat_rates_by_zone(lic_zone_df, pop, cat_feds):
    """Calcule taux/1000 pour une catégorie de fédérations dans une zone."""
    if pop <= 0:
        return None
    total = lic_zone_df[lic_zone_df["federation"].isin(cat_feds)]["total"].sum()
    return round(total / pop * 1000, 2)


def get_cat_rates_dep(dep, cat_feds):
    """Calcule taux/1000 pour une catégorie dans un département."""
    pop = _dep_pop_total.get(dep, 0)
    if pop <= 0 or dep not in dep_lic_rates:
        return None
    total_lic = sum(
        round(dep_lic_rates[dep].get(f, 0) * pop / 1000)
        for f in cat_feds if f in dep_lic_rates.get(dep, {})
    )
    return round(total_lic / pop * 1000, 2) if pop > 0 else None


def get_cat_rates_reg(reg, cat_feds):
    """Calcule taux/1000 pour une catégorie dans une région."""
    pop = _reg_pop_total.get(reg, 0)
    if pop <= 0 or reg not in reg_lic_rates:
        return None
    total_lic = sum(
        round(reg_lic_rates[reg].get(f, 0) * pop / 1000)
        for f in cat_feds if f in reg_lic_rates.get(reg, {})
    )
    return round(total_lic / pop * 1000, 2) if pop > 0 else None


def get_cat_rates_nat(cat_feds):
    """Calcule taux/1000 pour une catégorie au niveau national."""
    if _nat_pop_total <= 0:
        return None
    total_lic = sum(
        round(nat_lic_rates.get(f, 0) * _nat_pop_total / 1000)
        for f in cat_feds if f in nat_lic_rates
    )
    return round(total_lic / _nat_pop_total * 1000, 2)


def _build_pop_map_comm():
    """
    Retourne :
      _pop_map        : code_commune → population
      _strate_comm_map: code_commune → strate commune
      _strate_epci_map: code_commune → strate EPCI

    Stratégie EPCI : toutes les communes françaises appartiennent à un EPCI.
    On agrège les populations par EPCI depuis communes_ref (communes-france-2025.xlsx)
    pour obtenir une couverture complète (~67 M hab.), puis on assigne la strate EPCI
    à chaque commune via son epci_code dans communes_ref.
    """
    # Codes officiels depuis communes_ref (exclut les lignes NaN, arrondissements
    # Paris/Lyon/Marseille et toute ligne aberrante absente du référentiel officiel)
    _valid_codes = set()
    if "code_insee" in communes_ref.columns:
        _valid_codes = set(communes_ref["code_insee"].astype(str).str.strip().str.zfill(5))

    _comm_raw = comm_df[
        (comm_df["population"] > 0)
        & comm_df["code_commune"].notna()
        & (comm_df["code_commune"].astype(str).str.strip() != "")
    ]
    if _valid_codes:
        _comm_raw = _comm_raw[_comm_raw["code_commune"].astype(str).str.strip().str.zfill(5).isin(_valid_codes)]

    p = _comm_raw.set_index(
        _comm_raw["code_commune"].astype(str).str.strip().str.zfill(5)
    )["population"].to_dict()
    s = {k: assign_strate(v, STRATES_COMM) for k, v in p.items()}

    # ── Strate EPCI via communes_ref ─────────────────────────────────────────
    if "epci_code" in communes_ref.columns and "code_insee" in communes_ref.columns:
        ref = communes_ref.copy()
        ref["pop_ref"]   = pd.to_numeric(ref.get("population", 0), errors="coerce").fillna(0)
        ref["code_z5"]   = ref["code_insee"].astype(str).str.strip().str.zfill(5)
        ref["epci_norm"] = ref["epci_code"].apply(
            lambda x: str(x).strip()
            if pd.notna(x) and str(x).strip() not in ("nan", "ZZZZZZZZZ", "") else None
        )
        # Population par EPCI calculée depuis communes_ref
        epci_pop_ref = (
            ref[ref["epci_norm"].notna()]
            .groupby("epci_norm")["pop_ref"]
            .sum()
            .to_dict()
        )
        epci_strate = {k: assign_strate(v, STRATES_EPCI) for k, v in epci_pop_ref.items()}
        # Mapping commune → strate EPCI
        comm_epci_map = (
            ref[ref["epci_norm"].notna()]
            .set_index("code_z5")["epci_norm"]
            .to_dict()
        )
        s_epci = {code: epci_strate.get(comm_epci_map.get(code, ""), "—") for code in p}
    else:
        # Fallback : epci_code depuis comm_df
        epci_pop = epci_df.set_index("epci_code")["population"].to_dict() if not epci_df.empty else {}
        epci_strate = {}
        for k, v in epci_pop.items():
            try:
                epci_strate[str(int(float(str(k))))] = assign_strate(v, STRATES_EPCI)
            except (ValueError, TypeError):
                pass
        comm_epci_fb = {}
        for code, epci in comm_df.set_index("code_commune")["epci_code"].items():
            try:
                comm_epci_fb[str(code)] = str(int(float(str(epci))))
            except (ValueError, TypeError):
                pass
        s_epci = {code: epci_strate.get(comm_epci_fb.get(code, ""), "—") for code in p}

    return p, s, s_epci


_pop_map, _strate_comm_map, _strate_epci_map = _build_pop_map_comm()

# Population totale par strate (chaque commune comptée UNE SEULE FOIS)
_pop_par_strate_comm = {}
_pop_par_strate_epci = {}
for _code, _pop in _pop_map.items():
    _sc = _strate_comm_map.get(_code, "—")
    _se = _strate_epci_map.get(_code, "—")
    if _sc != "—":
        _pop_par_strate_comm[_sc] = _pop_par_strate_comm.get(_sc, 0) + _pop
    if _se != "—":
        _pop_par_strate_epci[_se] = _pop_par_strate_epci.get(_se, 0) + _pop


def _fed_strate_rates(src_df, strate_map, pop_par_strate):
    """
    Taux correct : sum(licenciés strate+fed) / pop_totale_strate × 1000
    La population par strate est pré-calculée depuis comm_df (une fois par commune).
    src_df : DataFrame avec colonnes code, federation, total.
    """
    if src_df.empty:
        return {}
    d = src_df.copy()
    d["strate"] = d["code"].map(strate_map).fillna("—")
    d = d[d["strate"] != "—"]
    grp = d.groupby(["strate", "federation"])["total"].sum().reset_index()
    res = {}
    for _, row in grp.iterrows():
        pop = pop_par_strate.get(row["strate"], 0)
        if pop > 0:
            rate = round(row["total"] / pop * 1000, 2)
            res.setdefault(row["strate"], {})[row["federation"]] = rate
    return res


def _equip_fam_strate_rates(strate_map, pop_par_strate):
    """
    Taux équipements / 1 000 hab. par famille et par strate.
    equip_fam_df a une ligne par commune → population comptée une seule fois.
    """
    if equip_fam_df.empty:
        return {}
    d = equip_fam_df.copy()
    d["strate"] = d["code_commune"].map(strate_map).fillna("—")
    d = d[d["strate"] != "—"]
    res = {}
    for fam_col in FAM_COLS:
        fam_name = fam_col.replace("equip_", "")
        grp = d.groupby("strate")[fam_col].sum()
        for strate, tot in grp.items():
            pop = pop_par_strate.get(strate, 0)
            if pop > 0:
                res.setdefault(strate, {})[fam_name] = round(tot / pop * 1000, 2)
    return res


# Pré-calcul : licenciés/1000 par fed et par strate (communes + EPCI)
lic_fed_comm_avgs   = _fed_strate_rates(lic_df,   _strate_comm_map, _pop_par_strate_comm)
lic_fed_epci_avgs   = _fed_strate_rates(lic_df,   _strate_epci_map, _pop_par_strate_epci)
# Pré-calcul : clubs/1000 par fed et par strate
clubs_fed_comm_avgs = _fed_strate_rates(clubs_df, _strate_comm_map, _pop_par_strate_comm)
clubs_fed_epci_avgs = _fed_strate_rates(clubs_df, _strate_epci_map, _pop_par_strate_epci)
# Pré-calcul : équipements/1000 par famille et par strate
equip_fam_comm_avgs = _equip_fam_strate_rates(_strate_comm_map, _pop_par_strate_comm)
equip_fam_epci_avgs = _equip_fam_strate_rates(_strate_epci_map, _pop_par_strate_epci)


# ─────────────────────────────────────────────────────────────────────────────
# FONCTIONS DE FILTRAGE SCOLAIRES
# ─────────────────────────────────────────────────────────────────────────────
def filter_scolaires_dep(dep_list):
    dep_set = set(dep_list) if isinstance(dep_list, (list, set)) else {dep_list}
    return (ecoles_df[ecoles_df["_dep_str"].isin(dep_set)],
            colleges_df[colleges_df["_dep_str"].isin(dep_set)],
            lycees_df[lycees_df["_dep_str"].isin(dep_set)])


def filter_scolaires_region(region):
    deps = [d for d, r in dep_region_map.items() if r == region]
    return filter_scolaires_dep(deps)


def filter_scolaires_epci(selected_epci):
    comm_epci = communes_ref[communes_ref["epci_nom"] == selected_epci][
        ["nom_norm", "dep_code"]].copy()

    def _f(df):
        m = df.merge(comm_epci, left_on=["_commune_norm", "_dep_code"],
                     right_on=["nom_norm", "dep_code"], how="inner")
        return m.drop(columns=["nom_norm", "dep_code"], errors="ignore")

    return _f(ecoles_df), _f(colleges_df), _f(lycees_df)


def filter_scolaires_commune(commune_norm, dep_str):
    return (ecoles_df[(ecoles_df["_commune_norm"] == commune_norm) & (ecoles_df["_dep_str"] == dep_str)],
            colleges_df[(colleges_df["_commune_norm"] == commune_norm) & (colleges_df["_dep_str"] == dep_str)],
            lycees_df[(lycees_df["_commune_norm"] == commune_norm) & (lycees_df["_dep_str"] == dep_str)])


def scolaires_totals(ec, co, ly):
    t_ec  = safe_int(ec["Nombre total d'élèves"].sum())                           if not ec.empty else 0
    mat   = safe_int(ec["Nombre d'élèves en pré-élémentaire hors ULIS"].sum())    if not ec.empty else 0
    prim  = safe_int(ec["Nombre d'élèves en élémentaire hors ULIS"].sum())        if not ec.empty else 0
    ulis_ec = safe_int(ec["Nombre d'élèves en ULIS"].sum())                       if not ec.empty else 0
    t_co  = safe_int(co["nombre_eleves_total"].sum())                              if not co.empty else 0
    t_ly  = safe_int(ly["Nombre d'élèves"].sum())                                  if not ly.empty else 0
    # Nombre d'établissements par type
    nb_mat  = int((ec["Nombre d'élèves en pré-élémentaire hors ULIS"] > 0).sum()) if not ec.empty else 0
    nb_prim = int((ec["Nombre d'élèves en élémentaire hors ULIS"] > 0).sum())     if not ec.empty else 0
    return dict(nb_ec=len(ec), nb_co=len(co), nb_ly=len(ly),
                nb_mat=nb_mat, nb_prim=nb_prim,
                t_ec=t_ec, t_co=t_co, t_ly=t_ly,
                total=t_ec + t_co + t_ly,
                maternelle=mat, primaire=prim, ulis_ec=ulis_ec)


# ─────────────────────────────────────────────────────────────────────────────
# CALCULS BESOINS SCOLAIRES
# ─────────────────────────────────────────────────────────────────────────────
HYP_BAS, HYP_HAU = 0.30, 0.50
NIVEAUX_SC = [
    ("Maternelle", 20, 3, 24, False),
    ("Primaire",   22, 3, 27, True),
    ("Collège",    25, 4, 36, True),
    ("Lycée",      28, 3, 40, True),
]


def calcul_gymnases(eff_mat, eff_prim, eff_col, eff_lyc):
    effectifs = [eff_mat, eff_prim, eff_col, eff_lyc]
    total = sum(effectifs)
    k9 = m9 = 0.0
    rows = []
    for (niveau, moy, h_eps, h_prog, inclus), eff in zip(NIVEAUX_SC, effectifs):
        pct   = eff / total * 100 if total > 0 else 0
        nb_cl = eff / moy if moy > 0 else 0
        g     = h_eps * HYP_BAS
        h     = h_eps * HYP_HAU
        j     = h_prog / g if g > 0 else 0
        l     = h_prog / h if h > 0 else 0
        k     = nb_cl / j if j > 0 else 0
        m     = nb_cl / l if l > 0 else 0
        if inclus:
            k9 += k
            m9 += m
        rows.append({
            "Niveau": niveau, "Effectifs": eff, "% total": f"{pct:.1f}%",
            "Nb classes": round(nb_cl, 1), "H. EPS/sem.": h_eps,
            "Cl./struct. (30%)": round(j, 1), "Besoin gym. (30%)": round(k, 2),
            "Cl./struct. (50%)": round(l, 1), "Besoin gym. (50%)": round(m, 2),
        })
    return round(k9, 2), round(m9, 2), rows


def calcul_piscines_sco(eff_mat, eff_prim, eff_col, eff_lyc):
    B30 = eff_mat * 0 * (1/3) * 5/16
    E30 = eff_mat * (1/3) * (1/3) * 5/16
    C33 = eff_prim * (2/5) * (1/3) * (2/3) * 5/20
    D34 = eff_prim * (2/5) * (1/3) * (1/3) * 6/20
    F33 = eff_prim * (4/5) * (1/3) * (2/3) * 5/25
    G34 = eff_prim * (4/5) * (1/3) * (1/3) * 6/25
    C37 = eff_col * (1/4) * (1/3) * (1/4) * 6/25
    D38 = eff_col * (1/4) * (1/3) * (3/4) * 7/25
    F37 = eff_col * (1/2) * (1/2) * (1/4) * 6/25
    G38 = eff_col * (1/2) * (1/2) * (3/4) * 7/25
    D40 = eff_lyc * (1/9) * (1/3) * 1 * 8/25
    C44 = B30 + C33 + C37 + D34 + D38 + D40
    F44 = E30 + F33 + F37 + G34 + G38 + D40
    I44 = (C44 + F44) / 2
    return round(C44 * 1.2, 0), round(F44 * 1.2, 0), round(I44 * 1.2, 0)


# ─────────────────────────────────────────────────────────────────────────────
# CALCULS BESOINS LICENCIÉS
# ─────────────────────────────────────────────────────────────────────────────
def calcul_besoins_lic(lic_zone, clubs_zone, equip_zone):
    """
    Calcule besoins vs offre actuelle.
    Écart = actuel − besoin
      → négatif  = déficit  (rouge, signe −)
      → positif  = excédent (vert,  pas de −)
    """
    rows_s, rows_d = [], []
    for cat in MODELE_EQUIP:
        eq_col = cat["equip_col"]
        coeff  = cat["coefficient"]
        actuel = int(equip_zone[eq_col].sum()) if eq_col in equip_zone.columns else 0
        besoin = 0.0
        lic_t = clubs_t = 0
        for d in cat["disciplines"]:
            fed = d["federation"]
            G, H, I, J = d["effectif"], d["entrainements"], d["duree"], d["dispo"]
            lf = int(lic_zone[lic_zone["federation"] == fed]["total"].sum())
            cf = int(clubs_zone[clubs_zone["federation"] == fed]["total"].sum())
            lic_t   += lf
            clubs_t += cf
            bk = lf / G * H * I / J if (lf > 0 and G > 0 and J > 0) else 0.0
            bl = bk * coeff
            besoin += bl
            rows_d.append({
                "Catégorie": cat["equipement"], "Fédération": fed,
                "Licenciés": lf, "Clubs": cf,
                "Besoin théorique": round(bk, 1),
                "Besoin majoré":    round(bl, 1),
            })
        # Écart = actuel − besoin  →  négatif = déficit
        rows_s.append({
            "Catégorie d'équipements": cat["equipement"],
            "Licenciés": lic_t,
            "Clubs":     clubs_t,
            "Besoins consolidés":  round(besoin, 1),
            "Équipements actuels": actuel,
            "Écart":               round(actuel - besoin, 1),
        })
    return pd.DataFrame(rows_s), pd.DataFrame(rows_d)


def get_licencies_aquatiques(lic_zone):
    """Retourne le tableau des licenciés des 3 fédérations aquatiques."""
    rows = []
    total = 0
    for fed in FEDS_AQUATIQUES:
        n = int(lic_zone[lic_zone["federation"] == fed]["total"].sum())
        total += n
        rows.append({"Fédération": fed, "Licenciés": n})
    return pd.DataFrame(rows), total


def calcul_m2_piscines_pop(pop):
    """Besoins en m² de plan d'eau selon 3 ratios par habitant."""
    return {label: round(pop * ratio) for label, ratio in RATIOS_M2}


# ─────────────────────────────────────────────────────────────────────────────
# COLORATION ÉCART
# Écart = actuel − besoin
#   < 0  →  déficit   → ROUGE
#   > 0  →  excédent  → VERT
# ─────────────────────────────────────────────────────────────────────────────
def color_ecart(val):
    if isinstance(val, (int, float)):
        if val < 0:
            return "color: #d9534f; font-weight:bold"   # rouge = déficit
        elif val > 0:
            return "color: #5cb85c; font-weight:bold"   # vert  = excédent
    return ""


LEGENDE_ECART = (
    "> 🔴 Écart négatif (−) = déficit d'équipements &nbsp;·&nbsp; "
    "🟢 Écart positif = offre excédentaire"
)

# ─────────────────────────────────────────────────────────────────────────────
# UI — TITRE
# ─────────────────────────────────────────────────────────────────────────────
_logo_path = os.path.join(os.path.dirname(__file__), "SportData_Territoires_480x100.png")
_logo_b64  = ""
_logo_html = ""
if os.path.exists(_logo_path):
    with open(_logo_path, "rb") as _lf:
        _logo_b64 = base64.b64encode(_lf.read()).decode()
    _logo_html = (
        f'<img src="data:image/png;base64,{_logo_b64}" '
        f'style="height:64px; max-width:220px; object-fit:contain; border-radius:6px;" />'
    )

_logo_box = ""
if _logo_b64:
    _logo_box = f"""
      <div style="flex-shrink:0; margin-left:20px;
                  background:#ffffff; border-radius:10px;
                  padding:10px 16px;
                  box-shadow:0 2px 8px rgba(0,0,0,0.25);
                  display:flex; align-items:center; justify-content:center;">
        <img src="data:image/png;base64,{_logo_b64}"
             style="height:60px; width:auto;" />
      </div>"""

st.markdown(
    f"""
    <div style="background: linear-gradient(135deg, #1a237e 0%, #0d47a1 60%, #1565c0 100%);
    padding: 24px 28px; border-radius: 14px; margin-bottom: 20px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.2);
    display:flex; align-items:center; justify-content:space-between;">
      <div style="flex:1;">
        <h1 style="color: white; margin: 0; font-size: 2em; font-weight: 800; letter-spacing: 0.3px;">
            🏅 SportData et territoires
        </h1>
        <p style="color: #bbdefb; margin: 8px 0 2px 0; font-size: 1.1em; font-weight: 500;">
            Décideurs du sport — par Patrick Bayeux
        </p>
      </div>
      {_logo_box}
    </div>
    """,
    unsafe_allow_html=True,
)
st.caption("Sources : Ministère des Sports 2023 · Ministère de l'Éducation nationale 2024")

# ─────────────────────────────────────────────────────────────────────────────
# TOTAUX FRANCE (pré-calculés)
# ─────────────────────────────────────────────────────────────────────────────
if not regions_agg.empty:
    _france_pop   = int(regions_agg["population"].sum())
    _france_lic   = int(regions_agg["nb_licencies"].sum())   if "nb_licencies"   in regions_agg.columns else 0
    _france_clubs = int(regions_agg["nb_clubs"].sum())       if "nb_clubs"       in regions_agg.columns else 0
    _france_equip = int(regions_agg["nb_equipements"].sum()) if "nb_equipements" in regions_agg.columns else 0
else:
    _france_pop   = int(comm_df["population"].sum())         if "population"     in comm_df.columns else 0
    _france_lic   = int(comm_df["nb_licencies"].sum())       if "nb_licencies"   in comm_df.columns else 0
    _france_clubs = int(comm_df["nb_clubs"].sum())           if "nb_clubs"       in comm_df.columns else 0
    _france_equip = int(comm_df["nb_equipements"].sum())     if "nb_equipements" in comm_df.columns else 0

# ─────────────────────────────────────────────────────────────────────────────
# SÉLECTEUR DE TERRITOIRE  —  double mode de navigation
# ─────────────────────────────────────────────────────────────────────────────
NIVEAUX_DIR = ["🇫🇷 France", "🌍 Région", "📍 Département",
               "🏛️ Intercommunalité (EPCI)", "🏘️ Commune"]

st.markdown("#### 🗺️ Sélection du territoire")
_mc, _ = st.columns([3, 2])
with _mc:
    mode_nav = st.radio(
        "Mode",
        ["🔍 Recherche directe", "🗺️ Navigation progressive"],
        horizontal=True,
        label_visibility="collapsed",
    )

# ── Variables résultats ───────────────────────────────────────────────────────
ec = co = ly = pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
codes           = set()
pop_affichee    = 0
nb_clubs = nb_lic = nb_equip = 0
lic_pour_1000 = clubs_pour_1000 = equip_pour_1000 = None
titre_zone = zone_slug = "zone"
niveau      = ""          # défini ici pour les deux modes
ctx_dep     = None        # département de contexte (pour comparaisons)
ctx_reg     = ""          # région de contexte

# ─────────────────────────────────────────────────────────────────────────────
# ══  MODE 1 — RECHERCHE DIRECTE  ════════════════════════════════════════════
# ─────────────────────────────────────────────────────────────────────────────
if mode_nav == "🔍 Recherche directe":
    col_niv, col_sel = st.columns([1, 3])
    with col_niv:
        niveau = st.selectbox("Niveau de territoire", NIVEAUX_DIR,
                              index=3, key="dir_niveau")
    with col_sel:

        # ── FRANCE ────────────────────────────────────────────────────────────
        if niveau == "🇫🇷 France":
            st.info("📊 Données nationales — France entière", icon="🇫🇷")
            codes           = set(comm_df["code_commune"])
            pop_affichee    = _france_pop
            nb_clubs        = _france_clubs
            nb_lic          = _france_lic
            nb_equip        = _france_equip
            lic_pour_1000   = round(nb_lic   / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            clubs_pour_1000 = round(nb_clubs / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            equip_pour_1000 = round(nb_equip / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            titre_zone      = "France (nationale)"
            zone_slug       = "france"
            ec, co, ly      = ecoles_df, colleges_df, lycees_df
            ctx_dep         = None
            ctx_reg         = ""

        # ── RÉGION ────────────────────────────────────────────────────────────
        elif niveau == "🌍 Région":
            if regions_agg.empty:
                st.warning("Données régionales non disponibles dans les fichiers de synthèse.")
                st.stop()
            recherche_r = st.text_input("🔍 Filtrer par nom", key="search_reg",
                                        placeholder="ex: Bretagne, Occitanie…")
            reg_list = sorted(regions_agg["region"].dropna().tolist())
            if recherche_r:
                reg_list = [r for r in reg_list if recherche_r.lower() in r.lower()]
            opts = ["— Sélectionner une région —"] + reg_list
            sel  = st.selectbox("Région", opts, key="dir_reg")
            if sel == "— Sélectionner une région —":
                st.info("Sélectionnez une région pour afficher les données.", icon="👆")
                st.stop()
            row             = regions_agg[regions_agg["region"] == sel].iloc[0]
            codes           = set(comm_df[comm_df["region"] == sel]["code_commune"])
            pop_affichee    = safe_int(row.get("population", 0))
            nb_clubs        = safe_int(row.get("nb_clubs", 0))
            nb_lic          = safe_int(row.get("nb_licencies", 0))
            nb_equip        = safe_int(row.get("nb_equipements", 0))
            lic_pour_1000   = round(nb_lic   / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            clubs_pour_1000 = round(nb_clubs / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            equip_pour_1000 = round(nb_equip / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            titre_zone      = sel
            zone_slug       = sel.replace(" ", "_").replace("/", "-")
            ec, co, ly      = filter_scolaires_region(sel)
            ctx_dep         = None
            ctx_reg         = sel

        # ── DÉPARTEMENT ───────────────────────────────────────────────────────
        elif niveau == "📍 Département":
            recherche_d = st.text_input("🔍 Filtrer par nom", key="search_dep",
                                        placeholder="ex: Isère, Rhône…")
            dep_list = deps_agg["label"].tolist()
            if recherche_d:
                dep_list = [d for d in dep_list if recherche_d.lower() in d.lower()]
            opts = ["— Sélectionner un département —"] + dep_list
            sel  = st.selectbox("Département", opts, key="dir_dep")
            if sel == "— Sélectionner un département —":
                st.info("Sélectionnez un département pour afficher les données.", icon="👆")
                st.stop()
            row             = deps_agg[deps_agg["label"] == sel].iloc[0]
            dep_code        = row["dep"]
            codes           = set(comm_df[comm_df["dep"] == dep_code]["code_commune"])
            pop_affichee    = safe_int(row.get("population", 0))
            nb_clubs        = safe_int(row.get("nb_clubs", 0))
            nb_lic          = safe_int(row.get("nb_licencies", 0))
            nb_equip        = safe_int(row.get("nb_equipements", 0))
            lic_pour_1000   = round(nb_lic   / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            clubs_pour_1000 = round(nb_clubs / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            equip_pour_1000 = round(nb_equip / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            titre_zone      = sel
            zone_slug       = dep_code
            ec, co, ly      = filter_scolaires_dep([dep_code])
            ctx_dep         = dep_code
            ctx_reg         = dep_region_map.get(dep_code, "")

        # ── EPCI ──────────────────────────────────────────────────────────────
        elif niveau == "🏛️ Intercommunalité (EPCI)":
            recherche_e = st.text_input("🔍 Filtrer par nom", key="search_epci",
                                        placeholder="ex: Métropole de Lyon…")
            epci_noms = epci_list["epci_nom"].tolist()
            if recherche_e:
                epci_noms = [e for e in epci_noms if recherche_e.lower() in e.lower()]
            opts     = ["— Sélectionner une intercommunalité —"] + epci_noms
            sel_epci = st.selectbox("Intercommunalité (EPCI)", opts, key="dir_epci")
            if sel_epci == "— Sélectionner une intercommunalité —":
                st.info("Sélectionnez une intercommunalité pour afficher les données.", icon="👆")
                st.stop()
            epci_row        = epci_list[epci_list["epci_nom"] == sel_epci].iloc[0]
            codes           = set(comm_df[comm_df["epci_nom"] == sel_epci]["code_commune"])
            pop_affichee    = safe_int(epci_row.get("population", 0))
            nb_clubs        = safe_int(epci_row.get("nb_clubs", 0))
            nb_lic          = safe_int(epci_row.get("nb_licencies", 0))
            nb_equip        = safe_int(epci_row.get("nb_equipements", 0))
            lic_pour_1000   = epci_row.get("licencies_pour_1000_hab")
            clubs_pour_1000 = epci_row.get("clubs_pour_1000_hab")
            equip_pour_1000 = epci_row.get("equip_pour_1000_hab")
            titre_zone      = sel_epci
            zone_slug       = sel_epci.replace(" ", "_").replace("/", "-")
            ec, co, ly      = filter_scolaires_epci(sel_epci)
            _epci_comm_rows = comm_df[comm_df["epci_nom"] == sel_epci]
            ctx_dep = _epci_comm_rows["dep"].value_counts().index[0] if not _epci_comm_rows.empty else None
            ctx_reg = dep_region_map.get(ctx_dep, "") if ctx_dep else ""

        # ── COMMUNE ───────────────────────────────────────────────────────────
        else:
            recherche_c = st.text_input("🔍 Filtrer par nom", key="search_comm",
                                        placeholder="ex: Grenoble, Saint-Malo…")
            comm_noms = communes_list["label"].tolist()
            if recherche_c:
                comm_noms = [c for c in comm_noms if recherche_c.lower() in c.lower()]
            opts     = ["— Sélectionner une commune —"] + comm_noms
            sel_comm = st.selectbox("Commune", opts, key="dir_comm")
            if sel_comm == "— Sélectionner une commune —":
                st.info("Sélectionnez une commune pour afficher les données.", icon="👆")
                st.stop()
            row             = communes_list[communes_list["label"] == sel_comm].iloc[0]
            codes           = {row["code_commune"]}
            pop_affichee    = safe_int(row.get("population", 0))
            nb_clubs        = safe_int(row.get("nb_clubs", 0))
            nb_lic          = safe_int(row.get("nb_licencies", 0))
            nb_equip        = safe_int(row.get("nb_equipements", 0))
            lic_pour_1000   = row.get("licencies_pour_1000_hab")
            clubs_pour_1000 = row.get("clubs_pour_1000_hab")
            equip_pour_1000 = row.get("equip_pour_1000_hab")
            titre_zone      = row["commune"].title() + f" ({row['dep']})"
            zone_slug       = row["commune"].replace(" ", "_").replace("/", "-")
            c_norm          = row["commune"].upper().strip()
            d_str           = row["dep"]
            ec, co, ly      = filter_scolaires_commune(c_norm, d_str)
            ctx_dep         = row["dep"]
            ctx_reg         = dep_region_map.get(row["dep"], "")

# ─────────────────────────────────────────────────────────────────────────────
# ══  MODE 2 — NAVIGATION PROGRESSIVE  ═══════════════════════════════════════
# ─────────────────────────────────────────────────────────────────────────────
else:
    st.markdown(
        "<small style='color:#555'>Déroulez les niveaux de gauche à droite "
        "— arrêtez-vous au niveau souhaité.</small>",
        unsafe_allow_html=True,
    )
    _prog_cols = st.columns([1, 1, 1, 1])

    # ── ① Région ──────────────────────────────────────────────────────────────
    reg_opts_prog = (["🇫🇷 France entière"] +
                     sorted(regions_agg["region"].dropna().tolist())
                     if not regions_agg.empty else ["🇫🇷 France entière"])
    with _prog_cols[0]:
        sel_p_reg = st.selectbox("① Région", reg_opts_prog, key="prog_reg")

    # ── Niveau France ──────────────────────────────────────────────────────────
    if sel_p_reg == "🇫🇷 France entière":
        codes           = set(comm_df["code_commune"])
        pop_affichee    = _france_pop
        nb_clubs        = _france_clubs
        nb_lic          = _france_lic
        nb_equip        = _france_equip
        lic_pour_1000   = round(nb_lic   / pop_affichee * 1000, 1) if pop_affichee > 0 else None
        clubs_pour_1000 = round(nb_clubs / pop_affichee * 1000, 1) if pop_affichee > 0 else None
        equip_pour_1000 = round(nb_equip / pop_affichee * 1000, 1) if pop_affichee > 0 else None
        titre_zone      = "France (nationale)"
        zone_slug       = "france"
        niveau          = "🇫🇷 France"
        ec, co, ly      = ecoles_df, colleges_df, lycees_df
        ctx_dep         = None
        ctx_reg         = ""
        # Masquer colonnes inutilisées
        with _prog_cols[1]: st.empty()
        with _prog_cols[2]: st.empty()
        with _prog_cols[3]: st.empty()

    else:
        # ── ② Département (filtré par région) ─────────────────────────────────
        deps_in_reg = sorted(
            deps_agg[deps_agg["region"] == sel_p_reg]["label"].tolist()
        )
        dep_opts_prog = [f"— Tous ({sel_p_reg}) —"] + deps_in_reg
        with _prog_cols[1]:
            sel_p_dep = st.selectbox("② Département", dep_opts_prog, key="prog_dep")

        if sel_p_dep == f"— Tous ({sel_p_reg}) —":
            # Niveau Région
            row             = regions_agg[regions_agg["region"] == sel_p_reg].iloc[0]
            codes           = set(comm_df[comm_df["region"] == sel_p_reg]["code_commune"])
            pop_affichee    = safe_int(row.get("population", 0))
            nb_clubs        = safe_int(row.get("nb_clubs", 0))
            nb_lic          = safe_int(row.get("nb_licencies", 0))
            nb_equip        = safe_int(row.get("nb_equipements", 0))
            lic_pour_1000   = round(nb_lic   / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            clubs_pour_1000 = round(nb_clubs / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            equip_pour_1000 = round(nb_equip / pop_affichee * 1000, 1) if pop_affichee > 0 else None
            titre_zone      = sel_p_reg
            zone_slug       = sel_p_reg.replace(" ", "_").replace("/", "-")
            niveau          = "🌍 Région"
            ec, co, ly      = filter_scolaires_region(sel_p_reg)
            ctx_dep         = None
            ctx_reg         = sel_p_reg
            with _prog_cols[2]: st.empty()
            with _prog_cols[3]: st.empty()

        else:
            # ── ③ EPCI (filtré par département) ───────────────────────────────
            dep_row  = deps_agg[deps_agg["label"] == sel_p_dep].iloc[0]
            dep_code = dep_row["dep"]
            epcis_in_dep = sorted(
                comm_df[comm_df["dep"] == dep_code]["epci_nom"]
                .dropna().unique().tolist()
            )
            epci_opts_prog = [f"— Tous ({sel_p_dep.split('—')[0].strip()}) —"] + epcis_in_dep
            with _prog_cols[2]:
                sel_p_epci = st.selectbox("③ Intercommunalité", epci_opts_prog, key="prog_epci")

            _dep_placeholder = sel_p_dep.split("—")[0].strip()
            if sel_p_epci == f"— Tous ({_dep_placeholder}) —":
                # Niveau Département
                codes           = set(comm_df[comm_df["dep"] == dep_code]["code_commune"])
                pop_affichee    = safe_int(dep_row.get("population", 0))
                nb_clubs        = safe_int(dep_row.get("nb_clubs", 0))
                nb_lic          = safe_int(dep_row.get("nb_licencies", 0))
                nb_equip        = safe_int(dep_row.get("nb_equipements", 0))
                lic_pour_1000   = round(nb_lic   / pop_affichee * 1000, 1) if pop_affichee > 0 else None
                clubs_pour_1000 = round(nb_clubs / pop_affichee * 1000, 1) if pop_affichee > 0 else None
                equip_pour_1000 = round(nb_equip / pop_affichee * 1000, 1) if pop_affichee > 0 else None
                titre_zone      = sel_p_dep
                zone_slug       = dep_code
                niveau          = "📍 Département"
                ec, co, ly      = filter_scolaires_dep([dep_code])
                ctx_dep         = dep_code
                ctx_reg         = dep_region_map.get(dep_code, "")
                with _prog_cols[3]: st.empty()

            else:
                # ── ④ Commune (filtrée par EPCI) ──────────────────────────────
                comms_in_epci = sorted(
                    communes_list[communes_list["epci_nom"] == sel_p_epci]["label"].tolist()
                )
                comm_opts_prog = [f"— Toutes ({sel_p_epci[:30]}…) —"] + comms_in_epci
                with _prog_cols[3]:
                    sel_p_comm = st.selectbox("④ Commune", comm_opts_prog, key="prog_comm")

                _epci_ph = f"— Toutes ({sel_p_epci[:30]}…) —"
                if sel_p_comm == _epci_ph:
                    # Niveau EPCI
                    epci_row        = epci_list[epci_list["epci_nom"] == sel_p_epci].iloc[0]
                    codes           = set(comm_df[comm_df["epci_nom"] == sel_p_epci]["code_commune"])
                    pop_affichee    = safe_int(epci_row.get("population", 0))
                    nb_clubs        = safe_int(epci_row.get("nb_clubs", 0))
                    nb_lic          = safe_int(epci_row.get("nb_licencies", 0))
                    nb_equip        = safe_int(epci_row.get("nb_equipements", 0))
                    lic_pour_1000   = epci_row.get("licencies_pour_1000_hab")
                    clubs_pour_1000 = epci_row.get("clubs_pour_1000_hab")
                    equip_pour_1000 = epci_row.get("equip_pour_1000_hab")
                    titre_zone      = sel_p_epci
                    zone_slug       = sel_p_epci.replace(" ", "_").replace("/", "-")
                    niveau          = "🏛️ Intercommunalité (EPCI)"
                    ec, co, ly      = filter_scolaires_epci(sel_p_epci)
                    _epci_comm_rows2 = comm_df[comm_df["epci_nom"] == sel_p_epci]
                    ctx_dep = _epci_comm_rows2["dep"].value_counts().index[0] if not _epci_comm_rows2.empty else None
                    ctx_reg = dep_region_map.get(ctx_dep, "") if ctx_dep else ""

                else:
                    # Niveau Commune
                    row             = communes_list[communes_list["label"] == sel_p_comm].iloc[0]
                    codes           = {row["code_commune"]}
                    pop_affichee    = safe_int(row.get("population", 0))
                    nb_clubs        = safe_int(row.get("nb_clubs", 0))
                    nb_lic          = safe_int(row.get("nb_licencies", 0))
                    nb_equip        = safe_int(row.get("nb_equipements", 0))
                    lic_pour_1000   = row.get("licencies_pour_1000_hab")
                    clubs_pour_1000 = row.get("clubs_pour_1000_hab")
                    equip_pour_1000 = row.get("equip_pour_1000_hab")
                    titre_zone      = row["commune"].title() + f" ({row['dep']})"
                    zone_slug       = row["commune"].replace(" ", "_").replace("/", "-")
                    niveau          = "🏘️ Commune"
                    c_norm          = row["commune"].upper().strip()
                    d_str           = row["dep"]
                    ec, co, ly      = filter_scolaires_commune(c_norm, d_str)
                    ctx_dep         = row["dep"]
                    ctx_reg         = dep_region_map.get(row["dep"], "")

# ─────────────────────────────────────────────────────────────────────────────
# FILTRAGES SPORTS
# ─────────────────────────────────────────────────────────────────────────────
lic_zone   = lic_df[lic_df["code"].isin(codes)]
clubs_zone = clubs_df[clubs_df["code"].isin(codes)]
equip_zone = equip_fam_df[equip_fam_df["code_commune"].isin(codes)]

# ─────────────────────────────────────────────────────────────────────────────
# TOTAUX SCOLAIRES
# ─────────────────────────────────────────────────────────────────────────────
sc = scolaires_totals(ec, co, ly)

# ─────────────────────────────────────────────────────────────────────────────
# KPI BANNER
# ─────────────────────────────────────────────────────────────────────────────
st.divider()
st.markdown(f"### 📍 {titre_zone}")

k1, k2, k3, k4 = st.columns(4)
k1.metric("👥 Population",         fmt(pop_affichee) + " hab."   if pop_affichee > 0 else "—")
k2.metric("🎓 Scolaires",           fmt(sc["total"]) + " élèves"  if sc["total"] > 0  else "—",
          help="Total écoles + collèges + lycées (Rentrée 2024)")
k3.metric("🎽 Licenciés",           fmt(nb_lic),
          delta=f"{rate_str(lic_pour_1000)} / 1 000 hab." if lic_pour_1000 is not None else None,
          delta_color="off")
k4.metric("⚽ Équipements sportifs", fmt(nb_equip),
          delta=f"{rate_str(equip_pour_1000)} / 1 000 hab." if equip_pour_1000 is not None else None,
          delta_color="off")

st.divider()

# ─────────────────────────────────────────────────────────────────────────────
# FLAGS STRATE — utilisés dans tous les onglets
# ─────────────────────────────────────────────────────────────────────────────
_is_comm = (niveau == "🏘️ Commune")
_is_epci = (niveau == "🏛️ Intercommunalité (EPCI)")

# ─────────────────────────────────────────────────────────────────────────────
# CHARGEMENT FOND DE CARTE EPCI (cached)
# ─────────────────────────────────────────────────────────────────────────────
@st.cache_data(show_spinner="Chargement du fond de carte…")
def load_carte_epci():
    """
    Construit un GeoDataFrame EPCI depuis le shapefile codes postaux.
    Pipeline : codes_postaux.shp → communes-france-2025.xlsx → dissolution par EPCI.
    Projection WGS84 (EPSG:4326) pour Folium.
    Retourne None si geopandas est absent ou le fichier introuvable.
    """
    try:
        import geopandas as gpd
    except ImportError:
        return None

    shp_path = BASE_DIR + "codes_postaux/codes_postaux_region.shp"
    if not os.path.exists(shp_path):
        return None

    # 1. Charger le shapefile (Lambert 93) — colonnes en majuscules
    gdf = gpd.read_file(shp_path)
    gdf.columns = [c.lower() for c in gdf.columns]   # normalise en minuscules
    gdf["cp5"] = gdf["id"].astype(str).str.strip().str.zfill(5)
    gdf = gdf[["cp5", "dep", "geometry"]].copy()

    # 2. Correspondance code postal → EPCI
    ref = pd.read_excel(
        SCOLAIRES_DIR + "communes-france-2025.xlsx", header=1,
        usecols=["code_postal", "dep_code", "dep_nom", "reg_nom", "epci_code", "epci_nom"],
    )
    ref = ref.dropna(subset=["code_postal"])
    ref["cp5"] = ref["code_postal"].apply(
        lambda x: str(int(float(x))).zfill(5) if pd.notna(x) else None
    )
    ref["dep_str"] = ref["dep_code"].apply(
        lambda x: dep_normalize(x) if pd.notna(x) else None
    )

    def _clean_epci(x):
        s = str(x).strip()
        if s in ("nan", "NaN", "", "ZZZZZZZZZ", "0"):
            return None
        try:
            return str(int(float(s)))
        except (ValueError, OverflowError):
            return None

    ref["epci_code"] = ref["epci_code"].apply(_clean_epci)
    # Exclure hors-EPCI (codes fictifs)
    ref = ref.dropna(subset=["cp5", "epci_code"])
    # Un code postal → plusieurs communes possibles : prendre la première
    cp_map = ref.drop_duplicates("cp5")[
        ["cp5", "dep_str", "dep_nom", "reg_nom", "epci_code", "epci_nom"]
    ].copy()

    # 3. Jointure shapefile ← mapping
    gdf = gdf.merge(cp_map, on="cp5", how="left")

    # 4. Dissolution par EPCI → polygones EPCI approchés
    valid = gdf[gdf["epci_code"].notna() & (gdf["epci_code"].astype(str) != "nan")].copy()
    gdf_epci = valid.dissolve(
        by="epci_code",
        aggfunc={
            "epci_nom": "first",
            "dep_str":  "first",
            "dep_nom":  "first",
            "reg_nom":  "first",
        },
    ).reset_index()

    # 5. Reprojection WGS84
    gdf_epci = gdf_epci.to_crs("EPSG:4326")
    gdf_epci["epci_code"] = gdf_epci["epci_code"].astype(str).str.strip()

    return gdf_epci


# ─────────────────────────────────────────────────────────────────────────────
# 8 ONGLETS
# ─────────────────────────────────────────────────────────────────────────────
(tab_sco, tab_lic, tab_clubs,
 tab_equip, tab_bs, tab_bl, tab_tb, tab_strate, tab_carte) = st.tabs([
    "🎓 Effectifs scolaires",
    "🎽 Licenciés",
    "🏟️ Clubs",
    "⚽ Équipements sportifs",
    "📐 Besoins scolaires",
    "📊 Besoins des licenciés",
    "📋 SportData Territoire",
    "📈 SportData — Strate & population",
    "🗺️ Cartes",
])

# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 1 — EFFECTIFS SCOLAIRES
# ═══════════════════════════════════════════════════════════════════════════════
with tab_sco:
    st.subheader("🎓 Effectifs scolaires — Rentrée 2024")

    if sc["total"] == 0:
        st.info("Aucune donnée scolaire pour ce territoire.")
    else:
        c1, c2, c3, c4 = st.columns(4)
        c1.metric("Écoles",    sc["nb_ec"],  help=f"{fmt(sc['t_ec'])} élèves")
        c2.metric("Collèges",  sc["nb_co"],  help=f"{fmt(sc['t_co'])} élèves")
        c3.metric("Lycées GT", sc["nb_ly"],  help=f"{fmt(sc['t_ly'])} élèves")
        c4.metric("Total élèves", fmt(sc["total"]))

        st.markdown("---")
        summary_data = {
            "Niveau":  ["🏫 Maternelle", "🏫 Primaire", "🏛️ Collège (6ème–3ème)", "🎓 Lycée GT", "**TOTAL**"],
            "Élèves":  [sc["maternelle"], sc["primaire"], sc["t_co"], sc["t_ly"], sc["total"]],
            "Part":    [
                f"{v / sc['total'] * 100:.1f} %" if sc["total"] > 0 else "—"
                for v in [sc["maternelle"], sc["primaire"], sc["t_co"], sc["t_ly"], sc["total"]]
            ],
        }
        st.dataframe(pd.DataFrame(summary_data), use_container_width=True, hide_index=True)
        st.markdown("---")

        sub1, sub2, sub3 = st.tabs(["🏫 Écoles", "🏛️ Collèges", "🎓 Lycées"])

        with sub1:
            if ec.empty:
                st.info("Aucune école dans cette zone.")
            elif niveau in ["🌍 Région", "📍 Département"] and len(ec) > 200:
                agg_ec = (ec.groupby(["Département", "Commune"]).agg(
                    Écoles=("Commune", "count"),
                    **{"Total élèves": ("Nombre total d'élèves", "sum"),
                       "Maternelle":   ("Nombre d'élèves en pré-élémentaire hors ULIS", "sum"),
                       "Primaire":     ("Nombre d'élèves en élémentaire hors ULIS", "sum")}
                ).reset_index().sort_values("Total élèves", ascending=False))
                st.caption(f"Vue agrégée par commune ({len(ec)} établissements)")
                st.dataframe(agg_ec, use_container_width=True, hide_index=True)
            else:
                cols_show = [c for c in [
                    "Commune", "Dénomination principale", "Patronyme", "Secteur", "REP", "REP +",
                    "Nombre total de classes", "Nombre total d'élèves",
                    "Nombre d'élèves en pré-élémentaire hors ULIS",
                    "Nombre d'élèves en élémentaire hors ULIS", "Nombre d'élèves en ULIS",
                    "Nombre d'élèves en CP hors ULIS", "Nombre d'élèves en CE1 hors ULIS",
                    "Nombre d'élèves en CE2 hors ULIS", "Nombre d'élèves en CM1 hors ULIS",
                    "Nombre d'élèves en CM2 hors ULIS",
                ] if c in ec.columns]
                rename_ec = {
                    "Dénomination principale": "Type", "Patronyme": "Nom",
                    "Nombre total de classes": "Classes", "Nombre total d'élèves": "Total",
                    "Nombre d'élèves en pré-élémentaire hors ULIS": "Maternelle",
                    "Nombre d'élèves en élémentaire hors ULIS": "Primaire",
                    "Nombre d'élèves en ULIS": "ULIS",
                    "Nombre d'élèves en CP hors ULIS": "CP",
                    "Nombre d'élèves en CE1 hors ULIS": "CE1",
                    "Nombre d'élèves en CE2 hors ULIS": "CE2",
                    "Nombre d'élèves en CM1 hors ULIS": "CM1",
                    "Nombre d'élèves en CM2 hors ULIS": "CM2",
                }
                st.dataframe(ec[cols_show].rename(columns=rename_ec).reset_index(drop=True),
                             use_container_width=True)

        with sub2:
            if co.empty:
                st.info("Aucun collège dans cette zone.")
            elif niveau in ["🌍 Région", "📍 Département"] and len(co) > 100:
                agg_co = (co.groupby(["Département", "Commune"]).agg(
                    Collèges=("Commune", "count"),
                    **{"Total élèves": ("nombre_eleves_total", "sum"),
                       "6ème": ("6èmes total", "sum"), "5ème": ("5èmes total", "sum"),
                       "4ème": ("4èmes total", "sum"), "3ème": ("3èmes total", "sum")}
                ).reset_index().sort_values("Total élèves", ascending=False))
                st.dataframe(agg_co, use_container_width=True, hide_index=True)
            else:
                cols_show = [c for c in [
                    "Commune", "Dénomination principale", "Patronyme", "Secteur",
                    "nombre_eleves_total", "6èmes total", "5èmes total", "4èmes total", "3èmes total",
                    "Nombre d'élèves total Segpa", "Nombre d'élèves total ULIS",
                ] if c in co.columns]
                rename_co = {
                    "Dénomination principale": "Type", "Patronyme": "Nom",
                    "nombre_eleves_total": "Total", "6èmes total": "6ème", "5èmes total": "5ème",
                    "4èmes total": "4ème", "3èmes total": "3ème",
                    "Nombre d'élèves total Segpa": "Segpa", "Nombre d'élèves total ULIS": "ULIS",
                }
                st.dataframe(co[cols_show].rename(columns=rename_co).reset_index(drop=True),
                             use_container_width=True)

        with sub3:
            if ly.empty:
                st.info("Aucun lycée GT dans cette zone.")
            elif niveau in ["🌍 Région", "📍 Département"] and len(ly) > 100:
                agg_ly = (ly.groupby(["Département", "Commune"]).agg(
                    Lycées=("Commune", "count"),
                    **{"Total élèves": ("Nombre d'élèves", "sum"), "2nde GT": ("2ndes GT", "sum")}
                ).reset_index().sort_values("Total élèves", ascending=False))
                st.dataframe(agg_ly, use_container_width=True, hide_index=True)
            else:
                cols_show = [c for c in [
                    "Commune", "Dénomination principale", "Patronyme", "Secteur",
                    "Nombre d'élèves", "2ndes GT",
                    "1ères G", "1ères STI2D", "1ères STL", "1ères STMG", "1ères ST2S",
                    "Terminales G", "Terminales STI2D", "Terminales STL",
                    "Terminales STMG", "Terminales ST2S",
                ] if c in ly.columns]
                rename_ly = {
                    "Dénomination principale": "Type", "Patronyme": "Nom",
                    "Nombre d'élèves": "Total",
                }
                st.dataframe(ly[cols_show].rename(columns=rename_ly).reset_index(drop=True),
                             use_container_width=True)

# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 2 — LICENCIÉS
# ═══════════════════════════════════════════════════════════════════════════════
with tab_lic:
    st.subheader("🎽 Licenciés sportifs — 2023")
    if lic_zone.empty or lic_zone["total"].sum() == 0:
        st.info("Aucun licencié enregistré pour ce territoire.")
    else:
        total_lic = lic_zone["total"].sum()
        c1, c2 = st.columns(2)
        c1.metric("Total licenciés", fmt(total_lic))
        if pop_affichee > 0:
            c2.metric("Taux", f"{total_lic / pop_affichee * 1000:.1f} pour 1 000 hab.")
        agg = (lic_zone.groupby("federation")["total"].sum().reset_index()
               .rename(columns={"federation": "Fédération", "total": "Licenciés"})
               .sort_values("Licenciés", ascending=False).reset_index(drop=True))
        agg["% du total"] = (agg["Licenciés"] / total_lic * 100).round(1).astype(str) + " %"
        if pop_affichee > 0:
            agg["Lic. / 1 000 hab."] = (agg["Licenciés"] / pop_affichee * 1000).round(2)
        if (_is_comm or _is_epci) and pop_affichee > 0:
            _li_avgs2  = lic_fed_comm_avgs if _is_comm else lic_fed_epci_avgs
            _l_strate  = assign_strate(pop_affichee, STRATES_COMM if _is_comm else STRATES_EPCI)
            agg["Lic. / 1 000 hab. (moy. strate)"] = agg["Fédération"].apply(
                lambda f: _li_avgs2.get(_l_strate, {}).get(f)
            )
        # Colonnes dep / région / France
        if ctx_dep and pop_affichee > 0:
            _d_rates = dep_lic_rates.get(ctx_dep, {})
            agg["Lic. / 1 000 hab. (dép.)"] = agg["Fédération"].apply(
                lambda f: round(_d_rates[f], 2) if f in _d_rates else None)
        if ctx_reg and pop_affichee > 0:
            _r_rates = reg_lic_rates.get(ctx_reg, {})
            agg["Lic. / 1 000 hab. (région)"] = agg["Fédération"].apply(
                lambda f: round(_r_rates[f], 2) if f in _r_rates else None)
        if nat_lic_rates:
            agg["Lic. / 1 000 hab. (France)"] = agg["Fédération"].apply(
                lambda f: nat_lic_rates.get(f))
        st.dataframe(agg, use_container_width=True, hide_index=True)

        # ── Typologie fédération ─────────────────────────────────────────────
        st.markdown("---")
        st.markdown("### 📊 Typologie des fédérations sportives")
        st.caption("Classement selon la catégorie réglementaire de chaque fédération (code national)")

        _total_zone_pop = pop_affichee if pop_affichee > 0 else 1
        _typo_rows = []
        for cat_label, (lo, hi) in FED_CATEGORIES.items():
            _cat_feds = [f for f, c in fed_code_map.items() if lo <= c <= hi]
            _cat_lic_zone = lic_zone[lic_zone["federation"].isin(_cat_feds)]["total"].sum()
            _row = {
                "Catégorie": cat_label,
                "Licenciés": int(_cat_lic_zone),
                "% du total": f"{_cat_lic_zone / total_lic * 100:.1f} %" if total_lic > 0 else "—",
                "/ 1 000 hab.": round(_cat_lic_zone / _total_zone_pop * 1000, 2) if pop_affichee > 0 else None,
            }
            # Strate (commune ou EPCI)
            if (_is_comm or _is_epci) and pop_affichee > 0:
                _strate_key2 = assign_strate(pop_affichee, STRATES_COMM if _is_comm else STRATES_EPCI)
                _strate_ref2 = lic_fed_comm_avgs if _is_comm else lic_fed_epci_avgs
                # Recalculer le taux strate pour la catégorie : sum des lic de la cat par strate / pop strate
                _pop_strate_ref = _pop_par_strate_comm if _is_comm else _pop_par_strate_epci
                _pop_s = _pop_strate_ref.get(_strate_key2, 0)
                _strate_map_ref = _strate_comm_map if _is_comm else _strate_epci_map
                _lic_cat_strate = sum(
                    lic_df[lic_df["federation"] == f]["code"].map(
                        lambda c: _strate_map_ref.get(c, "—")
                    ).eq(_strate_key2).sum() * 0  # comptage incorrect sans merge
                    for f in _cat_feds
                )
                # Approche correcte : from lic_df filtré sur codes de la strate
                _lic_strate_df = lic_df[lic_df["code"].map(_strate_map_ref).fillna("—") == _strate_key2]
                _lic_cat_strate_total = _lic_strate_df[_lic_strate_df["federation"].isin(_cat_feds)]["total"].sum()
                _row["Moy. strate / 1 000 hab."] = round(_lic_cat_strate_total / _pop_s * 1000, 2) if _pop_s > 0 else None
            # EPCI (si commune)
            if _is_comm and pop_affichee > 0:
                _comm_epci_nom = comm_df[comm_df["code_commune"].isin(codes)]["epci_nom"].iloc[0] \
                    if not comm_df[comm_df["code_commune"].isin(codes)].empty else None
                if _comm_epci_nom:
                    _epci_codes = set(comm_df[comm_df["epci_nom"] == _comm_epci_nom]["code_commune"])
                    _epci_pop_val = int(epci_df[epci_df["epci_nom"] == _comm_epci_nom]["population"].iloc[0]) \
                        if not epci_df[epci_df["epci_nom"] == _comm_epci_nom].empty else 0
                    _epci_lic_cat = lic_df[lic_df["code"].isin(_epci_codes) & lic_df["federation"].isin(_cat_feds)]["total"].sum()
                    _row["EPCI / 1 000 hab."] = round(_epci_lic_cat / _epci_pop_val * 1000, 2) if _epci_pop_val > 0 else None
            # Département
            if ctx_dep:
                _row["Dép. / 1 000 hab."] = get_cat_rates_dep(ctx_dep, _cat_feds)
            # Région
            if ctx_reg:
                _row["Région / 1 000 hab."] = get_cat_rates_reg(ctx_reg, _cat_feds)
            # France
            _row["France / 1 000 hab."] = get_cat_rates_nat(_cat_feds)
            _typo_rows.append(_row)

        _typo_df = pd.DataFrame(_typo_rows)
        st.dataframe(_typo_df, use_container_width=True, hide_index=True)

# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 3 — CLUBS
# ═══════════════════════════════════════════════════════════════════════════════
with tab_clubs:
    st.subheader("🏟️ Clubs sportifs — 2023")
    if clubs_zone.empty or clubs_zone["total"].sum() == 0:
        st.info("Aucun club enregistré pour ce territoire.")
    else:
        total_clubs = clubs_zone["total"].sum()
        c1, c2 = st.columns(2)
        c1.metric("Total clubs", fmt(total_clubs))
        if pop_affichee > 0:
            c2.metric("Taux", f"{total_clubs / pop_affichee * 1000:.2f} pour 1 000 hab.")
        agg = (clubs_zone.groupby("federation")["total"].sum().reset_index()
               .rename(columns={"federation": "Fédération", "total": "Clubs"})
               .sort_values("Clubs", ascending=False).reset_index(drop=True))
        agg["% du total"] = (agg["Clubs"] / total_clubs * 100).round(1).astype(str) + " %"
        if pop_affichee > 0:
            agg["Clubs / 1 000 hab."] = (agg["Clubs"] / pop_affichee * 1000).round(2)
        if (_is_comm or _is_epci) and pop_affichee > 0:
            _cl_avgs2  = clubs_fed_comm_avgs if _is_comm else clubs_fed_epci_avgs
            _c_strate  = assign_strate(pop_affichee, STRATES_COMM if _is_comm else STRATES_EPCI)
            agg["Clubs / 1 000 hab. (moy. strate)"] = agg["Fédération"].apply(
                lambda f: _cl_avgs2.get(_c_strate, {}).get(f)
            )
        # Colonnes dep / région / France
        if ctx_dep and pop_affichee > 0:
            _dc_rates = dep_clubs_rates.get(ctx_dep, {})
            agg["Clubs / 1 000 hab. (dép.)"] = agg["Fédération"].apply(
                lambda f: round(_dc_rates[f], 2) if f in _dc_rates else None)
        if ctx_reg and pop_affichee > 0:
            _rc_rates = reg_clubs_rates.get(ctx_reg, {})
            agg["Clubs / 1 000 hab. (région)"] = agg["Fédération"].apply(
                lambda f: round(_rc_rates[f], 2) if f in _rc_rates else None)
        if nat_clubs_rates:
            agg["Clubs / 1 000 hab. (France)"] = agg["Fédération"].apply(
                lambda f: nat_clubs_rates.get(f))
        st.dataframe(agg, use_container_width=True, hide_index=True)

# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 4 — ÉQUIPEMENTS SPORTIFS
# ═══════════════════════════════════════════════════════════════════════════════
with tab_equip:
    st.subheader("⚽ Équipements sportifs — 2023")
    if equip_zone.empty or nb_equip == 0:
        st.info("Aucun équipement enregistré pour ce territoire.")
    else:
        c1, c2 = st.columns(2)
        c1.metric("Total équipements", fmt(nb_equip))
        if pop_affichee > 0:
            c2.metric("Taux", f"{nb_equip / pop_affichee * 1000:.1f} pour 1 000 hab.")
        fam_sums = equip_zone[FAM_COLS].sum()
        fam_df = (fam_sums[fam_sums > 0].reset_index()
                  .rename(columns={"index": "Famille", 0: "Équipements"})
                  .sort_values("Équipements", ascending=False).reset_index(drop=True))
        fam_df["Famille"] = fam_df["Famille"].str.replace("equip_", "", regex=False)
        total_eq = fam_df["Équipements"].sum()
        fam_df["% du total"] = (fam_df["Équipements"] / total_eq * 100).round(1).astype(str) + " %"
        # Colonnes strate (communes et EPCI uniquement)
        if pop_affichee > 0:
            fam_df["/ 1 000 hab."] = (
                fam_df["Équipements"] / pop_affichee * 1000
            ).round(2)
        if (_is_comm or _is_epci) and pop_affichee > 0:
            _eq_avgs = equip_fam_comm_avgs if _is_comm else equip_fam_epci_avgs
            _eq_strate = assign_strate(pop_affichee, STRATES_COMM if _is_comm else STRATES_EPCI)
            fam_df["Moy. strate / 1 000 hab."] = fam_df["Famille"].apply(
                lambda f: _eq_avgs.get(_eq_strate, {}).get(f)
            )
        # Colonnes dep / région / France
        if ctx_dep and pop_affichee > 0:
            _de_rates = dep_equip_rates.get(ctx_dep, {})
            fam_df["/ 1 000 hab. (dép.)"] = fam_df["Famille"].apply(
                lambda f: round(_de_rates[f], 2) if f in _de_rates else None)
        if ctx_reg and pop_affichee > 0:
            _re_rates = reg_equip_rates.get(ctx_reg, {})
            fam_df["/ 1 000 hab. (région)"] = fam_df["Famille"].apply(
                lambda f: round(_re_rates[f], 2) if f in _re_rates else None)
        if nat_equip_rates:
            fam_df["/ 1 000 hab. (France)"] = fam_df["Famille"].apply(
                lambda f: nat_equip_rates.get(f))
        st.dataframe(fam_df, use_container_width=True, hide_index=True)

    # ── Section m² plan d'eau ─────────────────────────────────────────────────
    st.markdown("---")
    st.markdown("### 🏊 Offre en m² de plan d'eau — Bassins de natation (2023)")
    pis_nat, pis_type, pis_total_m2 = get_piscines_m2(codes)

    if pis_total_m2 == 0:
        st.info("Aucun bassin de natation recensé dans les données RES pour ce territoire.")
    else:
        # ── Métriques par nature (découvert / intérieur…) ─────────────────────
        st.markdown("##### Par nature de plan d'eau")
        NATURE_ICONS = {
            "Intérieur":         "🏗️",
            "Découvert":         "☀️",
            "Découvrable":       "🔄",
            "Extérieur couvert": "⛺",
            "Autres":            "🌿",
        }
        pis_nat_detail = pis_nat[pis_nat["Nature du plan d'eau"] != "**TOTAL**"]
        _pcols = st.columns(min(len(pis_nat_detail), 5))
        for col_ui, (_, prow) in zip(_pcols, pis_nat_detail.iterrows()):
            icon = NATURE_ICONS.get(prow["Nature du plan d'eau"], "🏊")
            col_ui.metric(
                f"{icon} {prow['Nature du plan d\'eau']}",
                f"{prow['m² total']:,} m²".replace(",", " "),
                help=f"{prow['Bassins']} bassin(s)",
            )
        st.metric(
            "🏊 Total m² plan d'eau",
            f"{pis_total_m2:,} m²".replace(",", " "),
            delta=f"{pis_total_m2 / pop_affichee:.4f} m²/hab." if pop_affichee > 0 else None,
            delta_color="off",
        )

        # ── Tableau nature + tableau type côte à côte ─────────────────────────
        col_nat, col_type = st.columns(2)
        with col_nat:
            st.markdown("**Détail par nature de plan d'eau**")
            def style_pis_nat(row):
                if row["Nature du plan d'eau"] == "**TOTAL**":
                    return ["font-weight:bold; background:#e3f2fd"] * len(row)
                return [""] * len(row)
            st.dataframe(pis_nat.style.apply(style_pis_nat, axis=1),
                         use_container_width=True, hide_index=True)

        with col_type:
            st.markdown("**Détail par type de bassin**")
            TYPE_ICONS = {"Sportif": "🏊", "Ludique": "🎡", "Mixte": "🔀", "Exercice": "🤸"}
            pis_type_detail = pis_type[pis_type["Type de bassin"] != "**TOTAL**"]
            t_cols = st.columns(min(len(pis_type_detail), 4))
            for col_ui, (_, trow) in zip(t_cols, pis_type_detail.iterrows()):
                icon = TYPE_ICONS.get(trow["Type de bassin"], "🏊")
                col_ui.metric(
                    f"{icon} {trow['Type de bassin']}",
                    f"{trow['m² total']:,} m²".replace(",", " "),
                    help=f"{trow['Bassins']} bassin(s)",
                )
            def style_pis_type(row):
                if row["Type de bassin"] == "**TOTAL**":
                    return ["font-weight:bold; background:#e3f2fd"] * len(row)
                return [""] * len(row)
            st.dataframe(pis_type.style.apply(style_pis_type, axis=1),
                         use_container_width=True, hide_index=True)

        st.caption(
            "Source : Recensement des Équipements Sportifs (RES) 2023 — "
            "Filtre : equip_type_name ∈ {Bassin sportif, Bassin ludique, "
            "Bassin mixte, Bassin d'exercices} · m² issus de equip_surf"
        )

# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 5 — BESOINS SCOLAIRES
# ═══════════════════════════════════════════════════════════════════════════════
with tab_bs:
    st.subheader("📐 Besoins scolaires en équipements sportifs couverts")
    st.caption("Modèle EPS — Ministère de l'Éducation nationale · Structure de référence : gymnase / salle multisports")

    st.markdown(
        "<div style='background:#FFFDE7; border:2px solid #F9A825; border-radius:8px;"
        "padding:10px 16px; margin-bottom:12px; font-size:0.9em;'>"
        "🟡 <b>Effectifs pré-remplis depuis les données 2024</b> de la zone sélectionnée. "
        "Modifiables à volonté.</div>",
        unsafe_allow_html=True,
    )

    def_mat  = safe_int(ec["Nombre d'élèves en pré-élémentaire hors ULIS"].sum()) if not ec.empty else 0
    def_prim = safe_int(ec["Nombre d'élèves en élémentaire hors ULIS"].sum())     if not ec.empty else 0
    def_col  = safe_int(co["nombre_eleves_total"].sum())                           if not co.empty else 0
    def_lyc  = safe_int(ly["Nombre d'élèves"].sum())                               if not ly.empty else 0

    c1, c2, c3, c4 = st.columns(4)
    eff_mat  = c1.number_input("🟡 Maternelle", min_value=0, value=def_mat,  step=1, key=f"mat_{zone_slug}")
    eff_prim = c2.number_input("🟡 Primaire",   min_value=0, value=def_prim, step=1, key=f"prim_{zone_slug}")
    eff_col  = c3.number_input("🟡 Collèges",   min_value=0, value=def_col,  step=1, key=f"col_{zone_slug}")
    eff_lyc  = c4.number_input("🟡 Lycées",     min_value=0, value=def_lyc,  step=1, key=f"lyc_{zone_slug}")

    gym_30, gym_50, rows_gym = calcul_gymnases(eff_mat, eff_prim, eff_col, eff_lyc)
    m2_bas, m2_haut, m2_moy  = calcul_piscines_sco(eff_mat, eff_prim, eff_col, eff_lyc)

    st.markdown("<br>", unsafe_allow_html=True)
    st.dataframe(pd.DataFrame(rows_gym), use_container_width=True, hide_index=True)

    st.markdown("<br>", unsafe_allow_html=True)
    _gcss = ("border-radius:12px; padding:26px 22px; text-align:center; "
             "box-shadow:0 3px 10px rgba(0,0,0,0.18); color:white;")
    cg1, cg2 = st.columns(2)
    with cg1:
        st.markdown(
            f"""<div style="background:#00B050; {_gcss}">
            <div style="font-size:1em; font-weight:700;">🟢 GYMNASES — Hypothèse 30 %</div>
            <div style="font-size:0.82em; opacity:.85; margin:5px 0 12px;">
              1 classe occupe 30 % du planning hebdomadaire</div>
            <div style="font-size:3.5em; font-weight:800; line-height:1.1;">{gym_30:.2f}</div>
            <div style="font-size:0.95em; margin-top:7px;">gymnases / salles multisports</div>
            </div>""", unsafe_allow_html=True)
    with cg2:
        st.markdown(
            f"""<div style="background:#1B5E20; {_gcss}">
            <div style="font-size:1em; font-weight:700;">🟢 GYMNASES — Hypothèse 50 %</div>
            <div style="font-size:0.82em; opacity:.85; margin:5px 0 12px;">
              1 classe occupe 50 % du planning hebdomadaire</div>
            <div style="font-size:3.5em; font-weight:800; line-height:1.1;">{gym_50:.2f}</div>
            <div style="font-size:0.95em; margin-top:7px;">gymnases / salles multisports</div>
            </div>""", unsafe_allow_html=True)

    st.markdown("<br>", unsafe_allow_html=True)
    st.markdown("#### 🏊 Besoins en m² de plan d'eau (piscines scolaires)")
    cp1, cp2, cp3 = st.columns(3)
    _pcss = ("border-radius:12px; padding:20px 14px; text-align:center; "
             "box-shadow:0 3px 8px rgba(0,0,0,0.15); color:white;")
    cp1.markdown(
        f"""<div style="background:#1565C0; {_pcss}">
        <div style="font-size:0.9em; font-weight:700;">🔵 HYPOTHÈSE BASSE</div>
        <div style="font-size:2.8em; font-weight:800; line-height:1.1;">{m2_bas:,.0f} m²</div>
        <div style="font-size:0.85em; opacity:.9;">de plan d'eau</div>
        </div>""", unsafe_allow_html=True)
    cp2.markdown(
        f"""<div style="background:#0D47A1; {_pcss}">
        <div style="font-size:0.9em; font-weight:700;">🔵 HYPOTHÈSE HAUTE</div>
        <div style="font-size:2.8em; font-weight:800; line-height:1.1;">{m2_haut:,.0f} m²</div>
        <div style="font-size:0.85em; opacity:.9;">de plan d'eau</div>
        </div>""", unsafe_allow_html=True)
    cp3.markdown(
        f"""<div style="background:#1976D2; {_pcss}">
        <div style="font-size:0.9em; font-weight:700;">🔵 HYPOTHÈSE MOYENNE</div>
        <div style="font-size:2.8em; font-weight:800; line-height:1.1;">{m2_moy:,.0f} m²</div>
        <div style="font-size:0.85em; opacity:.9;">de plan d'eau</div>
        </div>""", unsafe_allow_html=True)

    st.markdown(
        "<p style='font-size:0.78em; color:#888; margin-top:10px;'>"
        "⚠️ Totaux gymnase excluent la maternelle. Paramètres : 20 él./cl. maternelle · 22 primaire · "
        "25 collège · 28 lycée — EPS : 3h/sem. (mat./prim./lyc.), 4h/sem. (collège). "
        "Piscines : modèle théorique scolaire avec majoration 20 %.</p>",
        unsafe_allow_html=True)

# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 6 — BESOINS DES LICENCIÉS
# ═══════════════════════════════════════════════════════════════════════════════
with tab_bl:
    st.subheader("📊 Besoins des licenciés en équipements sportifs")
    st.caption(
        "Méthode : Besoin = Nb licenciés ÷ Effectif/terrain × Séances/sem. × Durée (h) ÷ Disponibilité (h/sem.) "
        "— majoré +15 % (salles) ou +20 % (terrains). "
        "Écart = Équipements actuels − Besoins consolidés."
    )

    if pop_affichee == 0:
        st.info("Aucune population renseignée — impossible de calculer les besoins.", icon="ℹ️")
    else:
        df_synth, df_detail = calcul_besoins_lic(lic_zone, clubs_zone, equip_zone)

        st.markdown("### 🏟️ Besoins consolidés par catégorie d'équipements")
        styled = df_synth.style.map(color_ecart, subset=["Écart"])
        st.dataframe(styled, use_container_width=True, hide_index=True)
        st.markdown(LEGENDE_ECART)

        with st.expander("🔍 Détail par fédération"):
            st.dataframe(df_detail, use_container_width=True, hide_index=True)

        # ── Section piscines ──────────────────────────────────────────────────
        st.markdown("---")
        st.markdown("### 🏊 Piscines — Fédérations aquatiques & estimation m² de plan d'eau")

        # Tableau licenciés aquatiques (sans m²)
        df_aq, total_lic_aq = get_licencies_aquatiques(lic_zone)
        col_aq1, col_aq2 = st.columns([2, 1])
        with col_aq1:
            st.markdown("**Licenciés des fédérations aquatiques**")
            st.dataframe(df_aq, use_container_width=True, hide_index=True)
            st.caption(f"Total : **{fmt(total_lic_aq)}** licenciés aquatiques")
        with col_aq2:
            eq_piscine = int(equip_zone["equip_Bassin de natation"].sum()) \
                if "equip_Bassin de natation" in equip_zone.columns else 0
            st.metric("🏊 Bassins de natation actuels", fmt(eq_piscine))

        # Estimation m² plan d'eau — 3 ratios par habitant
        st.markdown("<br>", unsafe_allow_html=True)
        st.markdown("**Estimation des besoins en m² de plan d'eau — ratios par habitant**")
        st.caption(
            f"Population de référence : **{fmt(pop_affichee)} hab.** · "
            "Ratios : 0,016 / 0,018 / 0,020 m²/hab."
        )

        m2_pop = calcul_m2_piscines_pop(pop_affichee)
        _pcss2 = ("border-radius:12px; padding:22px 16px; text-align:center; "
                  "box-shadow:0 3px 8px rgba(0,0,0,0.15); color:white;")
        pm1, pm2, pm3 = st.columns(3)
        pm1.markdown(
            f"""<div style="background:#1565C0; {_pcss2}">
            <div style="font-size:0.88em; font-weight:700;">🔵 0,016 m²/hab.</div>
            <div style="font-size:0.75em; opacity:.85; margin:4px 0 10px;">Hypothèse basse</div>
            <div style="font-size:2.6em; font-weight:800; line-height:1.1;">
                {m2_pop['Hypothèse basse']:,.0f} m²</div>
            <div style="font-size:0.82em; opacity:.9; margin-top:6px;">de plan d'eau</div>
            </div>""", unsafe_allow_html=True)
        pm2.markdown(
            f"""<div style="background:#0D47A1; {_pcss2}">
            <div style="font-size:0.88em; font-weight:700;">🔵 0,018 m²/hab.</div>
            <div style="font-size:0.75em; opacity:.85; margin:4px 0 10px;">Hypothèse moyenne</div>
            <div style="font-size:2.6em; font-weight:800; line-height:1.1;">
                {m2_pop['Hypothèse moyenne']:,.0f} m²</div>
            <div style="font-size:0.82em; opacity:.9; margin-top:6px;">de plan d'eau</div>
            </div>""", unsafe_allow_html=True)
        pm3.markdown(
            f"""<div style="background:#1976D2; {_pcss2}">
            <div style="font-size:0.88em; font-weight:700;">🔵 0,020 m²/hab.</div>
            <div style="font-size:0.75em; opacity:.85; margin:4px 0 10px;">Hypothèse haute</div>
            <div style="font-size:2.6em; font-weight:800; line-height:1.1;">
                {m2_pop['Hypothèse haute']:,.0f} m²</div>
            <div style="font-size:0.82em; opacity:.9; margin-top:6px;">de plan d'eau</div>
            </div>""", unsafe_allow_html=True)

        # Export
        st.divider()
        st.markdown("#### 📥 Exporter")
        e1, e2 = st.columns(2)
        with e1:
            st.download_button("⬇️ Synthèse équipements (CSV)",
                data=df_synth.to_csv(index=False, sep=";", encoding="utf-8-sig").encode("utf-8-sig"),
                file_name=f"besoins_equip_{zone_slug}.csv", mime="text/csv")
        with e2:
            st.download_button("⬇️ Licenciés aquatiques (CSV)",
                data=df_aq.to_csv(index=False, sep=";", encoding="utf-8-sig").encode("utf-8-sig"),
                file_name=f"licencies_aquatiques_{zone_slug}.csv", mime="text/csv")

# ═══════════════════════════════════════════════════════════════════════════════
# GÉNÉRATION PDF TABLEAU DE BORD — mise en page conforme au modèle PPTX
# ═══════════════════════════════════════════════════════════════════════════════
def generer_pdf_tableau_bord(
    titre_zone, niveau, pop_affichee, nb_lic, nb_clubs, nb_equip,
    lic_pour_1000, clubs_pour_1000, equip_pour_1000,
    sc, tb_lic_df, tb_equip_df, tb_clubs_df,
    gym30, gym50, m2bas, m2haut, m2moy,
    pis_total_m2, m2_pop,
    df_synth=None,
    logo_b64=None,
    strate_lbl=None,
    strate_lic=None,
    strate_clubs=None,
    strate_equip=None,
):
    """PDF mis en page comme le modèle PPTX : en-tête territoire, Démographie/Indicateurs,
    3 blocs sombres Offre, légende couleurs, Synthèse visuelle, Piscines."""
    from reportlab.lib.pagesizes import A4
    from reportlab.lib import colors as C
    from reportlab.lib.units import cm, mm
    from reportlab.platypus import (
        SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
        HRFlowable, Image as RLImage,
    )
    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
    from reportlab.lib.enums import TA_CENTER, TA_LEFT

    # ── Palette ──────────────────────────────────────────────────────────────
    NAVY     = C.HexColor("#1a237e")
    DARKBOX  = C.HexColor("#111827")
    GREEN    = C.HexColor("#27ae60")   # > +10 %  → vert
    YELLOW   = C.HexColor("#2196f3")   # +5 à +10 % → bleu
    ORANGE   = C.HexColor("#f39c12")   # -5 à -10 % → ambre
    RED      = C.HexColor("#c0392b")   # < -10 %  → rouge
    BLUE_IND = C.HexColor("#9e9e9e")   # ± 5 %    → gris
    LT_G     = C.HexColor("#e8f5e9")   # fond vert clair
    LT_R     = C.HexColor("#ffebee")   # fond rouge clair
    LT_Y     = C.HexColor("#e3f2fd")   # fond bleu clair (pour YELLOW/bleu)
    LT_O     = C.HexColor("#fff3e0")   # fond ambre clair
    LT_B     = C.HexColor("#f5f5f5")   # fond gris clair (pour BLUE_IND/gris)
    BLUE_L   = C.HexColor("#e3f2fd")
    GREY_L   = C.HexColor("#f5f5f5")
    GREY_D   = C.HexColor("#555555")
    GREY_DD  = C.HexColor("#dddddd")
    WHITE    = C.white
    PISBLUE  = C.HexColor("#01579b")

    buf = io.BytesIO()
    doc = SimpleDocTemplate(buf, pagesize=A4,
                            leftMargin=1*cm, rightMargin=1*cm,
                            topMargin=1*cm, bottomMargin=1*cm)
    W = 19*cm

    # ── Styles ───────────────────────────────────────────────────────────────
    _S = getSampleStyleSheet()
    def _sty(name, **kw):
        return ParagraphStyle(name, parent=_S["Normal"], **kw)

    sTitle  = _sty("Ti",  fontSize=14, fontName="Helvetica-Bold",
                   textColor=WHITE, leading=18)
    sSubT   = _sty("Su",  fontSize=9,  fontName="Helvetica",
                   textColor=C.HexColor("#bbdefb"))
    sDate   = _sty("Da",  fontSize=7.5,fontName="Helvetica-Oblique",
                   textColor=C.HexColor("#90caf9"))
    sSec    = _sty("Se",  fontSize=10, fontName="Helvetica-Bold",
                   textColor=WHITE, alignment=TA_CENTER)
    sNorm   = _sty("No",  fontSize=8.5,fontName="Helvetica")
    sBd     = _sty("Bd",  fontSize=8.5,fontName="Helvetica-Bold")
    sSmall  = _sty("Sm",  fontSize=7,  fontName="Helvetica", textColor=GREY_D)
    sCap    = _sty("Ca",  fontSize=7,  fontName="Helvetica-Oblique",
                   textColor=GREY_D, alignment=TA_CENTER)
    sWh     = _sty("Wh",  fontSize=9,  fontName="Helvetica", textColor=WHITE)
    sWhBd   = _sty("WB",  fontSize=11, fontName="Helvetica-Bold",
                   textColor=WHITE, alignment=TA_CENTER)
    sDkBig  = _sty("DB",  fontSize=15, fontName="Helvetica-Bold",
                   textColor=WHITE, alignment=TA_CENTER)
    sDkSm   = _sty("DS",  fontSize=7,  fontName="Helvetica",
                   textColor=C.HexColor("#aaaaaa"), alignment=TA_CENTER)
    sDkLine = _sty("DL",  fontSize=8,  fontName="Helvetica", textColor=WHITE, leading=11)

    # ── Helpers ───────────────────────────────────────────────────────────────
    def _f(v, dec=0):
        if v is None or (isinstance(v, float) and np.isnan(v)):
            return "—"
        if isinstance(v, (int, float)):
            return f"{v:,.{dec}f}".replace(",", " ")   # espace normale (pas \u202f)
        return str(v).replace("**", "")

    def _r(v):
        if v is None or (isinstance(v, float) and np.isnan(v)):
            return "—"
        return f"{v:.1f} ‰"

    def _get_ref(df, keyword, col):
        if df is None or df.empty or col not in df.columns:
            return None
        row = df[df.iloc[:, 0].astype(str).str.contains(keyword, na=False, regex=False)]
        if row.empty:
            return None
        v = row.iloc[0][col]
        return v if isinstance(v, (int, float)) and not np.isnan(v) else None

    def _raw_rates(df, rate_terr):
        """Liste de toutes les valeurs disponibles (territoire + références) pour le calcul de l'indicateur moyen."""
        vals = [rate_terr] if rate_terr is not None else []
        for col_n in ["Dép.", "Région", "France"]:
            v = _get_ref(df, "Total", col_n)
            if v is not None:
                vals.append(v)
        return vals

    def _col_avg(val, rate_values):
        """Indicateur moyen = moyenne de tous les niveaux disponibles.
        Seuils vs indicateur moyen :
          > +10 % → VERT  |  > +5 % → BLEU  |  ±5 % → GRIS
          < -5 % → AMBRE  |  < -10 % → ROUGE
        """
        vals = [v for v in rate_values
                if v is not None and isinstance(v, (int, float)) and not np.isnan(v)]
        if not vals or val is None:
            return BLUE_IND
        moy = sum(vals) / len(vals)
        if moy == 0:
            return BLUE_IND
        ratio = val / moy
        if ratio > 1.10:  return GREEN
        if ratio > 1.05:  return YELLOW
        if ratio >= 0.95: return BLUE_IND
        if ratio >= 0.90: return ORANGE
        return RED

    # Code couleur pour écart signé (positif = surplus = vert)
    def _col_e(ecart):
        if ecart is None:
            return GREY_D
        if ecart > 0:  return GREEN
        if ecart == 0: return BLUE_IND
        return RED

    def _lt(col):
        return {GREEN: LT_G, RED: LT_R, YELLOW: LT_Y,
                ORANGE: LT_O, BLUE_IND: LT_B}.get(col, GREY_L)

    def _h(col):
        return col.hexval() if hasattr(col, "hexval") else "#555555"

    def _dot(col):
        return f'<font color="{_h(col)}">●</font>'

    story = []

    # ═══════════════════════════════════════════════════════════════════════
    # 1 — EN-TÊTE TERRITOIRE + LOGO
    # ═══════════════════════════════════════════════════════════════════════
    niv_clean = (niveau.replace("🌍 ", "").replace("📍 ", "")
                       .replace("🏛️ ", "").replace("🏘️ ", "")
                       .replace("🇫🇷 ", ""))
    left_hdr = [
        Paragraph(titre_zone, sTitle),
        Paragraph(niv_clean, sSubT),
        Paragraph(pd.Timestamp.now().strftime("Édition du %d/%m/%Y"), sDate),
    ]
    if logo_b64:
        _li = io.BytesIO(base64.b64decode(logo_b64))
        right_hdr = RLImage(_li, width=4.8*cm, height=1.6*cm)
    else:
        right_hdr = Paragraph("", sNorm)

    hdr_tbl = Table([[left_hdr, right_hdr]], colWidths=[13*cm, 6*cm])
    hdr_tbl.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), NAVY),
        ("PADDING",    (0, 0), (-1, -1), 10),
        ("VALIGN",     (0, 0), (-1, -1), "MIDDLE"),
        ("ALIGN",      (1, 0), (1, 0),   "RIGHT"),
        ("LINEBELOW",  (0, 0), (-1, -1), 2, C.HexColor("#1565c0")),
    ]))
    story.append(hdr_tbl)
    story.append(Spacer(1, 2*mm))

    # ═══════════════════════════════════════════════════════════════════════
    # 2 — DÉMOGRAPHIE (gauche) | INDICATEURS (droite)
    # ═══════════════════════════════════════════════════════════════════════
    # Indicateur moyen (moyenne de tous les niveaux disponibles)
    _lic_rates_all   = _raw_rates(tb_lic_df,    lic_pour_1000)
    _equip_rates_all = _raw_rates(tb_equip_df,  equip_pour_1000)
    _clubs_rates_all = _raw_rates(tb_clubs_df,  clubs_pour_1000)

    col_lic   = _col_avg(lic_pour_1000,   _lic_rates_all)
    col_clubs = _col_avg(clubs_pour_1000, _clubs_rates_all)
    col_equip = _col_avg(equip_pour_1000, _equip_rates_all)

    # Indicateurs équipements depuis df_synth
    synth_map = {}
    if df_synth is not None and not df_synth.empty:
        for _, row in df_synth.iterrows():
            cat = str(row.get("Catégorie d'équipements", ""))
            try:    e = float(row.get("Écart", None))
            except: e = None
            synth_map[cat] = e

    # Actuel gymnases (pour indicateur scolaires dans colonne droite)
    _gym_actuel_ind = None
    if df_synth is not None and not df_synth.empty:
        _gyr = df_synth[df_synth.iloc[:, 0].astype(str).str.contains(
            "Salles multisports", na=False, regex=False)]
        if not _gyr.empty:
            try: _gym_actuel_ind = float(_gyr.iloc[0].get("Équipements actuels", None))
            except: pass

    def _sec_label(txt, width):
        t = Table([[Paragraph(txt, sSec)]], colWidths=[width])
        t.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, -1), NAVY),
            ("PADDING",    (0, 0), (-1, -1), 6),
        ]))
        return t

    # ── Colonne gauche : Démographie + Socle sportif territorial ─────────
    demo_items = [
        _sec_label("DÉMOGRAPHIE", 11*cm),
        Spacer(1, 1.5*mm),
    ]
    _pop_str = f"{_f(pop_affichee)} hab." + (f"  ({strate_lbl})" if strate_lbl else "")
    demo_data = [
        ["Population",     _pop_str],
        ["Total élèves",   f"{_f(sc.get('total', 0))}"],
        ["  • Maternelle", f"{_f(sc.get('maternelle', 0))} élèves / {_f(sc.get('nb_mat', 0))} étab."],
        ["  • Primaire",   f"{_f(sc.get('primaire', 0))} élèves / {_f(sc.get('nb_prim', 0))} étab."],
        ["  • Collèges",   f"{_f(sc.get('t_co', 0))} élèves / {_f(sc.get('nb_co', 0))} étab."],
        ["  • Lycées GT",  f"{_f(sc.get('t_ly', 0))} élèves / {_f(sc.get('nb_ly', 0))} étab."],
    ]
    d_tbl = Table(demo_data, colWidths=[4*cm, 7*cm])
    d_tbl.setStyle(TableStyle([
        ("FONTNAME",      (0, 0), (0, -1), "Helvetica-Bold"),
        ("FONTNAME",      (1, 0), (1, -1), "Helvetica"),
        ("FONTSIZE",      (0, 0), (-1, -1), 8),
        ("ROWBACKGROUNDS",(0, 0), (-1, -1), [WHITE, GREY_L]),
        ("GRID",          (0, 0), (-1, -1), 0.3, GREY_DD),
        ("PADDING",       (0, 0), (-1, -1), 4),
    ]))
    demo_items.append(d_tbl)
    demo_items.append(Spacer(1, 2*mm))

    # ── SPORT FÉDÉRAL : table Total + 5 catégories ───────────────────────
    demo_items.append(_sec_label("SPORT FÉDÉRAL", 11*cm))
    demo_items.append(Spacer(1, 1.5*mm))

    # Récupérer les taux par catégorie (licenciés) depuis tb_lic_df
    _CAT_KEYS = [
        ("olympiques",   "Féd.\nolympiques"),
        ("délégataires", "Féd. délég.\nnon ol."),
        ("multisports",  "Féd.\nmultisports"),
        ("handi",        "Féd. handi\n& para"),
        ("scolaires",    "Féd. scol.\n& univ."),
    ]
    _cat_lic_vals = []   # list of (short_label, rate_or_None)
    if tb_lic_df is not None and not tb_lic_df.empty and "Territoire" in tb_lic_df.columns:
        for _key, _short in _CAT_KEYS:
            _found = None
            for _, _row in tb_lic_df.iterrows():
                if _key in str(_row.iloc[0]).lower():
                    _v = _row.get("Territoire")
                    if _v is not None and isinstance(_v, (int, float)) and not np.isnan(_v):
                        _found = _v
                    break
            _cat_lic_vals.append((_short, _found))
    else:
        _cat_lic_vals = [(_short, None) for _, _short in _CAT_KEYS]

    # Largeurs colonnes : label | Total | 5 catégories
    _cw_lbl  = 1.4*cm
    _cw_tot  = 1.6*cm
    _cw_cat  = (11*cm - _cw_lbl - _cw_tot) / 5   # ≈ 1.6 cm chacune

    def _sf(v):   # format taux court
        if v is None: return "—"
        return f"{v:.1f} ‰"

    def _p_hdr(txt):
        return Paragraph(txt, _sty("sfh", fontSize=6.5, fontName="Helvetica-Bold",
                                    textColor=NAVY, alignment=TA_CENTER, leading=8))
    def _p_val(txt, bold=False):
        return Paragraph(txt, _sty("sfv", fontSize=7.5,
                                    fontName="Helvetica-Bold" if bold else "Helvetica",
                                    alignment=TA_CENTER, leading=9))
    def _p_lbl(txt):
        return Paragraph(txt, _sty("sfl", fontSize=8, fontName="Helvetica-Bold", leading=10))

    # En-têtes
    sf_hdr = ["", _p_hdr("Total")] + [_p_hdr(s) for s, _ in _cat_lic_vals]
    # Ligne Licenciés
    sf_lic  = [_p_lbl("Licenciés"), _p_val(_sf(lic_pour_1000), bold=True)] + \
              [_p_val(_sf(v)) for _, v in _cat_lic_vals]
    # Ligne Clubs (pas de répartition par catégorie disponible)
    sf_clubs = [_p_lbl("Clubs"), _p_val(_sf(clubs_pour_1000), bold=True)] + \
               [_p_val("—") for _ in _cat_lic_vals]

    sf_tbl = Table([sf_hdr, sf_lic, sf_clubs],
                   colWidths=[_cw_lbl, _cw_tot] + [_cw_cat]*5)
    _col_lic_hex  = _h(col_lic)
    _col_club_hex = _h(col_clubs)
    sf_tbl.setStyle(TableStyle([
        # En-tête
        ("FONTSIZE",      (0, 0), (-1, 0), 6.5),
        ("ROWBACKGROUNDS",(0, 0), (-1, -1), [GREY_L, WHITE, GREY_L]),
        ("GRID",          (0, 0), (-1, -1), 0.3, GREY_DD),
        ("PADDING",       (0, 0), (-1, -1), 3),
        ("VALIGN",        (0, 0), (-1, -1), "MIDDLE"),
        ("ALIGN",         (1, 0), (-1, -1), "CENTER"),
        # Case colorée Licenciés total
        ("BACKGROUND",    (1, 1), (1, 1), C.HexColor(_col_lic_hex)),
        ("TEXTCOLOR",     (1, 1), (1, 1), WHITE),
        ("FONTNAME",      (1, 1), (1, 1), "Helvetica-Bold"),
        # Case colorée Clubs total
        ("BACKGROUND",    (1, 2), (1, 2), C.HexColor(_col_club_hex)),
        ("TEXTCOLOR",     (1, 2), (1, 2), WHITE),
        ("FONTNAME",      (1, 2), (1, 2), "Helvetica-Bold"),
        # Ligne en-tête : trait bas
        ("LINEBELOW",     (0, 0), (-1, 0), 0.5, NAVY),
    ]))
    demo_items.append(sf_tbl)

    # ── Colonne droite : Équipements sportifs ─────────────────────────────
    ind_items = [
        _sec_label("ÉQUIPEMENTS SPORTIFS", 8*cm),
        Spacer(1, 1.5*mm),
    ]

    # Offre actuelle
    ind_items.append(Paragraph("Offre",
        _sty("phOffre", fontSize=7.5, fontName="Helvetica-Bold", textColor=NAVY,
             spaceAfter=1)))
    # Case colorée pour le taux équipements
    _eq_col_h = _h(col_equip)
    _eq_val_cell = Table([[Paragraph(
        f'<b>{_r(equip_pour_1000)}</b>',
        _sty("eqRate", fontSize=8, fontName="Helvetica-Bold",
             textColor=WHITE, alignment=TA_CENTER))]],
        colWidths=[2.2*cm])
    _eq_val_cell.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), C.HexColor(_eq_col_h)),
        ("PADDING",    (0, 0), (-1, -1), 3),
    ]))
    _eq_offre_tbl = Table([[
        Paragraph(f'Équipements sportifs : <b>{_f(nb_equip)}</b>',
                  _sty("eqNb", fontSize=8, fontName="Helvetica")),
        _eq_val_cell,
    ]], colWidths=[5.5*cm, 2.4*cm])
    _eq_offre_tbl.setStyle(TableStyle([
        ("VALIGN",        (0, 0), (-1, -1), "MIDDLE"),
        ("LEFTPADDING",   (0, 0), (-1, -1), 0),
        ("RIGHTPADDING",  (0, 0), (-1, -1), 0),
        ("TOPPADDING",    (0, 0), (-1, -1), 2),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 2),
    ]))
    ind_items.append(_eq_offre_tbl)
    ind_items.append(Spacer(1, 1.5*mm))

    # Besoins théoriques (sous-section)
    ind_items.append(Paragraph("Besoins théoriques",
        _sty("phBT", fontSize=7.5, fontName="Helvetica-Bold", textColor=NAVY,
             spaceAfter=1)))
    ind_items.append(Paragraph("Équipements types",
        _sty("ph2", fontSize=7, fontName="Helvetica-Bold", textColor=GREY_D,
             spaceAfter=1)))
    for full, short in [
        ("Courts de tennis",              "Courts de tennis"),
        ("Salles multisports / Gymnases", None),          # traitement spécial gymnases
        ("Terrains de grands jeux",       "Terrains de grands jeux"),
        ("Salles de combat",              "Salle de combat"),
    ]:
        e = synth_map.get(full)
        ce = _col_e(e)
        es = f" ({e:+.1f})" if e is not None else ""

        if short is None:
            # Gymnases : label + clubs sur ligne séparée + scolaires sur ligne séparée
            _sty_gym = lambda n, **kw: _sty(n, fontSize=8, fontName="Helvetica",
                                             leading=11, **kw)
            ind_items.append(Paragraph(
                "Gymnases",
                _sty("ieGymLbl", fontSize=8, fontName="Helvetica", leading=11)))
            # Ligne clubs
            _clubs_str = f"{e:+.1f}" if e is not None else "nd"
            ind_items.append(Paragraph(
                f'  {_dot(ce)}  clubs : {_clubs_str}',
                _sty_gym("ieGymClubs")))
            # Ligne scolaires 30 % / 50 %
            if gym30 is not None and _gym_actuel_ind is not None:
                e30 = _gym_actuel_ind - gym30
                c30 = _col_e(e30)
                s30 = f"{e30:+.2f}"
                if gym50 is not None:
                    e50 = _gym_actuel_ind - gym50
                    c50 = _col_e(e50)
                    s50 = f"{e50:+.2f}"
                else:
                    c50, s50 = GREY_D, "nd"
                ind_items.append(Paragraph(
                    f'  {_dot(c30)}  scolaires 30 %* : {s30}  &nbsp; {_dot(c50)}  scolaires 50 %* : {s50}',
                    _sty("ieGymSco", fontSize=7.5, fontName="Helvetica", leading=10)))
                ind_items.append(Paragraph(
                    "  <i>* du temps de l'EPS en gymnase</i>",
                    _sty("ieGymFn", fontSize=6.5, fontName="Helvetica-Oblique",
                         textColor=GREY_D, leading=9)))
        else:
            ind_items.append(Paragraph(
                f'{_dot(ce)}  {short}{es}',
                _sty(f"ie{short}", fontSize=8, fontName="Helvetica", leading=11)))

    ind_items.append(Spacer(1, 1.5*mm))
    ind_items.append(Paragraph("Piscines",
        _sty("ph3", fontSize=7.5, fontName="Helvetica-Bold", textColor=NAVY,
             spaceAfter=1)))
    e_sco = (pis_total_m2 - m2moy) if m2moy is not None else None
    c_sco = _col_e(e_sco)
    ind_items.append(Paragraph(
        f'{_dot(c_sco)}  Scolaires (moy.)** — '
        f'{(f"{e_sco:+,.0f}").replace(",", " ")} m²'
        if e_sco is not None else f'{_dot(GREY_D)}  Scolaires — nd',
        _sty("ipsc", fontSize=8, fontName="Helvetica", leading=11)))
    for lbl, key in [("0,016 m²/hab.", "Hypothèse basse"),
                     ("0,018 m²/hab.", "Hypothèse moyenne"),
                     ("0,020 m²/hab.", "Hypothèse haute")]:
        if m2_pop and key in m2_pop:
            er = pis_total_m2 - m2_pop[key]
            cr = _col_e(er)
            ind_items.append(Paragraph(
                f'{_dot(cr)}  {lbl} — {f"{er:+,.0f}".replace(",", " ")} m²',
                _sty(f"ir{lbl}", fontSize=8, fontName="Helvetica", leading=11)))
    ind_items.append(Paragraph(
        "<i>** besoin théorique moyen calculé sur les bases<br/>de la circulaire du 28-2-2022</i>",
        _sty("ipscfn", fontSize=6.5, fontName="Helvetica-Oblique",
             textColor=GREY_D, leading=8.5)))

    two_col = Table([[demo_items, ind_items]], colWidths=[11*cm, 8*cm])
    two_col.setStyle(TableStyle([
        ("VALIGN",         (0, 0), (-1, -1), "TOP"),
        ("LEFTPADDING",    (0, 0), (-1, -1), 0),
        ("RIGHTPADDING",   (0, 0), (-1, -1), 0),
        ("TOPPADDING",     (0, 0), (-1, -1), 0),
        ("BOTTOMPADDING",  (0, 0), (-1, -1), 0),
        ("LINEAFTER",      (0, 0), (0, -1),  1, GREY_DD),
    ]))
    story.append(two_col)
    story.append(Spacer(1, 2*mm))

    # ═══════════════════════════════════════════════════════════════════════
    # 3 — LÉGENDE CODE COULEUR (rectangles colorés)
    # ═══════════════════════════════════════════════════════════════════════
    def _leg_box(txt, bg):
        t = Table([[Paragraph(txt, _sty(f"lb{txt}", fontSize=7, fontName="Helvetica-Bold",
                                        textColor=WHITE, alignment=TA_CENTER, leading=9))]],
                  colWidths=[3.7*cm])
        t.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, -1), bg),
            ("PADDING",    (0, 0), (-1, -1), 4),
        ]))
        return t

    leg_row = Table([[
        _leg_box("> +10 %",      GREEN),
        _leg_box("+5 à +10 %",   YELLOW),
        _leg_box("± 5 %",        BLUE_IND),
        _leg_box("-5 à -10 %",   ORANGE),
        _leg_box("< -10 %",      RED),
    ]], colWidths=[3.8*cm] * 5)
    leg_row.setStyle(TableStyle([
        ("LEFTPADDING",  (0, 0), (-1, -1), 1),
        ("RIGHTPADDING", (0, 0), (-1, -1), 1),
        ("TOPPADDING",   (0, 0), (-1, -1), 0),
        ("BOTTOMPADDING",(0, 0), (-1, -1), 0),
    ]))
    story.append(leg_row)
    story.append(Spacer(1, 2*mm))

    # ═══════════════════════════════════════════════════════════════════════
    # 4 — EN-TÊTE SOMBRE "OFFRE SPORTIVE"
    # ═══════════════════════════════════════════════════════════════════════
    def _dark_header(label):
        t = Table([[Paragraph(label, sSec)]], colWidths=[W])
        t.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, -1), DARKBOX),
            ("PADDING",    (0, 0), (-1, -1), 5),
        ]))
        return t

    story.append(_dark_header("OFFRE SPORTIVE"))
    story.append(Spacer(1, 2*mm))

    # ═══════════════════════════════════════════════════════════════════════
    # 4 — TROIS BLOCS SOMBRES : Licenciés | Clubs | Équipements
    # ═══════════════════════════════════════════════════════════════════════
    def _dark_box(title, total_str, rate_rows, col_ind):
        col_h = _h(col_ind)
        lines = [
            Paragraph(f'{title}  <font color="{col_h}">●</font>',
                      _sty(f"dbt{title}", fontSize=8.5, fontName="Helvetica-Bold",
                           textColor=WHITE, alignment=TA_CENTER)),
            Spacer(1, 1*mm),
            Paragraph(f"<b>{total_str}</b>",
                      _sty(f"dbn{title}", fontSize=12, fontName="Helvetica-Bold",
                           textColor=WHITE, alignment=TA_CENTER)),
            Paragraph("total", _sty(f"dbl{title}", fontSize=6.5, fontName="Helvetica",
                                    textColor=C.HexColor("#aaaaaa"), alignment=TA_CENTER)),
            Spacer(1, 1.5*mm),
        ]
        for lbl, val in rate_rows:
            lines.append(Paragraph(
                f'<font color="#aaaaaa">{lbl} :</font>  <b><font color="white">{val}</font></b>',
                _sty(f"dbl{title}{lbl}", fontSize=7.5, fontName="Helvetica",
                     textColor=WHITE, leading=10)))
        box = Table([[lines]], colWidths=[5.9*cm])
        box.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, -1), DARKBOX),
            ("PADDING",    (0, 0), (-1, -1), 7),
            ("VALIGN",     (0, 0), (-1, -1), "TOP"),
            ("BOX",        (0, 0), (-1, -1), 1, C.HexColor("#2d3748")),
        ]))
        return box

    def _rates_from_df(df, rate_terr, strate_rate=None):
        """Retourne les lignes (libellé, taux formaté) + indicateur moyen + strate."""
        rows  = [("Territoire", _r(rate_terr))]
        raw   = [rate_terr] if rate_terr is not None else []
        for col_n, lbl in [("Dép.", "Département"), ("Région", "Région"), ("France", "National")]:
            v = _get_ref(df, "Total", col_n)
            if v is not None:
                rows.append((lbl, _r(v)))
                raw.append(v)
        # Ligne indicateur moyen
        if len(raw) > 1:
            moy = sum(raw) / len(raw)
            rows.append(("Indicateur moy.", _r(moy)))
        # Ligne strate (communes / EPCI uniquement)
        if strate_rate is not None:
            rows.append(("Indicateur strate", _r(strate_rate)))
        return rows

    box_lic   = _dark_box("LICENCIÉS",   _f(nb_lic),
                           _rates_from_df(tb_lic_df,   lic_pour_1000,   strate_lic),   col_lic)
    box_clubs = _dark_box("CLUBS",       _f(nb_clubs),
                           _rates_from_df(tb_clubs_df, clubs_pour_1000, strate_clubs), col_clubs)
    box_equip = _dark_box("ÉQUIPEMENTS", _f(nb_equip),
                           _rates_from_df(tb_equip_df, equip_pour_1000, strate_equip), col_equip)

    boxes = Table([[box_lic, box_clubs, box_equip]], colWidths=[6.33*cm, 6.33*cm, 6.33*cm])
    boxes.setStyle(TableStyle([
        ("LEFTPADDING",   (0, 0), (-1, -1), 1),
        ("RIGHTPADDING",  (0, 0), (-1, -1), 1),
        ("TOPPADDING",    (0, 0), (-1, -1), 0),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
    ]))
    story.append(boxes)
    story.append(Spacer(1, 2*mm))

    # ═══════════════════════════════════════════════════════════════════════
    # 6 — EN-TÊTE "BESOINS THÉORIQUES"
    # ═══════════════════════════════════════════════════════════════════════
    story.append(_dark_header("BESOINS THÉORIQUES CLUBS"))
    story.append(Spacer(1, 2*mm))

    # ═══════════════════════════════════════════════════════════════════════
    # 7 — SYNTHÈSE VISUELLE ÉQUIPEMENTS (cartes colorées comme l'app)
    # ═══════════════════════════════════════════════════════════════════════
    if df_synth is not None and not df_synth.empty:
        cards = []
        for _, row in df_synth.iterrows():
            cat   = str(row.get("Catégorie d'équipements", ""))
            besoin= row.get("Besoins consolidés", None)
            actuel= row.get("Équipements actuels", None)
            ecart = row.get("Écart", None)
            try:    ev = float(ecart)
            except: ev = None
            cc  = _col_e(ev)
            ltc = _lt(cc)
            cch = _h(cc)
            lth = _h(ltc)
            ev_str = (f'{ev:+.1f}') if ev is not None else "—"
            card = Table([[
                [Paragraph(cat, _sty(f"cc{cat}", fontSize=7, fontName="Helvetica-Bold",
                                     textColor=NAVY, alignment=TA_CENTER, leading=8)),
                 Spacer(1, 1*mm),
                 Paragraph(f'<font color="{cch}"><b>{ev_str}</b></font>',
                            _sty(f"cce{cat}", fontSize=13, fontName="Helvetica-Bold",
                                 alignment=TA_CENTER, leading=15)),
                 Paragraph(
                     f'Besoin : {_f(besoin, 1) if besoin is not None else "—"}<br/>'
                     f'Actuel : {_f(actuel) if actuel is not None else "—"}',
                     _sty(f"ccba{cat}", fontSize=6.5, fontName="Helvetica",
                          textColor=GREY_D, alignment=TA_CENTER, leading=8))]
            ]], colWidths=[4.5*cm])
            card.setStyle(TableStyle([
                ("BACKGROUND", (0, 0), (-1, -1), C.HexColor(lth)),
                ("PADDING",    (0, 0), (-1, -1), 6),
                ("BOX",        (0, 0), (-1, -1), 2, cc),
            ]))
            cards.append(card)

        while len(cards) < 4:
            cards.append(Paragraph("", sNorm))

        cards_row = Table([cards[:4]], colWidths=[4.75*cm] * 4)
        cards_row.setStyle(TableStyle([
            ("LEFTPADDING",  (0, 0), (-1, -1), 1),
            ("RIGHTPADDING", (0, 0), (-1, -1), 1),
        ]))
        story.append(cards_row)
        story.append(Spacer(1, 2*mm))

    # Gymnases — besoins scolaires (2 hypothèses) + clubs
    # Récupérer l'actuel gymnases depuis df_synth
    _actuel_gym = None
    if df_synth is not None and not df_synth.empty:
        _gym_row = df_synth[df_synth.iloc[:, 0].astype(str).str.contains("Salles multisports", na=False, regex=False)]
        if not _gym_row.empty:
            try: _actuel_gym = float(_gym_row.iloc[0].get("Équipements actuels", None))
            except: pass

    gym_hdr = Table([[Paragraph("SALLES MULTISPORTS / GYMNASES — BESOINS SCOLAIRES", sSec)]],
                    colWidths=[W])
    gym_hdr.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), C.HexColor("#263238")),
        ("PADDING",    (0, 0), (-1, -1), 5),
    ]))
    story.append(gym_hdr)
    story.append(Spacer(1, 1.5*mm))

    def _gym_card(label, gym_v, actuel_v, cw=9.3*cm):
        ev  = (actuel_v - gym_v) if (gym_v is not None and actuel_v is not None) else None
        cc  = _col_e(ev)
        ltc = _lt(cc)
        cch = _h(cc)
        lth = _h(ltc)
        ev_str     = f"{ev:+.2f}" if ev is not None else "—"
        actuel_str = f"{actuel_v:.0f}" if actuel_v is not None else "—"
        besoin_str = _f(gym_v, 2) if gym_v is not None else "—"
        content = [
            Paragraph(label,
                      _sty(f"gcl{label}", fontSize=7.5, fontName="Helvetica-Bold",
                           textColor=NAVY, alignment=TA_CENTER)),
            # Écart en gros
            Paragraph(f'<font color="{cch}"><b>{ev_str}</b></font>',
                      _sty(f"gcv{label}", fontSize=16, fontName="Helvetica-Bold",
                           alignment=TA_CENTER, leading=18)),
            Paragraph(f'Actuel : {actuel_str}  ·  Besoins théoriques : {besoin_str}',
                      _sty(f"gce{label}", fontSize=6.5, fontName="Helvetica",
                           textColor=GREY_D, alignment=TA_CENTER, leading=8)),
        ]
        card = Table([[content]], colWidths=[cw - 2*mm])
        card.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, -1), C.HexColor(lth)),
            ("PADDING",    (0, 0), (-1, -1), 5),
            ("BOX",        (0, 0), (-1, -1), 2, cc),
        ]))
        return card

    gym_row = Table([[
        _gym_card("Hypothèse 30 % (scolaires)", gym30, _actuel_gym),
        _gym_card("Hypothèse 50 % (scolaires)", gym50, _actuel_gym),
    ]], colWidths=[9.5*cm, 9.5*cm])
    gym_row.setStyle(TableStyle([
        ("LEFTPADDING",  (0, 0), (-1, -1), 1),
        ("RIGHTPADDING", (0, 0), (-1, -1), 1),
    ]))
    story.append(gym_row)
    story.append(Spacer(1, 2*mm))

    # ═══════════════════════════════════════════════════════════════════════
    # 8 — PISCINES
    # ═══════════════════════════════════════════════════════════════════════
    pis_hdr = Table([[Paragraph("PISCINES — OFFRE ACTUELLE & ESTIMATIONS DE BESOINS", sSec)]],
                    colWidths=[W])
    pis_hdr.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), PISBLUE),
        ("PADDING",    (0, 0), (-1, -1), 5),
    ]))
    story.append(pis_hdr)
    story.append(Spacer(1, 1.5*mm))
    story.append(Paragraph(
        f"Offre actuelle : <b>{_f(pis_total_m2)} m²</b> de plan d'eau",
        _sty("pisoff", fontSize=8, fontName="Helvetica")))
    story.append(Spacer(1, 1.5*mm))

    def _pis_card(label, besoin_v, offre_v, cw=6*cm):
        ev    = (offre_v - besoin_v) if besoin_v is not None else None
        cc    = _col_e(ev)
        ltc   = _lt(cc)
        cch   = _h(cc)
        lth   = _h(ltc)
        # Écart en gros (signé, en m²)
        ev_str = (f'{ev:+,.0f}'.replace(",", " ") + " m²") if ev is not None else "—"
        content = [
            Paragraph(label, _sty(f"pcl{label}", fontSize=7, fontName="Helvetica-Bold",
                                   textColor=NAVY, alignment=TA_CENTER, leading=8)),
            # Écart en gros caractères
            Paragraph(f'<font color="{cch}"><b>{ev_str}</b></font>',
                      _sty(f"pcv{label}", fontSize=11, fontName="Helvetica-Bold",
                           alignment=TA_CENTER, leading=13)),
            Paragraph(f'Offre : {_f(offre_v)} m²  ·  Besoin : {_f(besoin_v)} m²',
                      _sty(f"pce{label}", fontSize=6.5, fontName="Helvetica",
                           textColor=GREY_D, alignment=TA_CENTER, leading=8)),
        ]
        card = Table([[content]], colWidths=[cw - 2*mm])
        card.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, -1), C.HexColor(lth)),
            ("PADDING",    (0, 0), (-1, -1), 5),
            ("BOX",        (0, 0), (-1, -1), 2, cc),
        ]))
        return card

    # Scolaires + ratios regroupés en 2 lignes de 3
    story.append(Paragraph("Besoins scolaires (m²)",
        _sty("scth", fontSize=7.5, fontName="Helvetica-Bold", textColor=GREY_D)))
    sco_row = Table([[
        _pis_card("Hyp. basse",   m2bas,  pis_total_m2),
        _pis_card("Hyp. haute",   m2haut, pis_total_m2),
        _pis_card("Hyp. MOYENNE", m2moy,  pis_total_m2),
    ]], colWidths=[6.33*cm, 6.33*cm, 6.33*cm])
    sco_row.setStyle(TableStyle([
        ("LEFTPADDING",  (0, 0), (-1, -1), 1),
        ("RIGHTPADDING", (0, 0), (-1, -1), 1),
    ]))
    story.append(sco_row)
    story.append(Spacer(1, 1.5*mm))

    # Ratios par habitant
    if pop_affichee > 0 and m2_pop:
        story.append(Paragraph("Estimation par ratio / habitant",
            _sty("rth", fontSize=7.5, fontName="Helvetica-Bold", textColor=GREY_D)))
        rat_cards = [
            _pis_card(lbl, m2_pop.get(key), pis_total_m2)
            for lbl, key in [
                ("0,016 m²/hab. (basse)",   "Hypothèse basse"),
                ("0,018 m²/hab. (moyenne)", "Hypothèse moyenne"),
                ("0,020 m²/hab. (haute)",   "Hypothèse haute"),
            ] if key in (m2_pop or {})
        ]
        if rat_cards:
            while len(rat_cards) < 3:
                rat_cards.append(Paragraph("", sNorm))
            rat_row = Table([rat_cards], colWidths=[6.33*cm] * 3)
            rat_row.setStyle(TableStyle([
                ("LEFTPADDING",  (0, 0), (-1, -1), 1),
                ("RIGHTPADDING", (0, 0), (-1, -1), 1),
            ]))
            story.append(rat_row)

    # ═══════════════════════════════════════════════════════════════════════
    # 9 — PIED DE PAGE
    # ═══════════════════════════════════════════════════════════════════════
    story.append(Spacer(1, 2*mm))
    story.append(HRFlowable(width="100%", thickness=0.5, color=GREY_DD, spaceAfter=2))
    story.append(Paragraph(
        f"SportData et territoires — par Patrick Bayeux — Décideurs du sport  |  "
        f"Sources : Ministère des Sports 2023 · MEN 2024  |  "
        f"Édition du {pd.Timestamp.now().strftime('%d/%m/%Y')}",
        sCap))

    doc.build(story)
    buf.seek(0)
    return buf.getvalue()



# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 7 — TABLEAU DE BORD
# ═══════════════════════════════════════════════════════════════════════════════
with tab_tb:
    st.markdown(
        """<div style="background: linear-gradient(135deg, #1a237e 0%, #283593 100%);
        padding: 18px 24px; border-radius: 10px; margin-bottom: 18px;">
        <h2 style="color: white; margin: 0; font-size: 1.5em; font-weight: 800;">
            📋 Tableau de bord — SportData et territoires
        </h2>
        <p style="color: #bbdefb; margin: 4px 0 0 0; font-size: 0.9em;">par Patrick Bayeux</p>
        </div>""",
        unsafe_allow_html=True)

    st.markdown(f"**Territoire analysé :** {titre_zone}")
    st.markdown("---")

    # ── 1. Démographie & sport ───────────────────────────────────────────────
    st.markdown("### 👥 Démographie et pratique sportive")
    tb1, tb2, tb3, tb4, tb5 = st.columns(5)
    tb1.metric("🏠 Habitants",   fmt(pop_affichee) + " hab." if pop_affichee > 0 else "—")
    tb2.metric("🎽 Licenciés en club", fmt(nb_lic),
               delta=f"{rate_str(lic_pour_1000)} / 1 000 hab." if lic_pour_1000 is not None else None,
               delta_color="off")
    _taux_club = f"{nb_lic / pop_affichee * 100:.1f} %" if pop_affichee > 0 and nb_lic > 0 else "—"
    tb3.metric("📊 Taux de pratique en club", _taux_club,
               help="Licenciés / Population × 100")
    tb4.metric("🏟️ Clubs",      fmt(nb_clubs),
               delta=f"{rate_str(clubs_pour_1000)} / 1 000 hab." if clubs_pour_1000 is not None else None,
               delta_color="off")
    tb5.metric("⚽ Équipements", fmt(nb_equip),
               delta=f"{rate_str(equip_pour_1000)} / 1 000 hab." if equip_pour_1000 is not None else None,
               delta_color="off")

    # ── Strate de référence (communes et EPCI uniquement) ────────────────────
    if (_is_comm or _is_epci) and pop_affichee > 0:
        _strates_ref = STRATES_COMM  if _is_comm else STRATES_EPCI
        _avgs_ref    = comm_strates_avgs if _is_comm else epci_strates_avgs
        _strate_lbl  = assign_strate(pop_affichee, _strates_ref)

        _lic_avg   = _avgs_ref.get("lic",   {}).get(_strate_lbl)
        _clubs_avg = _avgs_ref.get("clubs", {}).get(_strate_lbl)
        _equip_avg = _avgs_ref.get("equip", {}).get(_strate_lbl)

        def _cmp_html(val, avg):
            """Retourne icône + texte coloré selon position vs moyenne de strate."""
            if val is None or avg is None or avg == 0:
                return "<span style='color:#9e9e9e'>—</span>"
            if val > avg:
                icon, color = "▲", "#27ae60"
            elif val < avg:
                icon, color = "▼", "#c0392b"
            else:
                icon, color = "=", "#9e9e9e"
            return (f"<span style='color:{color}; font-weight:600'>"
                    f"{icon} {val:.1f}</span>"
                    f"<span style='color:#555; font-size:0.85em'>"
                    f" vs moy. {avg:.1f}</span>")

        _lic_h   = _cmp_html(lic_pour_1000,    _lic_avg)
        _clubs_h = _cmp_html(clubs_pour_1000,  _clubs_avg)
        _equip_h = _cmp_html(equip_pour_1000,  _equip_avg)

        _type_lbl = "commune" if _is_comm else "intercommunalité"
        st.markdown(
            f"""<div style="background:#e8eaf6; border-left:4px solid #3949ab;
            border-radius:6px; padding:10px 18px; margin-top:6px; font-size:0.92em;">
            <b>📊 Strate de référence ({_type_lbl}) : {_strate_lbl}</b>
            &nbsp;—&nbsp; Comparaison aux moyennes de la strate :
            <br><br>
            <span style='margin-right:30px'>🎽 Licenciés / 1 000 hab. : {_lic_h}</span>
            <span style='margin-right:30px'>🏟️ Clubs / 1 000 hab. : {_clubs_h}</span>
            <span>⚽ Équipements / 1 000 hab. : {_equip_h}</span>
            <br><span style='color:#555; font-size:0.82em; margin-top:4px; display:block'>
            Moy. strate — 🎽 {f"{_lic_avg:.1f}" if _lic_avg else "—"} &nbsp;|&nbsp;
            🏟️ {f"{_clubs_avg:.1f}" if _clubs_avg else "—"} &nbsp;|&nbsp;
            ⚽ {f"{_equip_avg:.1f}" if _equip_avg else "—"} / 1 000 hab.</span>
            </div>""",
            unsafe_allow_html=True,
        )

    st.markdown("---")

    # ── 2. Scolaires ─────────────────────────────────────────────────────────
    st.markdown("### 🎓 Scolaires (Rentrée 2024)")
    ts1, ts2, ts3, ts4 = st.columns(4)
    ts1.metric("🏫 Écoles",       sc["nb_ec"],
               delta=f"{fmt(sc['t_ec'])} élèves" if sc['t_ec'] > 0 else None,
               delta_color="off")
    ts2.metric("🏛️ Collèges",    sc["nb_co"],
               delta=f"{fmt(sc['t_co'])} élèves" if sc['t_co'] > 0 else None,
               delta_color="off")
    ts3.metric("🎓 Lycées GT",    sc["nb_ly"],
               delta=f"{fmt(sc['t_ly'])} élèves" if sc['t_ly'] > 0 else None,
               delta_color="off")
    ts4.metric("📚 Total élèves", fmt(sc["total"]))

    st.markdown("---")

    # ── 2b. Data licenciés ───────────────────────────────────────────────────
    st.markdown("### 🎽 Data licenciés")
    st.caption("Taux de licenciés pour 1 000 hab. par catégorie de fédération — comparaison multi-niveaux")

    _tb_lic_rows = []
    _tb_total_lic_zone = lic_zone["total"].sum()
    _tb_pop = pop_affichee if pop_affichee > 0 else 1
    _tb_strate_key = None
    _tb_comm_epci_nom = None
    _tb_epci_codes = set()
    _tb_epci_pop_val = 0

    # Ligne Total
    _tb_total_row = {
        "Catégorie": "**Total licenciés**",
        "Territoire": round(_tb_total_lic_zone / _tb_pop * 1000, 2) if pop_affichee > 0 else None,
    }
    if (_is_comm or _is_epci) and pop_affichee > 0:
        _tb_strate_key = assign_strate(pop_affichee, STRATES_COMM if _is_comm else STRATES_EPCI)
        _tb_avgs_ref = comm_strates_avgs if _is_comm else epci_strates_avgs
        _tb_total_row["Strate"] = _tb_avgs_ref.get("lic", {}).get(_tb_strate_key)
    if _is_comm and pop_affichee > 0:
        _tb_comm_epci_nom = comm_df[comm_df["code_commune"].isin(codes)]["epci_nom"].iloc[0] \
            if not comm_df[comm_df["code_commune"].isin(codes)].empty else None
        if _tb_comm_epci_nom:
            _tb_epci_codes = set(comm_df[comm_df["epci_nom"] == _tb_comm_epci_nom]["code_commune"])
            _tb_epci_pop_val = int(epci_df[epci_df["epci_nom"] == _tb_comm_epci_nom]["population"].iloc[0]) \
                if not epci_df[epci_df["epci_nom"] == _tb_comm_epci_nom].empty else 0
            _tb_epci_lic = lic_df[lic_df["code"].isin(_tb_epci_codes)]["total"].sum()
            _tb_total_row["EPCI"] = round(_tb_epci_lic / _tb_epci_pop_val * 1000, 2) if _tb_epci_pop_val > 0 else None
    if ctx_dep:
        _tb_dep_lic_total = sum(
            round(dep_lic_rates[ctx_dep].get(f, 0) * _dep_pop_total.get(ctx_dep, 0) / 1000)
            for f in dep_lic_rates.get(ctx_dep, {})
        )
        _tb_dep_pop = _dep_pop_total.get(ctx_dep, 0)
        _tb_total_row["Dép."] = round(_tb_dep_lic_total / _tb_dep_pop * 1000, 2) if _tb_dep_pop > 0 else None
    if ctx_reg:
        _tb_reg_lic_total = sum(
            round(reg_lic_rates[ctx_reg].get(f, 0) * _reg_pop_total.get(ctx_reg, 0) / 1000)
            for f in reg_lic_rates.get(ctx_reg, {})
        )
        _tb_reg_pop = _reg_pop_total.get(ctx_reg, 0)
        _tb_total_row["Région"] = round(_tb_reg_lic_total / _tb_reg_pop * 1000, 2) if _tb_reg_pop > 0 else None
    _tb_nat_lic_total = lic_df["total"].sum()
    _tb_total_row["France"] = round(_tb_nat_lic_total / _nat_pop_total * 1000, 2) if _nat_pop_total > 0 else None
    _tb_lic_rows.append(_tb_total_row)

    # Lignes par catégorie
    for cat_label, (lo, hi) in FED_CATEGORIES.items():
        _cat_feds_tb = [f for f, c in fed_code_map.items() if lo <= c <= hi]
        _cat_lic_tb = lic_zone[lic_zone["federation"].isin(_cat_feds_tb)]["total"].sum()
        _tb_row = {
            "Catégorie": cat_label,
            "Territoire": round(_cat_lic_tb / _tb_pop * 1000, 2) if pop_affichee > 0 else None,
        }
        if (_is_comm or _is_epci) and pop_affichee > 0:
            _lic_strate_df2 = lic_df[lic_df["code"].map(
                _strate_comm_map if _is_comm else _strate_epci_map
            ).fillna("—") == _tb_strate_key]
            _cat_strate_tot = _lic_strate_df2[_lic_strate_df2["federation"].isin(_cat_feds_tb)]["total"].sum()
            _pop_s2 = (_pop_par_strate_comm if _is_comm else _pop_par_strate_epci).get(_tb_strate_key, 0)
            _tb_row["Strate"] = round(_cat_strate_tot / _pop_s2 * 1000, 2) if _pop_s2 > 0 else None
        if _is_comm and pop_affichee > 0 and _tb_comm_epci_nom:
            _epci_lic_cat_tb = lic_df[lic_df["code"].isin(_tb_epci_codes) & lic_df["federation"].isin(_cat_feds_tb)]["total"].sum()
            _tb_row["EPCI"] = round(_epci_lic_cat_tb / _tb_epci_pop_val * 1000, 2) if _tb_epci_pop_val > 0 else None
        if ctx_dep:
            _tb_row["Dép."] = get_cat_rates_dep(ctx_dep, _cat_feds_tb)
        if ctx_reg:
            _tb_row["Région"] = get_cat_rates_reg(ctx_reg, _cat_feds_tb)
        _tb_row["France"] = get_cat_rates_nat(_cat_feds_tb)
        _tb_lic_rows.append(_tb_row)

    _tb_lic_df = pd.DataFrame(_tb_lic_rows)
    st.dataframe(_tb_lic_df, use_container_width=True, hide_index=True)

    st.markdown("---")

    # ── 2c. Data équipements ─────────────────────────────────────────────────
    st.markdown("### ⚽ Data équipements")
    st.caption("Taux d'équipements pour 1 000 hab. par famille — comparaison multi-niveaux")

    # Familles ciblées pour le tableau de bord
    _EQUIP_FAMILLES_TB = [
        ("**Total équipements**",                None),
        ("Équipements sportifs de base (4 types)", [
            "Court de tennis", "Salle multisports", "Terrain de grands jeux", "Salle de combat",
        ]),
        ("Courts de tennis",         ["Court de tennis"]),
        ("Salles multisports / Gymnases", ["Salle multisports"]),
        ("Terrains de grands jeux",  ["Terrain de grands jeux"]),
        ("Salles de combat",         ["Salle de combat"]),
    ]

    # Déterminer les noms exacts de colonnes FAM_COLS disponibles
    _fam_names_available = [c.replace("equip_", "") for c in FAM_COLS]

    def _get_equip_total_zone(fam_list):
        """Somme des équipements de la zone pour une liste de familles (noms partiels)."""
        if fam_list is None:
            return int(equip_zone[FAM_COLS].sum().sum()) if not equip_zone.empty else 0
        total_eq = 0
        for fam in fam_list:
            # Chercher la colonne correspondante (correspondance partielle)
            matches = [c for c in FAM_COLS if fam.lower() in c.lower()]
            for col in matches:
                total_eq += int(equip_zone[col].sum()) if col in equip_zone.columns else 0
        return total_eq

    def _get_equip_rate_dep(dep, fam_list):
        if fam_list is None:
            return round(sum(dep_equip_rates.get(dep, {}).values()), 2) if dep in dep_equip_rates else None
        total = 0
        for fam in fam_list:
            matches = [k for k in dep_equip_rates.get(dep, {}) if fam.lower() in k.lower()]
            total += sum(dep_equip_rates[dep].get(k, 0) for k in matches)
        return round(total, 2) if dep in dep_equip_rates else None

    def _get_equip_rate_reg(reg, fam_list):
        if fam_list is None:
            return round(sum(reg_equip_rates.get(reg, {}).values()), 2) if reg in reg_equip_rates else None
        total = 0
        for fam in fam_list:
            matches = [k for k in reg_equip_rates.get(reg, {}) if fam.lower() in k.lower()]
            total += sum(reg_equip_rates[reg].get(k, 0) for k in matches)
        return round(total, 2) if reg in reg_equip_rates else None

    def _get_equip_rate_nat(fam_list):
        if fam_list is None:
            return round(sum(nat_equip_rates.values()), 2) if nat_equip_rates else None
        total = sum(v for k, v in nat_equip_rates.items() if any(fam.lower() in k.lower() for fam in fam_list))
        return round(total, 2)

    _tb_equip_rows = []
    if not equip_zone.empty:
        _tb_equip_total = int(equip_zone[FAM_COLS].sum().sum())
        if (_is_comm or _is_epci) and pop_affichee > 0:
            _eq_strate_key = assign_strate(pop_affichee, STRATES_COMM if _is_comm else STRATES_EPCI)
            _eq_avgs_ref = equip_fam_comm_avgs if _is_comm else equip_fam_epci_avgs
        for _eq_label, _eq_fams in _EQUIP_FAMILLES_TB:
            _eq_zone_total = _get_equip_total_zone(_eq_fams)
            _tb_eq_row = {
                "Catégorie": _eq_label,
                "Territoire": round(_eq_zone_total / _tb_pop * 1000, 2) if pop_affichee > 0 else None,
            }
            if (_is_comm or _is_epci) and pop_affichee > 0:
                if _eq_fams is None:
                    _eq_strate_tot = sum(_eq_avgs_ref.get(_eq_strate_key, {}).values())
                    _tb_eq_row["Strate"] = round(_eq_strate_tot, 2) if _eq_strate_tot else None
                else:
                    _eq_strate_sum = 0.0
                    _eq_strate_avgs_dict = _eq_avgs_ref.get(_eq_strate_key, {})
                    for _eq_fam_search in _eq_fams:
                        for _eq_key in _eq_strate_avgs_dict:
                            if _eq_fam_search.lower() in _eq_key.lower():
                                _eq_strate_sum += _eq_strate_avgs_dict[_eq_key]
                    _tb_eq_row["Strate"] = round(_eq_strate_sum, 2) if _eq_strate_sum else None
            if _is_comm and pop_affichee > 0 and _tb_comm_epci_nom:
                _epci_equip_zone = equip_fam_df[equip_fam_df["code_commune"].isin(_tb_epci_codes)]
                _epci_eq_total = _get_equip_total_zone(_eq_fams) if equip_zone.empty else \
                    int(sum(_epci_equip_zone[col].sum() for col in FAM_COLS if col in _epci_equip_zone.columns)) \
                    if _eq_fams is None else \
                    int(sum(_epci_equip_zone[col].sum() for fam in _eq_fams
                            for col in FAM_COLS if fam.lower() in col.lower() and col in _epci_equip_zone.columns))
                _tb_eq_row["EPCI"] = round(_epci_eq_total / _tb_epci_pop_val * 1000, 2) if _tb_epci_pop_val > 0 else None
            if ctx_dep:
                _tb_eq_row["Dép."] = _get_equip_rate_dep(ctx_dep, _eq_fams)
            if ctx_reg:
                _tb_eq_row["Région"] = _get_equip_rate_reg(ctx_reg, _eq_fams)
            _tb_eq_row["France"] = _get_equip_rate_nat(_eq_fams)
            _tb_equip_rows.append(_tb_eq_row)

    _tb_equip_df = pd.DataFrame(_tb_equip_rows)
    st.dataframe(_tb_equip_df, use_container_width=True, hide_index=True)

    # ── Data clubs (taux de référence pour le PDF) ────────────────────────────
    _tb_clubs_row = {"Catégorie": "**Total**"}
    if ctx_dep and dep_clubs_rates.get(ctx_dep):
        _tb_clubs_row["Dép."] = round(sum(dep_clubs_rates[ctx_dep].values()), 2)
    if ctx_reg and reg_clubs_rates.get(ctx_reg):
        _tb_clubs_row["Région"] = round(sum(reg_clubs_rates[ctx_reg].values()), 2)
    if nat_clubs_rates:
        _tb_clubs_row["France"] = round(sum(nat_clubs_rates.values()), 2)
    _tb_clubs_df = pd.DataFrame([_tb_clubs_row])

    st.markdown("---")

    # ── 3. Besoins scolaires ─────────────────────────────────────────────────
    st.markdown("### 📐 Besoins scolaires en équipements sportifs")

    _mat  = safe_int(ec["Nombre d'élèves en pré-élémentaire hors ULIS"].sum()) if not ec.empty else 0
    _prim = safe_int(ec["Nombre d'élèves en élémentaire hors ULIS"].sum())     if not ec.empty else 0
    _col  = safe_int(co["nombre_eleves_total"].sum())                           if not co.empty else 0
    _lyc  = safe_int(ly["Nombre d'élèves"].sum())                               if not ly.empty else 0

    _gym30, _gym50, _ = calcul_gymnases(_mat, _prim, _col, _lyc)
    _m2bas, _m2haut, _m2moy = calcul_piscines_sco(_mat, _prim, _col, _lyc)  # conservé pour rubrique Piscines

    bg1, bg2 = st.columns(2)
    bg1.metric("🏋️ Gymnases (hypothèse 30 %)", f"{_gym30:.2f}",
               help="30 % des classes utilisent simultanément un gymnase (hypothèse basse)")
    bg2.metric("🏋️ Gymnases (hypothèse 50 %)", f"{_gym50:.2f}",
               help="50 % des classes utilisent simultanément un gymnase (hypothèse haute)")
    st.caption("👉 Les besoins en m² de plan d'eau sont détaillés dans la rubrique **Piscines** ci-dessous.")

    st.markdown("---")

    # ── 3b. Piscines ─────────────────────────────────────────────────────────
    st.markdown("### 🏊 Piscines — Offre actuelle & estimations de besoins")

    _pis_nat_tb, _pis_type_tb, _pis_total_tb = get_piscines_m2(codes)
    _pis_nat_detail_tb  = _pis_nat_tb[_pis_nat_tb["Nature du plan d'eau"] != "**TOTAL**"]
    _pis_type_detail_tb = _pis_type_tb[_pis_type_tb["Type de bassin"] != "**TOTAL**"]

    NATURE_ICONS_TB = {
        "Intérieur":         "🏗️",
        "Découvert":         "☀️",
        "Découvrable":       "🔄",
        "Extérieur couvert": "⛺",
        "Autres":            "🌿",
    }
    TYPE_ICONS_TB = {"Sportif": "🏊", "Ludique": "🎡", "Mixte": "🔀", "Exercice": "🤸"}

    # — Offre actuelle —
    st.markdown("##### Offre actuelle en m² de plan d'eau")
    if _pis_total_tb == 0:
        st.info("Aucun bassin de natation recensé pour ce territoire dans le RES.")
    else:
        # Par nature
        _ptcols = st.columns(min(len(_pis_nat_detail_tb) + 1, 6))
        for col_ui, (_, prow) in zip(_ptcols, _pis_nat_detail_tb.iterrows()):
            icon = NATURE_ICONS_TB.get(prow["Nature du plan d'eau"], "🏊")
            col_ui.metric(
                f"{icon} {prow['Nature du plan d\'eau']}",
                f"{prow['m² total']:,} m²".replace(",", " "),
                help=f"{prow['Bassins']} bassin(s)",
            )
        _ptcols[-1].metric(
            "🏊 Total m²",
            f"{_pis_total_tb:,} m²".replace(",", " "),
            delta=f"{_pis_total_tb / pop_affichee:.4f} m²/hab." if pop_affichee > 0 else None,
            delta_color="off",
        )
        # Par type de bassin
        st.markdown("###### Par type de bassin (sportif / ludique / mixte / exercice)")
        _type_cols = st.columns(min(len(_pis_type_detail_tb), 4))
        for col_ui, (_, trow) in zip(_type_cols, _pis_type_detail_tb.iterrows()):
            icon = TYPE_ICONS_TB.get(trow["Type de bassin"], "🏊")
            col_ui.metric(
                f"{icon} {trow['Type de bassin']}",
                f"{trow['m² total']:,} m²".replace(",", " "),
                help=f"{trow['Bassins']} bassin(s)",
            )

    st.markdown("##### Besoins estimés en m² de plan d'eau")

    # — Besoins scolaires —
    _bs_col1, _bs_col2, _bs_col3 = st.columns(3)
    _bs_col1.metric("📐 Besoins scolaires — Bas",   f"{_m2bas:,.0f} m²".replace(",", " "),
                    help="Modèle EPS hypothèse basse (30 % des classes)")
    _bs_col2.metric("📐 Besoins scolaires — Haut",  f"{_m2haut:,.0f} m²".replace(",", " "),
                    help="Modèle EPS hypothèse haute (50 % des classes)")
    # Moyen — code couleur vert/rouge selon comparaison avec bassin sportif actuel
    _ecart_sco_moy = _pis_total_tb - _m2moy
    _sco_bg   = "#e8f5e9" if _ecart_sco_moy >= 0 else "#ffebee"
    _sco_bord = "#27ae60" if _ecart_sco_moy >= 0 else "#c0392b"
    _sco_icon = "🟢"      if _ecart_sco_moy >= 0 else "🔴"
    _fmt = lambda v: f"{v:,.0f}".replace(",", "\u202f")
    _bs_col3.markdown(
        f"""<div style="background:{_sco_bg}; border:2px solid {_sco_bord};
            border-radius:10px; padding:14px 10px; text-align:center; min-height:150px;">
            <div style="font-size:0.75em; font-weight:700; color:#333; margin-bottom:6px;">
                📐 Besoins scolaires — Moyen</div>
            <div style="font-size:1.6em; font-weight:800; color:{_sco_bord};">
                {_fmt(_m2moy)} m²</div>
            <div style="font-size:0.7em; color:#555; margin-top:4px;">
                Bassin sportif actuel : {_fmt(_pis_total_tb)} m²<br>
                Écart : {_fmt(_ecart_sco_moy) if _ecart_sco_moy < 0 else '+' + _fmt(_ecart_sco_moy)} m²</div>
            <div style="font-size:1.3em; margin-top:8px;">{_sco_icon}</div>
            </div>""",
        unsafe_allow_html=True)

    # — Ratios par habitant — cartes colorées vert/rouge selon signe de l'écart
    if pop_affichee > 0:
        _m2_pop_tb = calcul_m2_piscines_pop(pop_affichee)
        _rp1, _rp2, _rp3 = st.columns(3)
        for _col_rp, _label, _key in [
            (_rp1, "0,016 m²/hab. — Hypothèse basse",   "Hypothèse basse"),
            (_rp2, "0,018 m²/hab. — Hypothèse moyenne",  "Hypothèse moyenne"),
            (_rp3, "0,020 m²/hab. — Hypothèse haute",   "Hypothèse haute"),
        ]:
            _ecart_rp = _pis_total_tb - _m2_pop_tb[_key]
            _rp_bg   = "#e8f5e9" if _ecart_rp >= 0 else "#ffebee"
            _rp_bord = "#27ae60" if _ecart_rp >= 0 else "#c0392b"
            _rp_icon = "🟢"      if _ecart_rp >= 0 else "🔴"
            _ecart_rp_str = ('+' + _fmt(_ecart_rp)) if _ecart_rp >= 0 else _fmt(_ecart_rp)
            _col_rp.markdown(
                f"""<div style="background:{_rp_bg}; border:2px solid {_rp_bord};
                    border-radius:10px; padding:14px 10px; text-align:center; min-height:160px;">
                    <div style="font-size:0.75em; font-weight:700; color:#333; margin-bottom:6px;">
                        👥 {_label}</div>
                    <div style="font-size:1.6em; font-weight:800; color:{_rp_bord};">
                        {_fmt(_m2_pop_tb[_key])} m²</div>
                    <div style="font-size:0.7em; color:#555; margin-top:4px;">
                        Offre actuelle : {_fmt(_pis_total_tb)} m²<br>
                        Écart : {_ecart_rp_str} m²</div>
                    <div style="font-size:1.3em; margin-top:8px;">{_rp_icon}</div>
                    </div>""",
                unsafe_allow_html=True)

    st.caption(
        "Offre actuelle : RES 2023 — Besoins scolaires : modèle EPS (majoration 20 %) — "
        "Ratios par habitant : référentiel national (0,016 / 0,018 / 0,020 m²/hab.)"
    )
    st.markdown("---")

    # ── 4. Besoins clubs ─────────────────────────────────────────────────────
    st.markdown("### 🏟️ Besoins des clubs sportifs — Offre actuelle et écart")
    st.caption(LEGENDE_ECART.replace("> ", ""))

    if pop_affichee > 0 and not lic_zone.empty:
        df_tb_synth, _ = calcul_besoins_lic(lic_zone, clubs_zone, equip_zone)

        def color_ecart_tb(val):
            if isinstance(val, (int, float)):
                if val < 0:   return "color: #c0392b; font-weight:bold"   # déficit
                elif val > 0: return "color: #27ae60; font-weight:bold"   # excédent
            return ""

        styled_tb = df_tb_synth.style.map(color_ecart_tb, subset=["Écart"])
        st.dataframe(styled_tb, use_container_width=True, hide_index=True)

        # Synthèse visuelle
        st.markdown("#### Synthèse visuelle par type d'équipement")
        _cols = st.columns(len(MODELE_EQUIP))
        for col_ui, row in zip(_cols, df_tb_synth.itertuples()):
            ecart_val = row.Écart
            if ecart_val < 0:    # déficit
                bg_color, border, icon = "#ffebee", "#c0392b", "🔴"
            elif ecart_val > 0:  # excédent
                bg_color, border, icon = "#e8f5e9", "#27ae60", "🟢"
            else:
                bg_color, border, icon = "#f5f5f5", "#9e9e9e", "⚪"

            col_ui.markdown(
                f"""<div style="background:{bg_color}; border:2px solid {border};
                border-radius:10px; padding:14px 10px; text-align:center; min-height:160px;">
                <div style="font-size:0.75em; font-weight:700; color:#333; margin-bottom:6px;">
                    {row._1}</div>
                <div style="font-size:1.6em; font-weight:800; color:{border};">
                    {ecart_val:+.1f}</div>
                <div style="font-size:0.7em; color:#555; margin-top:4px;">
                    Besoin: {row._4:.1f}<br>Actuel: {row._5}</div>
                <div style="font-size:1.3em; margin-top:8px;">{icon}</div>
                </div>""",
                unsafe_allow_html=True)
    else:
        st.info("Données insuffisantes pour calculer les besoins des clubs.")

    # ── Export ───────────────────────────────────────────────────────────────
    st.markdown("---")
    st.markdown("#### 📥 Exporter le tableau de bord complet")

    if pop_affichee > 0:
        m2_pop_exp = calcul_m2_piscines_pop(pop_affichee)
        tb_export = {
            "Indicateur": [
                "Territoire", "Niveau",
                "Population",
                "Licenciés", "Taux licenciés pour 1 000 hab.",
                "Clubs", "Taux clubs pour 1 000 hab.",
                "Équipements sportifs", "Taux équipements pour 1 000 hab.",
                "Scolaires total (2024)", "dont Maternelle", "dont Primaire",
                "dont Collège", "dont Lycée GT",
                "Besoins gymnases (hyp. 30%)", "Besoins gymnases (hyp. 50%)",
                "Besoins m² piscines scolaires (bas)",
                "Besoins m² piscines scolaires (haut)",
                "Besoins m² piscines scolaires (moy.)",
                "m² plan d'eau 0,016 m²/hab. (basse)",
                "m² plan d'eau 0,018 m²/hab. (moyenne)",
                "m² plan d'eau 0,020 m²/hab. (haute)",
            ],
            "Valeur": [
                titre_zone,
                niveau.replace("🌍 ", "").replace("📍 ", "").replace("🏛️ ", "").replace("🏘️ ", ""),
                pop_affichee,
                nb_lic, rate_str(lic_pour_1000),
                nb_clubs, rate_str(clubs_pour_1000),
                nb_equip, rate_str(equip_pour_1000),
                sc["total"], sc["maternelle"], sc["primaire"], sc["t_co"], sc["t_ly"],
                _gym30, _gym50,
                _m2bas, _m2haut, _m2moy,
                m2_pop_exp["Hypothèse basse"],
                m2_pop_exp["Hypothèse moyenne"],
                m2_pop_exp["Hypothèse haute"],
            ],
        }

        if pop_affichee > 0 and not lic_zone.empty:
            df_tb_s, _ = calcul_besoins_lic(lic_zone, clubs_zone, equip_zone)
            for _, r in df_tb_s.iterrows():
                cat = r["Catégorie d'équipements"]
                tb_export["Indicateur"] += [
                    f"Besoins {cat} — consolidé",
                    f"Actuels {cat}",
                    f"Écart {cat}",
                ]
                tb_export["Valeur"] += [r["Besoins consolidés"], r["Équipements actuels"], r["Écart"]]

        df_tb_csv = pd.DataFrame(tb_export)
        _pdf_col1, _pdf_col2 = st.columns(2)
        _pdf_col1.download_button(
            "⬇️ Tableau de bord complet (CSV)",
            data=df_tb_csv.to_csv(index=False, sep=";", encoding="utf-8-sig").encode("utf-8-sig"),
            file_name=f"tableau_de_bord_{zone_slug}.csv",
            mime="text/csv",
        )
        with _pdf_col2:
            if st.button("📄 Générer le PDF du tableau de bord"):
                with st.spinner("Génération du PDF en cours…"):
                    try:
                        _df_synth_pdf = None
                        if not lic_zone.empty:
                            _df_synth_pdf, _ = calcul_besoins_lic(lic_zone, clubs_zone, equip_zone)
                        _m2_pop_pdf = calcul_m2_piscines_pop(pop_affichee) if pop_affichee > 0 else {}
                        # Données strate (communes / EPCI uniquement)
                        _pdf_strate_lbl = _pdf_strate_lic = _pdf_strate_clubs = _pdf_strate_equip = None
                        if (_is_comm or _is_epci) and pop_affichee > 0:
                            _pdf_strates_ref = STRATES_COMM if _is_comm else STRATES_EPCI
                            _pdf_avgs_ref    = comm_strates_avgs if _is_comm else epci_strates_avgs
                            _pdf_strate_lbl  = assign_strate(pop_affichee, _pdf_strates_ref)
                            _pdf_strate_lic   = _pdf_avgs_ref.get("lic",   {}).get(_pdf_strate_lbl)
                            _pdf_strate_clubs = _pdf_avgs_ref.get("clubs", {}).get(_pdf_strate_lbl)
                            _pdf_strate_equip = _pdf_avgs_ref.get("equip", {}).get(_pdf_strate_lbl)
                        _pdf_bytes = generer_pdf_tableau_bord(
                            titre_zone=titre_zone,
                            niveau=niveau,
                            pop_affichee=pop_affichee,
                            nb_lic=nb_lic,
                            nb_clubs=nb_clubs,
                            nb_equip=nb_equip,
                            lic_pour_1000=lic_pour_1000,
                            clubs_pour_1000=clubs_pour_1000,
                            equip_pour_1000=equip_pour_1000,
                            sc=sc,
                            tb_lic_df=_tb_lic_df,
                            tb_equip_df=_tb_equip_df,
                            tb_clubs_df=_tb_clubs_df,
                            gym30=_gym30,
                            gym50=_gym50,
                            m2bas=_m2bas,
                            m2haut=_m2haut,
                            m2moy=_m2moy,
                            pis_total_m2=_pis_total_tb,
                            m2_pop=_m2_pop_pdf,
                            df_synth=_df_synth_pdf,
                            logo_b64=_logo_b64 or None,
                            strate_lbl=_pdf_strate_lbl,
                            strate_lic=_pdf_strate_lic,
                            strate_clubs=_pdf_strate_clubs,
                            strate_equip=_pdf_strate_equip,
                        )
                        st.download_button(
                            "⬇️ Télécharger le PDF",
                            data=_pdf_bytes,
                            file_name=f"tableau_de_bord_{zone_slug}.pdf",
                            mime="application/pdf",
                        )
                    except Exception as _pdf_err:
                        st.error(f"Erreur lors de la génération du PDF : {_pdf_err}")

# ─────────────────────────────────────────────────────────────────────────────
# EXPORTS GÉNÉRAUX
# ─────────────────────────────────────────────────────────────────────────────
st.divider()
st.subheader("📥 Exports de données brutes")
e1, e2, e3 = st.columns(3)


def to_csv(df):
    return df.to_csv(index=False, sep=";", encoding="utf-8-sig").encode("utf-8-sig")


with e1:
    if not lic_zone.empty and lic_zone["total"].sum() > 0:
        agg_e = (lic_zone.groupby("federation")["total"].sum().reset_index()
                 .rename(columns={"federation": "Fédération", "total": "Licenciés"})
                 .sort_values("Licenciés", ascending=False))
        st.download_button("⬇️ Licenciés (CSV)", data=to_csv(agg_e),
                           file_name=f"licencies_{zone_slug}.csv", mime="text/csv")
with e2:
    if not clubs_zone.empty and clubs_zone["total"].sum() > 0:
        agg_e = (clubs_zone.groupby("federation")["total"].sum().reset_index()
                 .rename(columns={"federation": "Fédération", "total": "Clubs"})
                 .sort_values("Clubs", ascending=False))
        st.download_button("⬇️ Clubs (CSV)", data=to_csv(agg_e),
                           file_name=f"clubs_{zone_slug}.csv", mime="text/csv")
with e3:
    if not equip_zone.empty and nb_equip > 0:
        fam_sums = equip_zone[FAM_COLS].sum()
        fam_df_e = (fam_sums[fam_sums > 0].reset_index()
                    .rename(columns={"index": "Famille", 0: "Équipements"})
                    .sort_values("Équipements", ascending=False))
        fam_df_e["Famille"] = fam_df_e["Famille"].str.replace("equip_", "", regex=False)
        st.download_button("⬇️ Équipements (CSV)", data=to_csv(fam_df_e),
                           file_name=f"equipements_{zone_slug}.csv", mime="text/csv")

# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 8 — SportData — Strate & population
# ═══════════════════════════════════════════════════════════════════════════════
with tab_strate:
    st.markdown(
        """<div style="background: linear-gradient(135deg, #1b5e20 0%, #2e7d32 100%);
        padding: 18px 24px; border-radius: 10px; margin-bottom: 18px;">
        <h2 style="color: white; margin: 0; font-size: 1.5em; font-weight: 800;">
            📈 SportData — Strate &amp; population
        </h2>
        <p style="color: #c8e6c9; margin: 4px 0 0 0; font-size: 0.9em;">
        Taux pour 1 000 hab. par fédération / famille d'équipement — national et par strate de territoire</p>
        </div>""",
        unsafe_allow_html=True)

    # ── Données nationales de référence ───────────────────────────────────────
    # Population nationale : somme des populations par strate commune
    # (exactement le même dénominateur que celui utilisé dans les calculs par strate,
    # pour garantir que le taux national est une moyenne pondérée des taux de strate)
    _nat_pop_comm = float(sum(_pop_par_strate_comm.values()))
    _nat_pop      = _nat_pop_comm if _nat_pop_comm > 0 else float(comm_df[comm_df["population"] > 0]["population"].sum())
    _nat_lic   = lic_df.groupby("federation")["total"].sum().to_dict()
    _nat_clubs = clubs_df.groupby("federation")["total"].sum().to_dict()
    _nat_equip = {
        col.replace("equip_", ""): float(equip_fam_df[col].sum())
        for col in FAM_COLS
    } if not equip_fam_df.empty else {}

    # Libellés courts pour les colonnes strates
    _COMM_LABELS = {s[2]: s[2] for s in STRATES_COMM}
    _EPCI_LABELS = {s[2]: f"EPCI {s[2]}" for s in STRATES_EPCI}

    st.caption(
        "Taux calculés sur la somme des licenciés (ou clubs / équipements) "
        "divisée par la somme des populations, pour chaque strate."
    )

    # ── Helper : construire un tableau strate ─────────────────────────────────
    def build_strate_table(items, nat_dict, comm_avgs, epci_avgs, value_col):
        """
        items      : liste de libellés (fédérations ou familles)
        nat_dict   : {libellé: total_national}
        comm_avgs  : {strate_comm: {libellé: taux/1000}}
        epci_avgs  : {strate_epci: {libellé: taux/1000}}
        value_col  : nom de la colonne valeur ("Licenciés", "Clubs", "Équipements")
        """
        rows = []
        for item in items:
            nat_total = nat_dict.get(item, 0)
            nat_rate  = round(nat_total / _nat_pop * 1000, 2) if _nat_pop > 0 else None
            row = {
                "Fédération / Famille": item,
                f"{value_col} national": int(nat_total),
                "/ 1 000 hab. (national)": nat_rate,
            }
            for s_lbl in [s[2] for s in STRATES_COMM]:
                row[s_lbl] = comm_avgs.get(s_lbl, {}).get(item)
            for s_lbl in [s[2] for s in STRATES_EPCI]:
                row[f"EPCI {s_lbl}"] = epci_avgs.get(s_lbl, {}).get(item)
            rows.append(row)
        df_out = pd.DataFrame(rows)
        # Trier par total national décroissant
        df_out = df_out.sort_values(f"{value_col} national", ascending=False).reset_index(drop=True)
        return df_out

    def style_strate(df):
        """Met en relief la colonne nationale et grise les NaN."""
        return df.style.format(
            {c: "{:.2f}" for c in df.columns if c not in
             ["Fédération / Famille"] and df[c].dtype in ["float64", "float32"]},
            na_rep="—"
        ).set_properties(
            subset=[c for c in df.columns if "national" in c.lower()],
            **{"background-color": "#e8f5e9", "font-weight": "bold"}
        )

    # ── 1. Licenciés ──────────────────────────────────────────────────────────
    st.markdown("### 🎽 Licenciés — taux pour 1 000 hab. par strate")
    _all_feds_lic = sorted(lic_df["federation"].unique().tolist())
    _df_lic_strate = build_strate_table(
        _all_feds_lic, _nat_lic,
        lic_fed_comm_avgs, lic_fed_epci_avgs,
        "Licenciés"
    )
    st.markdown(
        "**Communes :** "
        + " · ".join(f"*{s[2]}*" for s in STRATES_COMM)
        + "  —  **EPCI :** "
        + " · ".join(f"*EPCI {s[2]}*" for s in STRATES_EPCI)
    )
    st.dataframe(style_strate(_df_lic_strate), use_container_width=True, hide_index=True)
    st.download_button(
        "⬇️ Télécharger (CSV)", data=_df_lic_strate.to_csv(index=False, sep=";", decimal=",",
        encoding="utf-8-sig").encode("utf-8-sig"),
        file_name="licencies_strates.csv", mime="text/csv", key="dl_lic_strate"
    )

    st.markdown("---")

    # ── 2. Clubs ──────────────────────────────────────────────────────────────
    st.markdown("### 🏟️ Clubs — taux pour 1 000 hab. par strate")
    _all_feds_clubs = sorted(clubs_df["federation"].unique().tolist())
    _df_clubs_strate = build_strate_table(
        _all_feds_clubs, _nat_clubs,
        clubs_fed_comm_avgs, clubs_fed_epci_avgs,
        "Clubs"
    )
    st.markdown(
        "**Communes :** "
        + " · ".join(f"*{s[2]}*" for s in STRATES_COMM)
        + "  —  **EPCI :** "
        + " · ".join(f"*EPCI {s[2]}*" for s in STRATES_EPCI)
    )
    st.dataframe(style_strate(_df_clubs_strate), use_container_width=True, hide_index=True)
    st.download_button(
        "⬇️ Télécharger (CSV)", data=_df_clubs_strate.to_csv(index=False, sep=";", decimal=",",
        encoding="utf-8-sig").encode("utf-8-sig"),
        file_name="clubs_strates.csv", mime="text/csv", key="dl_clubs_strate"
    )

    st.markdown("---")

    # ── 3. Équipements ────────────────────────────────────────────────────────
    st.markdown("### ⚽ Équipements sportifs — taux pour 1 000 hab. par strate")
    _all_fams = sorted(_nat_equip.keys())
    _df_equip_strate = build_strate_table(
        _all_fams, _nat_equip,
        equip_fam_comm_avgs, equip_fam_epci_avgs,
        "Équipements"
    )
    st.markdown(
        "**Communes :** "
        + " · ".join(f"*{s[2]}*" for s in STRATES_COMM)
        + "  —  **EPCI :** "
        + " · ".join(f"*EPCI {s[2]}*" for s in STRATES_EPCI)
    )
    st.dataframe(style_strate(_df_equip_strate), use_container_width=True, hide_index=True)
    st.download_button(
        "⬇️ Télécharger (CSV)", data=_df_equip_strate.to_csv(index=False, sep=";", decimal=",",
        encoding="utf-8-sig").encode("utf-8-sig"),
        file_name="equipements_strates.csv", mime="text/csv", key="dl_equip_strate"
    )


# ─────────────────────────────────────────────────────────────────────────────
# BESOINS THÉORIQUES PAR EPCI — piscines + clubs + gymnases scolaires (cached)
# ─────────────────────────────────────────────────────────────────────────────
@st.cache_data(show_spinner=False)
def _build_epci_besoins(_lic_df, _piscines_df, _eq_fam, _ec_df, _co_df, _ly_df, _ref):
    """
    Pré-calcule pour chaque EPCI :
      piscines  : m² offre, m²/hab, écarts (0.016 / 0.018 / 0.020 + scolaires moy)
      clubs     : écart = actuel − besoin théorique (4 catégories MODELE_EQUIP)
      scolaires : gym30, gym50, écarts gymnases scolaires
    """

    def _ce(x):
        s = str(x).strip()
        if s in ("nan", "NaN", "", "ZZZZZZZZZ", "0"):
            return None
        try:
            return str(int(float(s)))
        except (ValueError, OverflowError):
            return None

    # ── Mapping commune INSEE → EPCI ──────────────────────────────────────────
    ref_base = _ref[["code_insee", "epci_code", "population"]].copy()
    ref_base["code_insee"] = ref_base["code_insee"].astype(str).str.strip()
    ref_base["epci_code"]  = ref_base["epci_code"].apply(_ce)
    ref_base = ref_base.dropna(subset=["epci_code"])
    insee_epci = ref_base.set_index("code_insee")["epci_code"].to_dict()

    # ── Mapping code postal → EPCI (pour écoles) ─────────────────────────────
    ref_cp = pd.read_excel(
        SCOLAIRES_DIR + "communes-france-2025.xlsx", header=1,
        usecols=["code_postal", "epci_code"],
    )
    ref_cp["code_postal"] = ref_cp["code_postal"].apply(
        lambda x: str(int(float(x))).zfill(5) if pd.notna(x) else None
    )
    ref_cp["epci_code"] = ref_cp["epci_code"].apply(_ce)
    ref_cp = ref_cp.dropna(subset=["code_postal", "epci_code"])
    cp_epci = ref_cp.drop_duplicates("code_postal").set_index("code_postal")["epci_code"].to_dict()

    # ── Population par EPCI ───────────────────────────────────────────────────
    epci_pop = ref_base.groupby("epci_code")["population"].sum().to_dict()
    all_epcis = sorted(epci_pop.keys())

    # ── 1. PISCINES ──────────────────────────────────────────────────────────
    pis = _piscines_df[["code", "equip_bassin_surf"]].copy()
    pis["epci"] = pis["code"].map(insee_epci)
    pis_epci_s = pis.dropna(subset=["epci"]).groupby("epci")["equip_bassin_surf"].sum()

    # ── 2. EFFECTIFS SCOLAIRES PAR EPCI ──────────────────────────────────────
    _MAT  = "Nombre d'élèves en pré-élémentaire hors ULIS"
    _PRIM = "Nombre d'élèves en élémentaire hors ULIS"
    _COL  = "nombre_eleves_total"
    _LYC  = "Nombre d'élèves"

    ec2 = _ec_df.copy()
    ec2["_epci"] = ec2["Code Postal"].astype(str).str.strip().str.zfill(5).map(cp_epci)
    co2 = _co_df.copy()
    co2["_epci"] = co2["Code commune"].astype(str).str.strip().str.zfill(5).map(insee_epci)
    ly2 = _ly_df.copy()
    ly2["_epci"] = ly2["Code commune"].astype(str).str.strip().str.zfill(5).map(insee_epci)

    def _agg_col(df, grp_col, val_col):
        if val_col not in df.columns:
            return pd.Series(dtype=float)
        return df.dropna(subset=[grp_col]).groupby(grp_col)[val_col].sum()

    mat_s  = _agg_col(ec2, "_epci", _MAT)
    prim_s = _agg_col(ec2, "_epci", _PRIM)
    col_s  = _agg_col(co2, "_epci", _COL)
    lyc_s  = _agg_col(ly2, "_epci", _LYC)

    # ── 3. LICENCIÉS PAR EPCI × FÉDÉRATION ───────────────────────────────────
    lic2 = _lic_df.copy()
    lic2["epci"] = lic2["code"].map(insee_epci)
    lic_fed = lic2.dropna(subset=["epci"]).groupby(["epci", "federation"])["total"].sum()
    # → pivot wide : index=epci, columns=federation
    lic_pivot = lic_fed.unstack(fill_value=0)

    # ── 4. ÉQUIPEMENTS ACTUELS PAR EPCI ──────────────────────────────────────
    eq2 = _eq_fam.copy()
    eq2["epci"] = eq2["code_commune"].astype(str).str.strip().map(insee_epci)
    fam_cols = [c for c in eq2.columns if c.startswith("equip_")]
    eq_pivot = eq2.dropna(subset=["epci"]).groupby("epci")[fam_cols].sum()

    # ── Construction du résultat ───────────────────────────────────────────────
    result = pd.DataFrame({"epci_code": all_epcis})
    idx = result["epci_code"]  # Series pour mapping

    # Population
    result["population"] = idx.map(epci_pop).fillna(0)

    # Piscines
    result["pis_m2"]     = idx.map(pis_epci_s).fillna(0)
    result["pis_m2_hab"] = result.apply(
        lambda r: round(r["pis_m2"] / r["population"], 4) if r["population"] > 0 else None,
        axis=1,
    )
    for _lbl, _ratio in RATIOS_M2:
        _key = f"pis_e{int(_ratio * 1000):03d}"
        result[_key] = result.apply(
            lambda r, ra=_ratio: round(r["pis_m2"] - r["population"] * ra)
            if r["population"] > 0 else None,
            axis=1,
        )

    # Scolaires per EPCI → piscines scolaires + gymnases
    result["_mat"]  = idx.map(mat_s).fillna(0)
    result["_prim"] = idx.map(prim_s).fillna(0)
    result["_col"]  = idx.map(col_s).fillna(0)
    result["_lyc"]  = idx.map(lyc_s).fillna(0)

    def _gym_row(row):
        g30, g50, _ = calcul_gymnases(row["_mat"], row["_prim"], row["_col"], row["_lyc"])
        return pd.Series({"gym30": g30, "gym50": g50})

    def _pis_sco_row(row):
        _, _, moy = calcul_piscines_sco(row["_mat"], row["_prim"], row["_col"], row["_lyc"])
        return moy

    if result[["_mat", "_prim", "_col", "_lyc"]].sum().sum() > 0:
        gym_calc = result.apply(_gym_row, axis=1)
        result["gym30_besoin"] = gym_calc["gym30"]
        result["gym50_besoin"] = gym_calc["gym50"]
        result["pis_sco_besoin"] = result.apply(_pis_sco_row, axis=1)
    else:
        result["gym30_besoin"] = result["gym50_besoin"] = result["pis_sco_besoin"] = np.nan

    # Gymnases actuels (Salle multisports) par EPCI
    _gym_col = "equip_Salle multisports"
    result["gym_actuel"] = idx.map(
        eq_pivot[_gym_col] if _gym_col in eq_pivot.columns else pd.Series(dtype=float)
    ).fillna(0)

    result["gym30_ecart"] = (result["gym_actuel"] - result["gym30_besoin"]).round(2)
    result["gym50_ecart"] = (result["gym_actuel"] - result["gym50_besoin"]).round(2)
    result["pis_sco_ecart"] = (result["pis_m2"] - result["pis_sco_besoin"]).round(0)

    # Besoins clubs (MODELE_EQUIP) — écart = actuel − besoin théorique
    for cat in MODELE_EQUIP:
        eq_col   = cat["equip_col"]
        coeff    = cat["coefficient"]
        safe_k   = "bl_" + cat["equipement"].replace(" / ", "_").replace(" ", "_")

        besoin_s = pd.Series(0.0, index=result.index)
        for d in cat["disciplines"]:
            fed     = d["federation"]
            G, H, I, J = d["effectif"], d["entrainements"], d["duree"], d["dispo"]
            factor  = coeff / G * H * I / J if (G > 0 and J > 0) else 0.0
            fed_row = lic_pivot[fed] if fed in lic_pivot.columns else pd.Series(0, index=lic_pivot.index)
            besoin_s += idx.map(fed_row).fillna(0) * factor

        actuel_s = idx.map(
            eq_pivot[eq_col] if eq_col in eq_pivot.columns else pd.Series(dtype=float)
        ).fillna(0)

        result[safe_k] = (actuel_s - besoin_s).round(1)

    # Nettoyage colonnes de travail
    result = result.drop(columns=["_mat", "_prim", "_col", "_lyc"])
    return result


# ─────────────────────────────────────────────────────────────────────────────
# INDICATEURS DÉTAILLÉS PAR EPCI — équipements par famille (cached)
# ─────────────────────────────────────────────────────────────────────────────
@st.cache_data(show_spinner=False)
def _build_epci_detail(_eq_fam, _ref):
    """
    Agrège equip_fam_df (commune) → EPCI via communes_ref.
    Retourne :
      epci_eq   : DataFrame EPCI × familles (count + rate_*)
      nat_rates : {famille: taux_nat/1000}
      main_fams : [(famille, taux_nat)] triés desc, filtrés > 0.05 ‰
    """

    def _ce(x):
        s = str(x).strip()
        if s in ("nan", "NaN", "", "ZZZZZZZZZ", "0"):
            return None
        try:
            return str(int(float(s)))
        except (ValueError, OverflowError):
            return None

    fam_cols = [c for c in _eq_fam.columns if c.startswith("equip_")]

    # Jointure commune → epci_code
    ref2 = _ref[["code_insee", "epci_code"]].copy()
    ref2["code_insee"] = ref2["code_insee"].astype(str).str.strip()
    ref2["epci_code"]  = ref2["epci_code"].apply(_ce)
    ref2 = ref2.dropna(subset=["epci_code"])

    eq = _eq_fam.copy()
    eq["code_commune"] = eq["code_commune"].astype(str).str.strip()
    eq = eq.merge(ref2, left_on="code_commune", right_on="code_insee", how="inner")

    # Agrégation par EPCI
    agg = {"population": "sum"}
    for c in fam_cols:
        agg[c] = "sum"
    epci_eq = eq.groupby("epci_code").agg(agg).reset_index()

    # Taux pour 1 000 hab par famille
    for c in fam_cols:
        fname = c.replace("equip_", "")
        epci_eq[f"r_{fname}"] = np.where(
            epci_eq["population"] > 0,
            epci_eq[c] / epci_eq["population"] * 1000,
            np.nan,
        )

    # Moyennes nationales pondérées
    nat_pop = float(epci_eq["population"].sum())
    nat_rates = {}
    for c in fam_cols:
        fname = c.replace("equip_", "")
        nat_rates[fname] = (float(epci_eq[c].sum()) / nat_pop * 1000) if nat_pop > 0 else 0.0

    # Familles significatives (taux national > 0.05 ‰), triées desc
    main_fams = sorted(
        [(fname, r) for fname, r in nat_rates.items() if r > 0.05],
        key=lambda x: x[1],
        reverse=True,
    )
    return epci_eq, nat_rates, main_fams


# ─────────────────────────────────────────────────────────────────────────────
# INDICATEURS PRINCIPAUX PAR EPCI — depuis les sources primaires (cached)
# ─────────────────────────────────────────────────────────────────────────────
@st.cache_data(show_spinner=False)
def _build_epci_main(_lic_df, _clubs_df, _eq_fam, _ref):
    """
    Calcule licenciés / clubs / équipements par EPCI depuis lic_df, clubs_df,
    equip_fam_df — cohérent avec les indicateurs du panneau principal.
    """
    def _ce(x):
        s = str(x).strip()
        if s in ("nan", "NaN", "", "ZZZZZZZZZ", "0"):
            return None
        try:
            return str(int(float(s)))
        except (ValueError, OverflowError):
            return None

    # Mapping commune INSEE → EPCI (depuis communes_ref)
    # norm_code : zfill(5) pour les codes purement numériques ("1001" → "01001")
    def _nc(v):
        s = str(v).strip().split(".")[0]
        return s.zfill(5) if s.isdigit() else s

    ref2 = _ref[["code_insee", "epci_code"]].copy()
    ref2["code_insee"] = ref2["code_insee"].apply(_nc)
    ref2["epci_code"]  = ref2["epci_code"].apply(_ce)
    ref2 = ref2.dropna(subset=["epci_code"]).drop_duplicates("code_insee")

    # Population par EPCI
    pop_df = _eq_fam[["code_commune", "population"]].copy()
    pop_df["code_commune"] = pop_df["code_commune"].astype(str).str.strip()
    pop_epci = (
        pop_df.merge(ref2, left_on="code_commune", right_on="code_insee", how="inner")
        .groupby("epci_code")["population"].sum().reset_index()
    )

    # Licenciés par EPCI
    lic = _lic_df.copy()
    lic["code"] = lic["code"].astype(str).str.strip()
    lic_epci = (
        lic.merge(ref2, left_on="code", right_on="code_insee", how="inner")
        .groupby("epci_code")["total"].sum().reset_index()
        .rename(columns={"total": "nb_licencies"})
    )

    # Clubs par EPCI
    clubs = _clubs_df.copy()
    clubs["code"] = clubs["code"].astype(str).str.strip()
    clubs_epci = (
        clubs.merge(ref2, left_on="code", right_on="code_insee", how="inner")
        .groupby("epci_code")["total"].sum().reset_index()
        .rename(columns={"total": "nb_clubs"})
    )

    # Équipements totaux par EPCI
    fam_cols = [c for c in _eq_fam.columns if c.startswith("equip_")]
    eq = _eq_fam.copy()
    eq["code_commune"] = eq["code_commune"].astype(str).str.strip()
    eq = eq.merge(ref2, left_on="code_commune", right_on="code_insee", how="inner")
    eq["_total_equip"] = eq[fam_cols].sum(axis=1)
    equip_epci = (
        eq.groupby("epci_code")["_total_equip"].sum().reset_index()
        .rename(columns={"_total_equip": "nb_equipements"})
    )

    # Assemblage
    result = pop_epci.copy()
    for df in [lic_epci, clubs_epci, equip_epci]:
        result = result.merge(df, on="epci_code", how="left")
    result = result.fillna(0)

    # Taux pour 1 000 hab
    for col_raw, col_rate in [
        ("nb_licencies",   "licencies_pour_1000_hab"),
        ("nb_clubs",       "clubs_pour_1000_hab"),
        ("nb_equipements", "equip_pour_1000_hab"),
    ]:
        result[col_rate] = np.where(
            result["population"] > 0,
            result[col_raw] / result["population"] * 1000,
            np.nan,
        )

    result["epci_code"] = result["epci_code"].astype(str).str.strip()
    return result


# ═══════════════════════════════════════════════════════════════════════════════
# ONGLET 9 — CARTES
# ═══════════════════════════════════════════════════════════════════════════════
with tab_carte:
    st.markdown(
        """<div style="background: linear-gradient(135deg, #0d47a1 0%, #1565c0 100%);
        padding: 18px 24px; border-radius: 10px; margin-bottom: 18px;">
        <h2 style="color: white; margin: 0; font-size: 1.5em; font-weight: 800;">
            🗺️ Cartes SportData — Indicateurs territoriaux
        </h2>
        <p style="color: #bbdefb; margin: 4px 0 0 0; font-size: 0.9em;">
        Choroplèthe EPCI — survolez pour le résumé, cliquez pour le détail complet</p>
        </div>""",
        unsafe_allow_html=True,
    )

    # Vérification bibliothèques
    try:
        import geopandas as _gpd          # noqa
        import folium as _folium          # noqa
        import json as _json_mod
        import streamlit.components.v1 as _cmp
    except ImportError as _ie:
        st.error(f"Bibliothèque cartographique manquante : {_ie}")
        st.code("pip install geopandas folium")
        st.stop()

    # ── Synchronisation avec la navigation principale ─────────────────────────
    # Quand le territoire sélectionné dans le menu principal change, on met à
    # jour automatiquement les sélecteurs de la carte.
    _ctx_dep_c = ctx_dep if ctx_dep else ""
    _ctx_reg_c = ctx_reg if ctx_reg else ""

    # Niveau carte suggéré par la navigation principale
    _ctx_niv_default = (
        "Département" if _ctx_dep_c and niveau not in ("🇫🇷 France",)
        else "Région"  if _ctx_reg_c and niveau == "🌍 Région"
        else "France entière"
    )
    # Région déduite du département si ctx_reg non fourni
    _ctx_reg_guess = _ctx_reg_c or dep_region_map.get(_ctx_dep_c, "")

    # Signature du contexte : si elle change, on force la mise à jour des widgets
    _ctx_sig_c = f"{_ctx_dep_c}|{_ctx_reg_guess}|{niveau}"
    if st.session_state.get("_carte_ctx_sig") != _ctx_sig_c:
        st.session_state["_carte_ctx_sig"] = _ctx_sig_c
        st.session_state["carte_niv"] = _ctx_niv_default
        if _ctx_dep_c:
            st.session_state["carte_dep"] = _ctx_dep_c
        if _ctx_reg_guess:
            st.session_state["carte_reg"] = _ctx_reg_guess

    # ── Contrôles ─────────────────────────────────────────────────────────────
    _cc1, _cc2, _cc3 = st.columns([1, 1.5, 1.5])
    with _cc1:
        _niv = st.selectbox(
            "Niveau géographique",
            ["Département", "Région", "France entière"],
            key="carte_niv",
        )
    with _cc2:
        _dep_sel = _reg_sel = None
        if _niv == "Département":
            _dep_list = sorted(communes_ref["dep_str"].dropna().unique().tolist())
            # Index par défaut = département du contexte principal
            _dep_idx_default = 0
            if _ctx_dep_c and _ctx_dep_c in _dep_list:
                _dep_idx_default = _dep_list.index(_ctx_dep_c)
            _dep_sel = st.selectbox(
                "Département",
                _dep_list,
                index=_dep_idx_default,
                format_func=lambda d: f"{d} — {dep_names.get(d, d)}",
                key="carte_dep",
            )
        elif _niv == "Région":
            _reg_col_c = next(
                (c for c in communes_ref.columns if "reg_nom" in c.lower()), None
            )
            _reg_list = (
                sorted(communes_ref[_reg_col_c].dropna().unique().tolist())
                if _reg_col_c else []
            )
            # Index par défaut = région du contexte principal
            _reg_idx_default = 0
            if _ctx_reg_guess and _ctx_reg_guess in _reg_list:
                _reg_idx_default = _reg_list.index(_ctx_reg_guess)
            _reg_sel = st.selectbox(
                "Région", _reg_list,
                index=_reg_idx_default,
                key="carte_reg",
            )
        else:
            st.caption("Affichage France entière (~1 200 EPCI)")
    with _cc3:
        _ind_lbl = st.selectbox(
            "Indicateur de couleur",
            ["Licenciés / 1 000 hab.", "Clubs / 1 000 hab.", "Équipements / 1 000 hab."],
            key="carte_ind",
        )

    _IND_MAP = {
        "Licenciés / 1 000 hab.":   "licencies_pour_1000_hab",
        "Clubs / 1 000 hab.":       "clubs_pour_1000_hab",
        "Équipements / 1 000 hab.": "equip_pour_1000_hab",
    }
    _ind_col_c = _IND_MAP[_ind_lbl]
    _ind_short = _ind_lbl.split("/")[0].strip()

    # ── Palette (alignée sur le reste de l'application) ──────────────────────
    _CMAP_C = {
        "vert":  "#27ae60",
        "bleu":  "#2196f3",
        "gris":  "#9e9e9e",
        "ambre": "#f39c12",
        "rouge": "#c0392b",
    }

    # ── Données détaillées par EPCI (toutes familles d'équipements) ───────────
    _epci_detail_df, _nat_equip_rates, _main_fams = _build_epci_detail(
        equip_fam_df, communes_ref
    )
    _epci_detail_df["epci_code"] = _epci_detail_df["epci_code"].astype(str).str.strip()

    # ── Besoins théoriques par EPCI (piscines + clubs + gymnases) ─────────────
    _epci_besoins_df = _build_epci_besoins(
        lic_df, piscines_df, equip_fam_df,
        ecoles_df, colleges_df, lycees_df,
        communes_ref,
    )
    _epci_besoins_df["epci_code"] = _epci_besoins_df["epci_code"].astype(str).str.strip()

    # ── Données principales (licenciés / clubs / équipements total) ───────────
    # Calculées depuis les sources primaires pour cohérence avec le panneau principal
    _epci_all = _build_epci_main(lic_df, clubs_df, equip_fam_df, communes_ref)

    # Moyennes nationales pondérées pour les 3 indicateurs principaux
    def _nat_avg_w(df, col):
        v = df.dropna(subset=[col])
        ps = v["population"].sum()
        return float((v[col] * v["population"]).sum() / ps) if ps > 0 else None

    _nat_lic_c   = _nat_avg_w(_epci_all, "licencies_pour_1000_hab")
    _nat_clubs_c = _nat_avg_w(_epci_all, "clubs_pour_1000_hab")
    _nat_equip_c = _nat_avg_w(_epci_all, "equip_pour_1000_hab")
    _nat_avg_c   = {"licencies_pour_1000_hab": _nat_lic_c,
                    "clubs_pour_1000_hab":      _nat_clubs_c,
                    "equip_pour_1000_hab":      _nat_equip_c}
    _nat_str_c   = f"{_nat_avg_c[_ind_col_c]:.1f} ‰" if _nat_avg_c[_ind_col_c] else "nd"

    # ── Chargement fond de carte ──────────────────────────────────────────────
    _gdf_base = load_carte_epci()
    if _gdf_base is None:
        st.error(
            "❌ Fond de carte indisponible.  "
            "Vérifiez la présence du dossier `codes_postaux/` avec `codes_postaux_region.shp`."
        )
        st.stop()

    # ── Jointure géo ← indicateurs principaux ← équipements ← besoins ──────
    _gdf_m = _gdf_base.merge(_epci_all, on="epci_code", how="left")
    _gdf_m = _gdf_m.merge(
        _epci_detail_df.drop(columns=["population"], errors="ignore"),
        on="epci_code", how="left",
    )
    _gdf_m = _gdf_m.merge(
        _epci_besoins_df.drop(columns=["population"], errors="ignore"),
        on="epci_code", how="left",
    )

    # ── Filtrage géographique ─────────────────────────────────────────────────
    if _niv == "Département" and _dep_sel:
        # Normaliser le code sélectionné (sans zéro de tête superflu pour Corse etc.)
        _dep_norm = str(_dep_sel).strip().upper()
        # Construire un set de formats équivalents (avec et sans zéro de tête)
        _dep_variants = {_dep_norm, _dep_norm.lstrip("0") or "0", _dep_norm.zfill(2), _dep_norm.zfill(3)}
        _gdf_f = _gdf_m[
            _gdf_m["dep_str"].fillna("").apply(
                lambda d: str(d).strip().upper() in _dep_variants
                          or str(d).strip().zfill(2) == _dep_norm.zfill(2)
            )
        ].copy()
        # Fallback : chercher aussi via la région si résultat vide (EPCI multi-dép)
        if _gdf_f.empty and _ctx_reg_guess:
            _gdf_f = _gdf_m[_gdf_m["reg_nom"] == _ctx_reg_guess].copy()
    elif _niv == "Région" and _reg_sel:
        _gdf_f = _gdf_m[_gdf_m["reg_nom"] == _reg_sel].copy()
    else:
        _gdf_f = _gdf_m.copy()

    if _gdf_f.empty:
        st.warning(
            f"⚠️ Aucun EPCI trouvé pour ce territoire "
            f"({'dep=' + str(_dep_sel) if _niv == 'Département' else 'reg=' + str(_reg_sel)})."
        )
        st.stop()

    # ── Helpers couleur ───────────────────────────────────────────────────────
    def _col_from_ratio(r):
        """Retourne une couleur hex selon l'écart relatif à la moyenne nationale."""
        if r is None: return _CMAP_C["gris"]
        if r > 1.10:  return _CMAP_C["vert"]
        if r > 1.05:  return _CMAP_C["bleu"]
        if r >= 0.95: return _CMAP_C["gris"]
        if r >= 0.90: return _CMAP_C["ambre"]
        return _CMAP_C["rouge"]

    def _ratio(val, nat):
        if val is None or nat is None or nat == 0 or (isinstance(val, float) and pd.isna(val)):
            return None
        return val / nat

    def _badge_html(val, nat, suffix="‰"):
        """Génère une pastille colorée avec la valeur et l'écart %."""
        r = _ratio(val, nat)
        col = _col_from_ratio(r)
        if val is None or (isinstance(val, float) and pd.isna(val)):
            return "<span style='color:#aaa'>nd</span>"
        pct = (r - 1) * 100 if r is not None else None
        pct_str = f"{pct:+.0f}%" if pct is not None else ""
        return (
            f"<b>{val:.2f} {suffix}</b> "
            f"<span style='background:{col};color:white;padding:1px 5px;"
            f"border-radius:3px;font-size:10px'>{pct_str}</span>"
        )

    # ── Calcul couleur principale (indicateur sélectionné) ───────────────────
    _nat_main = _nat_avg_c[_ind_col_c]

    def _main_color(val):
        if val is None or (isinstance(val, float) and pd.isna(val)):
            return _CMAP_C["gris"]
        return _col_from_ratio(_ratio(val, _nat_main))

    _gdf_f = _gdf_f.copy()
    _gdf_f["_color"]  = _gdf_f[_ind_col_c].apply(_main_color)
    _gdf_f["Nom EPCI"] = _gdf_f["epci_nom"].fillna("—")

    # ── Construction du HTML de popup pour chaque EPCI ────────────────────────
    def _fmt_v(v, decimals=1, suffix=""):
        if v is None or (isinstance(v, float) and pd.isna(v)):
            return "nd"
        if decimals == 0:
            return f"{int(round(v)):,}".replace(",", " ") + (f" {suffix}" if suffix else "")
        return f"{v:.{decimals}f}" + (f" {suffix}" if suffix else "")

    def _popup_row(label, val, nat, suffix="‰", decimals=1, bold_label=False):
        bl = "font-weight:bold;" if bold_label else ""
        return (
            f"<tr><td style='padding:2px 6px 2px 0;white-space:nowrap;{bl}'>{label}</td>"
            f"<td style='padding:2px 0'>{_badge_html(val, nat, suffix)}</td></tr>"
        )

    def _ecart_row(label, ecart, suffix="", pos_is_good=True, decimals=1):
        """Ligne avec valeur d'écart brute (pas de référence nationale)."""
        if ecart is None or (isinstance(ecart, float) and pd.isna(ecart)):
            badge = "<span style='color:#aaa'>nd</span>"
        else:
            col = (_CMAP_C["vert"] if ecart > 0 else _CMAP_C["rouge"]) if pos_is_good \
                  else (_CMAP_C["rouge"] if ecart > 0 else _CMAP_C["vert"])
            sign = "+" if ecart > 0 else ""
            if decimals == 0:
                vstr = f"{sign}{int(round(ecart)):,}".replace(",", " ")
            else:
                vstr = f"{sign}{ecart:.{decimals}f}"
            if suffix:
                vstr += f" {suffix}"
            badge = f"<b style='color:{col}'>{vstr}</b>"
        return (
            f"<tr><td style='padding:2px 6px 2px 8px;white-space:nowrap;color:#555'>{label}</td>"
            f"<td style='padding:2px 0'>{badge}</td></tr>"
        )

    def _section_hdr(title):
        return (
            f"<tr><td colspan='2' style='padding:5px 0 2px 0;font-weight:bold;"
            f"font-size:11px;color:#333;border-top:1px solid #e0e0e0'>{title}</td></tr>"
        )

    def _make_popup(row):
        nom = row.get("Nom EPCI", "—")
        pop = row.get("population", None)
        pop_s = (f"{int(pop):,}".replace(",", " ") + " hab.") if pd.notna(pop) else "nd"
        lic   = row.get("licencies_pour_1000_hab", None)
        clubs = row.get("clubs_pour_1000_hab", None)
        equip = row.get("equip_pour_1000_hab", None)

        # ── 1. Indicateurs principaux ─────────────────────────────────────────
        main_rows = (
            _popup_row("Licenciés",   lic,   _nat_lic_c)   +
            _popup_row("Clubs",       clubs, _nat_clubs_c) +
            _popup_row("Équipements", equip, _nat_equip_c)
        )

        # ── 2. Offre équipements sportifs ─────────────────────────────────────
        offre_rows = ""
        for fname, nat_r in _main_fams:
            v = row.get(f"r_{fname}", None)
            if isinstance(v, float) and pd.isna(v):
                v = None
            offre_rows += _popup_row(fname, v, nat_r if nat_r > 0 else None, decimals=2)

        # ── 3. Besoins théoriques clubs ───────────────────────────────────────
        bl_rows = ""
        _bl_labels = {
            "bl_Courts_de_tennis":              "Courts de tennis",
            "bl_Salles_multisports_/_Gymnases": "Gymnases (clubs)",
            "bl_Terrains_de_grands_jeux":       "Terrains de grands jeux",
            "bl_Salles_de_combat":              "Salles de combat",
        }
        for key, lbl in _bl_labels.items():
            v = row.get(key, None)
            if isinstance(v, float) and pd.isna(v): v = None
            bl_rows += _ecart_row(lbl, v, decimals=1)

        # ── 4. Gymnases scolaires ─────────────────────────────────────────────
        gym_rows = ""
        g30 = row.get("gym30_ecart", None); g30 = None if isinstance(g30, float) and pd.isna(g30) else g30
        g50 = row.get("gym50_ecart", None); g50 = None if isinstance(g50, float) and pd.isna(g50) else g50
        gym_rows += _ecart_row("scolaires 30 %*", g30, decimals=2)
        gym_rows += _ecart_row("scolaires 50 %*", g50, decimals=2)

        # ── 5. Piscines ───────────────────────────────────────────────────────
        pis_m2   = row.get("pis_m2",   None)
        pis_hab  = row.get("pis_m2_hab", None)
        pis_sco  = row.get("pis_sco_ecart", None)
        pis_016  = row.get("pis_e016", None)
        pis_018  = row.get("pis_e018", None)
        pis_020  = row.get("pis_e020", None)
        for v in [pis_m2, pis_hab, pis_sco, pis_016, pis_018, pis_020]:
            if isinstance(v, float) and pd.isna(v): v = None
        pis_offre_s = f"{int(pis_m2):,}".replace(",", " ") + " m²" if pis_m2 and pis_m2 > 0 else "nd"
        pis_hab_s   = f"{pis_hab:.4f} m²/hab." if pis_hab else "nd"
        pis_rows = (
            f"<tr><td style='padding:2px 6px 2px 0' colspan='2'>"
            f"Offre : <b>{pis_offre_s}</b> — {pis_hab_s}</td></tr>"
        )
        pis_rows += _ecart_row("Scolaires (moy.)**", pis_sco, suffix="m²", decimals=0)
        pis_rows += _ecart_row("0,016 m²/hab.",      pis_016, suffix="m²", decimals=0)
        pis_rows += _ecart_row("0,018 m²/hab.",      pis_018, suffix="m²", decimals=0)
        pis_rows += _ecart_row("0,020 m²/hab.",      pis_020, suffix="m²", decimals=0)

        html = (
            f"<div style='font-family:Arial;font-size:12px;min-width:280px;max-width:380px'>"
            f"<b style='font-size:13px'>{nom}</b>"
            f"<div style='color:#777;font-size:11px;margin-bottom:5px'>{pop_s}</div>"
            # Indicateurs principaux
            f"<table style='border-collapse:collapse;width:100%'>"
            f"{_section_hdr('Pratique sportive (/ 1 000 hab.)')}"
            f"{main_rows}"
            # Offre équipements
            f"{_section_hdr('Offre équipements')}"
            f"<tr><td colspan='2'>"
            f"<div style='max-height:110px;overflow-y:auto'>"
            f"<table style='border-collapse:collapse;width:100%'>{offre_rows}</table>"
            f"</div></td></tr>"
            # Besoins clubs
            f"{_section_hdr('Besoins théoriques — clubs (écart actuel − besoin)')}"
            f"{bl_rows}"
            # Gymnases scolaires
            f"<tr><td style='padding:2px 6px 2px 0;color:#555' colspan='2'>"
            f"Gymnases scolaires :</td></tr>"
            f"{gym_rows}"
            f"<tr><td colspan='2' style='color:#999;font-size:10px;padding:1px 0 3px 8px'>"
            f"* % du temps EPS en gymnase</td></tr>"
            # Piscines
            f"{_section_hdr('Piscines — besoins théoriques (écart actuel − besoin)')}"
            f"{pis_rows}"
            f"<tr><td colspan='2' style='color:#999;font-size:10px;padding:1px 0 3px 0'>"
            f"** moy. circulaire 28-2-2022</td></tr>"
            f"</table></div>"
        )
        return html.replace('"', "&quot;")

    # ── Supprimer les lignes sans géométrie valide (évite bounds NaN → carte monde)
    import geopandas as _gpd_check
    _gdf_f = _gdf_f[_gdf_f.geometry.notna() & ~_gdf_f.geometry.is_empty].copy()
    if not isinstance(_gdf_f, _gpd_check.GeoDataFrame):
        _gdf_f = _gpd_check.GeoDataFrame(_gdf_f, geometry="geometry", crs="EPSG:4326")

    if _gdf_f.empty:
        st.warning("⚠️ Aucune géométrie valide pour ce territoire.")
        st.stop()

    _gdf_f["popup_html"] = _gdf_f.apply(_make_popup, axis=1)

    # ── Tooltip survol (résumé rapide) ────────────────────────────────────────
    _gdf_f["val_aff"]   = _gdf_f[_ind_col_c].apply(
        lambda v: f"{v:.1f} ‰" if pd.notna(v) else "nd"
    )
    _gdf_f["pop_aff"]   = _gdf_f["population"].apply(
        lambda v: f"{int(v):,}".replace(",", " ") + " hab." if pd.notna(v) else "nd"
    )
    _gdf_f["ecart_aff"] = _gdf_f[_ind_col_c].apply(
        lambda v: (
            f"{(v / _nat_main - 1)*100:+.0f} %" if _nat_main and pd.notna(v) else "nd"
        )
    )

    # ── Carte Folium ──────────────────────────────────────────────────────────
    _bnds = _gdf_f.total_bounds
    # Garde contre bounds invalides (NaN) → fallback France métropolitaine
    _FRANCE_BNDS = [-5.5, 41.0, 10.0, 51.5]
    if any(not np.isfinite(b) for b in _bnds):
        _bnds = _FRANCE_BNDS
    _center = [(_bnds[1] + _bnds[3]) / 2, (_bnds[0] + _bnds[2]) / 2]
    _zoom   = 9 if _niv == "Département" else (7 if _niv == "Région" else 6)

    _m = _folium.Map(
        location=_center, zoom_start=_zoom,
        tiles="CartoDB positron", control_scale=True,
    )

    _color_dict_c  = dict(zip(_gdf_f["epci_code"].astype(str), _gdf_f["_color"]))

    _cols_export = [
        "epci_code", "Nom EPCI",
        "val_aff", "pop_aff", "ecart_aff",
        "popup_html",
        "geometry",
    ]
    _geojson_data = _json_mod.loads(_gdf_f[_cols_export].to_json())

    _folium.GeoJson(
        _geojson_data,
        name="EPCI",
        style_function=lambda feat: {
            "fillColor": _color_dict_c.get(
                str(feat["properties"].get("epci_code", "")), _CMAP_C["gris"]
            ),
            "color":       "#ffffff",
            "weight":      0.8,
            "fillOpacity": 0.75,
        },
        highlight_function=lambda feat: {
            "fillColor": _color_dict_c.get(
                str(feat["properties"].get("epci_code", "")), _CMAP_C["gris"]
            ),
            "color":       "#222",
            "weight":      2.5,
            "fillOpacity": 0.9,
        },
        tooltip=_folium.GeoJsonTooltip(
            fields=["Nom EPCI", "val_aff", "ecart_aff", "pop_aff"],
            aliases=[
                "EPCI :",
                f"{_ind_short} :",
                "Écart moy. nat. :",
                "Population :",
            ],
            style=(
                "background-color:white;border:1px solid #ccc;border-radius:4px;"
                "font-size:12px;font-family:Arial;padding:6px;"
            ),
            sticky=True,
        ),
        popup=_folium.GeoJsonPopup(
            fields=["popup_html"],
            aliases=[""],
            labels=False,
            style=(
                "background-color:white;border:1px solid #ccc;border-radius:6px;"
                "font-family:Arial;font-size:12px;max-width:380px;"
            ),
            max_width=400,
        ),
    ).add_to(_m)

    # ── Légende ───────────────────────────────────────────────────────────────
    _leg_items = "".join(
        f"<div style='display:flex;align-items:center;margin:4px 0'>"
        f"<div style='width:16px;height:16px;background:{col};border-radius:3px;"
        f"margin-right:8px;flex-shrink:0'></div>{lbl}</div>"
        for col, lbl in [
            (_CMAP_C["vert"],  "&gt; +10 % — Très au-dessus"),
            (_CMAP_C["bleu"],  "+5 à +10 % — Au-dessus"),
            (_CMAP_C["gris"],  "± 5 % — Dans la norme"),
            (_CMAP_C["ambre"], "−5 à −10 % — En dessous"),
            (_CMAP_C["rouge"], "&lt; −10 % — Très en dessous"),
        ]
    )
    _leg_html = (
        f"<div style='position:fixed;bottom:30px;left:30px;z-index:1000;"
        f"background:white;padding:12px 16px;border-radius:8px;"
        f"border:1px solid #ccc;font-family:Arial;font-size:12px;"
        f"box-shadow:2px 2px 6px rgba(0,0,0,.2);min-width:190px;'>"
        f"<b style='font-size:13px'>{_ind_short}</b>"
        f"<div style='color:#777;font-size:11px;margin-bottom:8px'>"
        f"Moy. nationale : {_nat_str_c}</div>"
        f"{_leg_items}"
        f"<div style='color:#aaa;font-size:10px;margin-top:6px'>Cliquer sur un EPCI pour le détail</div>"
        f"</div>"
    )
    _m.get_root().html.add_child(_folium.Element(_leg_html))
    _m.fit_bounds([[_bnds[1], _bnds[0]], [_bnds[3], _bnds[2]]])

    # Affichage
    _cmp.html(_m._repr_html_(), height=620, scrolling=False)

    # ── Tableau récapitulatif ─────────────────────────────────────────────────
    st.markdown("---")
    _nb_epci = len(_gdf_f)
    st.markdown(
        f"**{_nb_epci} EPCI affichés** — Moy. nationale pondérée : **{_nat_str_c}**"
    )

    # Construction tableau multi-colonnes
    _recap_cols = {
        "EPCI":               "Nom EPCI",
        "Licenciés (‰)":      "licencies_pour_1000_hab",
        "Clubs (‰)":          "clubs_pour_1000_hab",
        "Équipements (‰)":    "equip_pour_1000_hab",
        "Population":         "population",
        "Département":        "dep_nom",
    }
    _df_recap = _gdf_f[[v for v in _recap_cols.values() if v in _gdf_f.columns]].copy()
    _df_recap.columns = [k for k, v in _recap_cols.items() if v in _gdf_f.columns]
    _df_recap = _df_recap.sort_values("Licenciés (‰)", ascending=False).reset_index(drop=True)
    _df_recap["Population"] = _df_recap["Population"].apply(
        lambda v: f"{int(v):,}".replace(",", " ") if pd.notna(v) else "nd"
    )
    for _kk in ["Licenciés (‰)", "Clubs (‰)", "Équipements (‰)"]:
        if _kk in _df_recap.columns:
            _df_recap[_kk] = _df_recap[_kk].apply(
                lambda v: round(v, 1) if pd.notna(v) else None
            )
    st.dataframe(_df_recap, use_container_width=True, hide_index=True)
