{"id":31705,"date":"2026-07-31T16:19:36","date_gmt":"2026-07-31T20:19:36","guid":{"rendered":"https:\/\/allianceenjesus.com\/?page_id=31705"},"modified":"2026-08-04T16:26:50","modified_gmt":"2026-08-04T20:26:50","slug":"alliance-films","status":"publish","type":"page","link":"https:\/\/allianceenjesus.com\/?page_id=31705","title":{"rendered":"ALLIANCE FILMS"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"31705\" class=\"elementor elementor-31705\">\n\t\t\t\t<div class=\"elementor-element elementor-element-95f365c e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"95f365c\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2acd896 elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-html\" data-id=\"2acd896\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!--\n    CATALOGUE FILMS \u2014 version WordPress \/ Elementor\n    =================================================\n    A coller tel quel dans un widget \"HTML\" (ou \"Code HTML\")\n    d'Elementor. Ne PAS coller <!DOCTYPE>, <html>, <head> ou\n    <body> \u2014 il n'y en a plus ici, uniquement le contenu du\n    widget.\n\n    Changements par rapport \u00e0 la version autonome :\n    - Toutes les r\u00e8gles CSS sont scop\u00e9es sous .wdfc-app pour\n      ne pas d\u00e9teindre sur le reste du th\u00e8me (les s\u00e9lecteurs\n      globaux comme \"button\", \"input, select\", \":root\" du\n      fichier d'origine s'appliquaient \u00e0 TOUT le site).\n    - Tous les id=\"\" sont pr\u00e9fix\u00e9s \"wdfc-\" pour \u00e9viter les\n      collisions avec d'autres widgets\/plugins de la page.\n    - Tout le JavaScript est encapsul\u00e9 dans une seule IIFE\n      (au lieu de variables globales comme currentPage,\n      currentResults, searchWikidata...) et d\u00e9marre apr\u00e8s le\n      DOM pr\u00eat.\n    - Le hack \"pleine largeur\" (width:100vw + translateX) a\n      \u00e9t\u00e9 retir\u00e9 car il casse la mise en page \u00e0 l'int\u00e9rieur\n      des conteneurs Elementor. Pour un rendu pleine largeur,\n      utilisez plut\u00f4t l'option native \"Stretch Section\" \/\n      \"\u00c9tirer la section\" d'Elementor sur la section qui\n      contient ce widget.\n    - Le reste (logique de recherche Wikidata, module TMDB,\n      export CSV, vue Cin\u00e9ma, etc.) est inchang\u00e9.\n\n    \u26a0\ufe0f La cl\u00e9 API TMDB reste visible dans le code source c\u00f4t\u00e9\n    navigateur (comme dans l'original) : n'importe qui peut la\n    lire en ouvrant l'inspecteur. Si c'est un probl\u00e8me pour\n    vous, il faudrait faire passer les appels TMDB par un\n    endpoint c\u00f4t\u00e9 serveur (ex. un shortcode PHP ou un plugin\n    \"code snippet\" qui masque la cl\u00e9) \u2014 dites-le moi si vous\n    voulez cette version.\n\n    Limite : ce widget utilise des id fixes, donc n'en placez\n    qu'UNE seule instance par page (deux copies sur la m\u00eame\n    page cr\u00e9eraient des id dupliqu\u00e9s et casseraient le script).\n-->\n\n<div class=\"wdfc-app\">\n\n<style>\n\n.wdfc-app, .wdfc-app * {\n    box-sizing: border-box;\n}\n\n.wdfc-app {\n    color-scheme: dark;\n    font-family: Arial, Helvetica, sans-serif !important;\n    background: #0b0b0d !important;\n    color: #fff !important;\n    margin: 0;\n    padding: 25px;\n}\n\n.wdfc-app .search-box {\n    background: #1d1d1d;\n    border-radius: 14px;\n    padding: 20px;\n    margin-bottom: 20px;\n}\n\n.wdfc-app .field-row {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\n    gap: 12px;\n    margin-bottom: 16px;\n}\n\n.wdfc-app .field-row:last-child { margin-bottom: 0; }\n\n.wdfc-app .field-row.search-row {\n    grid-template-columns: 3fr 1.4fr auto;\n}\n\n.wdfc-app .field label {\n    display: block;\n    font-size: 11.5px;\n    text-transform: uppercase;\n    letter-spacing: .4px;\n    opacity: .55;\n    margin-bottom: 5px;\n    color: #fff !important;\n}\n\n.wdfc-app .field.full-width { grid-column: 1 \/ -1; }\n\n.wdfc-app input,\n.wdfc-app select {\n    width: 100%;\n    padding: 12px;\n    border-radius: 8px;\n    border: 1px solid #444;\n    background: #111 !important;\n    color: #fff !important;\n    font-size: 14px;\n}\n\n.wdfc-app select[multiple] { padding: 8px; }\n\n.wdfc-app select:disabled,\n.wdfc-app input:disabled {\n    opacity: .4;\n    cursor: not-allowed;\n}\n\n.wdfc-app .field-hint {\n    font-size: 11.5px;\n    opacity: .55;\n    margin-top: 6px;\n    color: #fff !important;\n}\n\n.wdfc-app button {\n    padding: 12px 18px;\n    border: 0;\n    border-radius: 8px;\n    background: #f0c400 !important;\n    color: #111 !important;\n    font-weight: bold;\n    cursor: pointer;\n}\n\n.wdfc-app button:hover { opacity: .85; }\n.wdfc-app button:disabled { opacity: .4; cursor: not-allowed; }\n\n.wdfc-app .youtube-button { background: #ff0000 !important; color: #fff !important; }\n.wdfc-app .cinema-button { background: #2c2c30 !important; color: #fff !important; border: 1px solid #f0c400; }\n\n.wdfc-app .toggle-filter {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    padding: 12px;\n    border-radius: 8px;\n    border: 1px solid #444;\n    background: #111;\n    cursor: pointer;\n    user-select: none;\n}\n\n.wdfc-app .toggle-filter input {\n    width: 18px;\n    height: 18px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .toggle-filter span { font-size: 13.5px; color: #fff !important; }\n\n.wdfc-app .lexical-box {\n    border: 1px solid #444;\n    border-radius: 8px;\n    padding: 12px;\n    background: #111;\n}\n\n.wdfc-app .lexical-box legend {\n    font-size: 11.5px;\n    text-transform: uppercase;\n    letter-spacing: .4px;\n    opacity: .65;\n    padding: 0 6px;\n    color: #fff !important;\n}\n\n.wdfc-app .lexical-options {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 12px;\n    margin-top: 6px;\n}\n\n.wdfc-app .lexical-options label {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 13px;\n    cursor: pointer;\n    color: #fff !important;\n}\n\n.wdfc-app .lexical-options input {\n    width: 16px;\n    height: 16px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .language-options {\n    max-height: 190px;\n    overflow-y: auto;\n    border: 1px solid #444;\n    border-radius: 8px;\n    padding: 12px;\n    background: #111;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 10px 18px;\n}\n\n.wdfc-app .language-options.is-disabled {\n    opacity: .4;\n    pointer-events: none;\n}\n\n.wdfc-app .language-options label {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 13px;\n    cursor: pointer;\n    color: #fff !important;\n}\n\n.wdfc-app .language-options input {\n    width: 16px;\n    height: 16px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .status { margin: 20px 0; text-align: center; min-height: 25px; color: #fff !important; }\n\n.wdfc-app .progress {\n    width: 100%;\n    height: 8px;\n    background: #333;\n    border-radius: 10px;\n    overflow: hidden;\n    margin-top: 15px;\n}\n\n.wdfc-app .progress-bar {\n    width: 0%;\n    height: 100%;\n    background: #f0c400;\n    transition: width .2s;\n}\n\n.wdfc-app .results {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n    gap: 24px;\n}\n\n.wdfc-app .card {\n    position: relative;\n    background: #1a1a1d;\n    border-radius: 14px;\n    overflow: hidden;\n    box-shadow: 0 4px 14px rgba(0,0,0,.4);\n    transition: transform .25s ease, box-shadow .25s ease;\n    display: flex;\n    flex-direction: column;\n}\n\n.wdfc-app .card:hover {\n    transform: translateY(-6px);\n    box-shadow: 0 14px 30px rgba(0,0,0,.6);\n}\n\n.wdfc-app .poster-wrap {\n    position: relative;\n    width: 100%;\n    aspect-ratio: 2\/3;\n    background: #222;\n    overflow: hidden;\n}\n\n.wdfc-app .poster {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    display: block;\n    transition: transform .35s ease;\n}\n\n.wdfc-app .card:hover .poster { transform: scale(1.06); }\n\n.wdfc-app .poster-fallback-title {\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: 71%;\n    bottom: 6%;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    text-align: center;\n    padding: 8px 14px 0;\n    font-weight: bold;\n    font-size: clamp(13px, 1.4vw + 8px, 18px);\n    line-height: 1.25;\n    color: var(--wdfc-title-color, #00e5ff) !important;\n    text-shadow: 0 2px 8px rgba(0,0,0,.9);\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.wdfc-app .year-badge {\n    position: absolute;\n    top: 10px;\n    left: 10px;\n    background: rgba(0,0,0,.65);\n    backdrop-filter: blur(3px);\n    padding: 4px 10px;\n    border-radius: 20px;\n    font-size: 12px;\n    font-weight: bold;\n    color: #fff !important;\n}\n\n.wdfc-app .religious-badge {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    background: rgba(240,196,0,.9);\n    color: #111 !important;\n    padding: 4px 10px;\n    border-radius: 20px;\n    font-size: 11px;\n    font-weight: bold;\n}\n\n.wdfc-app .poster-overlay {\n    position: absolute;\n    inset: 0;\n    background: linear-gradient(to top, rgba(0,0,0,.92) 0%, rgba(0,0,0,.15) 55%, rgba(0,0,0,0) 100%);\n    opacity: 0;\n    transition: opacity .25s ease;\n    display: flex;\n    align-items: flex-end;\n    padding: 14px;\n}\n\n.wdfc-app .card:hover .poster-overlay { opacity: 1; }\n\n.wdfc-app .overlay-btn {\n    display: inline-block;\n    width: 100%;\n    text-align: center;\n    padding: 10px;\n    background: #f0c400;\n    color: #111 !important;\n    font-weight: bold;\n    text-decoration: none;\n    border-radius: 8px;\n    font-size: 13px;\n}\n\n.wdfc-app .card-content {\n    padding: 14px 16px 16px;\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n}\n\n.wdfc-app .meta-line {\n    font-size: 12.5px;\n    color: rgba(255,255,255,.75) !important;\n    line-height: 1.5;\n    overflow-wrap: break-word;\n}\n\n.wdfc-app .meta-line b { color: #fff !important; }\n\n.wdfc-app .card-links {\n    margin-top: auto;\n    display: flex;\n    gap: 8px;\n    padding-top: 8px;\n}\n\n.wdfc-app .link-primary,\n.wdfc-app .link-secondary {\n    text-decoration: none;\n    text-align: center;\n    border-radius: 7px;\n    font-size: 12.5px;\n    font-weight: bold;\n    padding: 9px 6px;\n}\n\n.wdfc-app .link-primary { flex: 2; background: #f0c400; color: #111 !important; }\n.wdfc-app .link-secondary { flex: 1; background: #2c2c30; color: #fff !important; }\n.wdfc-app .link-primary:hover,\n.wdfc-app .link-secondary:hover { opacity: .85; }\n\n.wdfc-app #wdfc-titleColorPicker {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 22px;\n    height: 22px;\n    padding: 0;\n    border: 2px solid #444;\n    border-radius: 50%;\n    cursor: pointer;\n    background: none;\n    overflow: hidden;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-webkit-color-swatch-wrapper {\n    padding: 0;\n    border-radius: 50%;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-webkit-color-swatch {\n    border: none;\n    border-radius: 50%;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-moz-color-swatch {\n    border: none;\n    border-radius: 50%;\n}\n\n.wdfc-app .pagination {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    gap: 15px;\n    margin-top: 30px;\n    flex-wrap: wrap;\n    color: #fff !important;\n}\n\n@media (max-width: 700px) {\n    .wdfc-app .field-row.search-row { grid-template-columns: 1fr; }\n}\n\n\/* ===== Module TMDB \"Plus de films\" ===== *\/\n\n.wdfc-app #wdfc-tmdbPlusButton {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: 8px;\n    margin: 8px 0;\n    padding: 10px 18px;\n    border: 0;\n    border-radius: 8px;\n    background: #01b4e4 !important;\n    color: #ffffff !important;\n    font-weight: 700;\n    cursor: pointer;\n    transition: opacity .2s, transform .2s;\n}\n\n.wdfc-app #wdfc-tmdbPlusButton:hover {\n    opacity: .88;\n    transform: translateY(-1px);\n}\n\n.wdfc-app #wdfc-tmdbPlusButton:disabled {\n    opacity: .5;\n    cursor: wait;\n    transform: none;\n}\n\n.wdfc-app #wdfc-tmdbPlusStatus {\n    display: none;\n    width: 100%;\n    box-sizing: border-box;\n    margin: 8px 0 14px;\n    padding: 10px 14px;\n    border-radius: 8px;\n    background: rgba(1,180,228,.12);\n    border: 1px solid rgba(1,180,228,.35);\n    color: #fff !important;\n    font-size: 13px;\n    line-height: 1.45;\n}\n\n<\/style>\n\n<section class=\"search-box\">\n\n<div class=\"field-row search-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-search\">Recherche libre<\/label>\n<input id=\"wdfc-search\" type=\"search\" placeholder=\"Mot ou partie de mot...\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-searchCategory\">Rechercher dans<\/label>\n<select id=\"wdfc-searchCategory\">\n<option value=\"title\" selected>Titre<\/option>\n<option value=\"director\">R\u00e9alisateur<\/option>\n<option value=\"cast\">Acteur<\/option>\n<option value=\"all\">Titre + r\u00e9alisateur + acteur<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-searchButton\">\ud83d\udd0e Rechercher<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-destinationSite\">Ouvrir les films sur<\/label>\n<select id=\"wdfc-destinationSite\">\n<option value=\"justwatch\" selected>JustWatch<\/option>\n<option value=\"allocine\">AlloCin\u00e9<\/option>\n<option value=\"imdb\">IMDb<\/option>\n<option value=\"custom\">URL personnalis\u00e9e<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-titleColorPicker\">Couleur des titres de films<\/label>\n<input id=\"wdfc-titleColorPicker\" type=\"color\" value=\"#00e5ff\">\n<\/div>\n\n<div class=\"field full-width\">\n<label for=\"wdfc-customSiteUrl\">URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus)<\/label>\n<input\n    id=\"wdfc-customSiteUrl\"\n    type=\"text\"\n    placeholder=\"https:\/\/exemple.com\/recherche?q={titre}\"\n    disabled\n>\n<div class=\"field-hint\">\nUtilisez <code>{titre}<\/code> dans l'URL pour qu'il soit remplac\u00e9 par le titre du film \u00e0 chaque clic. Sans <code>{titre}<\/code>, le titre est ajout\u00e9 automatiquement en param\u00e8tre <code>q=<\/code> \u00e0 la fin de l'URL saisie. Les films venant de TMDB (bouton \"Plus de films\") s'ouvrent toujours directement sur TMDB, quel que soit ce choix \u2014 voir note plus bas.\n<\/div>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-dateFilter\">Date de sortie<\/label>\n<select id=\"wdfc-dateFilter\">\n<option value=\"all\">Toutes les dates<\/option>\n<option value=\"with\">Avec date de sortie<\/option>\n<option value=\"without\">Sans date de sortie<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-day\">Derniers jours<\/label>\n<input id=\"wdfc-day\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-month\">Derniers mois<\/label>\n<input id=\"wdfc-month\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-year\">Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3)<\/label>\n<input id=\"wdfc-year\" type=\"number\" placeholder=\"Ex : 2026 ou 3\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-yearFrom\">Ann\u00e9e de (fourchette)<\/label>\n<input id=\"wdfc-yearFrom\" type=\"number\" placeholder=\"De\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-yearTo\">Ann\u00e9e \u00e0 (fourchette)<\/label>\n<input id=\"wdfc-yearTo\" type=\"number\" placeholder=\"\u00c0\" min=\"1\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-dateFrom\">Date pr\u00e9cise de d\u00e9but<\/label>\n<input id=\"wdfc-dateFrom\" type=\"date\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-dateTo\">Date pr\u00e9cise de fin<\/label>\n<input id=\"wdfc-dateTo\" type=\"date\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-titleFilter\">Titre<\/label>\n<select id=\"wdfc-titleFilter\">\n<option value=\"all\">Tous les films<\/option>\n<option value=\"with\">Avec un titre<\/option>\n<option value=\"without\">Sans titre<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-typeFilter\">Type<\/label>\n<select id=\"wdfc-typeFilter\">\n<option value=\"film\">Films uniquement<\/option>\n<option value=\"animation\">Dessins anim\u00e9s uniquement<\/option>\n<option value=\"film_animation\" selected>Films + dessins anim\u00e9s<\/option>\n<option value=\"all\">Films et \u0153uvres audiovisuelles (large)<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-sort\">Tri<\/label>\n<select id=\"wdfc-sort\">\n<option value=\"newest\">Plus r\u00e9cents<\/option>\n<option value=\"oldest\">Plus anciens<\/option>\n<option value=\"title\">Titre A-Z<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-pageSize\">R\u00e9sultats par page<\/label>\n<input id=\"wdfc-pageSize\" type=\"number\" min=\"1\" max=\"500\" value=\"20\" list=\"wdfc-pageSizePresets\">\n<datalist id=\"wdfc-pageSizePresets\">\n<option value=\"20\">\n<option value=\"50\">\n<option value=\"100\">\n<option value=\"200\">\n<\/datalist>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-maxResults\">Nombre maxi de r\u00e9sultats<\/label>\n<input id=\"wdfc-maxResults\" type=\"number\" placeholder=\"Illimit\u00e9\" min=\"1\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field full-width\">\n<label>Langue(s) du titre <span id=\"wdfc-languageCount\" style=\"opacity:.5; text-transform:none;\"><\/span><\/label>\n<div id=\"wdfc-languageOptions\" class=\"language-options\">\n<span style=\"opacity:.6; font-size:13px;\">Chargement des langues disponibles dans la base...<\/span>\n<\/div>\n\n<label class=\"toggle-filter\" style=\"margin-top:8px;\" for=\"wdfc-allLanguages\">\n<input type=\"checkbox\" id=\"wdfc-allLanguages\">\n<span>\ud83c\udf10 Toutes les langues (ignore la liste ci-dessus)<\/span>\n<\/label>\n\n<div class=\"field-hint\">\nCochez une, plusieurs, ou toutes les langues souhait\u00e9es. Cochez \u00ab Toutes les langues \u00bb pour ne faire aucun tri par langue.\n<\/div>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<label class=\"toggle-filter\" for=\"wdfc-religiousFilter\">\n<input type=\"checkbox\" id=\"wdfc-religiousFilter\" checked>\n<span>\u271d\ufe0f Films chr\u00e9tiens \/ religieux (genre \/ sujet Wikidata)<\/span>\n<\/label>\n\n<fieldset class=\"lexical-box\">\n<legend>\u00c9largir la recherche (mots-cl\u00e9s dans le titre)<\/legend>\n<div class=\"lexical-options\">\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"chr\u00e9tien,christian\"> Chr\u00e9tien<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"religieux,religious\"> Religieux<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"biblique,biblical,bible\"> Biblique<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"spirituel,spiritual\"> Spirituel<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"croyance,belief,faith\"> Croyance<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"\u00e9vangile,gospel\"> \u00c9vangile<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"dieu,\u00e9ternel,god,eternal\"> Dieu \/ \u00c9ternel<\/label>\n<\/div>\n<input\n    id=\"wdfc-lexicalCustom\"\n    type=\"text\"\n    placeholder=\"Autres mots-cl\u00e9s, s\u00e9par\u00e9s par une virgule (ex : messe, pri\u00e8re)\"\n    style=\"margin-top:8px;\"\n>\n<\/fieldset>\n\n<fieldset class=\"lexical-box\">\n<legend>Exclure les films contenant ces mots (titre)<\/legend>\n<input\n    id=\"wdfc-excludeWords\"\n    type=\"text\"\n    placeholder=\"Mots \u00e0 exclure, s\u00e9par\u00e9s par une virgule (ex : horreur, zombie)\"\n>\n<\/fieldset>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-cancelButton\" disabled>\u23f9 Annuler<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-resetButton\">\ud83e\uddf9 R\u00e9initialiser<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-pageInput\">Aller \u00e0 la page<\/label>\n<input id=\"wdfc-pageInput\" type=\"number\" min=\"1\" value=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-pageButton\">\ud83d\udcc4 Aller \u00e0 la page<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-exportButton\">\u2b07\ufe0f Exporter CSV<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-reloadButton\">\ud83d\udd04 Rechercher<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-youtubeButton\" class=\"youtube-button\" type=\"button\">\u25b6\ufe0f S\u00e9lection sur YouTube<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-cinemaButton\" class=\"cinema-button\" type=\"button\">\ud83c\udfa6 Vue Cin\u00e9ma<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"progress\"><div id=\"wdfc-progressBar\" class=\"progress-bar\"><\/div><\/div>\n\n<\/section>\n\n<div id=\"wdfc-status\" class=\"status\">Pr\u00eat.<\/div>\n\n<div id=\"wdfc-results\" class=\"results\"><\/div>\n\n<div class=\"pagination\">\n<button id=\"wdfc-previousButton\">\u25c0 Pr\u00e9c\u00e9dent<\/button>\n<span id=\"wdfc-pageInfo\">Page 1 \/ 1<\/span>\n<button id=\"wdfc-nextButton\">Suivant \u25b6<\/button>\n<\/div>\n\n<\/div>\n\n\n<script>\n(function () {\n\"use strict\";\n\nfunction init() {\n\n\/* =========================================================\n   CONFIGURATION\n========================================================= *\/\n\nconst WIKIDATA_ENDPOINT = \"https:\/\/query.wikidata.org\/sparql\";\n\nconst RELIGIOUS_GENRE_IDS = [\"wd:Q115122793\", \"wd:Q7717463\"];\n\nconst RELIGIOUS_SUBJECT_IDS = [\n    \"wd:Q5043\", \"wd:Q302\", \"wd:Q766677\", \"wd:Q190\", \"wd:Q194253\",\n    \"wd:Q5410500\", \"wd:Q1845\", \"wd:Q34274\", \"wd:Q34394\", \"wd:Q9174\",\n    \"wd:Q2728698\", \"wd:Q51628\", \"wd:Q51636\", \"wd:Q51624\",\n    \"wd:Q9077\", \"wd:Q81422\", \"wd:Q41370\", \"wd:Q9200\"\n];\n\nconst FILM_CLASS_ID = \"wd:Q11424\";\nconst ANIMATED_FILM_CLASS_ID = \"wd:Q202866\";\n\nconst COUNT_CAP = 3000;\nconst LANGUAGE_SAMPLE_SIZE = 20000;\nconst UPCOMING_LIMIT = 24;\n\nconst FALLBACK_LANGUAGE_CODES = [\n    \"fr\",\"en\",\"es\",\"de\",\"it\",\"pt\",\"nl\",\"ar\",\"ru\",\"zh\",\"ja\",\"ko\",\n    \"hi\",\"tr\",\"pl\",\"sv\",\"no\",\"da\",\"fi\",\"el\",\"he\",\"cs\",\"hu\",\"ro\",\"uk\"\n];\n\nconst DEFAULT_POSTER =\n    \"data:image\/svg+xml,\" +\n    encodeURIComponent(\n        '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 400 600\">' +\n        '<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">' +\n        '<stop offset=\"0\" stop-color=\"#34343e\"\/>' +\n        '<stop offset=\"1\" stop-color=\"#131316\"\/>' +\n        '<\/linearGradient><\/defs>' +\n        '<rect width=\"400\" height=\"600\" fill=\"url(#g)\"\/>' +\n        '<circle cx=\"200\" cy=\"248\" r=\"74\" fill=\"none\" stroke=\"#f0c400\" stroke-width=\"6\" opacity=\"0.9\"\/>' +\n        '<polygon points=\"184,210 184,286 250,248\" fill=\"#f0c400\" opacity=\"0.95\"\/>' +\n        '<text x=\"200\" y=\"412\" text-anchor=\"middle\" font-family=\"Arial, Helvetica, sans-serif\" ' +\n        'font-size=\"42\" font-weight=\"bold\" letter-spacing=\"10\" fill=\"#ffffff\" opacity=\"0.9\">FILM<\/text>' +\n        '<\/svg>'\n    );\n\n\/* =========================================================\n   VARIABLES\n========================================================= *\/\n\nlet currentPage = 1;\nlet totalPages = 1;\nlet totalKnown = true;\nlet hasNextPageUnknown = false;\nlet currentResults = [];\nlet abortController = null;\nlet isLoading = false;\n\n\/* =========================================================\n   ELEMENTS\n========================================================= *\/\n\nconst searchInput = document.getElementById(\"wdfc-search\");\nconst searchCategory = document.getElementById(\"wdfc-searchCategory\");\nconst destinationSiteSelect = document.getElementById(\"wdfc-destinationSite\");\nconst titleColorPicker = document.getElementById(\"wdfc-titleColorPicker\");\nconst customSiteUrlInput = document.getElementById(\"wdfc-customSiteUrl\");\nconst dayInput = document.getElementById(\"wdfc-day\");\nconst monthInput = document.getElementById(\"wdfc-month\");\nconst yearInput = document.getElementById(\"wdfc-year\");\nconst yearFromInput = document.getElementById(\"wdfc-yearFrom\");\nconst yearToInput = document.getElementById(\"wdfc-yearTo\");\nconst dateFromInput = document.getElementById(\"wdfc-dateFrom\");\nconst dateToInput = document.getElementById(\"wdfc-dateTo\");\nconst titleFilter = document.getElementById(\"wdfc-titleFilter\");\nconst dateFilter = document.getElementById(\"wdfc-dateFilter\");\nconst sortSelect = document.getElementById(\"wdfc-sort\");\nconst typeFilter = document.getElementById(\"wdfc-typeFilter\");\nconst religiousFilter = document.getElementById(\"wdfc-religiousFilter\");\nconst lexicalCustomInput = document.getElementById(\"wdfc-lexicalCustom\");\nconst excludeWordsInput = document.getElementById(\"wdfc-excludeWords\");\nconst languageOptionsContainer = document.getElementById(\"wdfc-languageOptions\");\nconst languageCountLabel = document.getElementById(\"wdfc-languageCount\");\nconst allLanguagesCheckbox = document.getElementById(\"wdfc-allLanguages\");\nconst pageSizeSelect = document.getElementById(\"wdfc-pageSize\");\nconst maxResultsInput = document.getElementById(\"wdfc-maxResults\");\nconst statusElement = document.getElementById(\"wdfc-status\");\nconst resultsElement = document.getElementById(\"wdfc-results\");\nconst progressBar = document.getElementById(\"wdfc-progressBar\");\nconst cancelButton = document.getElementById(\"wdfc-cancelButton\");\nconst pageInput = document.getElementById(\"wdfc-pageInput\");\nconst previousButton = document.getElementById(\"wdfc-previousButton\");\nconst nextButton = document.getElementById(\"wdfc-nextButton\");\nconst pageInfo = document.getElementById(\"wdfc-pageInfo\");\nconst youtubeButton = document.getElementById(\"wdfc-youtubeButton\");\nconst cinemaButton = document.getElementById(\"wdfc-cinemaButton\");\n\n\/* =========================================================\n   SITE D'OUVERTURE DES FILMS\n========================================================= *\/\n\nfunction getFilmDestinationUrl(film) {\n\n    if (film.isTMDB || film.source === \"TMDB\") {\n        return film.primaryLink;\n    }\n\n    const title = film.title || \"\";\n    const encodedTitle = encodeURIComponent(title);\n    const site = destinationSiteSelect.value;\n\n    if (site === \"allocine\") {\n        return title\n            ? `https:\/\/www.allocine.fr\/rechercher\/?q=${encodedTitle}`\n            : film.uri;\n    }\n\n    if (site === \"imdb\") {\n        if (film.imdb) return film.imdb;\n        return title\n            ? `https:\/\/www.imdb.com\/find\/?q=${encodedTitle}`\n            : film.uri;\n    }\n\n    if (site === \"custom\") {\n\n        const template = customSiteUrlInput.value.trim();\n\n        if (!template) return film.primaryLink;\n\n        if (template.includes(\"{titre}\")) {\n            return template.split(\"{titre}\").join(encodedTitle);\n        }\n\n        if (!title) return film.primaryLink;\n\n        const separator = template.includes(\"?\") ? \"&\" : \"?\";\n        return `${template}${separator}q=${encodedTitle}`;\n    }\n\n    if (!title) return film.uri;\n\n    return `https:\/\/www.justwatch.com\/fr\/recherche?q=${encodedTitle}`;\n}\n\n\/* =========================================================\n   UTILITAIRES DATE\n========================================================= *\/\n\nfunction daysAgo(n) {\n    const d = new Date();\n    d.setDate(d.getDate() - n);\n    return d;\n}\n\nfunction monthsAgo(n) {\n    const d = new Date();\n    d.setMonth(d.getMonth() - n);\n    return d;\n}\n\nfunction yearsAgo(n) {\n    const d = new Date();\n    d.setFullYear(d.getFullYear() - n);\n    return d;\n}\n\n\/* =========================================================\n   REQUETE SIMPLE (sans annulation), AVEC TENTATIVES AUTOMATIQUES\n========================================================= *\/\n\nasync function fetchWikidataOnce(query, attempt = 0) {\n\n    const response = await fetch(WIKIDATA_ENDPOINT, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\n            \"Accept\": \"application\/sparql-results+json\"\n        },\n        body: \"query=\" + encodeURIComponent(query)\n    });\n\n    if ([429, 502, 503, 504].includes(response.status) && attempt < 3) {\n\n        const delay = (attempt + 1) * 2500;\n\n        await new Promise(resolve => setTimeout(resolve, delay));\n\n        return fetchWikidataOnce(query, attempt + 1);\n    }\n\n    if (!response.ok) {\n        throw new Error(\"Erreur HTTP \" + response.status);\n    }\n\n    return response.json();\n}\n\n\/* =========================================================\n   LANGUES DISPONIBLES DANS LA BASE\n========================================================= *\/\n\nfunction languageDisplayName(code) {\n    try {\n        const displayNames = new Intl.DisplayNames([\"fr\"], { type: \"language\" });\n        const name = displayNames.of(code);\n        return name && name.toLowerCase() !== code.toLowerCase() ? name : code;\n    }\n    catch (error) {\n        return code;\n    }\n}\n\nfunction populateLanguageSelect(codes) {\n\n    const uniqueCodes = Array.from(new Set(codes)).filter(Boolean);\n\n    uniqueCodes.sort((a, b) =>\n        languageDisplayName(a).localeCompare(languageDisplayName(b), \"fr\")\n    );\n\n    languageOptionsContainer.innerHTML = \"\";\n\n    uniqueCodes.forEach(code => {\n\n        const label = document.createElement(\"label\");\n\n        const checkbox = document.createElement(\"input\");\n        checkbox.type = \"checkbox\";\n        checkbox.className = \"wdfc-languageCheckbox\";\n        checkbox.value = code;\n        if (code === \"fr\") checkbox.checked = true;\n\n        const span = document.createElement(\"span\");\n        span.textContent = languageDisplayName(code) + \" (\" + code + \")\";\n\n        label.appendChild(checkbox);\n        label.appendChild(span);\n        languageOptionsContainer.appendChild(label);\n    });\n\n    if (!languageOptionsContainer.querySelector(\".wdfc-languageCheckbox:checked\") && uniqueCodes.length) {\n        languageOptionsContainer.querySelector(\".wdfc-languageCheckbox\").checked = true;\n    }\n\n    languageCountLabel.textContent = \"(\" + uniqueCodes.length + \" langues trouv\u00e9es)\";\n}\n\nasync function loadAvailableLanguages() {\n\n    try {\n\n        const query = `\n            SELECT DISTINCT ?lang WHERE {\n                {\n                    SELECT DISTINCT ?item WHERE {\n                        ?item wdt:P31 ${FILM_CLASS_ID} .\n                    }\n                    LIMIT ${LANGUAGE_SAMPLE_SIZE}\n                }\n                ?item rdfs:label ?itemLabel .\n                BIND(LANG(?itemLabel) AS ?lang)\n            }\n            ORDER BY ?lang\n        `;\n\n        const data = await fetchWikidataOnce(query);\n\n        const codes = data.results.bindings\n            .map(row => row.lang.value)\n            .filter(code => code && code.length <= 8);\n\n        if (codes.length === 0) {\n            throw new Error(\"Aucune langue retourn\u00e9e\");\n        }\n\n        populateLanguageSelect(codes);\n    }\n    catch (error) {\n        console.warn(\"Langues Wikidata indisponibles, repli sur une liste par d\u00e9faut :\", error);\n        populateLanguageSelect(FALLBACK_LANGUAGE_CODES);\n    }\n}\n\n\/* =========================================================\n   LANGUES SELECTIONNEES\n========================================================= *\/\n\nfunction getSelectedLanguages() {\n\n    if (allLanguagesCheckbox.checked) {\n        return null;\n    }\n\n    const selected = Array.from(document.querySelectorAll(\".wdfc-languageCheckbox:checked\"))\n        .map(cb => cb.value)\n        .filter(v => v);\n\n    return selected.length ? selected : [\"fr\"];\n}\n\n\/* =========================================================\n   CONDITIONS DE BASE\n========================================================= *\/\n\nfunction buildBaseConditions() {\n\n    const search = searchInput.value.trim();\n\n    const selectedLanguages = getSelectedLanguages();\n\n    let labelCondition;\n\n    if (selectedLanguages === null) {\n        labelCondition = `?item rdfs:label ?itemLabel .`;\n    }\n    else {\n        const langList = selectedLanguages.map(l => `\"${l}\"`).join(\",\");\n        labelCondition = `\n            ?item rdfs:label ?itemLabel .\n            FILTER(LANG(?itemLabel) IN (${langList}))\n        `;\n    }\n\n    let typeCondition = `?item wdt:P31 ${FILM_CLASS_ID} .`;\n\n    if (typeFilter.value === \"animation\") {\n        typeCondition = `\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?animType . ?animType wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\n        `;\n    }\n    else if (typeFilter.value === \"film_animation\") {\n        typeCondition = `\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?filmType . ?filmType wdt:P279* ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?animType2 . ?animType2 wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\n        `;\n    }\n    else if (typeFilter.value === \"all\") {\n        typeCondition = `\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?type . ?type wdt:P279* ${FILM_CLASS_ID} . }\n        `;\n    }\n\n    const lexicalKeywords =\n        Array.from(document.querySelectorAll(\".wdfc-lexicalKeyword:checked\"))\n            .flatMap(cb => cb.value.split(\",\"));\n\n    const customKeywordsRaw = lexicalCustomInput.value.trim();\n\n    if (customKeywordsRaw) {\n        customKeywordsRaw\n            .split(\",\")\n            .map(word => word.trim())\n            .filter(Boolean)\n            .forEach(word => lexicalKeywords.push(word));\n    }\n\n    const hasLexicalFilter = lexicalKeywords.length > 0;\n\n    let religiousCondition = \"\";\n\n    if (religiousFilter.checked || hasLexicalFilter) {\n\n        const branches = [];\n\n        if (religiousFilter.checked) {\n\n            branches.push(`\n                {\n                    ?item wdt:P136 ?religiousGenre .\n                    VALUES ?religiousGenre { ${RELIGIOUS_GENRE_IDS.join(\" \")} }\n                }\n            `);\n\n            branches.push(`\n                {\n                    ?item wdt:P921 ?religiousSubject .\n                    VALUES ?religiousSubject { ${RELIGIOUS_SUBJECT_IDS.join(\" \")} }\n                }\n            `);\n        }\n\n        if (hasLexicalFilter) {\n            const lexicalOr = lexicalKeywords\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\n                .join(\" || \");\n\n            branches.push(`{ FILTER(${lexicalOr}) }`);\n        }\n\n        religiousCondition = branches.join(\" UNION \");\n    }\n\n    const excludeWordsRaw = excludeWordsInput.value.trim();\n    let excludeCondition = \"\";\n\n    if (excludeWordsRaw) {\n\n        const excludeWords = excludeWordsRaw\n            .split(\",\")\n            .map(word => word.trim())\n            .filter(Boolean);\n\n        if (excludeWords.length) {\n            const excludeOr = excludeWords\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\n                .join(\" || \");\n\n            excludeCondition = `FILTER(!(${excludeOr}))`;\n        }\n    }\n\n    let searchCondition = \"\";\n\n    if (search) {\n\n        const safeSearch = search.replace(\/\"\/g, '\\\\\"');\n        const category = searchCategory.value || \"title\";\n\n        const titleMatch = `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${safeSearch}\"))`;\n\n        const directorBlock = `\n            ?item wdt:P57 ?searchDirector .\n            ?searchDirector rdfs:label ?searchDirectorLabel .\n            FILTER(LANG(?searchDirectorLabel) IN (\"fr\",\"en\"))\n            FILTER(CONTAINS(LCASE(STR(?searchDirectorLabel)), LCASE(\"${safeSearch}\")))\n        `;\n\n        const castBlock = `\n            ?item wdt:P161 ?searchCast .\n            ?searchCast rdfs:label ?searchCastLabel .\n            FILTER(LANG(?searchCastLabel) IN (\"fr\",\"en\"))\n            FILTER(CONTAINS(LCASE(STR(?searchCastLabel)), LCASE(\"${safeSearch}\")))\n        `;\n\n        if (category === \"title\") {\n            searchCondition = `FILTER(${titleMatch})`;\n        }\n        else if (category === \"director\") {\n            searchCondition = directorBlock;\n        }\n        else if (category === \"cast\") {\n            searchCondition = castBlock;\n        }\n        else {\n            searchCondition = `\n                { FILTER(${titleMatch}) }\n                UNION\n                { ${directorBlock} }\n                UNION\n                { ${castBlock} }\n            `;\n        }\n    }\n\n    return `\n        ${typeCondition}\n        ${labelCondition}\n        ${religiousCondition}\n        ${excludeCondition}\n        ${searchCondition}\n    `;\n}\n\n\/* =========================================================\n   CONDITIONS DE DATE\n========================================================= *\/\n\nfunction buildWhereClause() {\n\n    const day = dayInput.value.trim();\n    const month = monthInput.value.trim();\n    const year = yearInput.value.trim();\n    const yearFrom = yearFromInput.value.trim();\n    const yearTo = yearToInput.value.trim();\n\n    const base = buildBaseConditions();\n\n    const dateFrom = dateFromInput.value;\n    const dateTo = dateToInput.value;\n\n    const hasFullDateRange = Boolean(dateFrom || dateTo);\n    const hasYearRange = Boolean(yearFrom || yearTo);\n    const hasRollingFilter = Boolean(day || month || year);\n\n    const hasDateFilter = hasFullDateRange || hasYearRange || hasRollingFilter;\n\n    let dateVariable = `OPTIONAL{ ?item wdt:P577 ?date . }`;\n\n    if (dateFilter.value === \"with\" || hasDateFilter) {\n        dateVariable = `?item wdt:P577 ?date .`;\n    }\n\n    if (dateFilter.value === \"without\") {\n        dateVariable = `FILTER NOT EXISTS{ ?item wdt:P577 ?anyDate . }`;\n    }\n\n    let dateFilterCondition = \"\";\n\n    if (hasDateFilter && dateFilter.value !== \"without\") {\n\n        const conditions = [];\n\n        if (hasFullDateRange) {\n            if (dateFrom) conditions.push(`?date >= \"${dateFrom}T00:00:00Z\"^^xsd:dateTime`);\n            if (dateTo) conditions.push(`?date <= \"${dateTo}T23:59:59Z\"^^xsd:dateTime`);\n        }\n        else if (hasYearRange) {\n            if (yearFrom) conditions.push(`YEAR(?date) >= ${Number(yearFrom)}`);\n            if (yearTo) conditions.push(`YEAR(?date) <= ${Number(yearTo)}`);\n        }\n        else {\n\n            const now = new Date();\n            const nowIso = now.toISOString();\n\n            if (day) {\n                conditions.push(`?date >= \"${daysAgo(Number(day)).toISOString()}\"^^xsd:dateTime`);\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n            }\n\n            if (month) {\n                conditions.push(`?date >= \"${monthsAgo(Number(month)).toISOString()}\"^^xsd:dateTime`);\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n            }\n\n            if (year) {\n\n                const yearNumber = Number(year);\n\n                if (yearNumber >= 100) {\n                    conditions.push(`YEAR(?date) = ${yearNumber}`);\n                }\n                else {\n                    conditions.push(`?date >= \"${yearsAgo(yearNumber).toISOString()}\"^^xsd:dateTime`);\n                    conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n                }\n            }\n        }\n\n        if (conditions.length) {\n            dateFilterCondition = `FILTER(${conditions.join(\" && \")})`;\n        }\n    }\n\n    return `\n        ${base}\n        ${dateVariable}\n        ${dateFilterCondition}\n    `;\n}\n\n\/* =========================================================\n   CONDITIONS \"A VENIR\" (vue Cin\u00e9ma)\n========================================================= *\/\n\nfunction buildUpcomingWhereClause() {\n\n    const base = buildBaseConditions();\n\n    return `\n        ${base}\n        ?item wdt:P577 ?date .\n        FILTER(?date > NOW())\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE COMPTAGE\n========================================================= *\/\n\nfunction buildCountQuery() {\n\n    const where = buildWhereClause();\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT (COUNT(*) AS ?count) WHERE {\n            SELECT DISTINCT ?item WHERE{ ${where} }\n            LIMIT ${COUNT_CAP}\n        }\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE LISTE (l\u00e9g\u00e8re, pagination r\u00e9elle)\n========================================================= *\/\n\nfunction buildListQuery(limit, offset) {\n\n    const where = buildWhereClause();\n\n    let order = \"DESC(?dateAgg)\";\n    if (sortSelect.value === \"oldest\") order = \"ASC(?dateAgg)\";\n    if (sortSelect.value === \"title\") order = \"LCASE(STR(?labelAgg))\";\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\n        WHERE{ ${where} }\n        GROUP BY ?item\n        ORDER BY ${order}\n        LIMIT ${limit}\n        OFFSET ${offset}\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE LISTE \"A VENIR\" (vue Cin\u00e9ma)\n========================================================= *\/\n\nfunction buildUpcomingListQuery(limit) {\n\n    const where = buildUpcomingWhereClause();\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\n        WHERE{ ${where} }\n        GROUP BY ?item\n        ORDER BY ASC(?dateAgg)\n        LIMIT ${limit}\n    `;\n}\n\n\/* =========================================================\n   REQUETE D'ENRICHISSEMENT\n========================================================= *\/\n\nfunction buildDetailsQuery(itemUris) {\n\n    const values = itemUris.map(uri => `<${uri}>`).join(\" \");\n\n    return `\n        SELECT ?item\n               (GROUP_CONCAT(DISTINCT ?directorLabel; separator=\", \") AS ?directors)\n               (GROUP_CONCAT(DISTINCT ?castLabel; separator=\", \") AS ?cast)\n               (SAMPLE(?website) AS ?website)\n               (SAMPLE(?imdb) AS ?imdb)\n        WHERE{\n            VALUES ?item { ${values} }\n            OPTIONAL{\n                ?item wdt:P57 ?director .\n                ?director rdfs:label ?directorLabel .\n                FILTER(LANG(?directorLabel) IN (\"fr\",\"en\"))\n            }\n            OPTIONAL{\n                ?item wdt:P161 ?cast .\n                ?cast rdfs:label ?castLabel .\n                FILTER(LANG(?castLabel) IN (\"fr\",\"en\"))\n            }\n            OPTIONAL{ ?item wdt:P856 ?website . }\n            OPTIONAL{\n                ?item wdt:P345 ?imdbId .\n                BIND(CONCAT(\"https:\/\/www.imdb.com\/title\/\", ?imdbId) AS ?imdb)\n            }\n        }\n        GROUP BY ?item\n    `;\n}\n\n\/* =========================================================\n   REQUETE AVEC RETRY (POST, pour \u00e9viter les URL trop longues)\n========================================================= *\/\n\nasync function fetchWikidata(query, attempt = 0) {\n\n    const response = await fetch(WIKIDATA_ENDPOINT, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\n            \"Accept\": \"application\/sparql-results+json\"\n        },\n        body: \"query=\" + encodeURIComponent(query),\n        signal: abortController.signal\n    });\n\n    if ([429, 502, 503, 504].includes(response.status)) {\n\n        if (attempt < 3) {\n\n            const delay = (attempt + 1) * 2500;\n\n            statusElement.textContent =\n                \"\u23f3 Wikidata est temporairement charg\u00e9. Nouvelle tentative dans \"\n                + (delay \/ 1000) + \" secondes...\";\n\n            await new Promise(resolve => setTimeout(resolve, delay));\n\n            return fetchWikidata(query, attempt + 1);\n        }\n    }\n\n    if (!response.ok) {\n        throw new Error(\"Erreur HTTP \" + response.status);\n    }\n\n    return response.json();\n}\n\n\/* =========================================================\n   RECHERCHE (comptage -> liste -> enrichissement)\n========================================================= *\/\n\nasync function searchWikidata() {\n\n    if (isLoading) return;\n\n    isLoading = true;\n    abortController = new AbortController();\n    cancelButton.disabled = false;\n    progressBar.style.width = \"10%\";\n    resultsElement.innerHTML = \"\";\n    statusElement.textContent = \"\u23f3 Pr\u00e9paration de la recherche...\";\n\n    try {\n\n        if (titleFilter.value === \"without\") {\n            currentResults = [];\n            totalPages = 1;\n            statusElement.textContent =\n                \"\u26a0\ufe0f Wikidata ne permet pas de r\u00e9cup\u00e9rer fiablement les films \"\n                + \"sans titre dans cette requ\u00eate l\u00e9g\u00e8re.\";\n            renderResults();\n            return;\n        }\n\n        const pageSize = Math.max(1, Number(pageSizeSelect.value) || 20);\n\n        const maxResultsRaw = maxResultsInput.value.trim();\n        const maxResults = maxResultsRaw ? Math.max(1, Number(maxResultsRaw)) : null;\n\n        statusElement.textContent = \"\u23f3 Comptage des r\u00e9sultats...\";\n        progressBar.style.width = \"20%\";\n\n        let rawCount = null;\n        let cappedAtLimit = false;\n        let effectiveTotal = null;\n\n        try {\n            const countData = await fetchWikidata(buildCountQuery());\n            rawCount = Number(countData.results.bindings[0]?.count?.value || 0);\n            cappedAtLimit = rawCount >= COUNT_CAP;\n        }\n        catch (countError) {\n\n            if (countError.name === \"AbortError\") {\n                throw countError;\n            }\n\n            console.warn(\"Comptage indisponible, repli sans total exact :\", countError);\n\n            rawCount = null;\n        }\n\n        totalKnown = rawCount !== null;\n\n        if (totalKnown) {\n\n            effectiveTotal = maxResults ? Math.min(rawCount, maxResults) : rawCount;\n            totalPages = Math.max(1, Math.ceil(effectiveTotal \/ pageSize));\n\n            if (currentPage > totalPages) {\n                currentPage = totalPages;\n            }\n\n            if (effectiveTotal === 0) {\n                currentResults = [];\n                progressBar.style.width = \"100%\";\n                statusElement.textContent = \"\u274c Aucun r\u00e9sultat.\";\n                renderResults();\n                return;\n            }\n        }\n        else {\n            totalPages = Math.max(totalPages, currentPage);\n        }\n\n        statusElement.textContent = \"\u23f3 Recherche des films...\";\n        progressBar.style.width = \"40%\";\n\n        const offset = (currentPage - 1) * pageSize;\n\n        let limit = pageSize;\n\n        if (maxResults) {\n            limit = Math.min(pageSize, Math.max(0, maxResults - offset));\n        }\n\n        const fetchLimit = totalKnown ? limit : limit + 1;\n\n        const listData = await fetchWikidata(buildListQuery(fetchLimit, offset));\n        let listRows = listData.results.bindings;\n\n        if (!totalKnown) {\n            hasNextPageUnknown = listRows.length > limit;\n            listRows = listRows.slice(0, limit);\n            totalPages = hasNextPageUnknown ? currentPage + 1 : currentPage;\n        }\n\n        progressBar.style.width = \"60%\";\n\n        if (listRows.length === 0) {\n            currentResults = [];\n            progressBar.style.width = \"100%\";\n            statusElement.textContent = \"\u274c Aucun r\u00e9sultat sur cette page.\";\n            renderResults();\n            return;\n        }\n\n        statusElement.textContent = \"\u23f3 R\u00e9cup\u00e9ration des affiches et informations...\";\n\n        const itemUris = listRows.map(row => row.item.value);\n        const detailsData = await fetchWikidata(buildDetailsQuery(itemUris));\n\n        const detailsByUri = {};\n        detailsData.results.bindings.forEach(row => {\n            detailsByUri[row.item.value] = row;\n        });\n\n        progressBar.style.width = \"90%\";\n\n        currentResults = listRows.map(row => convertResult(row, detailsByUri[row.item.value]));\n\n        progressBar.style.width = \"100%\";\n\n        let statusMessage;\n\n        if (totalKnown) {\n\n            statusMessage = \"\u2705 \" + effectiveTotal + \" r\u00e9sultat(s) trouv\u00e9(s)\";\n\n            if (cappedAtLimit && !maxResults) {\n                statusMessage += \" (au moins \" + COUNT_CAP + \" \u2014 affinez la recherche pour un compte exact)\";\n            }\n        }\n        else {\n            statusMessage =\n                \"\u26a0\ufe0f Total exact impossible \u00e0 calculer pour cette recherche \"\n                + \"(trop de crit\u00e8res combin\u00e9s). \"\n                + listRows.length + \" r\u00e9sultat(s) affich\u00e9(s) sur CETTE PAGE \"\n                + \"uniquement \u2014 ce n'est pas le total r\u00e9el, qui peut \u00eatre bien \"\n                + \"plus \u00e9lev\u00e9.\";\n        }\n\n        statusElement.textContent = statusMessage + \".\";\n\n        renderResults();\n\n    }\n    catch (error) {\n\n        if (error.name === \"AbortError\") {\n            statusElement.textContent = \"\u23f9 Recherche annul\u00e9e.\";\n        }\n        else {\n            console.error(\"Wikidata :\", error);\n            statusElement.textContent = \"\u274c Impossible d'interroger Wikidata : \" + error.message;\n        }\n    }\n    finally {\n        isLoading = false;\n        cancelButton.disabled = true;\n        abortController = null;\n    }\n}\n\n\/* =========================================================\n   CONVERSION (fusion ligne liste + ligne d\u00e9tails)\n========================================================= *\/\n\nfunction convertResult(listRow, detailsRow) {\n\n    const details = detailsRow || {};\n\n    const website = details.website ? details.website.value : \"\";\n    const imdb = details.imdb ? details.imdb.value : \"\";\n    const uri = listRow.item ? listRow.item.value : \"\";\n\n    const primaryLink = website || imdb || uri;\n\n    return {\n        uri,\n        title: listRow.labelAgg ? listRow.labelAgg.value : \"\",\n        poster: \"\",\n        directors: details.directors ? details.directors.value : \"\",\n        cast: details.cast ? details.cast.value : \"\",\n        date: listRow.dateAgg ? listRow.dateAgg.value : \"\",\n        website,\n        imdb,\n        primaryLink,\n        source: \"wikidata\"\n    };\n}\n\n\/* =========================================================\n   AFFICHAGE\n========================================================= *\/\n\nfunction renderResults() {\n\n    resultsElement.innerHTML = \"\";\n\n    const isReligiousSearch =\n        religiousFilter.checked\n        || document.querySelectorAll(\".wdfc-lexicalKeyword:checked\").length > 0;\n\n    currentResults.forEach(film => {\n\n        const card = document.createElement(\"article\");\n        card.className = \"card\";\n\n        if (film.tmdbId) {\n            card.dataset.tmdbId = film.tmdbId;\n        }\n\n        const year = film.date ? new Date(film.date).getFullYear() : \"\";\n\n        const castList = film.cast\n            ? film.cast.split(\", \").slice(0, 4).join(\", \")\n            : \"\";\n\n        const hasRealPoster = Boolean(film.poster);\n        const filmTitle = film.title || \"Titre non renseign\u00e9\";\n        const destinationUrl = getFilmDestinationUrl(film);\n\n        card.innerHTML = `\n\n            <div class=\"poster-wrap\">\n\n                <img decoding=\"async\" class=\"poster\" src=\"${film.poster || DEFAULT_POSTER}\"\n                     alt=\"${escapeHTML(filmTitle)}\" loading=\"lazy\">\n\n                ${!hasRealPoster ? `<div class=\"poster-fallback-title\">${escapeHTML(filmTitle)}<\/div>` : \"\"}\n\n                ${year ? `<div class=\"year-badge\">${year}<\/div>` : \"\"}\n\n                ${isReligiousSearch ? `<div class=\"religious-badge\">\u271d\ufe0f Religieux<\/div>` : \"\"}\n\n                <a class=\"poster-overlay\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\n                    <span class=\"overlay-btn\">\u25b6 Voir le film<\/span>\n                <\/a>\n\n            <\/div>\n\n            <div class=\"card-content\">\n\n                ${film.directors ? `<div class=\"meta-line\"><b>R\u00e9alisation :<\/b> ${escapeHTML(film.directors)}<\/div>` : \"\"}\n\n                ${castList ? `<div class=\"meta-line\"><b>Avec :<\/b> ${escapeHTML(castList)}<\/div>` : \"\"}\n\n                <div class=\"meta-line\">\ud83d\udcc5 ${formatDate(film.date)}<\/div>\n\n                <div class=\"card-links\">\n                    <a class=\"link-primary\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\u25b6 Voir le film<\/a>\n                    <a class=\"link-secondary\" href=\"${film.uri}\" target=\"_blank\" rel=\"noopener\">${film.isTMDB ? \"TMDB\" : \"Wikidata\"}<\/a>\n                <\/div>\n\n            <\/div>\n        `;\n\n        resultsElement.appendChild(card);\n    });\n\n    updatePagination();\n}\n\n\/* =========================================================\n   PAGINATION\n========================================================= *\/\n\nfunction updatePagination() {\n\n    if (totalKnown) {\n        pageInfo.textContent = \"Page \" + currentPage + \" \/ \" + totalPages;\n        previousButton.disabled = currentPage <= 1;\n        nextButton.disabled = currentPage >= totalPages;\n        pageInput.max = totalPages;\n    }\n    else {\n        pageInfo.textContent = \"Page \" + currentPage + \" (total non calcul\u00e9)\";\n        previousButton.disabled = currentPage <= 1;\n        nextButton.disabled = !hasNextPageUnknown;\n        pageInput.removeAttribute(\"max\");\n    }\n}\n\n\/* =========================================================\n   DATE \/ SECURITE\n========================================================= *\/\n\nfunction formatDate(date) {\n\n    if (!date) return \"Date non renseign\u00e9e\";\n\n    const d = new Date(date);\n\n    if (isNaN(d)) return date;\n\n    return d.toLocaleDateString(\"fr-FR\");\n}\n\nfunction escapeHTML(value) {\n    const div = document.createElement(\"div\");\n    div.textContent = value ? String(value) : \"\";\n    return div.innerHTML;\n}\n\n\/* =========================================================\n   VUE CINEMA\n========================================================= *\/\n\nfunction renderCinemaCard(film) {\n\n    const title = film.title || \"Titre non renseign\u00e9\";\n    const year = film.date ? new Date(film.date).getFullYear() : \"\";\n    const poster = film.poster || DEFAULT_POSTER;\n    const hasPoster = Boolean(film.poster);\n    const destinationUrl = getFilmDestinationUrl(film);\n\n    return `\n        <a class=\"cinema-card\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\n            <div class=\"cinema-poster-wrap\">\n                <img decoding=\"async\" src=\"${poster}\" alt=\"${escapeHTML(title)}\" loading=\"lazy\">\n                ${!hasPoster ? `<div class=\"cinema-fallback-title\">${escapeHTML(title)}<\/div>` : \"\"}\n                ${year ? `<div class=\"cinema-year\">${year}<\/div>` : \"\"}\n            <\/div>\n            <div class=\"cinema-title\">${escapeHTML(title)}<\/div>\n        <\/a>\n    `;\n}\n\nfunction renderCinemaHtml(nowShowing, upcoming, loading) {\n\n    const nowCards = nowShowing.map(renderCinemaCard).join(\"\");\n\n    let upcomingSection = \"\";\n\n    if (loading) {\n        upcomingSection = `<p class=\"cinema-loading\">\u23f3 Recherche des films \u00e0 venir dans la base...<\/p>`;\n    }\n    else if (upcoming.length) {\n        upcomingSection = `\n            <h2 class=\"cinema-section-title\">\ud83d\udd1c Prochainement<\/h2>\n            <div class=\"cinema-grid\">${upcoming.map(renderCinemaCard).join(\"\")}<\/div>\n        `;\n    }\n\n    return `\n        <!DOCTYPE html>\n        <html lang=\"fr\">\n        <head>\n        <meta charset=\"UTF-8\">\n        <title>Au cin\u00e9ma \u2014 S\u00e9lection<\/title>\n        <style>\n            * { box-sizing: border-box; }\n            body { margin:0; font-family: Arial, Helvetica, sans-serif; background:#0b0b0d; color:#fff; padding:28px; }\n            h1 { text-align:center; margin-bottom: 6px; }\n            .cinema-subtitle { text-align:center; opacity:.6; margin-bottom:28px; font-size:13px; }\n            .cinema-section-title { margin: 34px 0 16px; font-size:18px; border-left:4px solid #f0c400; padding-left:10px; }\n            .cinema-loading { opacity:.6; margin-top:20px; }\n            .cinema-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(170px,1fr)); gap:20px; }\n            .cinema-card { color:#fff; text-decoration:none; display:block; background:#1a1a1d; border-radius:12px; overflow:hidden; transition: transform .2s ease; }\n            .cinema-card:hover { transform: translateY(-4px); }\n            .cinema-poster-wrap { position:relative; width:100%; aspect-ratio:2\/3; background:#222; overflow:hidden; }\n            .cinema-poster-wrap img { width:100%; height:100%; object-fit:cover; display:block; }\n            .cinema-fallback-title {\n                position:absolute; left:0; right:0; top:71%; bottom:6%;\n                display:flex; align-items:flex-start; justify-content:center;\n                text-align:center; padding:8px 12px 0; font-weight:bold;\n                font-size:13px; line-height:1.25; text-shadow:0 2px 6px rgba(0,0,0,.9);\n            }\n            .cinema-year { position:absolute; top:8px; left:8px; background:rgba(0,0,0,.65); padding:3px 8px; border-radius:14px; font-size:11px; font-weight:bold; }\n            .cinema-title { padding:10px 12px 14px; font-size:13.5px; font-weight:bold; line-height:1.3; }\n        <\/style>\n        <\/head>\n        <body>\n            <h1>\ud83c\udfac Au cin\u00e9ma<\/h1>\n            <div class=\"cinema-subtitle\">Cliquez sur une affiche pour acc\u00e9der directement \u00e0 la page de visionnage du film<\/div>\n            <div class=\"cinema-grid\">${nowCards || \"<p>Aucun film \u00e0 afficher.<\/p>\"}<\/div>\n            ${upcomingSection}\n        <\/body>\n        <\/html>\n    `;\n}\n\nasync function openCinemaView() {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun film \u00e0 afficher. Lancez d'abord une recherche.\");\n        return;\n    }\n\n    const win = window.open(\"\", \"_blank\");\n\n    if (!win) {\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la vue cin\u00e9ma.\");\n        return;\n    }\n\n    win.document.open();\n    win.document.write(renderCinemaHtml(currentResults, [], true));\n    win.document.close();\n\n    let upcomingFilms = [];\n\n    try {\n\n        const listData = await fetchWikidataOnce(buildUpcomingListQuery(UPCOMING_LIMIT));\n        const rows = listData.results.bindings;\n\n        if (rows.length) {\n\n            const uris = rows.map(row => row.item.value);\n            const detailsData = await fetchWikidataOnce(buildDetailsQuery(uris));\n\n            const detailsByUri = {};\n            detailsData.results.bindings.forEach(row => {\n                detailsByUri[row.item.value] = row;\n            });\n\n            upcomingFilms = rows.map(row => convertResult(row, detailsByUri[row.item.value]));\n        }\n    }\n    catch (error) {\n        console.warn(\"Films \u00e0 venir indisponibles :\", error);\n    }\n\n    if (!win.closed) {\n        win.document.open();\n        win.document.write(renderCinemaHtml(currentResults, upcomingFilms, false));\n        win.document.close();\n    }\n}\n\n\/* =========================================================\n   EVENEMENTS\n========================================================= *\/\n\ncancelButton.addEventListener(\"click\", function () {\n    if (abortController) abortController.abort();\n});\n\ndocument.getElementById(\"wdfc-searchButton\").addEventListener(\"click\", function () {\n    currentPage = 1;\n    searchWikidata();\n});\n\nsearchInput.addEventListener(\"keydown\", function (event) {\n    if (event.key === \"Enter\") {\n        currentPage = 1;\n        searchWikidata();\n    }\n});\n\nnextButton.addEventListener(\"click\", function () {\n    if (currentPage < totalPages) {\n        currentPage++;\n        searchWikidata();\n    }\n});\n\npreviousButton.addEventListener(\"click\", function () {\n    if (currentPage > 1) {\n        currentPage--;\n        searchWikidata();\n    }\n});\n\ndocument.getElementById(\"wdfc-pageButton\").addEventListener(\"click\", function () {\n    let page = Number(pageInput.value);\n    if (page < 1) page = 1;\n    if (totalKnown && page > totalPages) page = totalPages;\n    currentPage = page;\n    searchWikidata();\n});\n\ndocument.getElementById(\"wdfc-reloadButton\").addEventListener(\"click\", searchWikidata);\n\nallLanguagesCheckbox.addEventListener(\"change\", function () {\n    languageOptionsContainer.classList.toggle(\"is-disabled\", allLanguagesCheckbox.checked);\n});\n\ndestinationSiteSelect.addEventListener(\"change\", function () {\n    customSiteUrlInput.disabled = destinationSiteSelect.value !== \"custom\";\n    if (currentResults.length) renderResults();\n});\n\nconst TITLE_COLOR_STORAGE_KEY = \"wdfcFilmCatalogTitleColor\";\n\ntitleColorPicker.addEventListener(\"input\", function () {\n    document.querySelector(\".wdfc-app\").style.setProperty(\"--wdfc-title-color\", titleColorPicker.value);\n\n    try {\n        localStorage.setItem(TITLE_COLOR_STORAGE_KEY, titleColorPicker.value);\n    }\n    catch (error) {\n        console.warn(\"Impossible de sauvegarder la couleur choisie :\", error);\n    }\n});\n\ncustomSiteUrlInput.addEventListener(\"input\", function () {\n    if (destinationSiteSelect.value === \"custom\" && currentResults.length) {\n        renderResults();\n    }\n});\n\nyoutubeButton.addEventListener(\"click\", function () {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun film \u00e0 afficher pour le moment. Lancez d'abord une recherche.\");\n        return;\n    }\n\n    const win = window.open(\"\", \"_blank\");\n\n    if (!win) {\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la s\u00e9lection YouTube.\");\n        return;\n    }\n\n    const linksHtml = currentResults.map(film => {\n        const title = film.title || \"Titre non renseign\u00e9\";\n        const query = encodeURIComponent(title + \" film complet\");\n        const url = \"https:\/\/www.youtube.com\/results?search_query=\" + query;\n        return \"<li><a href=\\\"\" + url + \"\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">\"\n            + escapeHTML(title) + \"<\/a><\/li>\";\n    }).join(\"\");\n\n    win.document.write(\n        \"<!DOCTYPE html><html lang=\\\"fr\\\"><head><meta charset=\\\"UTF-8\\\">\"\n        + \"<title>S\u00e9lection sur YouTube<\/title>\"\n        + \"<style>body{font-family:Arial,Helvetica,sans-serif;background:#0b0b0d;color:#fff;padding:24px;}\"\n        + \"h1{font-size:20px;} a{color:#f0c400;text-decoration:none;} a:hover{text-decoration:underline;}\"\n        + \"li{margin-bottom:10px;font-size:15px;}<\/style><\/head>\"\n        + \"<body><h1>\ud83c\udfac Recherche YouTube \u2014 s\u00e9lection actuelle (\" + currentResults.length + \" film(s))<\/h1>\"\n        + \"<ul>\" + linksHtml + \"<\/ul><\/body><\/html>\"\n    );\n\n    win.document.close();\n});\n\ncinemaButton.addEventListener(\"click\", openCinemaView);\n\ndocument.getElementById(\"wdfc-resetButton\").addEventListener(\"click\", function () {\n\n    if (isLoading && abortController) abortController.abort();\n\n    searchInput.value = \"\";\n    searchCategory.value = \"title\";\n    destinationSiteSelect.value = \"justwatch\";\n    customSiteUrlInput.value = \"\";\n    customSiteUrlInput.disabled = true;\n    dayInput.value = \"\";\n    monthInput.value = \"\";\n    yearInput.value = \"\";\n    yearFromInput.value = \"\";\n    yearToInput.value = \"\";\n    dateFromInput.value = \"\";\n    dateToInput.value = \"\";\n    titleFilter.value = \"all\";\n    dateFilter.value = \"all\";\n    typeFilter.value = \"film_animation\";\n\n    document.querySelectorAll(\".wdfc-languageCheckbox\").forEach(cb => {\n        cb.checked = (cb.value === \"fr\");\n    });\n    allLanguagesCheckbox.checked = false;\n    languageOptionsContainer.classList.remove(\"is-disabled\");\n\n    religiousFilter.checked = true;\n\n    document.querySelectorAll(\".wdfc-lexicalKeyword\").forEach(cb => { cb.checked = false; });\n    lexicalCustomInput.value = \"\";\n    excludeWordsInput.value = \"\";\n\n    sortSelect.value = \"newest\";\n    pageSizeSelect.value = \"20\";\n    maxResultsInput.value = \"\";\n\n    currentPage = 1;\n    totalPages = 1;\n    totalKnown = true;\n    hasNextPageUnknown = false;\n    currentResults = [];\n\n    resultsElement.innerHTML = \"\";\n    progressBar.style.width = \"0%\";\n    pageInput.value = 1;\n    pageInfo.textContent = \"Page 1 \/ 1\";\n\n    statusElement.textContent = \"Filtres r\u00e9initialis\u00e9s.\";\n});\n\ndocument.getElementById(\"wdfc-exportButton\").addEventListener(\"click\", function () {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun r\u00e9sultat \u00e0 exporter.\");\n        return;\n    }\n\n    const header = [\"Titre\", \"R\u00e9alisateur(s)\", \"Casting\", \"Date\", \"Lien\", \"Wikidata\", \"Source\"];\n\n    const rows = currentResults.map(film => [\n        film.title, film.directors, film.cast, film.date, film.primaryLink, film.uri, film.source\n    ]);\n\n    const csv = [header, ...rows]\n        .map(row => row.map(value =>\n            '\"' + String(value || \"\").replace(\/\"\/g, '\"\"') + '\"'\n        ).join(\";\"))\n        .join(\"\\n\");\n\n    const blob = new Blob([\"\\ufeff\" + csv], { type: \"text\/csv;charset=utf-8\" });\n    const url = URL.createObjectURL(blob);\n    const link = document.createElement(\"a\");\n    link.href = url;\n    link.download = \"films-wikidata.csv\";\n    link.click();\n    URL.revokeObjectURL(url);\n});\n\n\/* =========================================================\n   INITIALISATION\n========================================================= *\/\n\nstatusElement.textContent = \"Pr\u00eat. Entrez un terme ou lancez une recherche.\";\nloadAvailableLanguages();\n\ntry {\n    const savedTitleColor = localStorage.getItem(TITLE_COLOR_STORAGE_KEY);\n    if (savedTitleColor) {\n        titleColorPicker.value = savedTitleColor;\n        document.querySelector(\".wdfc-app\").style.setProperty(\"--wdfc-title-color\", savedTitleColor);\n    }\n}\ncatch (error) {\n    console.warn(\"Impossible de relire la couleur sauvegard\u00e9e :\", error);\n}\n\n\/* =========================================================================================\n   MODULE TMDB - PLUS DE FILMS CHRETIENS\n   (imbriqu\u00e9 ici pour partager le scope avec le reste du script \u2014 currentResults,\n   renderResults, etc. restent accessibles sans passer par window global)\n========================================================================================= *\/\n\n(function () {\n\n    \"use strict\";\n\n    const TMDB_API_KEY = \"af5576c58fe6fd7fab447fbb4dd2423a\";\n    const TMDB_API = \"https:\/\/api.themoviedb.org\/3\";\n    const TMDB_LANGUAGE = \"fr-FR\";\n    const TMDB_REGION = \"FR\";\n    const TMDB_MAX_PAGES = 500;\n\n    const CHRISTIAN_KEYWORDS = [\n        \"christianity\", \"christian\", \"christian faith\", \"christian film\",\n        \"religious film\", \"religion\", \"faith\", \"bible\", \"biblical\",\n        \"jesus christ\", \"jesus\", \"gospel\", \"resurrection\", \"crucifixion\",\n        \"church\", \"evangelical christianity\"\n    ];\n\n    const typeFilterEl = document.getElementById(\"wdfc-typeFilter\");\n    const pageSizeSelectEl = document.getElementById(\"wdfc-pageSize\");\n    const maxResultsInputEl = document.getElementById(\"wdfc-maxResults\");\n    const sortSelectEl = document.getElementById(\"wdfc-sort\");\n    const religiousFilterEl = document.getElementById(\"wdfc-religiousFilter\");\n    const allLanguagesCheckboxEl = document.getElementById(\"wdfc-allLanguages\");\n    const languageOptionsContainerEl = document.getElementById(\"wdfc-languageOptions\");\n    const statusElementEl = document.getElementById(\"wdfc-status\");\n    const progressBarEl = document.getElementById(\"wdfc-progressBar\");\n    const resultsElementEl = document.getElementById(\"wdfc-results\");\n\n    if (!typeFilterEl || !pageSizeSelectEl || !maxResultsInputEl || !sortSelectEl) {\n        console.warn(\"TMDB Plus : un ou plusieurs filtres n\u00e9cessaires sont absents.\");\n        return;\n    }\n\n    function createTMDBButton() {\n\n        if (document.getElementById(\"wdfc-tmdbPlusButton\")) return;\n\n        const button = document.createElement(\"button\");\n        button.id = \"wdfc-tmdbPlusButton\";\n        button.type = \"button\";\n        button.textContent = \"\ud83c\udfac Plus de films\";\n\n        const searchButtonEl = document.querySelector(\n            '#wdfc-searchButton, [data-action=\"search\"], button[type=\"submit\"]'\n        );\n\n        if (searchButtonEl && searchButtonEl.parentNode) {\n            searchButtonEl.parentNode.appendChild(button);\n        }\n        else if (resultsElementEl && resultsElementEl.parentNode) {\n            resultsElementEl.parentNode.insertBefore(button, resultsElementEl);\n        }\n        else {\n            document.body.appendChild(button);\n        }\n\n        const status = document.createElement(\"div\");\n        status.id = \"wdfc-tmdbPlusStatus\";\n\n        if (button.parentNode) {\n            button.parentNode.insertBefore(status, button.nextSibling);\n        }\n\n        button.addEventListener(\"click\", function () {\n            searchTMDB();\n        });\n    }\n\n    function setTMDBStatus(message) {\n        const element = document.getElementById(\"wdfc-tmdbPlusStatus\");\n        if (element) {\n            element.textContent = message;\n            element.style.display = \"block\";\n        }\n        if (statusElementEl) {\n            statusElementEl.textContent = message;\n        }\n    }\n\n    function normalizeTitle(value) {\n        return String(value || \"\")\n            .toLowerCase()\n            .normalize(\"NFD\")\n            .replace(\/[\\u0300-\\u036f]\/g, \"\")\n            .replace(\/[^\\p{L}\\p{N}]+\/gu, \" \")\n            .trim();\n    }\n\n    function getYear(value) {\n        if (!value) return \"\";\n        const match = String(value).match(\/\\b(18|19|20)\\d{2}\\b\/);\n        return match ? match[0] : \"\";\n    }\n\n    function getTMDBType() {\n        const value = typeFilterEl.value;\n        if (value === \"film\") return [\"movie\"];\n        if (value === \"animation\") return [\"animation\"];\n        if (value === \"film_animation\") return [\"movie\", \"animation\"];\n        if (value === \"all\") return [\"movie\", \"animation\"];\n        return [\"movie\"];\n    }\n\n    function getSelectedLanguagesTMDB() {\n        if (allLanguagesCheckboxEl && allLanguagesCheckboxEl.checked) return [];\n        if (!languageOptionsContainerEl) return [];\n        const checked = languageOptionsContainerEl.querySelectorAll('input[type=\"checkbox\"]:checked');\n        const languages = [];\n        checked.forEach(function (checkbox) {\n            const language = checkbox.value || checkbox.dataset.lang || checkbox.dataset.language || \"\";\n            if (language) languages.push(language.toLowerCase());\n        });\n        return [...new Set(languages)];\n    }\n\n    async function findKeywordId(keyword) {\n        const params = new URLSearchParams({ api_key: TMDB_API_KEY, query: keyword, page: \"1\" });\n        const response = await fetch(TMDB_API + \"\/search\/keyword?\" + params.toString());\n        if (!response.ok) throw new Error(\"Recherche de mot-cl\u00e9 TMDB : HTTP \" + response.status);\n        const data = await response.json();\n        if (!data.results || !data.results.length) return null;\n        const exact = data.results.find(function (item) {\n            return normalizeTitle(item.name) === normalizeTitle(keyword);\n        });\n        return (exact || data.results[0]).id;\n    }\n\n    async function getKeywordIds() {\n        const results = await Promise.all(\n            CHRISTIAN_KEYWORDS.map(function (keyword) {\n                return findKeywordId(keyword).catch(function (error) {\n                    console.warn(\"Mot-cl\u00e9 ignor\u00e9 :\", keyword, error);\n                    return null;\n                });\n            })\n        );\n        return [...new Set(results.filter(Boolean))];\n    }\n\n    async function fetchTMDBPage(keywordId, page, animationOnly) {\n        const params = new URLSearchParams({\n            api_key: TMDB_API_KEY,\n            language: TMDB_LANGUAGE,\n            region: TMDB_REGION,\n            with_keywords: String(keywordId),\n            sort_by: \"primary_release_date.desc\",\n            include_adult: \"false\",\n            page: String(page)\n        });\n        if (animationOnly) params.set(\"with_genres\", \"16\");\n        const response = await fetch(TMDB_API + \"\/discover\/movie?\" + params.toString());\n        if (!response.ok) throw new Error(\"TMDB Discover : HTTP \" + response.status);\n        return await response.json();\n    }\n\n    function matchesLanguage(movie, languages) {\n        if (!languages.length) return true;\n        const originalLanguage = String(movie.original_language || \"\").toLowerCase();\n        return languages.includes(originalLanguage);\n    }\n\n    function isExistingResult(movie) {\n        if (!Array.isArray(currentResults)) return false;\n        const tmdbId = String(movie.id);\n        const title = normalizeTitle(movie.title || movie.original_title);\n        const year = getYear(movie.release_date);\n\n        return currentResults.some(function (existing) {\n            if (existing.tmdbId && String(existing.tmdbId) === tmdbId) return true;\n            const existingTitle = normalizeTitle(existing.title);\n            const existingYear = getYear(existing.date || existing.year || existing.release_date);\n            if (title && existingTitle && title === existingTitle && year && existingYear && year === existingYear) return true;\n            if (title && existingTitle && title === existingTitle && (!year || !existingYear)) return true;\n            return false;\n        });\n    }\n\n    function convertTMDBResult(movie) {\n        const title = movie.title || movie.original_title || \"Titre non renseign\u00e9\";\n        return {\n            uri: \"tmdb:\" + movie.id,\n            title: title,\n            poster: \"\",\n            image: \"\",\n            directors: \"\",\n            cast: \"\",\n            actors: \"\",\n            date: movie.release_date || \"\",\n            year: getYear(movie.release_date),\n            description: movie.overview || \"\",\n            imdb: \"\",\n            website: \"\",\n            primaryLink: \"https:\/\/www.themoviedb.org\/movie\/\" + movie.id,\n            tmdbId: String(movie.id),\n            source: \"TMDB\",\n            isTMDB: true\n        };\n    }\n\n    async function collectTMDBResults() {\n\n        const selectedTypes = getTMDBType();\n        const languages = getSelectedLanguagesTMDB();\n        const maxRaw = maxResultsInputEl.value.trim();\n        const maxResults = maxRaw ? Math.max(1, Number(maxRaw)) : null;\n        const target = maxResults || Infinity;\n\n        const keywordIds = await getKeywordIds();\n        if (!keywordIds.length) {\n            throw new Error(\"Aucun mot-cl\u00e9 chr\u00e9tien reconnu par TMDB.\");\n        }\n\n        const unique = new Map();\n\n        for (const type of selectedTypes) {\n\n            const animationOnly = type === \"animation\";\n\n            for (const keywordId of keywordIds) {\n\n                let page = 1;\n                let totalPages = 1;\n\n                do {\n\n                    if (unique.size >= target) break;\n\n                    setTMDBStatus(\n                        \"\u23f3 TMDB : recherche \" + type + \" \u2014 mot-cl\u00e9 \" + keywordId\n                        + \" \u2014 page \" + page + \" \/ \" + totalPages\n                    );\n\n                    const data = await fetchTMDBPage(keywordId, page, animationOnly);\n\n                    totalPages = Math.min(Number(data.total_pages || 1), TMDB_MAX_PAGES);\n\n                    const results = data.results || [];\n\n                    results.forEach(function (movie) {\n                        if (!matchesLanguage(movie, languages)) return;\n                        const title = normalizeTitle(movie.title || movie.original_title);\n                        const year = getYear(movie.release_date);\n                        const key = title + \"|\" + year;\n                        if (!unique.has(key)) unique.set(key, movie);\n                    });\n\n                    page++;\n\n                    if (maxResults && unique.size >= maxResults) break;\n\n                }\n                while (page <= totalPages);\n            }\n        }\n\n        let movies = Array.from(unique.values());\n\n        if (sortSelectEl.value === \"oldest\") {\n            movies.sort(function (a, b) {\n                return String(a.release_date || \"\").localeCompare(String(b.release_date || \"\"));\n            });\n        }\n        else if (sortSelectEl.value === \"title\") {\n            movies.sort(function (a, b) {\n                return normalizeTitle(a.title).localeCompare(normalizeTitle(b.title));\n            });\n        }\n        else {\n            movies.sort(function (a, b) {\n                return String(b.release_date || \"\").localeCompare(String(a.release_date || \"\"));\n            });\n        }\n\n        if (maxResults) movies = movies.slice(0, maxResults);\n\n        return movies;\n    }\n\n    async function searchTMDB() {\n\n        const button = document.getElementById(\"wdfc-tmdbPlusButton\");\n        if (button.disabled) return;\n\n        if (religiousFilterEl && !religiousFilterEl.checked) {\n            const proceed = confirm(\n                \"Le filtre \u00ab Films chr\u00e9tiens \/ religieux \u00bb est d\u00e9sactiv\u00e9.\\n\\n\"\n                + \"La recherche TMDB de ce bouton est sp\u00e9cialement con\u00e7ue \"\n                + \"pour les films chr\u00e9tiens et religieux.\\n\\n\"\n                + \"Voulez-vous continuer ?\"\n            );\n            if (!proceed) return;\n        }\n\n        button.disabled = true;\n        if (progressBarEl) progressBarEl.style.width = \"10%\";\n\n        try {\n\n            setTMDBStatus(\"\u23f3 Pr\u00e9paration de la recherche TMDB...\");\n\n            const movies = await collectTMDBResults();\n\n            if (progressBarEl) progressBarEl.style.width = \"70%\";\n\n            let added = 0;\n            let duplicates = 0;\n\n            movies.forEach(function (movie) {\n                if (isExistingResult(movie)) {\n                    duplicates++;\n                    return;\n                }\n                const converted = convertTMDBResult(movie);\n                currentResults.push(converted);\n                added++;\n            });\n\n            if (sortSelectEl.value === \"title\") {\n                currentResults.sort(function (a, b) {\n                    return normalizeTitle(a.title).localeCompare(normalizeTitle(b.title));\n                });\n            }\n            else if (sortSelectEl.value === \"oldest\") {\n                currentResults.sort(function (a, b) {\n                    return String(a.date || \"\").localeCompare(String(b.date || \"\"));\n                });\n            }\n            else {\n                currentResults.sort(function (a, b) {\n                    return String(b.date || \"\").localeCompare(String(a.date || \"\"));\n                });\n            }\n\n            if (typeof renderResults === \"function\") renderResults();\n\n            if (progressBarEl) progressBarEl.style.width = \"100%\";\n\n            setTMDBStatus(\n                \"\u2705 TMDB termin\u00e9 : \" + added + \" nouveau(x) film(s) ajout\u00e9(s). \"\n                + duplicates + \" doublon(s) ignor\u00e9(s).\"\n            );\n\n        }\n        catch (error) {\n            console.error(\"TMDB Plus :\", error);\n            setTMDBStatus(\"\u274c Erreur TMDB : \" + error.message);\n            if (progressBarEl) progressBarEl.style.width = \"100%\";\n        }\n        finally {\n            button.disabled = false;\n        }\n    }\n\n    createTMDBButton();\n\n    console.log(\"\u2705 Module TMDB \u00ab Plus de films \u00bb charg\u00e9.\");\n\n})();\n\n} \/\/ fin de init()\n\nif (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", init);\n}\nelse {\n    init();\n}\n\n})();\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-122df8f e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"122df8f\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-64cde32 elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-html\" data-id=\"64cde32\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!--\n    CATALOGUE FILMS \u2014 version WordPress \/ Elementor\n    =================================================\n    A coller tel quel dans un widget \"HTML\" (ou \"Code HTML\")\n    d'Elementor. Ne PAS coller <!DOCTYPE>, <html>, <head> ou\n    <body> \u2014 il n'y en a plus ici, uniquement le contenu du\n    widget.\n\n    Changements par rapport \u00e0 la version autonome :\n    - Toutes les r\u00e8gles CSS sont scop\u00e9es sous .wdfc-app pour\n      ne pas d\u00e9teindre sur le reste du th\u00e8me (les s\u00e9lecteurs\n      globaux comme \"button\", \"input, select\", \":root\" du\n      fichier d'origine s'appliquaient \u00e0 TOUT le site).\n    - Tous les id=\"\" sont pr\u00e9fix\u00e9s \"wdfc-\" pour \u00e9viter les\n      collisions avec d'autres widgets\/plugins de la page.\n    - Tout le JavaScript est encapsul\u00e9 dans une seule IIFE\n      (au lieu de variables globales comme currentPage,\n      currentResults, searchWikidata...) et d\u00e9marre apr\u00e8s le\n      DOM pr\u00eat.\n    - Le hack \"pleine largeur\" (width:100vw + translateX) a\n      \u00e9t\u00e9 retir\u00e9 car il casse la mise en page \u00e0 l'int\u00e9rieur\n      des conteneurs Elementor. Pour un rendu pleine largeur,\n      utilisez plut\u00f4t l'option native \"Stretch Section\" \/\n      \"\u00c9tirer la section\" d'Elementor sur la section qui\n      contient ce widget.\n    - Le reste (logique de recherche Wikidata, module TMDB,\n      export CSV, vue Cin\u00e9ma, etc.) est inchang\u00e9.\n\n    \u26a0\ufe0f La cl\u00e9 API TMDB reste visible dans le code source c\u00f4t\u00e9\n    navigateur (comme dans l'original) : n'importe qui peut la\n    lire en ouvrant l'inspecteur. Si c'est un probl\u00e8me pour\n    vous, il faudrait faire passer les appels TMDB par un\n    endpoint c\u00f4t\u00e9 serveur (ex. un shortcode PHP ou un plugin\n    \"code snippet\" qui masque la cl\u00e9) \u2014 dites-le moi si vous\n    voulez cette version.\n\n    Limite : ce widget utilise des id fixes, donc n'en placez\n    qu'UNE seule instance par page (deux copies sur la m\u00eame\n    page cr\u00e9eraient des id dupliqu\u00e9s et casseraient le script).\n-->\n\n<div class=\"wdfc-app\">\n\n<style>\n\n.wdfc-app, .wdfc-app * {\n    box-sizing: border-box;\n}\n\n.wdfc-app {\n    color-scheme: dark;\n    font-family: Arial, Helvetica, sans-serif !important;\n    background: #0b0b0d !important;\n    color: #fff !important;\n    margin: 0;\n    padding: 25px;\n}\n\n.wdfc-app .search-box {\n    background: #1d1d1d;\n    border-radius: 14px;\n    padding: 20px;\n    margin-bottom: 20px;\n}\n\n.wdfc-app .field-row {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\n    gap: 12px;\n    margin-bottom: 16px;\n}\n\n.wdfc-app .field-row:last-child { margin-bottom: 0; }\n\n.wdfc-app .field-row.search-row {\n    grid-template-columns: 3fr 1.4fr auto;\n}\n\n.wdfc-app .field label {\n    display: block;\n    font-size: 11.5px;\n    text-transform: uppercase;\n    letter-spacing: .4px;\n    opacity: .55;\n    margin-bottom: 5px;\n    color: #fff !important;\n}\n\n.wdfc-app .field.full-width { grid-column: 1 \/ -1; }\n\n.wdfc-app input,\n.wdfc-app select {\n    width: 100%;\n    padding: 12px;\n    border-radius: 8px;\n    border: 1px solid #444;\n    background: #111 !important;\n    color: #fff !important;\n    font-size: 14px;\n}\n\n.wdfc-app select[multiple] { padding: 8px; }\n\n.wdfc-app select:disabled,\n.wdfc-app input:disabled {\n    opacity: .4;\n    cursor: not-allowed;\n}\n\n.wdfc-app .field-hint {\n    font-size: 11.5px;\n    opacity: .55;\n    margin-top: 6px;\n    color: #fff !important;\n}\n\n.wdfc-app button {\n    padding: 12px 18px;\n    border: 0;\n    border-radius: 8px;\n    background: #f0c400 !important;\n    color: #111 !important;\n    font-weight: bold;\n    cursor: pointer;\n}\n\n.wdfc-app button:hover { opacity: .85; }\n.wdfc-app button:disabled { opacity: .4; cursor: not-allowed; }\n\n.wdfc-app .youtube-button { background: #ff0000 !important; color: #fff !important; }\n.wdfc-app .cinema-button { background: #2c2c30 !important; color: #fff !important; border: 1px solid #f0c400; }\n\n.wdfc-app .toggle-filter {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    padding: 12px;\n    border-radius: 8px;\n    border: 1px solid #444;\n    background: #111;\n    cursor: pointer;\n    user-select: none;\n}\n\n.wdfc-app .toggle-filter input {\n    width: 18px;\n    height: 18px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .toggle-filter span { font-size: 13.5px; color: #fff !important; }\n\n.wdfc-app .lexical-box {\n    border: 1px solid #444;\n    border-radius: 8px;\n    padding: 12px;\n    background: #111;\n}\n\n.wdfc-app .lexical-box legend {\n    font-size: 11.5px;\n    text-transform: uppercase;\n    letter-spacing: .4px;\n    opacity: .65;\n    padding: 0 6px;\n    color: #fff !important;\n}\n\n.wdfc-app .lexical-options {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 12px;\n    margin-top: 6px;\n}\n\n.wdfc-app .lexical-options label {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 13px;\n    cursor: pointer;\n    color: #fff !important;\n}\n\n.wdfc-app .lexical-options input {\n    width: 16px;\n    height: 16px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .language-options {\n    max-height: 190px;\n    overflow-y: auto;\n    border: 1px solid #444;\n    border-radius: 8px;\n    padding: 12px;\n    background: #111;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 10px 18px;\n}\n\n.wdfc-app .language-options.is-disabled {\n    opacity: .4;\n    pointer-events: none;\n}\n\n.wdfc-app .language-options label {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 13px;\n    cursor: pointer;\n    color: #fff !important;\n}\n\n.wdfc-app .language-options input {\n    width: 16px;\n    height: 16px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .status { margin: 20px 0; text-align: center; min-height: 25px; color: #fff !important; }\n\n.wdfc-app .progress {\n    width: 100%;\n    height: 8px;\n    background: #333;\n    border-radius: 10px;\n    overflow: hidden;\n    margin-top: 15px;\n}\n\n.wdfc-app .progress-bar {\n    width: 0%;\n    height: 100%;\n    background: #f0c400;\n    transition: width .2s;\n}\n\n.wdfc-app .results {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n    gap: 24px;\n}\n\n.wdfc-app .card {\n    position: relative;\n    background: #1a1a1d;\n    border-radius: 14px;\n    overflow: hidden;\n    box-shadow: 0 4px 14px rgba(0,0,0,.4);\n    transition: transform .25s ease, box-shadow .25s ease;\n    display: flex;\n    flex-direction: column;\n}\n\n.wdfc-app .card:hover {\n    transform: translateY(-6px);\n    box-shadow: 0 14px 30px rgba(0,0,0,.6);\n}\n\n.wdfc-app .poster-wrap {\n    position: relative;\n    width: 100%;\n    aspect-ratio: 2\/3;\n    background: #222;\n    overflow: hidden;\n}\n\n.wdfc-app .poster {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    display: block;\n    transition: transform .35s ease;\n}\n\n.wdfc-app .card:hover .poster { transform: scale(1.06); }\n\n.wdfc-app .poster-fallback-title {\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: 71%;\n    bottom: 6%;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    text-align: center;\n    padding: 8px 14px 0;\n    font-weight: bold;\n    font-size: clamp(13px, 1.4vw + 8px, 18px);\n    line-height: 1.25;\n    color: var(--wdfc-title-color, #00e5ff) !important;\n    text-shadow: 0 2px 8px rgba(0,0,0,.9);\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.wdfc-app .year-badge {\n    position: absolute;\n    top: 10px;\n    left: 10px;\n    background: rgba(0,0,0,.65);\n    backdrop-filter: blur(3px);\n    padding: 4px 10px;\n    border-radius: 20px;\n    font-size: 12px;\n    font-weight: bold;\n    color: #fff !important;\n}\n\n.wdfc-app .religious-badge {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    background: rgba(240,196,0,.9);\n    color: #111 !important;\n    padding: 4px 10px;\n    border-radius: 20px;\n    font-size: 11px;\n    font-weight: bold;\n}\n\n.wdfc-app .poster-overlay {\n    position: absolute;\n    inset: 0;\n    background: linear-gradient(to top, rgba(0,0,0,.92) 0%, rgba(0,0,0,.15) 55%, rgba(0,0,0,0) 100%);\n    opacity: 0;\n    transition: opacity .25s ease;\n    display: flex;\n    align-items: flex-end;\n    padding: 14px;\n}\n\n.wdfc-app .card:hover .poster-overlay { opacity: 1; }\n\n.wdfc-app .overlay-btn {\n    display: inline-block;\n    width: 100%;\n    text-align: center;\n    padding: 10px;\n    background: #f0c400;\n    color: #111 !important;\n    font-weight: bold;\n    text-decoration: none;\n    border-radius: 8px;\n    font-size: 13px;\n}\n\n.wdfc-app .card-content {\n    padding: 14px 16px 16px;\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n}\n\n.wdfc-app .meta-line {\n    font-size: 12.5px;\n    color: rgba(255,255,255,.75) !important;\n    line-height: 1.5;\n    overflow-wrap: break-word;\n}\n\n.wdfc-app .meta-line b { color: #fff !important; }\n\n.wdfc-app .card-links {\n    margin-top: auto;\n    display: flex;\n    gap: 8px;\n    padding-top: 8px;\n}\n\n.wdfc-app .link-primary,\n.wdfc-app .link-secondary {\n    text-decoration: none;\n    text-align: center;\n    border-radius: 7px;\n    font-size: 12.5px;\n    font-weight: bold;\n    padding: 9px 6px;\n}\n\n.wdfc-app .link-primary { flex: 2; background: #f0c400; color: #111 !important; }\n.wdfc-app .link-secondary { flex: 1; background: #2c2c30; color: #fff !important; }\n.wdfc-app .link-primary:hover,\n.wdfc-app .link-secondary:hover { opacity: .85; }\n\n.wdfc-app #wdfc-titleColorPicker {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 22px;\n    height: 22px;\n    padding: 0;\n    border: 2px solid #444;\n    border-radius: 50%;\n    cursor: pointer;\n    background: none;\n    overflow: hidden;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-webkit-color-swatch-wrapper {\n    padding: 0;\n    border-radius: 50%;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-webkit-color-swatch {\n    border: none;\n    border-radius: 50%;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-moz-color-swatch {\n    border: none;\n    border-radius: 50%;\n}\n\n.wdfc-app .pagination {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    gap: 15px;\n    margin-top: 30px;\n    flex-wrap: wrap;\n    color: #fff !important;\n}\n\n@media (max-width: 700px) {\n    .wdfc-app .field-row.search-row { grid-template-columns: 1fr; }\n}\n\n\/* ===== Module TMDB \"Plus de films\" ===== *\/\n\n.wdfc-app #wdfc-tmdbPlusButton {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: 8px;\n    margin: 8px 0;\n    padding: 10px 18px;\n    border: 0;\n    border-radius: 8px;\n    background: #01b4e4 !important;\n    color: #ffffff !important;\n    font-weight: 700;\n    cursor: pointer;\n    transition: opacity .2s, transform .2s;\n}\n\n.wdfc-app #wdfc-tmdbPlusButton:hover {\n    opacity: .88;\n    transform: translateY(-1px);\n}\n\n.wdfc-app #wdfc-tmdbPlusButton:disabled {\n    opacity: .5;\n    cursor: wait;\n    transform: none;\n}\n\n.wdfc-app #wdfc-tmdbPlusStatus {\n    display: none;\n    width: 100%;\n    box-sizing: border-box;\n    margin: 8px 0 14px;\n    padding: 10px 14px;\n    border-radius: 8px;\n    background: rgba(1,180,228,.12);\n    border: 1px solid rgba(1,180,228,.35);\n    color: #fff !important;\n    font-size: 13px;\n    line-height: 1.45;\n}\n\n<\/style>\n\n<section class=\"search-box\">\n\n<div class=\"field-row search-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-search\">Recherche libre<\/label>\n<input id=\"wdfc-search\" type=\"search\" placeholder=\"Mot ou partie de mot...\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-searchCategory\">Rechercher dans<\/label>\n<select id=\"wdfc-searchCategory\">\n<option value=\"title\" selected>Titre<\/option>\n<option value=\"director\">R\u00e9alisateur<\/option>\n<option value=\"cast\">Acteur<\/option>\n<option value=\"all\">Titre + r\u00e9alisateur + acteur<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-searchButton\">\ud83d\udd0e Rechercher<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-destinationSite\">Ouvrir les films sur<\/label>\n<select id=\"wdfc-destinationSite\">\n<option value=\"justwatch\" selected>JustWatch<\/option>\n<option value=\"allocine\">AlloCin\u00e9<\/option>\n<option value=\"imdb\">IMDb<\/option>\n<option value=\"custom\">URL personnalis\u00e9e<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-titleColorPicker\">Couleur des titres de films<\/label>\n<input id=\"wdfc-titleColorPicker\" type=\"color\" value=\"#00e5ff\">\n<\/div>\n\n<div class=\"field full-width\">\n<label for=\"wdfc-customSiteUrl\">URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus)<\/label>\n<input\n    id=\"wdfc-customSiteUrl\"\n    type=\"text\"\n    placeholder=\"https:\/\/exemple.com\/recherche?q={titre}\"\n    disabled\n>\n<div class=\"field-hint\">\nUtilisez <code>{titre}<\/code> dans l'URL pour qu'il soit remplac\u00e9 par le titre du film \u00e0 chaque clic. Sans <code>{titre}<\/code>, le titre est ajout\u00e9 automatiquement en param\u00e8tre <code>q=<\/code> \u00e0 la fin de l'URL saisie. Les films venant de TMDB (bouton \"Plus de films\") s'ouvrent toujours directement sur TMDB, quel que soit ce choix \u2014 voir note plus bas.\n<\/div>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-dateFilter\">Date de sortie<\/label>\n<select id=\"wdfc-dateFilter\">\n<option value=\"all\">Toutes les dates<\/option>\n<option value=\"with\">Avec date de sortie<\/option>\n<option value=\"without\">Sans date de sortie<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-day\">Derniers jours<\/label>\n<input id=\"wdfc-day\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-month\">Derniers mois<\/label>\n<input id=\"wdfc-month\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-year\">Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3)<\/label>\n<input id=\"wdfc-year\" type=\"number\" placeholder=\"Ex : 2026 ou 3\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-yearFrom\">Ann\u00e9e de (fourchette)<\/label>\n<input id=\"wdfc-yearFrom\" type=\"number\" placeholder=\"De\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-yearTo\">Ann\u00e9e \u00e0 (fourchette)<\/label>\n<input id=\"wdfc-yearTo\" type=\"number\" placeholder=\"\u00c0\" min=\"1\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-dateFrom\">Date pr\u00e9cise de d\u00e9but<\/label>\n<input id=\"wdfc-dateFrom\" type=\"date\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-dateTo\">Date pr\u00e9cise de fin<\/label>\n<input id=\"wdfc-dateTo\" type=\"date\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-titleFilter\">Titre<\/label>\n<select id=\"wdfc-titleFilter\">\n<option value=\"all\">Tous les films<\/option>\n<option value=\"with\">Avec un titre<\/option>\n<option value=\"without\">Sans titre<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-typeFilter\">Type<\/label>\n<select id=\"wdfc-typeFilter\">\n<option value=\"film\">Films uniquement<\/option>\n<option value=\"animation\">Dessins anim\u00e9s uniquement<\/option>\n<option value=\"film_animation\" selected>Films + dessins anim\u00e9s<\/option>\n<option value=\"all\">Films et \u0153uvres audiovisuelles (large)<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-sort\">Tri<\/label>\n<select id=\"wdfc-sort\">\n<option value=\"newest\">Plus r\u00e9cents<\/option>\n<option value=\"oldest\">Plus anciens<\/option>\n<option value=\"title\">Titre A-Z<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-pageSize\">R\u00e9sultats par page<\/label>\n<input id=\"wdfc-pageSize\" type=\"number\" min=\"1\" max=\"500\" value=\"20\" list=\"wdfc-pageSizePresets\">\n<datalist id=\"wdfc-pageSizePresets\">\n<option value=\"20\">\n<option value=\"50\">\n<option value=\"100\">\n<option value=\"200\">\n<\/datalist>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-maxResults\">Nombre maxi de r\u00e9sultats<\/label>\n<input id=\"wdfc-maxResults\" type=\"number\" placeholder=\"Illimit\u00e9\" min=\"1\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field full-width\">\n<label>Langue(s) du titre <span id=\"wdfc-languageCount\" style=\"opacity:.5; text-transform:none;\"><\/span><\/label>\n<div id=\"wdfc-languageOptions\" class=\"language-options\">\n<span style=\"opacity:.6; font-size:13px;\">Chargement des langues disponibles dans la base...<\/span>\n<\/div>\n\n<label class=\"toggle-filter\" style=\"margin-top:8px;\" for=\"wdfc-allLanguages\">\n<input type=\"checkbox\" id=\"wdfc-allLanguages\">\n<span>\ud83c\udf10 Toutes les langues (ignore la liste ci-dessus)<\/span>\n<\/label>\n\n<div class=\"field-hint\">\nCochez une, plusieurs, ou toutes les langues souhait\u00e9es. Cochez \u00ab Toutes les langues \u00bb pour ne faire aucun tri par langue.\n<\/div>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<label class=\"toggle-filter\" for=\"wdfc-religiousFilter\">\n<input type=\"checkbox\" id=\"wdfc-religiousFilter\" checked>\n<span>\u271d\ufe0f Films chr\u00e9tiens \/ religieux (genre \/ sujet Wikidata)<\/span>\n<\/label>\n\n<fieldset class=\"lexical-box\">\n<legend>\u00c9largir la recherche (mots-cl\u00e9s dans le titre)<\/legend>\n<div class=\"lexical-options\">\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"chr\u00e9tien,christian\"> Chr\u00e9tien<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"religieux,religious\"> Religieux<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"biblique,biblical,bible\"> Biblique<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"spirituel,spiritual\"> Spirituel<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"croyance,belief,faith\"> Croyance<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"\u00e9vangile,gospel\"> \u00c9vangile<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"dieu,\u00e9ternel,god,eternal\"> Dieu \/ \u00c9ternel<\/label>\n<\/div>\n<input\n    id=\"wdfc-lexicalCustom\"\n    type=\"text\"\n    placeholder=\"Autres mots-cl\u00e9s, s\u00e9par\u00e9s par une virgule (ex : messe, pri\u00e8re)\"\n    style=\"margin-top:8px;\"\n>\n<\/fieldset>\n\n<fieldset class=\"lexical-box\">\n<legend>Exclure les films contenant ces mots (titre)<\/legend>\n<input\n    id=\"wdfc-excludeWords\"\n    type=\"text\"\n    placeholder=\"Mots \u00e0 exclure, s\u00e9par\u00e9s par une virgule (ex : horreur, zombie)\"\n>\n<\/fieldset>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-cancelButton\" disabled>\u23f9 Annuler<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-resetButton\">\ud83e\uddf9 R\u00e9initialiser<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-pageInput\">Aller \u00e0 la page<\/label>\n<input id=\"wdfc-pageInput\" type=\"number\" min=\"1\" value=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-pageButton\">\ud83d\udcc4 Aller \u00e0 la page<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-exportButton\">\u2b07\ufe0f Exporter CSV<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-reloadButton\">\ud83d\udd04 Rechercher<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-youtubeButton\" class=\"youtube-button\" type=\"button\">\u25b6\ufe0f S\u00e9lection sur YouTube<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-cinemaButton\" class=\"cinema-button\" type=\"button\">\ud83c\udfa6 Vue Cin\u00e9ma<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"progress\"><div id=\"wdfc-progressBar\" class=\"progress-bar\"><\/div><\/div>\n\n<\/section>\n\n<div id=\"wdfc-status\" class=\"status\">Pr\u00eat.<\/div>\n\n<div id=\"wdfc-results\" class=\"results\"><\/div>\n\n<div class=\"pagination\">\n<button id=\"wdfc-previousButton\">\u25c0 Pr\u00e9c\u00e9dent<\/button>\n<span id=\"wdfc-pageInfo\">Page 1 \/ 1<\/span>\n<button id=\"wdfc-nextButton\">Suivant \u25b6<\/button>\n<\/div>\n\n<\/div>\n\n\n<script>\n(function () {\n\"use strict\";\n\nfunction init() {\n\n\/* =========================================================\n   CONFIGURATION\n========================================================= *\/\n\nconst WIKIDATA_ENDPOINT = \"https:\/\/query.wikidata.org\/sparql\";\n\nconst RELIGIOUS_GENRE_IDS = [\"wd:Q115122793\", \"wd:Q7717463\"];\n\nconst RELIGIOUS_SUBJECT_IDS = [\n    \"wd:Q5043\", \"wd:Q302\", \"wd:Q766677\", \"wd:Q190\", \"wd:Q194253\",\n    \"wd:Q5410500\", \"wd:Q1845\", \"wd:Q34274\", \"wd:Q34394\", \"wd:Q9174\",\n    \"wd:Q2728698\", \"wd:Q51628\", \"wd:Q51636\", \"wd:Q51624\",\n    \"wd:Q9077\", \"wd:Q81422\", \"wd:Q41370\", \"wd:Q9200\"\n];\n\nconst FILM_CLASS_ID = \"wd:Q11424\";\nconst ANIMATED_FILM_CLASS_ID = \"wd:Q202866\";\n\nconst COUNT_CAP = 3000;\nconst LANGUAGE_SAMPLE_SIZE = 20000;\nconst UPCOMING_LIMIT = 24;\n\nconst FALLBACK_LANGUAGE_CODES = [\n    \"fr\",\"en\",\"es\",\"de\",\"it\",\"pt\",\"nl\",\"ar\",\"ru\",\"zh\",\"ja\",\"ko\",\n    \"hi\",\"tr\",\"pl\",\"sv\",\"no\",\"da\",\"fi\",\"el\",\"he\",\"cs\",\"hu\",\"ro\",\"uk\"\n];\n\nconst DEFAULT_POSTER =\n    \"data:image\/svg+xml,\" +\n    encodeURIComponent(\n        '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 400 600\">' +\n        '<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">' +\n        '<stop offset=\"0\" stop-color=\"#34343e\"\/>' +\n        '<stop offset=\"1\" stop-color=\"#131316\"\/>' +\n        '<\/linearGradient><\/defs>' +\n        '<rect width=\"400\" height=\"600\" fill=\"url(#g)\"\/>' +\n        '<circle cx=\"200\" cy=\"248\" r=\"74\" fill=\"none\" stroke=\"#f0c400\" stroke-width=\"6\" opacity=\"0.9\"\/>' +\n        '<polygon points=\"184,210 184,286 250,248\" fill=\"#f0c400\" opacity=\"0.95\"\/>' +\n        '<text x=\"200\" y=\"412\" text-anchor=\"middle\" font-family=\"Arial, Helvetica, sans-serif\" ' +\n        'font-size=\"42\" font-weight=\"bold\" letter-spacing=\"10\" fill=\"#ffffff\" opacity=\"0.9\">FILM<\/text>' +\n        '<\/svg>'\n    );\n\n\/* =========================================================\n   VARIABLES\n========================================================= *\/\n\nlet currentPage = 1;\nlet totalPages = 1;\nlet totalKnown = true;\nlet hasNextPageUnknown = false;\nlet currentResults = [];\nlet abortController = null;\nlet isLoading = false;\n\n\/* =========================================================\n   ELEMENTS\n========================================================= *\/\n\nconst searchInput = document.getElementById(\"wdfc-search\");\nconst searchCategory = document.getElementById(\"wdfc-searchCategory\");\nconst destinationSiteSelect = document.getElementById(\"wdfc-destinationSite\");\nconst titleColorPicker = document.getElementById(\"wdfc-titleColorPicker\");\nconst customSiteUrlInput = document.getElementById(\"wdfc-customSiteUrl\");\nconst dayInput = document.getElementById(\"wdfc-day\");\nconst monthInput = document.getElementById(\"wdfc-month\");\nconst yearInput = document.getElementById(\"wdfc-year\");\nconst yearFromInput = document.getElementById(\"wdfc-yearFrom\");\nconst yearToInput = document.getElementById(\"wdfc-yearTo\");\nconst dateFromInput = document.getElementById(\"wdfc-dateFrom\");\nconst dateToInput = document.getElementById(\"wdfc-dateTo\");\nconst titleFilter = document.getElementById(\"wdfc-titleFilter\");\nconst dateFilter = document.getElementById(\"wdfc-dateFilter\");\nconst sortSelect = document.getElementById(\"wdfc-sort\");\nconst typeFilter = document.getElementById(\"wdfc-typeFilter\");\nconst religiousFilter = document.getElementById(\"wdfc-religiousFilter\");\nconst lexicalCustomInput = document.getElementById(\"wdfc-lexicalCustom\");\nconst excludeWordsInput = document.getElementById(\"wdfc-excludeWords\");\nconst languageOptionsContainer = document.getElementById(\"wdfc-languageOptions\");\nconst languageCountLabel = document.getElementById(\"wdfc-languageCount\");\nconst allLanguagesCheckbox = document.getElementById(\"wdfc-allLanguages\");\nconst pageSizeSelect = document.getElementById(\"wdfc-pageSize\");\nconst maxResultsInput = document.getElementById(\"wdfc-maxResults\");\nconst statusElement = document.getElementById(\"wdfc-status\");\nconst resultsElement = document.getElementById(\"wdfc-results\");\nconst progressBar = document.getElementById(\"wdfc-progressBar\");\nconst cancelButton = document.getElementById(\"wdfc-cancelButton\");\nconst pageInput = document.getElementById(\"wdfc-pageInput\");\nconst previousButton = document.getElementById(\"wdfc-previousButton\");\nconst nextButton = document.getElementById(\"wdfc-nextButton\");\nconst pageInfo = document.getElementById(\"wdfc-pageInfo\");\nconst youtubeButton = document.getElementById(\"wdfc-youtubeButton\");\nconst cinemaButton = document.getElementById(\"wdfc-cinemaButton\");\n\n\/* =========================================================\n   SITE D'OUVERTURE DES FILMS\n========================================================= *\/\n\nfunction getFilmDestinationUrl(film) {\n\n    if (film.isTMDB || film.source === \"TMDB\") {\n        return film.primaryLink;\n    }\n\n    const title = film.title || \"\";\n    const encodedTitle = encodeURIComponent(title);\n    const site = destinationSiteSelect.value;\n\n    if (site === \"allocine\") {\n        return title\n            ? `https:\/\/www.allocine.fr\/rechercher\/?q=${encodedTitle}`\n            : film.uri;\n    }\n\n    if (site === \"imdb\") {\n        if (film.imdb) return film.imdb;\n        return title\n            ? `https:\/\/www.imdb.com\/find\/?q=${encodedTitle}`\n            : film.uri;\n    }\n\n    if (site === \"custom\") {\n\n        const template = customSiteUrlInput.value.trim();\n\n        if (!template) return film.primaryLink;\n\n        if (template.includes(\"{titre}\")) {\n            return template.split(\"{titre}\").join(encodedTitle);\n        }\n\n        if (!title) return film.primaryLink;\n\n        const separator = template.includes(\"?\") ? \"&\" : \"?\";\n        return `${template}${separator}q=${encodedTitle}`;\n    }\n\n    if (!title) return film.uri;\n\n    return `https:\/\/www.justwatch.com\/fr\/recherche?q=${encodedTitle}`;\n}\n\n\/* =========================================================\n   UTILITAIRES DATE\n========================================================= *\/\n\nfunction daysAgo(n) {\n    const d = new Date();\n    d.setDate(d.getDate() - n);\n    return d;\n}\n\nfunction monthsAgo(n) {\n    const d = new Date();\n    d.setMonth(d.getMonth() - n);\n    return d;\n}\n\nfunction yearsAgo(n) {\n    const d = new Date();\n    d.setFullYear(d.getFullYear() - n);\n    return d;\n}\n\n\/* =========================================================\n   REQUETE SIMPLE (sans annulation), AVEC TENTATIVES AUTOMATIQUES\n========================================================= *\/\n\nasync function fetchWikidataOnce(query, attempt = 0) {\n\n    const response = await fetch(WIKIDATA_ENDPOINT, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\n            \"Accept\": \"application\/sparql-results+json\"\n        },\n        body: \"query=\" + encodeURIComponent(query)\n    });\n\n    if ([429, 502, 503, 504].includes(response.status) && attempt < 3) {\n\n        const delay = (attempt + 1) * 2500;\n\n        await new Promise(resolve => setTimeout(resolve, delay));\n\n        return fetchWikidataOnce(query, attempt + 1);\n    }\n\n    if (!response.ok) {\n        throw new Error(\"Erreur HTTP \" + response.status);\n    }\n\n    return response.json();\n}\n\n\/* =========================================================\n   LANGUES DISPONIBLES DANS LA BASE\n========================================================= *\/\n\nfunction languageDisplayName(code) {\n    try {\n        const displayNames = new Intl.DisplayNames([\"fr\"], { type: \"language\" });\n        const name = displayNames.of(code);\n        return name && name.toLowerCase() !== code.toLowerCase() ? name : code;\n    }\n    catch (error) {\n        return code;\n    }\n}\n\nfunction populateLanguageSelect(codes) {\n\n    const uniqueCodes = Array.from(new Set(codes)).filter(Boolean);\n\n    uniqueCodes.sort((a, b) =>\n        languageDisplayName(a).localeCompare(languageDisplayName(b), \"fr\")\n    );\n\n    languageOptionsContainer.innerHTML = \"\";\n\n    uniqueCodes.forEach(code => {\n\n        const label = document.createElement(\"label\");\n\n        const checkbox = document.createElement(\"input\");\n        checkbox.type = \"checkbox\";\n        checkbox.className = \"wdfc-languageCheckbox\";\n        checkbox.value = code;\n        if (code === \"fr\") checkbox.checked = true;\n\n        const span = document.createElement(\"span\");\n        span.textContent = languageDisplayName(code) + \" (\" + code + \")\";\n\n        label.appendChild(checkbox);\n        label.appendChild(span);\n        languageOptionsContainer.appendChild(label);\n    });\n\n    if (!languageOptionsContainer.querySelector(\".wdfc-languageCheckbox:checked\") && uniqueCodes.length) {\n        languageOptionsContainer.querySelector(\".wdfc-languageCheckbox\").checked = true;\n    }\n\n    languageCountLabel.textContent = \"(\" + uniqueCodes.length + \" langues trouv\u00e9es)\";\n}\n\nasync function loadAvailableLanguages() {\n\n    try {\n\n        const query = `\n            SELECT DISTINCT ?lang WHERE {\n                {\n                    SELECT DISTINCT ?item WHERE {\n                        ?item wdt:P31 ${FILM_CLASS_ID} .\n                    }\n                    LIMIT ${LANGUAGE_SAMPLE_SIZE}\n                }\n                ?item rdfs:label ?itemLabel .\n                BIND(LANG(?itemLabel) AS ?lang)\n            }\n            ORDER BY ?lang\n        `;\n\n        const data = await fetchWikidataOnce(query);\n\n        const codes = data.results.bindings\n            .map(row => row.lang.value)\n            .filter(code => code && code.length <= 8);\n\n        if (codes.length === 0) {\n            throw new Error(\"Aucune langue retourn\u00e9e\");\n        }\n\n        populateLanguageSelect(codes);\n    }\n    catch (error) {\n        console.warn(\"Langues Wikidata indisponibles, repli sur une liste par d\u00e9faut :\", error);\n        populateLanguageSelect(FALLBACK_LANGUAGE_CODES);\n    }\n}\n\n\/* =========================================================\n   LANGUES SELECTIONNEES\n========================================================= *\/\n\nfunction getSelectedLanguages() {\n\n    if (allLanguagesCheckbox.checked) {\n        return null;\n    }\n\n    const selected = Array.from(document.querySelectorAll(\".wdfc-languageCheckbox:checked\"))\n        .map(cb => cb.value)\n        .filter(v => v);\n\n    return selected.length ? selected : [\"fr\"];\n}\n\n\/* =========================================================\n   CONDITIONS DE BASE\n========================================================= *\/\n\nfunction buildBaseConditions() {\n\n    const search = searchInput.value.trim();\n\n    const selectedLanguages = getSelectedLanguages();\n\n    let labelCondition;\n\n    if (selectedLanguages === null) {\n        labelCondition = `?item rdfs:label ?itemLabel .`;\n    }\n    else {\n        const langList = selectedLanguages.map(l => `\"${l}\"`).join(\",\");\n        labelCondition = `\n            ?item rdfs:label ?itemLabel .\n            FILTER(LANG(?itemLabel) IN (${langList}))\n        `;\n    }\n\n    let typeCondition = `?item wdt:P31 ${FILM_CLASS_ID} .`;\n\n    if (typeFilter.value === \"animation\") {\n        typeCondition = `\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?animType . ?animType wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\n        `;\n    }\n    else if (typeFilter.value === \"film_animation\") {\n        typeCondition = `\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?filmType . ?filmType wdt:P279* ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?animType2 . ?animType2 wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\n        `;\n    }\n    else if (typeFilter.value === \"all\") {\n        typeCondition = `\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?type . ?type wdt:P279* ${FILM_CLASS_ID} . }\n        `;\n    }\n\n    const lexicalKeywords =\n        Array.from(document.querySelectorAll(\".wdfc-lexicalKeyword:checked\"))\n            .flatMap(cb => cb.value.split(\",\"));\n\n    const customKeywordsRaw = lexicalCustomInput.value.trim();\n\n    if (customKeywordsRaw) {\n        customKeywordsRaw\n            .split(\",\")\n            .map(word => word.trim())\n            .filter(Boolean)\n            .forEach(word => lexicalKeywords.push(word));\n    }\n\n    const hasLexicalFilter = lexicalKeywords.length > 0;\n\n    let religiousCondition = \"\";\n\n    if (religiousFilter.checked || hasLexicalFilter) {\n\n        const branches = [];\n\n        if (religiousFilter.checked) {\n\n            branches.push(`\n                {\n                    ?item wdt:P136 ?religiousGenre .\n                    VALUES ?religiousGenre { ${RELIGIOUS_GENRE_IDS.join(\" \")} }\n                }\n            `);\n\n            branches.push(`\n                {\n                    ?item wdt:P921 ?religiousSubject .\n                    VALUES ?religiousSubject { ${RELIGIOUS_SUBJECT_IDS.join(\" \")} }\n                }\n            `);\n        }\n\n        if (hasLexicalFilter) {\n            const lexicalOr = lexicalKeywords\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\n                .join(\" || \");\n\n            branches.push(`{ FILTER(${lexicalOr}) }`);\n        }\n\n        religiousCondition = branches.join(\" UNION \");\n    }\n\n    const excludeWordsRaw = excludeWordsInput.value.trim();\n    let excludeCondition = \"\";\n\n    if (excludeWordsRaw) {\n\n        const excludeWords = excludeWordsRaw\n            .split(\",\")\n            .map(word => word.trim())\n            .filter(Boolean);\n\n        if (excludeWords.length) {\n            const excludeOr = excludeWords\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\n                .join(\" || \");\n\n            excludeCondition = `FILTER(!(${excludeOr}))`;\n        }\n    }\n\n    let searchCondition = \"\";\n\n    if (search) {\n\n        const safeSearch = search.replace(\/\"\/g, '\\\\\"');\n        const category = searchCategory.value || \"title\";\n\n        const titleMatch = `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${safeSearch}\"))`;\n\n        const directorBlock = `\n            ?item wdt:P57 ?searchDirector .\n            ?searchDirector rdfs:label ?searchDirectorLabel .\n            FILTER(LANG(?searchDirectorLabel) IN (\"fr\",\"en\"))\n            FILTER(CONTAINS(LCASE(STR(?searchDirectorLabel)), LCASE(\"${safeSearch}\")))\n        `;\n\n        const castBlock = `\n            ?item wdt:P161 ?searchCast .\n            ?searchCast rdfs:label ?searchCastLabel .\n            FILTER(LANG(?searchCastLabel) IN (\"fr\",\"en\"))\n            FILTER(CONTAINS(LCASE(STR(?searchCastLabel)), LCASE(\"${safeSearch}\")))\n        `;\n\n        if (category === \"title\") {\n            searchCondition = `FILTER(${titleMatch})`;\n        }\n        else if (category === \"director\") {\n            searchCondition = directorBlock;\n        }\n        else if (category === \"cast\") {\n            searchCondition = castBlock;\n        }\n        else {\n            searchCondition = `\n                { FILTER(${titleMatch}) }\n                UNION\n                { ${directorBlock} }\n                UNION\n                { ${castBlock} }\n            `;\n        }\n    }\n\n    return `\n        ${typeCondition}\n        ${labelCondition}\n        ${religiousCondition}\n        ${excludeCondition}\n        ${searchCondition}\n    `;\n}\n\n\/* =========================================================\n   CONDITIONS DE DATE\n========================================================= *\/\n\nfunction buildWhereClause() {\n\n    const day = dayInput.value.trim();\n    const month = monthInput.value.trim();\n    const year = yearInput.value.trim();\n    const yearFrom = yearFromInput.value.trim();\n    const yearTo = yearToInput.value.trim();\n\n    const base = buildBaseConditions();\n\n    const dateFrom = dateFromInput.value;\n    const dateTo = dateToInput.value;\n\n    const hasFullDateRange = Boolean(dateFrom || dateTo);\n    const hasYearRange = Boolean(yearFrom || yearTo);\n    const hasRollingFilter = Boolean(day || month || year);\n\n    const hasDateFilter = hasFullDateRange || hasYearRange || hasRollingFilter;\n\n    let dateVariable = `OPTIONAL{ ?item wdt:P577 ?date . }`;\n\n    if (dateFilter.value === \"with\" || hasDateFilter) {\n        dateVariable = `?item wdt:P577 ?date .`;\n    }\n\n    if (dateFilter.value === \"without\") {\n        dateVariable = `FILTER NOT EXISTS{ ?item wdt:P577 ?anyDate . }`;\n    }\n\n    let dateFilterCondition = \"\";\n\n    if (hasDateFilter && dateFilter.value !== \"without\") {\n\n        const conditions = [];\n\n        if (hasFullDateRange) {\n            if (dateFrom) conditions.push(`?date >= \"${dateFrom}T00:00:00Z\"^^xsd:dateTime`);\n            if (dateTo) conditions.push(`?date <= \"${dateTo}T23:59:59Z\"^^xsd:dateTime`);\n        }\n        else if (hasYearRange) {\n            if (yearFrom) conditions.push(`YEAR(?date) >= ${Number(yearFrom)}`);\n            if (yearTo) conditions.push(`YEAR(?date) <= ${Number(yearTo)}`);\n        }\n        else {\n\n            const now = new Date();\n            const nowIso = now.toISOString();\n\n            if (day) {\n                conditions.push(`?date >= \"${daysAgo(Number(day)).toISOString()}\"^^xsd:dateTime`);\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n            }\n\n            if (month) {\n                conditions.push(`?date >= \"${monthsAgo(Number(month)).toISOString()}\"^^xsd:dateTime`);\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n            }\n\n            if (year) {\n\n                const yearNumber = Number(year);\n\n                if (yearNumber >= 100) {\n                    conditions.push(`YEAR(?date) = ${yearNumber}`);\n                }\n                else {\n                    conditions.push(`?date >= \"${yearsAgo(yearNumber).toISOString()}\"^^xsd:dateTime`);\n                    conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n                }\n            }\n        }\n\n        if (conditions.length) {\n            dateFilterCondition = `FILTER(${conditions.join(\" && \")})`;\n        }\n    }\n\n    return `\n        ${base}\n        ${dateVariable}\n        ${dateFilterCondition}\n    `;\n}\n\n\/* =========================================================\n   CONDITIONS \"A VENIR\" (vue Cin\u00e9ma)\n========================================================= *\/\n\nfunction buildUpcomingWhereClause() {\n\n    const base = buildBaseConditions();\n\n    return `\n        ${base}\n        ?item wdt:P577 ?date .\n        FILTER(?date > NOW())\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE COMPTAGE\n========================================================= *\/\n\nfunction buildCountQuery() {\n\n    const where = buildWhereClause();\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT (COUNT(*) AS ?count) WHERE {\n            SELECT DISTINCT ?item WHERE{ ${where} }\n            LIMIT ${COUNT_CAP}\n        }\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE LISTE (l\u00e9g\u00e8re, pagination r\u00e9elle)\n========================================================= *\/\n\nfunction buildListQuery(limit, offset) {\n\n    const where = buildWhereClause();\n\n    let order = \"DESC(?dateAgg)\";\n    if (sortSelect.value === \"oldest\") order = \"ASC(?dateAgg)\";\n    if (sortSelect.value === \"title\") order = \"LCASE(STR(?labelAgg))\";\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\n        WHERE{ ${where} }\n        GROUP BY ?item\n        ORDER BY ${order}\n        LIMIT ${limit}\n        OFFSET ${offset}\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE LISTE \"A VENIR\" (vue Cin\u00e9ma)\n========================================================= *\/\n\nfunction buildUpcomingListQuery(limit) {\n\n    const where = buildUpcomingWhereClause();\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\n        WHERE{ ${where} }\n        GROUP BY ?item\n        ORDER BY ASC(?dateAgg)\n        LIMIT ${limit}\n    `;\n}\n\n\/* =========================================================\n   REQUETE D'ENRICHISSEMENT\n========================================================= *\/\n\nfunction buildDetailsQuery(itemUris) {\n\n    const values = itemUris.map(uri => `<${uri}>`).join(\" \");\n\n    return `\n        SELECT ?item\n               (SAMPLE(?poster) AS ?poster)\n               (SAMPLE(?image) AS ?image)\n               (GROUP_CONCAT(DISTINCT ?directorLabel; separator=\", \") AS ?directors)\n               (GROUP_CONCAT(DISTINCT ?castLabel; separator=\", \") AS ?cast)\n               (SAMPLE(?website) AS ?website)\n               (SAMPLE(?imdb) AS ?imdb)\n        WHERE{\n            VALUES ?item { ${values} }\n            OPTIONAL{ ?item wdt:P3383 ?poster . }\n            OPTIONAL{ ?item wdt:P18 ?image . }\n            OPTIONAL{\n                ?item wdt:P57 ?director .\n                ?director rdfs:label ?directorLabel .\n                FILTER(LANG(?directorLabel) IN (\"fr\",\"en\"))\n            }\n            OPTIONAL{\n                ?item wdt:P161 ?cast .\n                ?cast rdfs:label ?castLabel .\n                FILTER(LANG(?castLabel) IN (\"fr\",\"en\"))\n            }\n            OPTIONAL{ ?item wdt:P856 ?website . }\n            OPTIONAL{\n                ?item wdt:P345 ?imdbId .\n                BIND(CONCAT(\"https:\/\/www.imdb.com\/title\/\", ?imdbId) AS ?imdb)\n            }\n        }\n        GROUP BY ?item\n    `;\n}\n\n\/* =========================================================\n   REQUETE AVEC RETRY (POST, pour \u00e9viter les URL trop longues)\n========================================================= *\/\n\nasync function fetchWikidata(query, attempt = 0) {\n\n    const response = await fetch(WIKIDATA_ENDPOINT, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\n            \"Accept\": \"application\/sparql-results+json\"\n        },\n        body: \"query=\" + encodeURIComponent(query),\n        signal: abortController.signal\n    });\n\n    if ([429, 502, 503, 504].includes(response.status)) {\n\n        if (attempt < 3) {\n\n            const delay = (attempt + 1) * 2500;\n\n            statusElement.textContent =\n                \"\u23f3 Wikidata est temporairement charg\u00e9. Nouvelle tentative dans \"\n                + (delay \/ 1000) + \" secondes...\";\n\n            await new Promise(resolve => setTimeout(resolve, delay));\n\n            return fetchWikidata(query, attempt + 1);\n        }\n    }\n\n    if (!response.ok) {\n        throw new Error(\"Erreur HTTP \" + response.status);\n    }\n\n    return response.json();\n}\n\n\/* =========================================================\n   RECHERCHE (comptage -> liste -> enrichissement)\n========================================================= *\/\n\nasync function searchWikidata() {\n\n    if (isLoading) return;\n\n    isLoading = true;\n    abortController = new AbortController();\n    cancelButton.disabled = false;\n    progressBar.style.width = \"10%\";\n    resultsElement.innerHTML = \"\";\n    statusElement.textContent = \"\u23f3 Pr\u00e9paration de la recherche...\";\n\n    try {\n\n        if (titleFilter.value === \"without\") {\n            currentResults = [];\n            totalPages = 1;\n            statusElement.textContent =\n                \"\u26a0\ufe0f Wikidata ne permet pas de r\u00e9cup\u00e9rer fiablement les films \"\n                + \"sans titre dans cette requ\u00eate l\u00e9g\u00e8re.\";\n            renderResults();\n            return;\n        }\n\n        const pageSize = Math.max(1, Number(pageSizeSelect.value) || 20);\n\n        const maxResultsRaw = maxResultsInput.value.trim();\n        const maxResults = maxResultsRaw ? Math.max(1, Number(maxResultsRaw)) : null;\n\n        statusElement.textContent = \"\u23f3 Comptage des r\u00e9sultats...\";\n        progressBar.style.width = \"20%\";\n\n        let rawCount = null;\n        let cappedAtLimit = false;\n        let effectiveTotal = null;\n\n        try {\n            const countData = await fetchWikidata(buildCountQuery());\n            rawCount = Number(countData.results.bindings[0]?.count?.value || 0);\n            cappedAtLimit = rawCount >= COUNT_CAP;\n        }\n        catch (countError) {\n\n            if (countError.name === \"AbortError\") {\n                throw countError;\n            }\n\n            console.warn(\"Comptage indisponible, repli sans total exact :\", countError);\n\n            rawCount = null;\n        }\n\n        totalKnown = rawCount !== null;\n\n        if (totalKnown) {\n\n            effectiveTotal = maxResults ? Math.min(rawCount, maxResults) : rawCount;\n            totalPages = Math.max(1, Math.ceil(effectiveTotal \/ pageSize));\n\n            if (currentPage > totalPages) {\n                currentPage = totalPages;\n            }\n\n            if (effectiveTotal === 0) {\n                currentResults = [];\n                progressBar.style.width = \"100%\";\n                statusElement.textContent = \"\u274c Aucun r\u00e9sultat.\";\n                renderResults();\n                return;\n            }\n        }\n        else {\n            totalPages = Math.max(totalPages, currentPage);\n        }\n\n        statusElement.textContent = \"\u23f3 Recherche des films...\";\n        progressBar.style.width = \"40%\";\n\n        const offset = (currentPage - 1) * pageSize;\n\n        let limit = pageSize;\n\n        if (maxResults) {\n            limit = Math.min(pageSize, Math.max(0, maxResults - offset));\n        }\n\n        const fetchLimit = totalKnown ? limit : limit + 1;\n\n        const listData = await fetchWikidata(buildListQuery(fetchLimit, offset));\n        let listRows = listData.results.bindings;\n\n        if (!totalKnown) {\n            hasNextPageUnknown = listRows.length > limit;\n            listRows = listRows.slice(0, limit);\n            totalPages = hasNextPageUnknown ? currentPage + 1 : currentPage;\n        }\n\n        progressBar.style.width = \"60%\";\n\n        if (listRows.length === 0) {\n            currentResults = [];\n            progressBar.style.width = \"100%\";\n            statusElement.textContent = \"\u274c Aucun r\u00e9sultat sur cette page.\";\n            renderResults();\n            return;\n        }\n\n        statusElement.textContent = \"\u23f3 R\u00e9cup\u00e9ration des affiches et informations...\";\n\n        const itemUris = listRows.map(row => row.item.value);\n        const detailsData = await fetchWikidata(buildDetailsQuery(itemUris));\n\n        const detailsByUri = {};\n        detailsData.results.bindings.forEach(row => {\n            detailsByUri[row.item.value] = row;\n        });\n\n        progressBar.style.width = \"90%\";\n\n        currentResults = listRows.map(row => convertResult(row, detailsByUri[row.item.value]));\n\n        progressBar.style.width = \"100%\";\n\n        let statusMessage;\n\n        if (totalKnown) {\n\n            statusMessage = \"\u2705 \" + effectiveTotal + \" r\u00e9sultat(s) trouv\u00e9(s)\";\n\n            if (cappedAtLimit && !maxResults) {\n                statusMessage += \" (au moins \" + COUNT_CAP + \" \u2014 affinez la recherche pour un compte exact)\";\n            }\n        }\n        else {\n            statusMessage =\n                \"\u26a0\ufe0f Total exact impossible \u00e0 calculer pour cette recherche \"\n                + \"(trop de crit\u00e8res combin\u00e9s). \"\n                + listRows.length + \" r\u00e9sultat(s) affich\u00e9(s) sur CETTE PAGE \"\n                + \"uniquement \u2014 ce n'est pas le total r\u00e9el, qui peut \u00eatre bien \"\n                + \"plus \u00e9lev\u00e9.\";\n        }\n\n        statusElement.textContent = statusMessage + \".\";\n\n        renderResults();\n\n    }\n    catch (error) {\n\n        if (error.name === \"AbortError\") {\n            statusElement.textContent = \"\u23f9 Recherche annul\u00e9e.\";\n        }\n        else {\n            console.error(\"Wikidata :\", error);\n            statusElement.textContent = \"\u274c Impossible d'interroger Wikidata : \" + error.message;\n        }\n    }\n    finally {\n        isLoading = false;\n        cancelButton.disabled = true;\n        abortController = null;\n    }\n}\n\n\/* =========================================================\n   CONVERSION (fusion ligne liste + ligne d\u00e9tails)\n========================================================= *\/\n\nfunction convertResult(listRow, detailsRow) {\n\n    const details = detailsRow || {};\n\n    const poster = details.poster\n        ? details.poster.value\n        : (details.image ? details.image.value : \"\");\n\n    const website = details.website ? details.website.value : \"\";\n    const imdb = details.imdb ? details.imdb.value : \"\";\n    const uri = listRow.item ? listRow.item.value : \"\";\n\n    const primaryLink = website || imdb || uri;\n\n    return {\n        uri,\n        title: listRow.labelAgg ? listRow.labelAgg.value : \"\",\n        poster,\n        directors: details.directors ? details.directors.value : \"\",\n        cast: details.cast ? details.cast.value : \"\",\n        date: listRow.dateAgg ? listRow.dateAgg.value : \"\",\n        website,\n        imdb,\n        primaryLink,\n        source: \"wikidata\"\n    };\n}\n\n\/* =========================================================\n   AFFICHAGE\n========================================================= *\/\n\nfunction renderResults() {\n\n    resultsElement.innerHTML = \"\";\n\n    const isReligiousSearch =\n        religiousFilter.checked\n        || document.querySelectorAll(\".wdfc-lexicalKeyword:checked\").length > 0;\n\n    currentResults.forEach(film => {\n\n        const card = document.createElement(\"article\");\n        card.className = \"card\";\n\n        if (film.tmdbId) {\n            card.dataset.tmdbId = film.tmdbId;\n        }\n\n        const year = film.date ? new Date(film.date).getFullYear() : \"\";\n\n        const castList = film.cast\n            ? film.cast.split(\", \").slice(0, 4).join(\", \")\n            : \"\";\n\n        const hasRealPoster = Boolean(film.poster);\n        const filmTitle = film.title || \"Titre non renseign\u00e9\";\n        const destinationUrl = getFilmDestinationUrl(film);\n\n        card.innerHTML = `\n\n            <div class=\"poster-wrap\">\n\n                <img decoding=\"async\" class=\"poster\" src=\"${film.poster || DEFAULT_POSTER}\"\n                     alt=\"${escapeHTML(filmTitle)}\" loading=\"lazy\">\n\n                ${!hasRealPoster ? `<div class=\"poster-fallback-title\">${escapeHTML(filmTitle)}<\/div>` : \"\"}\n\n                ${year ? `<div class=\"year-badge\">${year}<\/div>` : \"\"}\n\n                ${isReligiousSearch ? `<div class=\"religious-badge\">\u271d\ufe0f Religieux<\/div>` : \"\"}\n\n                <a class=\"poster-overlay\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\n                    <span class=\"overlay-btn\">\u25b6 Voir le film<\/span>\n                <\/a>\n\n            <\/div>\n\n            <div class=\"card-content\">\n\n                ${film.directors ? `<div class=\"meta-line\"><b>R\u00e9alisation :<\/b> ${escapeHTML(film.directors)}<\/div>` : \"\"}\n\n                ${castList ? `<div class=\"meta-line\"><b>Avec :<\/b> ${escapeHTML(castList)}<\/div>` : \"\"}\n\n                <div class=\"meta-line\">\ud83d\udcc5 ${formatDate(film.date)}<\/div>\n\n                <div class=\"card-links\">\n                    <a class=\"link-primary\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\u25b6 Voir le film<\/a>\n                    <a class=\"link-secondary\" href=\"${film.uri}\" target=\"_blank\" rel=\"noopener\">${film.isTMDB ? \"TMDB\" : \"Wikidata\"}<\/a>\n                <\/div>\n\n            <\/div>\n        `;\n\n        resultsElement.appendChild(card);\n    });\n\n    updatePagination();\n}\n\n\/* =========================================================\n   PAGINATION\n========================================================= *\/\n\nfunction updatePagination() {\n\n    if (totalKnown) {\n        pageInfo.textContent = \"Page \" + currentPage + \" \/ \" + totalPages;\n        previousButton.disabled = currentPage <= 1;\n        nextButton.disabled = currentPage >= totalPages;\n        pageInput.max = totalPages;\n    }\n    else {\n        pageInfo.textContent = \"Page \" + currentPage + \" (total non calcul\u00e9)\";\n        previousButton.disabled = currentPage <= 1;\n        nextButton.disabled = !hasNextPageUnknown;\n        pageInput.removeAttribute(\"max\");\n    }\n}\n\n\/* =========================================================\n   DATE \/ SECURITE\n========================================================= *\/\n\nfunction formatDate(date) {\n\n    if (!date) return \"Date non renseign\u00e9e\";\n\n    const d = new Date(date);\n\n    if (isNaN(d)) return date;\n\n    return d.toLocaleDateString(\"fr-FR\");\n}\n\nfunction escapeHTML(value) {\n    const div = document.createElement(\"div\");\n    div.textContent = value ? String(value) : \"\";\n    return div.innerHTML;\n}\n\n\/* =========================================================\n   VUE CINEMA\n========================================================= *\/\n\nfunction renderCinemaCard(film) {\n\n    const title = film.title || \"Titre non renseign\u00e9\";\n    const year = film.date ? new Date(film.date).getFullYear() : \"\";\n    const poster = film.poster || DEFAULT_POSTER;\n    const hasPoster = Boolean(film.poster);\n    const destinationUrl = getFilmDestinationUrl(film);\n\n    return `\n        <a class=\"cinema-card\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\n            <div class=\"cinema-poster-wrap\">\n                <img decoding=\"async\" src=\"${poster}\" alt=\"${escapeHTML(title)}\" loading=\"lazy\">\n                ${!hasPoster ? `<div class=\"cinema-fallback-title\">${escapeHTML(title)}<\/div>` : \"\"}\n                ${year ? `<div class=\"cinema-year\">${year}<\/div>` : \"\"}\n            <\/div>\n            <div class=\"cinema-title\">${escapeHTML(title)}<\/div>\n        <\/a>\n    `;\n}\n\nfunction renderCinemaHtml(nowShowing, upcoming, loading) {\n\n    const nowCards = nowShowing.map(renderCinemaCard).join(\"\");\n\n    let upcomingSection = \"\";\n\n    if (loading) {\n        upcomingSection = `<p class=\"cinema-loading\">\u23f3 Recherche des films \u00e0 venir dans la base...<\/p>`;\n    }\n    else if (upcoming.length) {\n        upcomingSection = `\n            <h2 class=\"cinema-section-title\">\ud83d\udd1c Prochainement<\/h2>\n            <div class=\"cinema-grid\">${upcoming.map(renderCinemaCard).join(\"\")}<\/div>\n        `;\n    }\n\n    return `\n        <!DOCTYPE html>\n        <html lang=\"fr\">\n        <head>\n        <meta charset=\"UTF-8\">\n        <title>Au cin\u00e9ma \u2014 S\u00e9lection<\/title>\n        <style>\n            * { box-sizing: border-box; }\n            body { margin:0; font-family: Arial, Helvetica, sans-serif; background:#0b0b0d; color:#fff; padding:28px; }\n            h1 { text-align:center; margin-bottom: 6px; }\n            .cinema-subtitle { text-align:center; opacity:.6; margin-bottom:28px; font-size:13px; }\n            .cinema-section-title { margin: 34px 0 16px; font-size:18px; border-left:4px solid #f0c400; padding-left:10px; }\n            .cinema-loading { opacity:.6; margin-top:20px; }\n            .cinema-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(170px,1fr)); gap:20px; }\n            .cinema-card { color:#fff; text-decoration:none; display:block; background:#1a1a1d; border-radius:12px; overflow:hidden; transition: transform .2s ease; }\n            .cinema-card:hover { transform: translateY(-4px); }\n            .cinema-poster-wrap { position:relative; width:100%; aspect-ratio:2\/3; background:#222; overflow:hidden; }\n            .cinema-poster-wrap img { width:100%; height:100%; object-fit:cover; display:block; }\n            .cinema-fallback-title {\n                position:absolute; left:0; right:0; top:71%; bottom:6%;\n                display:flex; align-items:flex-start; justify-content:center;\n                text-align:center; padding:8px 12px 0; font-weight:bold;\n                font-size:13px; line-height:1.25; text-shadow:0 2px 6px rgba(0,0,0,.9);\n            }\n            .cinema-year { position:absolute; top:8px; left:8px; background:rgba(0,0,0,.65); padding:3px 8px; border-radius:14px; font-size:11px; font-weight:bold; }\n            .cinema-title { padding:10px 12px 14px; font-size:13.5px; font-weight:bold; line-height:1.3; }\n        <\/style>\n        <\/head>\n        <body>\n            <h1>\ud83c\udfac Au cin\u00e9ma<\/h1>\n            <div class=\"cinema-subtitle\">Cliquez sur une affiche pour acc\u00e9der directement \u00e0 la page de visionnage du film<\/div>\n            <div class=\"cinema-grid\">${nowCards || \"<p>Aucun film \u00e0 afficher.<\/p>\"}<\/div>\n            ${upcomingSection}\n        <\/body>\n        <\/html>\n    `;\n}\n\nasync function openCinemaView() {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun film \u00e0 afficher. Lancez d'abord une recherche.\");\n        return;\n    }\n\n    const win = window.open(\"\", \"_blank\");\n\n    if (!win) {\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la vue cin\u00e9ma.\");\n        return;\n    }\n\n    win.document.open();\n    win.document.write(renderCinemaHtml(currentResults, [], true));\n    win.document.close();\n\n    let upcomingFilms = [];\n\n    try {\n\n        const listData = await fetchWikidataOnce(buildUpcomingListQuery(UPCOMING_LIMIT));\n        const rows = listData.results.bindings;\n\n        if (rows.length) {\n\n            const uris = rows.map(row => row.item.value);\n            const detailsData = await fetchWikidataOnce(buildDetailsQuery(uris));\n\n            const detailsByUri = {};\n            detailsData.results.bindings.forEach(row => {\n                detailsByUri[row.item.value] = row;\n            });\n\n            upcomingFilms = rows.map(row => convertResult(row, detailsByUri[row.item.value]));\n        }\n    }\n    catch (error) {\n        console.warn(\"Films \u00e0 venir indisponibles :\", error);\n    }\n\n    if (!win.closed) {\n        win.document.open();\n        win.document.write(renderCinemaHtml(currentResults, upcomingFilms, false));\n        win.document.close();\n    }\n}\n\n\/* =========================================================\n   EVENEMENTS\n========================================================= *\/\n\ncancelButton.addEventListener(\"click\", function () {\n    if (abortController) abortController.abort();\n});\n\ndocument.getElementById(\"wdfc-searchButton\").addEventListener(\"click\", function () {\n    currentPage = 1;\n    searchWikidata();\n});\n\nsearchInput.addEventListener(\"keydown\", function (event) {\n    if (event.key === \"Enter\") {\n        currentPage = 1;\n        searchWikidata();\n    }\n});\n\nnextButton.addEventListener(\"click\", function () {\n    if (currentPage < totalPages) {\n        currentPage++;\n        searchWikidata();\n    }\n});\n\npreviousButton.addEventListener(\"click\", function () {\n    if (currentPage > 1) {\n        currentPage--;\n        searchWikidata();\n    }\n});\n\ndocument.getElementById(\"wdfc-pageButton\").addEventListener(\"click\", function () {\n    let page = Number(pageInput.value);\n    if (page < 1) page = 1;\n    if (totalKnown && page > totalPages) page = totalPages;\n    currentPage = page;\n    searchWikidata();\n});\n\ndocument.getElementById(\"wdfc-reloadButton\").addEventListener(\"click\", searchWikidata);\n\nallLanguagesCheckbox.addEventListener(\"change\", function () {\n    languageOptionsContainer.classList.toggle(\"is-disabled\", allLanguagesCheckbox.checked);\n});\n\ndestinationSiteSelect.addEventListener(\"change\", function () {\n    customSiteUrlInput.disabled = destinationSiteSelect.value !== \"custom\";\n    if (currentResults.length) renderResults();\n});\n\nconst TITLE_COLOR_STORAGE_KEY = \"wdfcFilmCatalogTitleColor\";\n\ntitleColorPicker.addEventListener(\"input\", function () {\n    document.querySelector(\".wdfc-app\").style.setProperty(\"--wdfc-title-color\", titleColorPicker.value);\n\n    try {\n        localStorage.setItem(TITLE_COLOR_STORAGE_KEY, titleColorPicker.value);\n    }\n    catch (error) {\n        console.warn(\"Impossible de sauvegarder la couleur choisie :\", error);\n    }\n});\n\ncustomSiteUrlInput.addEventListener(\"input\", function () {\n    if (destinationSiteSelect.value === \"custom\" && currentResults.length) {\n        renderResults();\n    }\n});\n\nyoutubeButton.addEventListener(\"click\", function () {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun film \u00e0 afficher pour le moment. Lancez d'abord une recherche.\");\n        return;\n    }\n\n    const win = window.open(\"\", \"_blank\");\n\n    if (!win) {\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la s\u00e9lection YouTube.\");\n        return;\n    }\n\n    const linksHtml = currentResults.map(film => {\n        const title = film.title || \"Titre non renseign\u00e9\";\n        const query = encodeURIComponent(title + \" film complet\");\n        const url = \"https:\/\/www.youtube.com\/results?search_query=\" + query;\n        return \"<li><a href=\\\"\" + url + \"\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">\"\n            + escapeHTML(title) + \"<\/a><\/li>\";\n    }).join(\"\");\n\n    win.document.write(\n        \"<!DOCTYPE html><html lang=\\\"fr\\\"><head><meta charset=\\\"UTF-8\\\">\"\n        + \"<title>S\u00e9lection sur YouTube<\/title>\"\n        + \"<style>body{font-family:Arial,Helvetica,sans-serif;background:#0b0b0d;color:#fff;padding:24px;}\"\n        + \"h1{font-size:20px;} a{color:#f0c400;text-decoration:none;} a:hover{text-decoration:underline;}\"\n        + \"li{margin-bottom:10px;font-size:15px;}<\/style><\/head>\"\n        + \"<body><h1>\ud83c\udfac Recherche YouTube \u2014 s\u00e9lection actuelle (\" + currentResults.length + \" film(s))<\/h1>\"\n        + \"<ul>\" + linksHtml + \"<\/ul><\/body><\/html>\"\n    );\n\n    win.document.close();\n});\n\ncinemaButton.addEventListener(\"click\", openCinemaView);\n\ndocument.getElementById(\"wdfc-resetButton\").addEventListener(\"click\", function () {\n\n    if (isLoading && abortController) abortController.abort();\n\n    searchInput.value = \"\";\n    searchCategory.value = \"title\";\n    destinationSiteSelect.value = \"justwatch\";\n    customSiteUrlInput.value = \"\";\n    customSiteUrlInput.disabled = true;\n    dayInput.value = \"\";\n    monthInput.value = \"\";\n    yearInput.value = \"\";\n    yearFromInput.value = \"\";\n    yearToInput.value = \"\";\n    dateFromInput.value = \"\";\n    dateToInput.value = \"\";\n    titleFilter.value = \"all\";\n    dateFilter.value = \"all\";\n    typeFilter.value = \"film_animation\";\n\n    document.querySelectorAll(\".wdfc-languageCheckbox\").forEach(cb => {\n        cb.checked = (cb.value === \"fr\");\n    });\n    allLanguagesCheckbox.checked = false;\n    languageOptionsContainer.classList.remove(\"is-disabled\");\n\n    religiousFilter.checked = true;\n\n    document.querySelectorAll(\".wdfc-lexicalKeyword\").forEach(cb => { cb.checked = false; });\n    lexicalCustomInput.value = \"\";\n    excludeWordsInput.value = \"\";\n\n    sortSelect.value = \"newest\";\n    pageSizeSelect.value = \"20\";\n    maxResultsInput.value = \"\";\n\n    currentPage = 1;\n    totalPages = 1;\n    totalKnown = true;\n    hasNextPageUnknown = false;\n    currentResults = [];\n\n    resultsElement.innerHTML = \"\";\n    progressBar.style.width = \"0%\";\n    pageInput.value = 1;\n    pageInfo.textContent = \"Page 1 \/ 1\";\n\n    statusElement.textContent = \"Filtres r\u00e9initialis\u00e9s.\";\n});\n\ndocument.getElementById(\"wdfc-exportButton\").addEventListener(\"click\", function () {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun r\u00e9sultat \u00e0 exporter.\");\n        return;\n    }\n\n    const header = [\"Titre\", \"R\u00e9alisateur(s)\", \"Casting\", \"Date\", \"Lien\", \"Wikidata\", \"Source\"];\n\n    const rows = currentResults.map(film => [\n        film.title, film.directors, film.cast, film.date, film.primaryLink, film.uri, film.source\n    ]);\n\n    const csv = [header, ...rows]\n        .map(row => row.map(value =>\n            '\"' + String(value || \"\").replace(\/\"\/g, '\"\"') + '\"'\n        ).join(\";\"))\n        .join(\"\\n\");\n\n    const blob = new Blob([\"\\ufeff\" + csv], { type: \"text\/csv;charset=utf-8\" });\n    const url = URL.createObjectURL(blob);\n    const link = document.createElement(\"a\");\n    link.href = url;\n    link.download = \"films-wikidata.csv\";\n    link.click();\n    URL.revokeObjectURL(url);\n});\n\n\/* =========================================================\n   INITIALISATION\n========================================================= *\/\n\nstatusElement.textContent = \"Pr\u00eat. Entrez un terme ou lancez une recherche.\";\nloadAvailableLanguages();\n\ntry {\n    const savedTitleColor = localStorage.getItem(TITLE_COLOR_STORAGE_KEY);\n    if (savedTitleColor) {\n        titleColorPicker.value = savedTitleColor;\n        document.querySelector(\".wdfc-app\").style.setProperty(\"--wdfc-title-color\", savedTitleColor);\n    }\n}\ncatch (error) {\n    console.warn(\"Impossible de relire la couleur sauvegard\u00e9e :\", error);\n}\n\n\/* =========================================================================================\n   MODULE TMDB - PLUS DE FILMS CHRETIENS\n   (imbriqu\u00e9 ici pour partager le scope avec le reste du script \u2014 currentResults,\n   renderResults, etc. restent accessibles sans passer par window global)\n========================================================================================= *\/\n\n(function () {\n\n    \"use strict\";\n\n    const TMDB_API_KEY = \"af5576c58fe6fd7fab447fbb4dd2423a\";\n    const TMDB_API = \"https:\/\/api.themoviedb.org\/3\";\n    const TMDB_LANGUAGE = \"fr-FR\";\n    const TMDB_REGION = \"FR\";\n    const TMDB_MAX_PAGES = 500;\n\n    const CHRISTIAN_KEYWORDS = [\n        \"christianity\", \"christian\", \"christian faith\", \"christian film\",\n        \"religious film\", \"religion\", \"faith\", \"bible\", \"biblical\",\n        \"jesus christ\", \"jesus\", \"gospel\", \"resurrection\", \"crucifixion\",\n        \"church\", \"evangelical christianity\"\n    ];\n\n    const typeFilterEl = document.getElementById(\"wdfc-typeFilter\");\n    const pageSizeSelectEl = document.getElementById(\"wdfc-pageSize\");\n    const maxResultsInputEl = document.getElementById(\"wdfc-maxResults\");\n    const sortSelectEl = document.getElementById(\"wdfc-sort\");\n    const religiousFilterEl = document.getElementById(\"wdfc-religiousFilter\");\n    const allLanguagesCheckboxEl = document.getElementById(\"wdfc-allLanguages\");\n    const languageOptionsContainerEl = document.getElementById(\"wdfc-languageOptions\");\n    const statusElementEl = document.getElementById(\"wdfc-status\");\n    const progressBarEl = document.getElementById(\"wdfc-progressBar\");\n    const resultsElementEl = document.getElementById(\"wdfc-results\");\n\n    if (!typeFilterEl || !pageSizeSelectEl || !maxResultsInputEl || !sortSelectEl) {\n        console.warn(\"TMDB Plus : un ou plusieurs filtres n\u00e9cessaires sont absents.\");\n        return;\n    }\n\n    function createTMDBButton() {\n\n        if (document.getElementById(\"wdfc-tmdbPlusButton\")) return;\n\n        const button = document.createElement(\"button\");\n        button.id = \"wdfc-tmdbPlusButton\";\n        button.type = \"button\";\n        button.textContent = \"\ud83c\udfac Plus de films\";\n\n        const searchButtonEl = document.querySelector(\n            '#wdfc-searchButton, [data-action=\"search\"], button[type=\"submit\"]'\n        );\n\n        if (searchButtonEl && searchButtonEl.parentNode) {\n            searchButtonEl.parentNode.appendChild(button);\n        }\n        else if (resultsElementEl && resultsElementEl.parentNode) {\n            resultsElementEl.parentNode.insertBefore(button, resultsElementEl);\n        }\n        else {\n            document.body.appendChild(button);\n        }\n\n        const status = document.createElement(\"div\");\n        status.id = \"wdfc-tmdbPlusStatus\";\n\n        if (button.parentNode) {\n            button.parentNode.insertBefore(status, button.nextSibling);\n        }\n\n        button.addEventListener(\"click\", function () {\n            searchTMDB();\n        });\n    }\n\n    function setTMDBStatus(message) {\n        const element = document.getElementById(\"wdfc-tmdbPlusStatus\");\n        if (element) {\n            element.textContent = message;\n            element.style.display = \"block\";\n        }\n        if (statusElementEl) {\n            statusElementEl.textContent = message;\n        }\n    }\n\n    function normalizeTitle(value) {\n        return String(value || \"\")\n            .toLowerCase()\n            .normalize(\"NFD\")\n            .replace(\/[\\u0300-\\u036f]\/g, \"\")\n            .replace(\/[^\\p{L}\\p{N}]+\/gu, \" \")\n            .trim();\n    }\n\n    function getYear(value) {\n        if (!value) return \"\";\n        const match = String(value).match(\/\\b(18|19|20)\\d{2}\\b\/);\n        return match ? match[0] : \"\";\n    }\n\n    function getTMDBType() {\n        const value = typeFilterEl.value;\n        if (value === \"film\") return [\"movie\"];\n        if (value === \"animation\") return [\"animation\"];\n        if (value === \"film_animation\") return [\"movie\", \"animation\"];\n        if (value === \"all\") return [\"movie\", \"animation\"];\n        return [\"movie\"];\n    }\n\n    function getSelectedLanguagesTMDB() {\n        if (allLanguagesCheckboxEl && allLanguagesCheckboxEl.checked) return [];\n        if (!languageOptionsContainerEl) return [];\n        const checked = languageOptionsContainerEl.querySelectorAll('input[type=\"checkbox\"]:checked');\n        const languages = [];\n        checked.forEach(function (checkbox) {\n            const language = checkbox.value || checkbox.dataset.lang || checkbox.dataset.language || \"\";\n            if (language) languages.push(language.toLowerCase());\n        });\n        return [...new Set(languages)];\n    }\n\n    async function findKeywordId(keyword) {\n        const params = new URLSearchParams({ api_key: TMDB_API_KEY, query: keyword, page: \"1\" });\n        const response = await fetch(TMDB_API + \"\/search\/keyword?\" + params.toString());\n        if (!response.ok) throw new Error(\"Recherche de mot-cl\u00e9 TMDB : HTTP \" + response.status);\n        const data = await response.json();\n        if (!data.results || !data.results.length) return null;\n        const exact = data.results.find(function (item) {\n            return normalizeTitle(item.name) === normalizeTitle(keyword);\n        });\n        return (exact || data.results[0]).id;\n    }\n\n    async function getKeywordIds() {\n        const results = await Promise.all(\n            CHRISTIAN_KEYWORDS.map(function (keyword) {\n                return findKeywordId(keyword).catch(function (error) {\n                    console.warn(\"Mot-cl\u00e9 ignor\u00e9 :\", keyword, error);\n                    return null;\n                });\n            })\n        );\n        return [...new Set(results.filter(Boolean))];\n    }\n\n    async function fetchTMDBPage(keywordId, page, animationOnly) {\n        const params = new URLSearchParams({\n            api_key: TMDB_API_KEY,\n            language: TMDB_LANGUAGE,\n            region: TMDB_REGION,\n            with_keywords: String(keywordId),\n            sort_by: \"primary_release_date.desc\",\n            include_adult: \"false\",\n            page: String(page)\n        });\n        if (animationOnly) params.set(\"with_genres\", \"16\");\n        const response = await fetch(TMDB_API + \"\/discover\/movie?\" + params.toString());\n        if (!response.ok) throw new Error(\"TMDB Discover : HTTP \" + response.status);\n        return await response.json();\n    }\n\n    function matchesLanguage(movie, languages) {\n        if (!languages.length) return true;\n        const originalLanguage = String(movie.original_language || \"\").toLowerCase();\n        return languages.includes(originalLanguage);\n    }\n\n    function isExistingResult(movie) {\n        if (!Array.isArray(currentResults)) return false;\n        const tmdbId = String(movie.id);\n        const title = normalizeTitle(movie.title || movie.original_title);\n        const year = getYear(movie.release_date);\n\n        return currentResults.some(function (existing) {\n            if (existing.tmdbId && String(existing.tmdbId) === tmdbId) return true;\n            const existingTitle = normalizeTitle(existing.title);\n            const existingYear = getYear(existing.date || existing.year || existing.release_date);\n            if (title && existingTitle && title === existingTitle && year && existingYear && year === existingYear) return true;\n            if (title && existingTitle && title === existingTitle && (!year || !existingYear)) return true;\n            return false;\n        });\n    }\n\n    const TMDB_IMAGE_BASE = \"https:\/\/image.tmdb.org\/t\/p\/w500\";\n\n    function convertTMDBResult(movie) {\n        const title = movie.title || movie.original_title || \"Titre non renseign\u00e9\";\n        const poster = movie.poster_path ? (TMDB_IMAGE_BASE + movie.poster_path) : \"\";\n        return {\n            uri: \"tmdb:\" + movie.id,\n            title: title,\n            poster: poster,\n            image: poster,\n            directors: \"\",\n            cast: \"\",\n            actors: \"\",\n            date: movie.release_date || \"\",\n            year: getYear(movie.release_date),\n            description: movie.overview || \"\",\n            imdb: \"\",\n            website: \"\",\n            primaryLink: \"https:\/\/www.themoviedb.org\/movie\/\" + movie.id,\n            tmdbId: String(movie.id),\n            source: \"TMDB\",\n            isTMDB: true\n        };\n    }\n\n    async function collectTMDBResults() {\n\n        const selectedTypes = getTMDBType();\n        const languages = getSelectedLanguagesTMDB();\n        const maxRaw = maxResultsInputEl.value.trim();\n        const maxResults = maxRaw ? Math.max(1, Number(maxRaw)) : null;\n        const target = maxResults || Infinity;\n\n        const keywordIds = await getKeywordIds();\n        if (!keywordIds.length) {\n            throw new Error(\"Aucun mot-cl\u00e9 chr\u00e9tien reconnu par TMDB.\");\n        }\n\n        const unique = new Map();\n\n        for (const type of selectedTypes) {\n\n            const animationOnly = type === \"animation\";\n\n            for (const keywordId of keywordIds) {\n\n                let page = 1;\n                let totalPages = 1;\n\n                do {\n\n                    if (unique.size >= target) break;\n\n                    setTMDBStatus(\n                        \"\u23f3 TMDB : recherche \" + type + \" \u2014 mot-cl\u00e9 \" + keywordId\n                        + \" \u2014 page \" + page + \" \/ \" + totalPages\n                    );\n\n                    const data = await fetchTMDBPage(keywordId, page, animationOnly);\n\n                    totalPages = Math.min(Number(data.total_pages || 1), TMDB_MAX_PAGES);\n\n                    const results = data.results || [];\n\n                    results.forEach(function (movie) {\n                        if (!matchesLanguage(movie, languages)) return;\n                        const title = normalizeTitle(movie.title || movie.original_title);\n                        const year = getYear(movie.release_date);\n                        const key = title + \"|\" + year;\n                        if (!unique.has(key)) unique.set(key, movie);\n                    });\n\n                    page++;\n\n                    if (maxResults && unique.size >= maxResults) break;\n\n                }\n                while (page <= totalPages);\n            }\n        }\n\n        let movies = Array.from(unique.values());\n\n        if (sortSelectEl.value === \"oldest\") {\n            movies.sort(function (a, b) {\n                return String(a.release_date || \"\").localeCompare(String(b.release_date || \"\"));\n            });\n        }\n        else if (sortSelectEl.value === \"title\") {\n            movies.sort(function (a, b) {\n                return normalizeTitle(a.title).localeCompare(normalizeTitle(b.title));\n            });\n        }\n        else {\n            movies.sort(function (a, b) {\n                return String(b.release_date || \"\").localeCompare(String(a.release_date || \"\"));\n            });\n        }\n\n        if (maxResults) movies = movies.slice(0, maxResults);\n\n        return movies;\n    }\n\n    async function searchTMDB() {\n\n        const button = document.getElementById(\"wdfc-tmdbPlusButton\");\n        if (button.disabled) return;\n\n        if (religiousFilterEl && !religiousFilterEl.checked) {\n            const proceed = confirm(\n                \"Le filtre \u00ab Films chr\u00e9tiens \/ religieux \u00bb est d\u00e9sactiv\u00e9.\\n\\n\"\n                + \"La recherche TMDB de ce bouton est sp\u00e9cialement con\u00e7ue \"\n                + \"pour les films chr\u00e9tiens et religieux.\\n\\n\"\n                + \"Voulez-vous continuer ?\"\n            );\n            if (!proceed) return;\n        }\n\n        button.disabled = true;\n        if (progressBarEl) progressBarEl.style.width = \"10%\";\n\n        try {\n\n            setTMDBStatus(\"\u23f3 Pr\u00e9paration de la recherche TMDB...\");\n\n            const movies = await collectTMDBResults();\n\n            if (progressBarEl) progressBarEl.style.width = \"70%\";\n\n            let added = 0;\n            let duplicates = 0;\n\n            movies.forEach(function (movie) {\n                if (isExistingResult(movie)) {\n                    duplicates++;\n                    return;\n                }\n                const converted = convertTMDBResult(movie);\n                currentResults.push(converted);\n                added++;\n            });\n\n            if (sortSelectEl.value === \"title\") {\n                currentResults.sort(function (a, b) {\n                    return normalizeTitle(a.title).localeCompare(normalizeTitle(b.title));\n                });\n            }\n            else if (sortSelectEl.value === \"oldest\") {\n                currentResults.sort(function (a, b) {\n                    return String(a.date || \"\").localeCompare(String(b.date || \"\"));\n                });\n            }\n            else {\n                currentResults.sort(function (a, b) {\n                    return String(b.date || \"\").localeCompare(String(a.date || \"\"));\n                });\n            }\n\n            if (typeof renderResults === \"function\") renderResults();\n\n            if (progressBarEl) progressBarEl.style.width = \"100%\";\n\n            setTMDBStatus(\n                \"\u2705 TMDB termin\u00e9 : \" + added + \" nouveau(x) film(s) ajout\u00e9(s). \"\n                + duplicates + \" doublon(s) ignor\u00e9(s).\"\n            );\n\n        }\n        catch (error) {\n            console.error(\"TMDB Plus :\", error);\n            setTMDBStatus(\"\u274c Erreur TMDB : \" + error.message);\n            if (progressBarEl) progressBarEl.style.width = \"100%\";\n        }\n        finally {\n            button.disabled = false;\n        }\n    }\n\n    createTMDBButton();\n\n    console.log(\"\u2705 Module TMDB \u00ab Plus de films \u00bb charg\u00e9.\");\n\n})();\n\n} \/\/ fin de init()\n\nif (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", init);\n}\nelse {\n    init();\n}\n\n})();\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-e60b9c2 e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"e60b9c2\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-b1313fa elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-spacer\" data-id=\"b1313fa\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-4fff5c5 e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"4fff5c5\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-6ebe8d3 elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-html\" data-id=\"6ebe8d3\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!--\n    CATALOGUE FILMS \u2014 version WordPress \/ Elementor\n    =================================================\n    A coller tel quel dans un widget \"HTML\" (ou \"Code HTML\")\n    d'Elementor. Ne PAS coller <!DOCTYPE>, <html>, <head> ou\n    <body> \u2014 il n'y en a plus ici, uniquement le contenu du\n    widget.\n\n    Changements par rapport \u00e0 la version autonome :\n    - Toutes les r\u00e8gles CSS sont scop\u00e9es sous .wdfc-app pour\n      ne pas d\u00e9teindre sur le reste du th\u00e8me (les s\u00e9lecteurs\n      globaux comme \"button\", \"input, select\", \":root\" du\n      fichier d'origine s'appliquaient \u00e0 TOUT le site).\n    - Tous les id=\"\" sont pr\u00e9fix\u00e9s \"wdfc-\" pour \u00e9viter les\n      collisions avec d'autres widgets\/plugins de la page.\n    - Tout le JavaScript est encapsul\u00e9 dans une seule IIFE\n      (au lieu de variables globales comme currentPage,\n      currentResults, searchWikidata...) et d\u00e9marre apr\u00e8s le\n      DOM pr\u00eat.\n    - Le hack \"pleine largeur\" (width:100vw + translateX) a\n      \u00e9t\u00e9 retir\u00e9 car il casse la mise en page \u00e0 l'int\u00e9rieur\n      des conteneurs Elementor. Pour un rendu pleine largeur,\n      utilisez plut\u00f4t l'option native \"Stretch Section\" \/\n      \"\u00c9tirer la section\" d'Elementor sur la section qui\n      contient ce widget.\n    - Le reste (logique de recherche Wikidata, module TMDB,\n      export CSV, vue Cin\u00e9ma, etc.) est inchang\u00e9.\n\n    \u26a0\ufe0f La cl\u00e9 API TMDB reste visible dans le code source c\u00f4t\u00e9\n    navigateur (comme dans l'original) : n'importe qui peut la\n    lire en ouvrant l'inspecteur. Si c'est un probl\u00e8me pour\n    vous, il faudrait faire passer les appels TMDB par un\n    endpoint c\u00f4t\u00e9 serveur (ex. un shortcode PHP ou un plugin\n    \"code snippet\" qui masque la cl\u00e9) \u2014 dites-le moi si vous\n    voulez cette version.\n\n    Limite : ce widget utilise des id fixes, donc n'en placez\n    qu'UNE seule instance par page (deux copies sur la m\u00eame\n    page cr\u00e9eraient des id dupliqu\u00e9s et casseraient le script).\n-->\n\n<div class=\"wdfc-app\">\n\n<style>\n\n.wdfc-app, .wdfc-app * {\n    box-sizing: border-box;\n}\n\n.wdfc-app {\n    color-scheme: dark;\n    font-family: Arial, Helvetica, sans-serif !important;\n    background: #0b0b0d !important;\n    color: #fff !important;\n    margin: 0;\n    padding: 25px;\n}\n\n.wdfc-app .search-box {\n    background: #1d1d1d;\n    border-radius: 14px;\n    padding: 20px;\n    margin-bottom: 20px;\n}\n\n.wdfc-app .field-row {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\n    gap: 12px;\n    margin-bottom: 16px;\n}\n\n.wdfc-app .field-row:last-child { margin-bottom: 0; }\n\n.wdfc-app .field-row.search-row {\n    grid-template-columns: 3fr 1.4fr auto;\n}\n\n.wdfc-app .field label {\n    display: block;\n    font-size: 11.5px;\n    text-transform: uppercase;\n    letter-spacing: .4px;\n    opacity: .55;\n    margin-bottom: 5px;\n    color: #fff !important;\n}\n\n.wdfc-app .field.full-width { grid-column: 1 \/ -1; }\n\n.wdfc-app input,\n.wdfc-app select {\n    width: 100%;\n    padding: 12px;\n    border-radius: 8px;\n    border: 1px solid #444;\n    background: #111 !important;\n    color: #fff !important;\n    font-size: 14px;\n}\n\n.wdfc-app select[multiple] { padding: 8px; }\n\n.wdfc-app select:disabled,\n.wdfc-app input:disabled {\n    opacity: .4;\n    cursor: not-allowed;\n}\n\n.wdfc-app .field-hint {\n    font-size: 11.5px;\n    opacity: .55;\n    margin-top: 6px;\n    color: #fff !important;\n}\n\n.wdfc-app button {\n    padding: 12px 18px;\n    border: 0;\n    border-radius: 8px;\n    background: #f0c400 !important;\n    color: #111 !important;\n    font-weight: bold;\n    cursor: pointer;\n}\n\n.wdfc-app button:hover { opacity: .85; }\n.wdfc-app button:disabled { opacity: .4; cursor: not-allowed; }\n\n.wdfc-app .youtube-button { background: #ff0000 !important; color: #fff !important; }\n.wdfc-app .cinema-button { background: #2c2c30 !important; color: #fff !important; border: 1px solid #f0c400; }\n\n.wdfc-app .toggle-filter {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    padding: 12px;\n    border-radius: 8px;\n    border: 1px solid #444;\n    background: #111;\n    cursor: pointer;\n    user-select: none;\n}\n\n.wdfc-app .toggle-filter input {\n    width: 18px;\n    height: 18px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .toggle-filter span { font-size: 13.5px; color: #fff !important; }\n\n.wdfc-app .lexical-box {\n    border: 1px solid #444;\n    border-radius: 8px;\n    padding: 12px;\n    background: #111;\n}\n\n.wdfc-app .lexical-box legend {\n    font-size: 11.5px;\n    text-transform: uppercase;\n    letter-spacing: .4px;\n    opacity: .65;\n    padding: 0 6px;\n    color: #fff !important;\n}\n\n.wdfc-app .lexical-options {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 12px;\n    margin-top: 6px;\n}\n\n.wdfc-app .lexical-options label {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 13px;\n    cursor: pointer;\n    color: #fff !important;\n}\n\n.wdfc-app .lexical-options input {\n    width: 16px;\n    height: 16px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .language-options {\n    max-height: 190px;\n    overflow-y: auto;\n    border: 1px solid #444;\n    border-radius: 8px;\n    padding: 12px;\n    background: #111;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 10px 18px;\n}\n\n.wdfc-app .language-options.is-disabled {\n    opacity: .4;\n    pointer-events: none;\n}\n\n.wdfc-app .language-options label {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 13px;\n    cursor: pointer;\n    color: #fff !important;\n}\n\n.wdfc-app .language-options input {\n    width: 16px;\n    height: 16px;\n    accent-color: #f0c400;\n    cursor: pointer;\n}\n\n.wdfc-app .status { margin: 20px 0; text-align: center; min-height: 25px; color: #fff !important; }\n\n.wdfc-app .progress {\n    width: 100%;\n    height: 8px;\n    background: #333;\n    border-radius: 10px;\n    overflow: hidden;\n    margin-top: 15px;\n}\n\n.wdfc-app .progress-bar {\n    width: 0%;\n    height: 100%;\n    background: #f0c400;\n    transition: width .2s;\n}\n\n.wdfc-app .results {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n    gap: 24px;\n}\n\n.wdfc-app .card {\n    position: relative;\n    background: #1a1a1d;\n    border-radius: 14px;\n    overflow: hidden;\n    box-shadow: 0 4px 14px rgba(0,0,0,.4);\n    transition: transform .25s ease, box-shadow .25s ease;\n    display: flex;\n    flex-direction: column;\n}\n\n.wdfc-app .card:hover {\n    transform: translateY(-6px);\n    box-shadow: 0 14px 30px rgba(0,0,0,.6);\n}\n\n.wdfc-app .poster-wrap {\n    position: relative;\n    width: 100%;\n    aspect-ratio: 2\/3;\n    background: #222;\n    overflow: hidden;\n}\n\n.wdfc-app .poster {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    display: block;\n    transition: transform .35s ease;\n}\n\n.wdfc-app .card:hover .poster { transform: scale(1.06); }\n\n.wdfc-app .poster-fallback-title {\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: 71%;\n    bottom: 6%;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    text-align: center;\n    padding: 8px 14px 0;\n    font-weight: bold;\n    font-size: clamp(13px, 1.4vw + 8px, 18px);\n    line-height: 1.25;\n    color: var(--wdfc-title-color, #00e5ff) !important;\n    text-shadow: 0 2px 8px rgba(0,0,0,.9);\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.wdfc-app .year-badge {\n    position: absolute;\n    top: 10px;\n    left: 10px;\n    background: rgba(0,0,0,.65);\n    backdrop-filter: blur(3px);\n    padding: 4px 10px;\n    border-radius: 20px;\n    font-size: 12px;\n    font-weight: bold;\n    color: #fff !important;\n}\n\n.wdfc-app .religious-badge {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    background: rgba(240,196,0,.9);\n    color: #111 !important;\n    padding: 4px 10px;\n    border-radius: 20px;\n    font-size: 11px;\n    font-weight: bold;\n}\n\n.wdfc-app .poster-overlay {\n    position: absolute;\n    inset: 0;\n    background: linear-gradient(to top, rgba(0,0,0,.92) 0%, rgba(0,0,0,.15) 55%, rgba(0,0,0,0) 100%);\n    opacity: 0;\n    transition: opacity .25s ease;\n    display: flex;\n    align-items: flex-end;\n    padding: 14px;\n}\n\n.wdfc-app .card:hover .poster-overlay { opacity: 1; }\n\n.wdfc-app .overlay-btn {\n    display: inline-block;\n    width: 100%;\n    text-align: center;\n    padding: 10px;\n    background: #f0c400;\n    color: #111 !important;\n    font-weight: bold;\n    text-decoration: none;\n    border-radius: 8px;\n    font-size: 13px;\n}\n\n.wdfc-app .card-content {\n    padding: 14px 16px 16px;\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n}\n\n.wdfc-app .meta-line {\n    font-size: 12.5px;\n    color: rgba(255,255,255,.75) !important;\n    line-height: 1.5;\n    overflow-wrap: break-word;\n}\n\n.wdfc-app .meta-line b { color: #fff !important; }\n\n.wdfc-app .card-links {\n    margin-top: auto;\n    display: flex;\n    gap: 8px;\n    padding-top: 8px;\n}\n\n.wdfc-app .link-primary,\n.wdfc-app .link-secondary {\n    text-decoration: none;\n    text-align: center;\n    border-radius: 7px;\n    font-size: 12.5px;\n    font-weight: bold;\n    padding: 9px 6px;\n}\n\n.wdfc-app .link-primary { flex: 2; background: #f0c400; color: #111 !important; }\n.wdfc-app .link-secondary { flex: 1; background: #2c2c30; color: #fff !important; }\n.wdfc-app .link-primary:hover,\n.wdfc-app .link-secondary:hover { opacity: .85; }\n\n.wdfc-app #wdfc-titleColorPicker {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 22px;\n    height: 22px;\n    padding: 0;\n    border: 2px solid #444;\n    border-radius: 50%;\n    cursor: pointer;\n    background: none;\n    overflow: hidden;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-webkit-color-swatch-wrapper {\n    padding: 0;\n    border-radius: 50%;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-webkit-color-swatch {\n    border: none;\n    border-radius: 50%;\n}\n\n.wdfc-app #wdfc-titleColorPicker::-moz-color-swatch {\n    border: none;\n    border-radius: 50%;\n}\n\n.wdfc-app .pagination {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    gap: 15px;\n    margin-top: 30px;\n    flex-wrap: wrap;\n    color: #fff !important;\n}\n\n@media (max-width: 700px) {\n    .wdfc-app .field-row.search-row { grid-template-columns: 1fr; }\n}\n\n\/* ===== Module TMDB \"Plus de films\" ===== *\/\n\n.wdfc-app #wdfc-tmdbPlusButton {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: 8px;\n    margin: 8px 0;\n    padding: 10px 18px;\n    border: 0;\n    border-radius: 8px;\n    background: #01b4e4 !important;\n    color: #ffffff !important;\n    font-weight: 700;\n    cursor: pointer;\n    transition: opacity .2s, transform .2s;\n}\n\n.wdfc-app #wdfc-tmdbPlusButton:hover {\n    opacity: .88;\n    transform: translateY(-1px);\n}\n\n.wdfc-app #wdfc-tmdbPlusButton:disabled {\n    opacity: .5;\n    cursor: wait;\n    transform: none;\n}\n\n.wdfc-app #wdfc-tmdbPlusStatus {\n    display: none;\n    width: 100%;\n    box-sizing: border-box;\n    margin: 8px 0 14px;\n    padding: 10px 14px;\n    border-radius: 8px;\n    background: rgba(1,180,228,.12);\n    border: 1px solid rgba(1,180,228,.35);\n    color: #fff !important;\n    font-size: 13px;\n    line-height: 1.45;\n}\n\n.wdfc-app .tmdb-title-poster-placeholder {\n    width: 100%;\n    height: 100%;\n    min-height: 260px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    box-sizing: border-box;\n    padding: 24px;\n    text-align: center;\n    background: linear-gradient(135deg, #292932, #111116);\n    color: #ffffff !important;\n    font-size: clamp(18px, 2vw, 28px);\n    font-weight: 700;\n    line-height: 1.3;\n    overflow-wrap: anywhere;\n}\n\n<\/style>\n\n<section class=\"search-box\">\n\n<div class=\"field-row search-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-search\">Recherche libre<\/label>\n<input id=\"wdfc-search\" type=\"search\" placeholder=\"Mot ou partie de mot...\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-searchCategory\">Rechercher dans<\/label>\n<select id=\"wdfc-searchCategory\">\n<option value=\"title\" selected>Titre<\/option>\n<option value=\"director\">R\u00e9alisateur<\/option>\n<option value=\"cast\">Acteur<\/option>\n<option value=\"all\">Titre + r\u00e9alisateur + acteur<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-searchButton\">\ud83d\udd0e Rechercher<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-destinationSite\">Ouvrir les films sur<\/label>\n<select id=\"wdfc-destinationSite\">\n<option value=\"justwatch\" selected>JustWatch<\/option>\n<option value=\"allocine\">AlloCin\u00e9<\/option>\n<option value=\"imdb\">IMDb<\/option>\n<option value=\"custom\">URL personnalis\u00e9e<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-titleColorPicker\">Couleur des titres de films<\/label>\n<input id=\"wdfc-titleColorPicker\" type=\"color\" value=\"#00e5ff\">\n<\/div>\n\n<div class=\"field full-width\">\n<label for=\"wdfc-customSiteUrl\">URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus)<\/label>\n<input\n    id=\"wdfc-customSiteUrl\"\n    type=\"text\"\n    placeholder=\"https:\/\/exemple.com\/recherche?q={titre}\"\n    disabled\n>\n<div class=\"field-hint\">\nUtilisez <code>{titre}<\/code> dans l'URL pour qu'il soit remplac\u00e9 par le titre du film \u00e0 chaque clic. Sans <code>{titre}<\/code>, le titre est ajout\u00e9 automatiquement en param\u00e8tre <code>q=<\/code> \u00e0 la fin de l'URL saisie. Les films venant de TMDB (bouton \"Plus de films\") s'ouvrent toujours directement sur TMDB, quel que soit ce choix \u2014 voir note plus bas.\n<\/div>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-dateFilter\">Date de sortie<\/label>\n<select id=\"wdfc-dateFilter\">\n<option value=\"all\">Toutes les dates<\/option>\n<option value=\"with\">Avec date de sortie<\/option>\n<option value=\"without\">Sans date de sortie<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-day\">Derniers jours<\/label>\n<input id=\"wdfc-day\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-month\">Derniers mois<\/label>\n<input id=\"wdfc-month\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-year\">Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3)<\/label>\n<input id=\"wdfc-year\" type=\"number\" placeholder=\"Ex : 2026 ou 3\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-yearFrom\">Ann\u00e9e de (fourchette)<\/label>\n<input id=\"wdfc-yearFrom\" type=\"number\" placeholder=\"De\" min=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-yearTo\">Ann\u00e9e \u00e0 (fourchette)<\/label>\n<input id=\"wdfc-yearTo\" type=\"number\" placeholder=\"\u00c0\" min=\"1\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-dateFrom\">Date pr\u00e9cise de d\u00e9but<\/label>\n<input id=\"wdfc-dateFrom\" type=\"date\">\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-dateTo\">Date pr\u00e9cise de fin<\/label>\n<input id=\"wdfc-dateTo\" type=\"date\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-titleFilter\">Titre<\/label>\n<select id=\"wdfc-titleFilter\">\n<option value=\"all\">Tous les films<\/option>\n<option value=\"with\">Avec un titre<\/option>\n<option value=\"without\">Sans titre<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-typeFilter\">Type<\/label>\n<select id=\"wdfc-typeFilter\">\n<option value=\"film\">Films uniquement<\/option>\n<option value=\"animation\">Dessins anim\u00e9s uniquement<\/option>\n<option value=\"film_animation\" selected>Films + dessins anim\u00e9s<\/option>\n<option value=\"all\">Films et \u0153uvres audiovisuelles (large)<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-sort\">Tri<\/label>\n<select id=\"wdfc-sort\">\n<option value=\"newest\">Plus r\u00e9cents<\/option>\n<option value=\"oldest\">Plus anciens<\/option>\n<option value=\"title\">Titre A-Z<\/option>\n<\/select>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-pageSize\">R\u00e9sultats par page<\/label>\n<input id=\"wdfc-pageSize\" type=\"number\" min=\"1\" max=\"500\" value=\"20\" list=\"wdfc-pageSizePresets\">\n<datalist id=\"wdfc-pageSizePresets\">\n<option value=\"20\">\n<option value=\"50\">\n<option value=\"100\">\n<option value=\"200\">\n<\/datalist>\n<\/div>\n\n<div class=\"field\">\n<label for=\"wdfc-maxResults\">Nombre maxi de r\u00e9sultats<\/label>\n<input id=\"wdfc-maxResults\" type=\"number\" placeholder=\"Illimit\u00e9\" min=\"1\">\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field full-width\">\n<label>Langue(s) du titre <span id=\"wdfc-languageCount\" style=\"opacity:.5; text-transform:none;\"><\/span><\/label>\n<div id=\"wdfc-languageOptions\" class=\"language-options\">\n<span style=\"opacity:.6; font-size:13px;\">Chargement des langues disponibles dans la base...<\/span>\n<\/div>\n\n<label class=\"toggle-filter\" style=\"margin-top:8px;\" for=\"wdfc-allLanguages\">\n<input type=\"checkbox\" id=\"wdfc-allLanguages\">\n<span>\ud83c\udf10 Toutes les langues (ignore la liste ci-dessus)<\/span>\n<\/label>\n\n<div class=\"field-hint\">\nCochez une, plusieurs, ou toutes les langues souhait\u00e9es. Cochez \u00ab Toutes les langues \u00bb pour ne faire aucun tri par langue.\n<\/div>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<label class=\"toggle-filter\" for=\"wdfc-religiousFilter\">\n<input type=\"checkbox\" id=\"wdfc-religiousFilter\" checked>\n<span>\u271d\ufe0f Films chr\u00e9tiens \/ religieux (genre \/ sujet Wikidata)<\/span>\n<\/label>\n\n<fieldset class=\"lexical-box\">\n<legend>\u00c9largir la recherche (mots-cl\u00e9s dans le titre)<\/legend>\n<div class=\"lexical-options\">\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"chr\u00e9tien,christian\"> Chr\u00e9tien<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"religieux,religious\"> Religieux<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"biblique,biblical,bible\"> Biblique<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"spirituel,spiritual\"> Spirituel<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"croyance,belief,faith\"> Croyance<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"\u00e9vangile,gospel\"> \u00c9vangile<\/label>\n<label><input type=\"checkbox\" class=\"wdfc-lexicalKeyword\" value=\"dieu,\u00e9ternel,god,eternal\"> Dieu \/ \u00c9ternel<\/label>\n<\/div>\n<input\n    id=\"wdfc-lexicalCustom\"\n    type=\"text\"\n    placeholder=\"Autres mots-cl\u00e9s, s\u00e9par\u00e9s par une virgule (ex : messe, pri\u00e8re)\"\n    style=\"margin-top:8px;\"\n>\n<\/fieldset>\n\n<fieldset class=\"lexical-box\">\n<legend>Exclure les films contenant ces mots (titre)<\/legend>\n<input\n    id=\"wdfc-excludeWords\"\n    type=\"text\"\n    placeholder=\"Mots \u00e0 exclure, s\u00e9par\u00e9s par une virgule (ex : horreur, zombie)\"\n>\n<\/fieldset>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-cancelButton\" disabled>\u23f9 Annuler<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-resetButton\">\ud83e\uddf9 R\u00e9initialiser<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"field-row\">\n\n<div class=\"field\">\n<label for=\"wdfc-pageInput\">Aller \u00e0 la page<\/label>\n<input id=\"wdfc-pageInput\" type=\"number\" min=\"1\" value=\"1\">\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-pageButton\">\ud83d\udcc4 Aller \u00e0 la page<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-exportButton\">\u2b07\ufe0f Exporter CSV<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-reloadButton\">\ud83d\udd04 Rechercher<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-youtubeButton\" class=\"youtube-button\" type=\"button\">\u25b6\ufe0f S\u00e9lection sur YouTube<\/button>\n<\/div>\n\n<div class=\"field\">\n<label>&nbsp;<\/label>\n<button id=\"wdfc-cinemaButton\" class=\"cinema-button\" type=\"button\">\ud83c\udfa6 Vue Cin\u00e9ma<\/button>\n<\/div>\n\n<\/div>\n\n\n<div class=\"progress\"><div id=\"wdfc-progressBar\" class=\"progress-bar\"><\/div><\/div>\n\n<\/section>\n\n<div id=\"wdfc-status\" class=\"status\">Pr\u00eat.<\/div>\n\n<div id=\"wdfc-results\" class=\"results\"><\/div>\n\n<div class=\"pagination\">\n<button id=\"wdfc-previousButton\">\u25c0 Pr\u00e9c\u00e9dent<\/button>\n<span id=\"wdfc-pageInfo\">Page 1 \/ 1<\/span>\n<button id=\"wdfc-nextButton\">Suivant \u25b6<\/button>\n<\/div>\n\n<\/div>\n\n\n<script>\n(function () {\n\"use strict\";\n\nfunction init() {\n\n\/* =========================================================\n   CONFIGURATION\n========================================================= *\/\n\nconst WIKIDATA_ENDPOINT = \"https:\/\/query.wikidata.org\/sparql\";\n\nconst RELIGIOUS_GENRE_IDS = [\"wd:Q115122793\", \"wd:Q7717463\"];\n\nconst RELIGIOUS_SUBJECT_IDS = [\n    \"wd:Q5043\", \"wd:Q302\", \"wd:Q766677\", \"wd:Q190\", \"wd:Q194253\",\n    \"wd:Q5410500\", \"wd:Q1845\", \"wd:Q34274\", \"wd:Q34394\", \"wd:Q9174\",\n    \"wd:Q2728698\", \"wd:Q51628\", \"wd:Q51636\", \"wd:Q51624\",\n    \"wd:Q9077\", \"wd:Q81422\", \"wd:Q41370\", \"wd:Q9200\"\n];\n\nconst FILM_CLASS_ID = \"wd:Q11424\";\nconst ANIMATED_FILM_CLASS_ID = \"wd:Q202866\";\n\nconst COUNT_CAP = 3000;\nconst LANGUAGE_SAMPLE_SIZE = 20000;\nconst UPCOMING_LIMIT = 24;\n\nconst FALLBACK_LANGUAGE_CODES = [\n    \"fr\",\"en\",\"es\",\"de\",\"it\",\"pt\",\"nl\",\"ar\",\"ru\",\"zh\",\"ja\",\"ko\",\n    \"hi\",\"tr\",\"pl\",\"sv\",\"no\",\"da\",\"fi\",\"el\",\"he\",\"cs\",\"hu\",\"ro\",\"uk\"\n];\n\nconst DEFAULT_POSTER =\n    \"data:image\/svg+xml,\" +\n    encodeURIComponent(\n        '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 400 600\">' +\n        '<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">' +\n        '<stop offset=\"0\" stop-color=\"#34343e\"\/>' +\n        '<stop offset=\"1\" stop-color=\"#131316\"\/>' +\n        '<\/linearGradient><\/defs>' +\n        '<rect width=\"400\" height=\"600\" fill=\"url(#g)\"\/>' +\n        '<circle cx=\"200\" cy=\"248\" r=\"74\" fill=\"none\" stroke=\"#f0c400\" stroke-width=\"6\" opacity=\"0.9\"\/>' +\n        '<polygon points=\"184,210 184,286 250,248\" fill=\"#f0c400\" opacity=\"0.95\"\/>' +\n        '<text x=\"200\" y=\"412\" text-anchor=\"middle\" font-family=\"Arial, Helvetica, sans-serif\" ' +\n        'font-size=\"42\" font-weight=\"bold\" letter-spacing=\"10\" fill=\"#ffffff\" opacity=\"0.9\">FILM<\/text>' +\n        '<\/svg>'\n    );\n\n\/* =========================================================\n   VARIABLES\n========================================================= *\/\n\nlet currentPage = 1;\nlet totalPages = 1;\nlet totalKnown = true;\nlet hasNextPageUnknown = false;\nlet currentResults = [];\nlet abortController = null;\nlet isLoading = false;\n\n\/* =========================================================\n   ELEMENTS\n========================================================= *\/\n\nconst searchInput = document.getElementById(\"wdfc-search\");\nconst searchCategory = document.getElementById(\"wdfc-searchCategory\");\nconst destinationSiteSelect = document.getElementById(\"wdfc-destinationSite\");\nconst titleColorPicker = document.getElementById(\"wdfc-titleColorPicker\");\nconst customSiteUrlInput = document.getElementById(\"wdfc-customSiteUrl\");\nconst dayInput = document.getElementById(\"wdfc-day\");\nconst monthInput = document.getElementById(\"wdfc-month\");\nconst yearInput = document.getElementById(\"wdfc-year\");\nconst yearFromInput = document.getElementById(\"wdfc-yearFrom\");\nconst yearToInput = document.getElementById(\"wdfc-yearTo\");\nconst dateFromInput = document.getElementById(\"wdfc-dateFrom\");\nconst dateToInput = document.getElementById(\"wdfc-dateTo\");\nconst titleFilter = document.getElementById(\"wdfc-titleFilter\");\nconst dateFilter = document.getElementById(\"wdfc-dateFilter\");\nconst sortSelect = document.getElementById(\"wdfc-sort\");\nconst typeFilter = document.getElementById(\"wdfc-typeFilter\");\nconst religiousFilter = document.getElementById(\"wdfc-religiousFilter\");\nconst lexicalCustomInput = document.getElementById(\"wdfc-lexicalCustom\");\nconst excludeWordsInput = document.getElementById(\"wdfc-excludeWords\");\nconst languageOptionsContainer = document.getElementById(\"wdfc-languageOptions\");\nconst languageCountLabel = document.getElementById(\"wdfc-languageCount\");\nconst allLanguagesCheckbox = document.getElementById(\"wdfc-allLanguages\");\nconst pageSizeSelect = document.getElementById(\"wdfc-pageSize\");\nconst maxResultsInput = document.getElementById(\"wdfc-maxResults\");\nconst statusElement = document.getElementById(\"wdfc-status\");\nconst resultsElement = document.getElementById(\"wdfc-results\");\nconst progressBar = document.getElementById(\"wdfc-progressBar\");\nconst cancelButton = document.getElementById(\"wdfc-cancelButton\");\nconst pageInput = document.getElementById(\"wdfc-pageInput\");\nconst previousButton = document.getElementById(\"wdfc-previousButton\");\nconst nextButton = document.getElementById(\"wdfc-nextButton\");\nconst pageInfo = document.getElementById(\"wdfc-pageInfo\");\nconst youtubeButton = document.getElementById(\"wdfc-youtubeButton\");\nconst cinemaButton = document.getElementById(\"wdfc-cinemaButton\");\n\n\/* =========================================================\n   SITE D'OUVERTURE DES FILMS\n========================================================= *\/\n\nfunction getFilmDestinationUrl(film) {\n\n    if (film.isTMDB || film.source === \"TMDB\") {\n        return film.primaryLink;\n    }\n\n    const title = film.title || \"\";\n    const encodedTitle = encodeURIComponent(title);\n    const site = destinationSiteSelect.value;\n\n    if (site === \"allocine\") {\n        return title\n            ? `https:\/\/www.allocine.fr\/rechercher\/?q=${encodedTitle}`\n            : film.uri;\n    }\n\n    if (site === \"imdb\") {\n        if (film.imdb) return film.imdb;\n        return title\n            ? `https:\/\/www.imdb.com\/find\/?q=${encodedTitle}`\n            : film.uri;\n    }\n\n    if (site === \"custom\") {\n\n        const template = customSiteUrlInput.value.trim();\n\n        if (!template) return film.primaryLink;\n\n        if (template.includes(\"{titre}\")) {\n            return template.split(\"{titre}\").join(encodedTitle);\n        }\n\n        if (!title) return film.primaryLink;\n\n        const separator = template.includes(\"?\") ? \"&\" : \"?\";\n        return `${template}${separator}q=${encodedTitle}`;\n    }\n\n    if (!title) return film.uri;\n\n    return `https:\/\/www.justwatch.com\/fr\/recherche?q=${encodedTitle}`;\n}\n\n\/* =========================================================\n   UTILITAIRES DATE\n========================================================= *\/\n\nfunction daysAgo(n) {\n    const d = new Date();\n    d.setDate(d.getDate() - n);\n    return d;\n}\n\nfunction monthsAgo(n) {\n    const d = new Date();\n    d.setMonth(d.getMonth() - n);\n    return d;\n}\n\nfunction yearsAgo(n) {\n    const d = new Date();\n    d.setFullYear(d.getFullYear() - n);\n    return d;\n}\n\n\/* =========================================================\n   REQUETE SIMPLE (sans annulation), AVEC TENTATIVES AUTOMATIQUES\n========================================================= *\/\n\nasync function fetchWikidataOnce(query, attempt = 0) {\n\n    const response = await fetch(WIKIDATA_ENDPOINT, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\n            \"Accept\": \"application\/sparql-results+json\"\n        },\n        body: \"query=\" + encodeURIComponent(query)\n    });\n\n    if ([429, 502, 503, 504].includes(response.status) && attempt < 3) {\n\n        const delay = (attempt + 1) * 2500;\n\n        await new Promise(resolve => setTimeout(resolve, delay));\n\n        return fetchWikidataOnce(query, attempt + 1);\n    }\n\n    if (!response.ok) {\n        throw new Error(\"Erreur HTTP \" + response.status);\n    }\n\n    return response.json();\n}\n\n\/* =========================================================\n   LANGUES DISPONIBLES DANS LA BASE\n========================================================= *\/\n\nfunction languageDisplayName(code) {\n    try {\n        const displayNames = new Intl.DisplayNames([\"fr\"], { type: \"language\" });\n        const name = displayNames.of(code);\n        return name && name.toLowerCase() !== code.toLowerCase() ? name : code;\n    }\n    catch (error) {\n        return code;\n    }\n}\n\nfunction populateLanguageSelect(codes) {\n\n    const uniqueCodes = Array.from(new Set(codes)).filter(Boolean);\n\n    uniqueCodes.sort((a, b) =>\n        languageDisplayName(a).localeCompare(languageDisplayName(b), \"fr\")\n    );\n\n    languageOptionsContainer.innerHTML = \"\";\n\n    uniqueCodes.forEach(code => {\n\n        const label = document.createElement(\"label\");\n\n        const checkbox = document.createElement(\"input\");\n        checkbox.type = \"checkbox\";\n        checkbox.className = \"wdfc-languageCheckbox\";\n        checkbox.value = code;\n        if (code === \"fr\") checkbox.checked = true;\n\n        const span = document.createElement(\"span\");\n        span.textContent = languageDisplayName(code) + \" (\" + code + \")\";\n\n        label.appendChild(checkbox);\n        label.appendChild(span);\n        languageOptionsContainer.appendChild(label);\n    });\n\n    if (!languageOptionsContainer.querySelector(\".wdfc-languageCheckbox:checked\") && uniqueCodes.length) {\n        languageOptionsContainer.querySelector(\".wdfc-languageCheckbox\").checked = true;\n    }\n\n    languageCountLabel.textContent = \"(\" + uniqueCodes.length + \" langues trouv\u00e9es)\";\n}\n\nasync function loadAvailableLanguages() {\n\n    try {\n\n        const query = `\n            SELECT DISTINCT ?lang WHERE {\n                {\n                    SELECT DISTINCT ?item WHERE {\n                        ?item wdt:P31 ${FILM_CLASS_ID} .\n                    }\n                    LIMIT ${LANGUAGE_SAMPLE_SIZE}\n                }\n                ?item rdfs:label ?itemLabel .\n                BIND(LANG(?itemLabel) AS ?lang)\n            }\n            ORDER BY ?lang\n        `;\n\n        const data = await fetchWikidataOnce(query);\n\n        const codes = data.results.bindings\n            .map(row => row.lang.value)\n            .filter(code => code && code.length <= 8);\n\n        if (codes.length === 0) {\n            throw new Error(\"Aucune langue retourn\u00e9e\");\n        }\n\n        populateLanguageSelect(codes);\n    }\n    catch (error) {\n        console.warn(\"Langues Wikidata indisponibles, repli sur une liste par d\u00e9faut :\", error);\n        populateLanguageSelect(FALLBACK_LANGUAGE_CODES);\n    }\n}\n\n\/* =========================================================\n   LANGUES SELECTIONNEES\n========================================================= *\/\n\nfunction getSelectedLanguages() {\n\n    if (allLanguagesCheckbox.checked) {\n        return null;\n    }\n\n    const selected = Array.from(document.querySelectorAll(\".wdfc-languageCheckbox:checked\"))\n        .map(cb => cb.value)\n        .filter(v => v);\n\n    return selected.length ? selected : [\"fr\"];\n}\n\n\/* =========================================================\n   CONDITIONS DE BASE\n========================================================= *\/\n\nfunction buildBaseConditions() {\n\n    const search = searchInput.value.trim();\n\n    const selectedLanguages = getSelectedLanguages();\n\n    let labelCondition;\n\n    if (selectedLanguages === null) {\n        labelCondition = `?item rdfs:label ?itemLabel .`;\n    }\n    else {\n        const langList = selectedLanguages.map(l => `\"${l}\"`).join(\",\");\n        labelCondition = `\n            ?item rdfs:label ?itemLabel .\n            FILTER(LANG(?itemLabel) IN (${langList}))\n        `;\n    }\n\n    let typeCondition = `?item wdt:P31 ${FILM_CLASS_ID} .`;\n\n    if (typeFilter.value === \"animation\") {\n        typeCondition = `\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?animType . ?animType wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\n        `;\n    }\n    else if (typeFilter.value === \"film_animation\") {\n        typeCondition = `\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?filmType . ?filmType wdt:P279* ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?animType2 . ?animType2 wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\n        `;\n    }\n    else if (typeFilter.value === \"all\") {\n        typeCondition = `\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\n            UNION\n            { ?item wdt:P31 ?type . ?type wdt:P279* ${FILM_CLASS_ID} . }\n        `;\n    }\n\n    const lexicalKeywords =\n        Array.from(document.querySelectorAll(\".wdfc-lexicalKeyword:checked\"))\n            .flatMap(cb => cb.value.split(\",\"));\n\n    const customKeywordsRaw = lexicalCustomInput.value.trim();\n\n    if (customKeywordsRaw) {\n        customKeywordsRaw\n            .split(\",\")\n            .map(word => word.trim())\n            .filter(Boolean)\n            .forEach(word => lexicalKeywords.push(word));\n    }\n\n    const hasLexicalFilter = lexicalKeywords.length > 0;\n\n    let religiousCondition = \"\";\n\n    if (religiousFilter.checked || hasLexicalFilter) {\n\n        const branches = [];\n\n        if (religiousFilter.checked) {\n\n            branches.push(`\n                {\n                    ?item wdt:P136 ?religiousGenre .\n                    VALUES ?religiousGenre { ${RELIGIOUS_GENRE_IDS.join(\" \")} }\n                }\n            `);\n\n            branches.push(`\n                {\n                    ?item wdt:P921 ?religiousSubject .\n                    VALUES ?religiousSubject { ${RELIGIOUS_SUBJECT_IDS.join(\" \")} }\n                }\n            `);\n        }\n\n        if (hasLexicalFilter) {\n            const lexicalOr = lexicalKeywords\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\n                .join(\" || \");\n\n            branches.push(`{ FILTER(${lexicalOr}) }`);\n        }\n\n        religiousCondition = branches.join(\" UNION \");\n    }\n\n    const excludeWordsRaw = excludeWordsInput.value.trim();\n    let excludeCondition = \"\";\n\n    if (excludeWordsRaw) {\n\n        const excludeWords = excludeWordsRaw\n            .split(\",\")\n            .map(word => word.trim())\n            .filter(Boolean);\n\n        if (excludeWords.length) {\n            const excludeOr = excludeWords\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\n                .join(\" || \");\n\n            excludeCondition = `FILTER(!(${excludeOr}))`;\n        }\n    }\n\n    let searchCondition = \"\";\n\n    if (search) {\n\n        const safeSearch = search.replace(\/\"\/g, '\\\\\"');\n        const category = searchCategory.value || \"title\";\n\n        const titleMatch = `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${safeSearch}\"))`;\n\n        const directorBlock = `\n            ?item wdt:P57 ?searchDirector .\n            ?searchDirector rdfs:label ?searchDirectorLabel .\n            FILTER(LANG(?searchDirectorLabel) IN (\"fr\",\"en\"))\n            FILTER(CONTAINS(LCASE(STR(?searchDirectorLabel)), LCASE(\"${safeSearch}\")))\n        `;\n\n        const castBlock = `\n            ?item wdt:P161 ?searchCast .\n            ?searchCast rdfs:label ?searchCastLabel .\n            FILTER(LANG(?searchCastLabel) IN (\"fr\",\"en\"))\n            FILTER(CONTAINS(LCASE(STR(?searchCastLabel)), LCASE(\"${safeSearch}\")))\n        `;\n\n        if (category === \"title\") {\n            searchCondition = `FILTER(${titleMatch})`;\n        }\n        else if (category === \"director\") {\n            searchCondition = directorBlock;\n        }\n        else if (category === \"cast\") {\n            searchCondition = castBlock;\n        }\n        else {\n            searchCondition = `\n                { FILTER(${titleMatch}) }\n                UNION\n                { ${directorBlock} }\n                UNION\n                { ${castBlock} }\n            `;\n        }\n    }\n\n    return `\n        ${typeCondition}\n        ${labelCondition}\n        ${religiousCondition}\n        ${excludeCondition}\n        ${searchCondition}\n    `;\n}\n\n\/* =========================================================\n   CONDITIONS DE DATE\n========================================================= *\/\n\nfunction buildWhereClause() {\n\n    const day = dayInput.value.trim();\n    const month = monthInput.value.trim();\n    const year = yearInput.value.trim();\n    const yearFrom = yearFromInput.value.trim();\n    const yearTo = yearToInput.value.trim();\n\n    const base = buildBaseConditions();\n\n    const dateFrom = dateFromInput.value;\n    const dateTo = dateToInput.value;\n\n    const hasFullDateRange = Boolean(dateFrom || dateTo);\n    const hasYearRange = Boolean(yearFrom || yearTo);\n    const hasRollingFilter = Boolean(day || month || year);\n\n    const hasDateFilter = hasFullDateRange || hasYearRange || hasRollingFilter;\n\n    let dateVariable = `OPTIONAL{ ?item wdt:P577 ?date . }`;\n\n    if (dateFilter.value === \"with\" || hasDateFilter) {\n        dateVariable = `?item wdt:P577 ?date .`;\n    }\n\n    if (dateFilter.value === \"without\") {\n        dateVariable = `FILTER NOT EXISTS{ ?item wdt:P577 ?anyDate . }`;\n    }\n\n    let dateFilterCondition = \"\";\n\n    if (hasDateFilter && dateFilter.value !== \"without\") {\n\n        const conditions = [];\n\n        if (hasFullDateRange) {\n            if (dateFrom) conditions.push(`?date >= \"${dateFrom}T00:00:00Z\"^^xsd:dateTime`);\n            if (dateTo) conditions.push(`?date <= \"${dateTo}T23:59:59Z\"^^xsd:dateTime`);\n        }\n        else if (hasYearRange) {\n            if (yearFrom) conditions.push(`YEAR(?date) >= ${Number(yearFrom)}`);\n            if (yearTo) conditions.push(`YEAR(?date) <= ${Number(yearTo)}`);\n        }\n        else {\n\n            const now = new Date();\n            const nowIso = now.toISOString();\n\n            if (day) {\n                conditions.push(`?date >= \"${daysAgo(Number(day)).toISOString()}\"^^xsd:dateTime`);\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n            }\n\n            if (month) {\n                conditions.push(`?date >= \"${monthsAgo(Number(month)).toISOString()}\"^^xsd:dateTime`);\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n            }\n\n            if (year) {\n\n                const yearNumber = Number(year);\n\n                if (yearNumber >= 100) {\n                    conditions.push(`YEAR(?date) = ${yearNumber}`);\n                }\n                else {\n                    conditions.push(`?date >= \"${yearsAgo(yearNumber).toISOString()}\"^^xsd:dateTime`);\n                    conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\n                }\n            }\n        }\n\n        if (conditions.length) {\n            dateFilterCondition = `FILTER(${conditions.join(\" && \")})`;\n        }\n    }\n\n    return `\n        ${base}\n        ${dateVariable}\n        ${dateFilterCondition}\n    `;\n}\n\n\/* =========================================================\n   CONDITIONS \"A VENIR\" (vue Cin\u00e9ma)\n========================================================= *\/\n\nfunction buildUpcomingWhereClause() {\n\n    const base = buildBaseConditions();\n\n    return `\n        ${base}\n        ?item wdt:P577 ?date .\n        FILTER(?date > NOW())\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE COMPTAGE\n========================================================= *\/\n\nfunction buildCountQuery() {\n\n    const where = buildWhereClause();\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT (COUNT(*) AS ?count) WHERE {\n            SELECT DISTINCT ?item WHERE{ ${where} }\n            LIMIT ${COUNT_CAP}\n        }\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE LISTE (l\u00e9g\u00e8re, pagination r\u00e9elle)\n========================================================= *\/\n\nfunction buildListQuery(limit, offset) {\n\n    const where = buildWhereClause();\n\n    let order = \"DESC(?dateAgg)\";\n    if (sortSelect.value === \"oldest\") order = \"ASC(?dateAgg)\";\n    if (sortSelect.value === \"title\") order = \"LCASE(STR(?labelAgg))\";\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\n        WHERE{ ${where} }\n        GROUP BY ?item\n        ORDER BY ${order}\n        LIMIT ${limit}\n        OFFSET ${offset}\n    `;\n}\n\n\/* =========================================================\n   REQUETE DE LISTE \"A VENIR\" (vue Cin\u00e9ma)\n========================================================= *\/\n\nfunction buildUpcomingListQuery(limit) {\n\n    const where = buildUpcomingWhereClause();\n\n    return `\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\n        WHERE{ ${where} }\n        GROUP BY ?item\n        ORDER BY ASC(?dateAgg)\n        LIMIT ${limit}\n    `;\n}\n\n\/* =========================================================\n   REQUETE D'ENRICHISSEMENT\n========================================================= *\/\n\nfunction buildDetailsQuery(itemUris) {\n\n    const values = itemUris.map(uri => `<${uri}>`).join(\" \");\n\n    return `\n        SELECT ?item\n               (SAMPLE(?poster) AS ?poster)\n               (SAMPLE(?image) AS ?image)\n               (GROUP_CONCAT(DISTINCT ?directorLabel; separator=\", \") AS ?directors)\n               (GROUP_CONCAT(DISTINCT ?castLabel; separator=\", \") AS ?cast)\n               (SAMPLE(?website) AS ?website)\n               (SAMPLE(?imdb) AS ?imdb)\n        WHERE{\n            VALUES ?item { ${values} }\n            OPTIONAL{ ?item wdt:P3383 ?poster . }\n            OPTIONAL{ ?item wdt:P18 ?image . }\n            OPTIONAL{\n                ?item wdt:P57 ?director .\n                ?director rdfs:label ?directorLabel .\n                FILTER(LANG(?directorLabel) IN (\"fr\",\"en\"))\n            }\n            OPTIONAL{\n                ?item wdt:P161 ?cast .\n                ?cast rdfs:label ?castLabel .\n                FILTER(LANG(?castLabel) IN (\"fr\",\"en\"))\n            }\n            OPTIONAL{ ?item wdt:P856 ?website . }\n            OPTIONAL{\n                ?item wdt:P345 ?imdbId .\n                BIND(CONCAT(\"https:\/\/www.imdb.com\/title\/\", ?imdbId) AS ?imdb)\n            }\n        }\n        GROUP BY ?item\n    `;\n}\n\n\/* =========================================================\n   REQUETE AVEC RETRY (POST, pour \u00e9viter les URL trop longues)\n========================================================= *\/\n\nasync function fetchWikidata(query, attempt = 0) {\n\n    const response = await fetch(WIKIDATA_ENDPOINT, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\n            \"Accept\": \"application\/sparql-results+json\"\n        },\n        body: \"query=\" + encodeURIComponent(query),\n        signal: abortController.signal\n    });\n\n    if ([429, 502, 503, 504].includes(response.status)) {\n\n        if (attempt < 3) {\n\n            const delay = (attempt + 1) * 2500;\n\n            statusElement.textContent =\n                \"\u23f3 Wikidata est temporairement charg\u00e9. Nouvelle tentative dans \"\n                + (delay \/ 1000) + \" secondes...\";\n\n            await new Promise(resolve => setTimeout(resolve, delay));\n\n            return fetchWikidata(query, attempt + 1);\n        }\n    }\n\n    if (!response.ok) {\n        throw new Error(\"Erreur HTTP \" + response.status);\n    }\n\n    return response.json();\n}\n\n\/* =========================================================\n   RECHERCHE (comptage -> liste -> enrichissement)\n========================================================= *\/\n\nasync function searchWikidata() {\n\n    if (isLoading) return;\n\n    isLoading = true;\n    abortController = new AbortController();\n    cancelButton.disabled = false;\n    progressBar.style.width = \"10%\";\n    resultsElement.innerHTML = \"\";\n    statusElement.textContent = \"\u23f3 Pr\u00e9paration de la recherche...\";\n\n    try {\n\n        if (titleFilter.value === \"without\") {\n            currentResults = [];\n            totalPages = 1;\n            statusElement.textContent =\n                \"\u26a0\ufe0f Wikidata ne permet pas de r\u00e9cup\u00e9rer fiablement les films \"\n                + \"sans titre dans cette requ\u00eate l\u00e9g\u00e8re.\";\n            renderResults();\n            return;\n        }\n\n        const pageSize = Math.max(1, Number(pageSizeSelect.value) || 20);\n\n        const maxResultsRaw = maxResultsInput.value.trim();\n        const maxResults = maxResultsRaw ? Math.max(1, Number(maxResultsRaw)) : null;\n\n        statusElement.textContent = \"\u23f3 Comptage des r\u00e9sultats...\";\n        progressBar.style.width = \"20%\";\n\n        let rawCount = null;\n        let cappedAtLimit = false;\n        let effectiveTotal = null;\n\n        try {\n            const countData = await fetchWikidata(buildCountQuery());\n            rawCount = Number(countData.results.bindings[0]?.count?.value || 0);\n            cappedAtLimit = rawCount >= COUNT_CAP;\n        }\n        catch (countError) {\n\n            if (countError.name === \"AbortError\") {\n                throw countError;\n            }\n\n            console.warn(\"Comptage indisponible, repli sans total exact :\", countError);\n\n            rawCount = null;\n        }\n\n        totalKnown = rawCount !== null;\n\n        if (totalKnown) {\n\n            effectiveTotal = maxResults ? Math.min(rawCount, maxResults) : rawCount;\n            totalPages = Math.max(1, Math.ceil(effectiveTotal \/ pageSize));\n\n            if (currentPage > totalPages) {\n                currentPage = totalPages;\n            }\n\n            if (effectiveTotal === 0) {\n                currentResults = [];\n                progressBar.style.width = \"100%\";\n                statusElement.textContent = \"\u274c Aucun r\u00e9sultat.\";\n                renderResults();\n                return;\n            }\n        }\n        else {\n            totalPages = Math.max(totalPages, currentPage);\n        }\n\n        statusElement.textContent = \"\u23f3 Recherche des films...\";\n        progressBar.style.width = \"40%\";\n\n        const offset = (currentPage - 1) * pageSize;\n\n        let limit = pageSize;\n\n        if (maxResults) {\n            limit = Math.min(pageSize, Math.max(0, maxResults - offset));\n        }\n\n        const fetchLimit = totalKnown ? limit : limit + 1;\n\n        const listData = await fetchWikidata(buildListQuery(fetchLimit, offset));\n        let listRows = listData.results.bindings;\n\n        if (!totalKnown) {\n            hasNextPageUnknown = listRows.length > limit;\n            listRows = listRows.slice(0, limit);\n            totalPages = hasNextPageUnknown ? currentPage + 1 : currentPage;\n        }\n\n        progressBar.style.width = \"60%\";\n\n        if (listRows.length === 0) {\n            currentResults = [];\n            progressBar.style.width = \"100%\";\n            statusElement.textContent = \"\u274c Aucun r\u00e9sultat sur cette page.\";\n            renderResults();\n            return;\n        }\n\n        statusElement.textContent = \"\u23f3 R\u00e9cup\u00e9ration des affiches et informations...\";\n\n        const itemUris = listRows.map(row => row.item.value);\n        const detailsData = await fetchWikidata(buildDetailsQuery(itemUris));\n\n        const detailsByUri = {};\n        detailsData.results.bindings.forEach(row => {\n            detailsByUri[row.item.value] = row;\n        });\n\n        progressBar.style.width = \"90%\";\n\n        currentResults = listRows.map(row => convertResult(row, detailsByUri[row.item.value]));\n\n        progressBar.style.width = \"100%\";\n\n        let statusMessage;\n\n        if (totalKnown) {\n\n            statusMessage = \"\u2705 \" + effectiveTotal + \" r\u00e9sultat(s) trouv\u00e9(s)\";\n\n            if (cappedAtLimit && !maxResults) {\n                statusMessage += \" (au moins \" + COUNT_CAP + \" \u2014 affinez la recherche pour un compte exact)\";\n            }\n        }\n        else {\n            statusMessage =\n                \"\u26a0\ufe0f Total exact impossible \u00e0 calculer pour cette recherche \"\n                + \"(trop de crit\u00e8res combin\u00e9s). \"\n                + listRows.length + \" r\u00e9sultat(s) affich\u00e9(s) sur CETTE PAGE \"\n                + \"uniquement \u2014 ce n'est pas le total r\u00e9el, qui peut \u00eatre bien \"\n                + \"plus \u00e9lev\u00e9.\";\n        }\n\n        statusElement.textContent = statusMessage + \".\";\n\n        renderResults();\n\n    }\n    catch (error) {\n\n        if (error.name === \"AbortError\") {\n            statusElement.textContent = \"\u23f9 Recherche annul\u00e9e.\";\n        }\n        else {\n            console.error(\"Wikidata :\", error);\n            statusElement.textContent = \"\u274c Impossible d'interroger Wikidata : \" + error.message;\n        }\n    }\n    finally {\n        isLoading = false;\n        cancelButton.disabled = true;\n        abortController = null;\n    }\n}\n\n\/* =========================================================\n   CONVERSION (fusion ligne liste + ligne d\u00e9tails)\n========================================================= *\/\n\nfunction convertResult(listRow, detailsRow) {\n\n    const details = detailsRow || {};\n\n    const poster = details.poster\n        ? details.poster.value\n        : (details.image ? details.image.value : \"\");\n\n    const website = details.website ? details.website.value : \"\";\n    const imdb = details.imdb ? details.imdb.value : \"\";\n    const uri = listRow.item ? listRow.item.value : \"\";\n\n    const primaryLink = website || imdb || uri;\n\n    return {\n        uri,\n        title: listRow.labelAgg ? listRow.labelAgg.value : \"\",\n        poster,\n        directors: details.directors ? details.directors.value : \"\",\n        cast: details.cast ? details.cast.value : \"\",\n        date: listRow.dateAgg ? listRow.dateAgg.value : \"\",\n        website,\n        imdb,\n        primaryLink,\n        source: \"wikidata\"\n    };\n}\n\n\/* =========================================================\n   AFFICHAGE\n========================================================= *\/\n\nfunction renderResults() {\n\n    resultsElement.innerHTML = \"\";\n\n    const isReligiousSearch =\n        religiousFilter.checked\n        || document.querySelectorAll(\".wdfc-lexicalKeyword:checked\").length > 0;\n\n    currentResults.forEach(film => {\n\n        const card = document.createElement(\"article\");\n        card.className = \"card\";\n\n        if (film.tmdbId) {\n            card.dataset.tmdbId = film.tmdbId;\n        }\n\n        const year = film.date ? new Date(film.date).getFullYear() : \"\";\n\n        const castList = film.cast\n            ? film.cast.split(\", \").slice(0, 4).join(\", \")\n            : \"\";\n\n        const hasRealPoster = Boolean(film.poster);\n        const filmTitle = film.title || \"Titre non renseign\u00e9\";\n        const destinationUrl = getFilmDestinationUrl(film);\n\n        card.innerHTML = `\n\n            <div class=\"poster-wrap\">\n\n                <img decoding=\"async\" class=\"poster\" src=\"${film.poster || DEFAULT_POSTER}\"\n                     alt=\"${escapeHTML(filmTitle)}\" loading=\"lazy\">\n\n                ${!hasRealPoster ? `<div class=\"poster-fallback-title\">${escapeHTML(filmTitle)}<\/div>` : \"\"}\n\n                ${year ? `<div class=\"year-badge\">${year}<\/div>` : \"\"}\n\n                ${isReligiousSearch ? `<div class=\"religious-badge\">\u271d\ufe0f Religieux<\/div>` : \"\"}\n\n                <a class=\"poster-overlay\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\n                    <span class=\"overlay-btn\">\u25b6 Voir le film<\/span>\n                <\/a>\n\n            <\/div>\n\n            <div class=\"card-content\">\n\n                ${film.directors ? `<div class=\"meta-line\"><b>R\u00e9alisation :<\/b> ${escapeHTML(film.directors)}<\/div>` : \"\"}\n\n                ${castList ? `<div class=\"meta-line\"><b>Avec :<\/b> ${escapeHTML(castList)}<\/div>` : \"\"}\n\n                <div class=\"meta-line\">\ud83d\udcc5 ${formatDate(film.date)}<\/div>\n\n                <div class=\"card-links\">\n                    <a class=\"link-primary\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\u25b6 Voir le film<\/a>\n                    <a class=\"link-secondary\" href=\"${film.uri}\" target=\"_blank\" rel=\"noopener\">${film.isTMDB ? \"TMDB\" : \"Wikidata\"}<\/a>\n                <\/div>\n\n            <\/div>\n        `;\n\n        resultsElement.appendChild(card);\n    });\n\n    updatePagination();\n}\n\n\/* =========================================================\n   PAGINATION\n========================================================= *\/\n\nfunction updatePagination() {\n\n    if (totalKnown) {\n        pageInfo.textContent = \"Page \" + currentPage + \" \/ \" + totalPages;\n        previousButton.disabled = currentPage <= 1;\n        nextButton.disabled = currentPage >= totalPages;\n        pageInput.max = totalPages;\n    }\n    else {\n        pageInfo.textContent = \"Page \" + currentPage + \" (total non calcul\u00e9)\";\n        previousButton.disabled = currentPage <= 1;\n        nextButton.disabled = !hasNextPageUnknown;\n        pageInput.removeAttribute(\"max\");\n    }\n}\n\n\/* =========================================================\n   DATE \/ SECURITE\n========================================================= *\/\n\nfunction formatDate(date) {\n\n    if (!date) return \"Date non renseign\u00e9e\";\n\n    const d = new Date(date);\n\n    if (isNaN(d)) return date;\n\n    return d.toLocaleDateString(\"fr-FR\");\n}\n\nfunction escapeHTML(value) {\n    const div = document.createElement(\"div\");\n    div.textContent = value ? String(value) : \"\";\n    return div.innerHTML;\n}\n\n\/* =========================================================\n   VUE CINEMA\n========================================================= *\/\n\nfunction renderCinemaCard(film) {\n\n    const title = film.title || \"Titre non renseign\u00e9\";\n    const year = film.date ? new Date(film.date).getFullYear() : \"\";\n    const poster = film.poster || DEFAULT_POSTER;\n    const hasPoster = Boolean(film.poster);\n    const destinationUrl = getFilmDestinationUrl(film);\n\n    return `\n        <a class=\"cinema-card\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\n            <div class=\"cinema-poster-wrap\">\n                <img decoding=\"async\" src=\"${poster}\" alt=\"${escapeHTML(title)}\" loading=\"lazy\">\n                ${!hasPoster ? `<div class=\"cinema-fallback-title\">${escapeHTML(title)}<\/div>` : \"\"}\n                ${year ? `<div class=\"cinema-year\">${year}<\/div>` : \"\"}\n            <\/div>\n            <div class=\"cinema-title\">${escapeHTML(title)}<\/div>\n        <\/a>\n    `;\n}\n\nfunction renderCinemaHtml(nowShowing, upcoming, loading) {\n\n    const nowCards = nowShowing.map(renderCinemaCard).join(\"\");\n\n    let upcomingSection = \"\";\n\n    if (loading) {\n        upcomingSection = `<p class=\"cinema-loading\">\u23f3 Recherche des films \u00e0 venir dans la base...<\/p>`;\n    }\n    else if (upcoming.length) {\n        upcomingSection = `\n            <h2 class=\"cinema-section-title\">\ud83d\udd1c Prochainement<\/h2>\n            <div class=\"cinema-grid\">${upcoming.map(renderCinemaCard).join(\"\")}<\/div>\n        `;\n    }\n\n    return `\n        <!DOCTYPE html>\n        <html lang=\"fr\">\n        <head>\n        <meta charset=\"UTF-8\">\n        <title>Au cin\u00e9ma \u2014 S\u00e9lection<\/title>\n        <style>\n            * { box-sizing: border-box; }\n            body { margin:0; font-family: Arial, Helvetica, sans-serif; background:#0b0b0d; color:#fff; padding:28px; }\n            h1 { text-align:center; margin-bottom: 6px; }\n            .cinema-subtitle { text-align:center; opacity:.6; margin-bottom:28px; font-size:13px; }\n            .cinema-section-title { margin: 34px 0 16px; font-size:18px; border-left:4px solid #f0c400; padding-left:10px; }\n            .cinema-loading { opacity:.6; margin-top:20px; }\n            .cinema-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(170px,1fr)); gap:20px; }\n            .cinema-card { color:#fff; text-decoration:none; display:block; background:#1a1a1d; border-radius:12px; overflow:hidden; transition: transform .2s ease; }\n            .cinema-card:hover { transform: translateY(-4px); }\n            .cinema-poster-wrap { position:relative; width:100%; aspect-ratio:2\/3; background:#222; overflow:hidden; }\n            .cinema-poster-wrap img { width:100%; height:100%; object-fit:cover; display:block; }\n            .cinema-fallback-title {\n                position:absolute; left:0; right:0; top:71%; bottom:6%;\n                display:flex; align-items:flex-start; justify-content:center;\n                text-align:center; padding:8px 12px 0; font-weight:bold;\n                font-size:13px; line-height:1.25; text-shadow:0 2px 6px rgba(0,0,0,.9);\n            }\n            .cinema-year { position:absolute; top:8px; left:8px; background:rgba(0,0,0,.65); padding:3px 8px; border-radius:14px; font-size:11px; font-weight:bold; }\n            .cinema-title { padding:10px 12px 14px; font-size:13.5px; font-weight:bold; line-height:1.3; }\n        <\/style>\n        <\/head>\n        <body>\n            <h1>\ud83c\udfac Au cin\u00e9ma<\/h1>\n            <div class=\"cinema-subtitle\">Cliquez sur une affiche pour acc\u00e9der directement \u00e0 la page de visionnage du film<\/div>\n            <div class=\"cinema-grid\">${nowCards || \"<p>Aucun film \u00e0 afficher.<\/p>\"}<\/div>\n            ${upcomingSection}\n        <\/body>\n        <\/html>\n    `;\n}\n\nasync function openCinemaView() {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun film \u00e0 afficher. Lancez d'abord une recherche.\");\n        return;\n    }\n\n    const win = window.open(\"\", \"_blank\");\n\n    if (!win) {\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la vue cin\u00e9ma.\");\n        return;\n    }\n\n    win.document.open();\n    win.document.write(renderCinemaHtml(currentResults, [], true));\n    win.document.close();\n\n    let upcomingFilms = [];\n\n    try {\n\n        const listData = await fetchWikidataOnce(buildUpcomingListQuery(UPCOMING_LIMIT));\n        const rows = listData.results.bindings;\n\n        if (rows.length) {\n\n            const uris = rows.map(row => row.item.value);\n            const detailsData = await fetchWikidataOnce(buildDetailsQuery(uris));\n\n            const detailsByUri = {};\n            detailsData.results.bindings.forEach(row => {\n                detailsByUri[row.item.value] = row;\n            });\n\n            upcomingFilms = rows.map(row => convertResult(row, detailsByUri[row.item.value]));\n        }\n    }\n    catch (error) {\n        console.warn(\"Films \u00e0 venir indisponibles :\", error);\n    }\n\n    if (!win.closed) {\n        win.document.open();\n        win.document.write(renderCinemaHtml(currentResults, upcomingFilms, false));\n        win.document.close();\n    }\n}\n\n\/* =========================================================\n   EVENEMENTS\n========================================================= *\/\n\ncancelButton.addEventListener(\"click\", function () {\n    if (abortController) abortController.abort();\n});\n\ndocument.getElementById(\"wdfc-searchButton\").addEventListener(\"click\", function () {\n    currentPage = 1;\n    searchWikidata();\n});\n\nsearchInput.addEventListener(\"keydown\", function (event) {\n    if (event.key === \"Enter\") {\n        currentPage = 1;\n        searchWikidata();\n    }\n});\n\nnextButton.addEventListener(\"click\", function () {\n    if (currentPage < totalPages) {\n        currentPage++;\n        searchWikidata();\n    }\n});\n\npreviousButton.addEventListener(\"click\", function () {\n    if (currentPage > 1) {\n        currentPage--;\n        searchWikidata();\n    }\n});\n\ndocument.getElementById(\"wdfc-pageButton\").addEventListener(\"click\", function () {\n    let page = Number(pageInput.value);\n    if (page < 1) page = 1;\n    if (totalKnown && page > totalPages) page = totalPages;\n    currentPage = page;\n    searchWikidata();\n});\n\ndocument.getElementById(\"wdfc-reloadButton\").addEventListener(\"click\", searchWikidata);\n\nallLanguagesCheckbox.addEventListener(\"change\", function () {\n    languageOptionsContainer.classList.toggle(\"is-disabled\", allLanguagesCheckbox.checked);\n});\n\ndestinationSiteSelect.addEventListener(\"change\", function () {\n    customSiteUrlInput.disabled = destinationSiteSelect.value !== \"custom\";\n    if (currentResults.length) renderResults();\n});\n\nconst TITLE_COLOR_STORAGE_KEY = \"wdfcFilmCatalogTitleColor\";\n\ntitleColorPicker.addEventListener(\"input\", function () {\n    document.querySelector(\".wdfc-app\").style.setProperty(\"--wdfc-title-color\", titleColorPicker.value);\n\n    try {\n        localStorage.setItem(TITLE_COLOR_STORAGE_KEY, titleColorPicker.value);\n    }\n    catch (error) {\n        console.warn(\"Impossible de sauvegarder la couleur choisie :\", error);\n    }\n});\n\ncustomSiteUrlInput.addEventListener(\"input\", function () {\n    if (destinationSiteSelect.value === \"custom\" && currentResults.length) {\n        renderResults();\n    }\n});\n\nyoutubeButton.addEventListener(\"click\", function () {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun film \u00e0 afficher pour le moment. Lancez d'abord une recherche.\");\n        return;\n    }\n\n    const win = window.open(\"\", \"_blank\");\n\n    if (!win) {\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la s\u00e9lection YouTube.\");\n        return;\n    }\n\n    const linksHtml = currentResults.map(film => {\n        const title = film.title || \"Titre non renseign\u00e9\";\n        const query = encodeURIComponent(title + \" film complet\");\n        const url = \"https:\/\/www.youtube.com\/results?search_query=\" + query;\n        return \"<li><a href=\\\"\" + url + \"\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">\"\n            + escapeHTML(title) + \"<\/a><\/li>\";\n    }).join(\"\");\n\n    win.document.write(\n        \"<!DOCTYPE html><html lang=\\\"fr\\\"><head><meta charset=\\\"UTF-8\\\">\"\n        + \"<title>S\u00e9lection sur YouTube<\/title>\"\n        + \"<style>body{font-family:Arial,Helvetica,sans-serif;background:#0b0b0d;color:#fff;padding:24px;}\"\n        + \"h1{font-size:20px;} a{color:#f0c400;text-decoration:none;} a:hover{text-decoration:underline;}\"\n        + \"li{margin-bottom:10px;font-size:15px;}<\/style><\/head>\"\n        + \"<body><h1>\ud83c\udfac Recherche YouTube \u2014 s\u00e9lection actuelle (\" + currentResults.length + \" film(s))<\/h1>\"\n        + \"<ul>\" + linksHtml + \"<\/ul><\/body><\/html>\"\n    );\n\n    win.document.close();\n});\n\ncinemaButton.addEventListener(\"click\", openCinemaView);\n\ndocument.getElementById(\"wdfc-resetButton\").addEventListener(\"click\", function () {\n\n    if (isLoading && abortController) abortController.abort();\n\n    searchInput.value = \"\";\n    searchCategory.value = \"title\";\n    destinationSiteSelect.value = \"justwatch\";\n    customSiteUrlInput.value = \"\";\n    customSiteUrlInput.disabled = true;\n    dayInput.value = \"\";\n    monthInput.value = \"\";\n    yearInput.value = \"\";\n    yearFromInput.value = \"\";\n    yearToInput.value = \"\";\n    dateFromInput.value = \"\";\n    dateToInput.value = \"\";\n    titleFilter.value = \"all\";\n    dateFilter.value = \"all\";\n    typeFilter.value = \"film_animation\";\n\n    document.querySelectorAll(\".wdfc-languageCheckbox\").forEach(cb => {\n        cb.checked = (cb.value === \"fr\");\n    });\n    allLanguagesCheckbox.checked = false;\n    languageOptionsContainer.classList.remove(\"is-disabled\");\n\n    religiousFilter.checked = true;\n\n    document.querySelectorAll(\".wdfc-lexicalKeyword\").forEach(cb => { cb.checked = false; });\n    lexicalCustomInput.value = \"\";\n    excludeWordsInput.value = \"\";\n\n    sortSelect.value = \"newest\";\n    pageSizeSelect.value = \"20\";\n    maxResultsInput.value = \"\";\n\n    currentPage = 1;\n    totalPages = 1;\n    totalKnown = true;\n    hasNextPageUnknown = false;\n    currentResults = [];\n\n    resultsElement.innerHTML = \"\";\n    progressBar.style.width = \"0%\";\n    pageInput.value = 1;\n    pageInfo.textContent = \"Page 1 \/ 1\";\n\n    statusElement.textContent = \"Filtres r\u00e9initialis\u00e9s.\";\n});\n\ndocument.getElementById(\"wdfc-exportButton\").addEventListener(\"click\", function () {\n\n    if (currentResults.length === 0) {\n        alert(\"Aucun r\u00e9sultat \u00e0 exporter.\");\n        return;\n    }\n\n    const header = [\"Titre\", \"R\u00e9alisateur(s)\", \"Casting\", \"Date\", \"Lien\", \"Wikidata\", \"Source\"];\n\n    const rows = currentResults.map(film => [\n        film.title, film.directors, film.cast, film.date, film.primaryLink, film.uri, film.source\n    ]);\n\n    const csv = [header, ...rows]\n        .map(row => row.map(value =>\n            '\"' + String(value || \"\").replace(\/\"\/g, '\"\"') + '\"'\n        ).join(\";\"))\n        .join(\"\\n\");\n\n    const blob = new Blob([\"\\ufeff\" + csv], { type: \"text\/csv;charset=utf-8\" });\n    const url = URL.createObjectURL(blob);\n    const link = document.createElement(\"a\");\n    link.href = url;\n    link.download = \"films-wikidata.csv\";\n    link.click();\n    URL.revokeObjectURL(url);\n});\n\n\/* =========================================================\n   INITIALISATION\n========================================================= *\/\n\nstatusElement.textContent = \"Pr\u00eat. Entrez un terme ou lancez une recherche.\";\nloadAvailableLanguages();\n\ntry {\n    const savedTitleColor = localStorage.getItem(TITLE_COLOR_STORAGE_KEY);\n    if (savedTitleColor) {\n        titleColorPicker.value = savedTitleColor;\n        document.querySelector(\".wdfc-app\").style.setProperty(\"--wdfc-title-color\", savedTitleColor);\n    }\n}\ncatch (error) {\n    console.warn(\"Impossible de relire la couleur sauvegard\u00e9e :\", error);\n}\n\n\/* =========================================================================================\n   MODULE TMDB - PLUS DE FILMS CHRETIENS\n   (imbriqu\u00e9 ici pour partager le scope avec le reste du script \u2014 currentResults,\n   renderResults, etc. restent accessibles sans passer par window global)\n========================================================================================= *\/\n\n(function () {\n\n    \"use strict\";\n\n    const TMDB_API_KEY = \"af5576c58fe6fd7fab447fbb4dd2423a\";\n    const TMDB_API = \"https:\/\/api.themoviedb.org\/3\";\n    const TMDB_LANGUAGE = \"fr-FR\";\n    const TMDB_REGION = \"FR\";\n    const TMDB_MAX_PAGES = 500;\n\n    const CHRISTIAN_KEYWORDS = [\n        \"christianity\", \"christian\", \"christian faith\", \"christian film\",\n        \"religious film\", \"religion\", \"faith\", \"bible\", \"biblical\",\n        \"jesus christ\", \"jesus\", \"gospel\", \"resurrection\", \"crucifixion\",\n        \"church\", \"evangelical christianity\"\n    ];\n\n    const typeFilterEl = document.getElementById(\"wdfc-typeFilter\");\n    const pageSizeSelectEl = document.getElementById(\"wdfc-pageSize\");\n    const maxResultsInputEl = document.getElementById(\"wdfc-maxResults\");\n    const sortSelectEl = document.getElementById(\"wdfc-sort\");\n    const religiousFilterEl = document.getElementById(\"wdfc-religiousFilter\");\n    const allLanguagesCheckboxEl = document.getElementById(\"wdfc-allLanguages\");\n    const languageOptionsContainerEl = document.getElementById(\"wdfc-languageOptions\");\n    const statusElementEl = document.getElementById(\"wdfc-status\");\n    const progressBarEl = document.getElementById(\"wdfc-progressBar\");\n    const resultsElementEl = document.getElementById(\"wdfc-results\");\n\n    if (!typeFilterEl || !pageSizeSelectEl || !maxResultsInputEl || !sortSelectEl) {\n        console.warn(\"TMDB Plus : un ou plusieurs filtres n\u00e9cessaires sont absents.\");\n        return;\n    }\n\n    function createTMDBButton() {\n\n        if (document.getElementById(\"wdfc-tmdbPlusButton\")) return;\n\n        const button = document.createElement(\"button\");\n        button.id = \"wdfc-tmdbPlusButton\";\n        button.type = \"button\";\n        button.textContent = \"\ud83c\udfac Plus de films\";\n\n        const searchButtonEl = document.querySelector(\n            '#wdfc-searchButton, [data-action=\"search\"], button[type=\"submit\"]'\n        );\n\n        if (searchButtonEl && searchButtonEl.parentNode) {\n            searchButtonEl.parentNode.appendChild(button);\n        }\n        else if (resultsElementEl && resultsElementEl.parentNode) {\n            resultsElementEl.parentNode.insertBefore(button, resultsElementEl);\n        }\n        else {\n            document.body.appendChild(button);\n        }\n\n        const status = document.createElement(\"div\");\n        status.id = \"wdfc-tmdbPlusStatus\";\n\n        if (button.parentNode) {\n            button.parentNode.insertBefore(status, button.nextSibling);\n        }\n\n        button.addEventListener(\"click\", function () {\n            searchTMDB();\n        });\n    }\n\n    function setTMDBStatus(message) {\n        const element = document.getElementById(\"wdfc-tmdbPlusStatus\");\n        if (element) {\n            element.textContent = message;\n            element.style.display = \"block\";\n        }\n        if (statusElementEl) {\n            statusElementEl.textContent = message;\n        }\n    }\n\n    function normalizeTitle(value) {\n        return String(value || \"\")\n            .toLowerCase()\n            .normalize(\"NFD\")\n            .replace(\/[\\u0300-\\u036f]\/g, \"\")\n            .replace(\/[^\\p{L}\\p{N}]+\/gu, \" \")\n            .trim();\n    }\n\n    function getYear(value) {\n        if (!value) return \"\";\n        const match = String(value).match(\/\\b(18|19|20)\\d{2}\\b\/);\n        return match ? match[0] : \"\";\n    }\n\n    function getTMDBType() {\n        const value = typeFilterEl.value;\n        if (value === \"film\") return [\"movie\"];\n        if (value === \"animation\") return [\"animation\"];\n        if (value === \"film_animation\") return [\"movie\", \"animation\"];\n        if (value === \"all\") return [\"movie\", \"animation\"];\n        return [\"movie\"];\n    }\n\n    function getSelectedLanguagesTMDB() {\n        if (allLanguagesCheckboxEl && allLanguagesCheckboxEl.checked) return [];\n        if (!languageOptionsContainerEl) return [];\n        const checked = languageOptionsContainerEl.querySelectorAll('input[type=\"checkbox\"]:checked');\n        const languages = [];\n        checked.forEach(function (checkbox) {\n            const language = checkbox.value || checkbox.dataset.lang || checkbox.dataset.language || \"\";\n            if (language) languages.push(language.toLowerCase());\n        });\n        return [...new Set(languages)];\n    }\n\n    async function findKeywordId(keyword) {\n        const params = new URLSearchParams({ api_key: TMDB_API_KEY, query: keyword, page: \"1\" });\n        const response = await fetch(TMDB_API + \"\/search\/keyword?\" + params.toString());\n        if (!response.ok) throw new Error(\"Recherche de mot-cl\u00e9 TMDB : HTTP \" + response.status);\n        const data = await response.json();\n        if (!data.results || !data.results.length) return null;\n        const exact = data.results.find(function (item) {\n            return normalizeTitle(item.name) === normalizeTitle(keyword);\n        });\n        return (exact || data.results[0]).id;\n    }\n\n    async function getKeywordIds() {\n        const results = await Promise.all(\n            CHRISTIAN_KEYWORDS.map(function (keyword) {\n                return findKeywordId(keyword).catch(function (error) {\n                    console.warn(\"Mot-cl\u00e9 ignor\u00e9 :\", keyword, error);\n                    return null;\n                });\n            })\n        );\n        return [...new Set(results.filter(Boolean))];\n    }\n\n    async function fetchTMDBPage(keywordId, page, animationOnly) {\n        const params = new URLSearchParams({\n            api_key: TMDB_API_KEY,\n            language: TMDB_LANGUAGE,\n            region: TMDB_REGION,\n            with_keywords: String(keywordId),\n            sort_by: \"primary_release_date.desc\",\n            include_adult: \"false\",\n            page: String(page)\n        });\n        if (animationOnly) params.set(\"with_genres\", \"16\");\n        const response = await fetch(TMDB_API + \"\/discover\/movie?\" + params.toString());\n        if (!response.ok) throw new Error(\"TMDB Discover : HTTP \" + response.status);\n        return await response.json();\n    }\n\n    function matchesLanguage(movie, languages) {\n        if (!languages.length) return true;\n        const originalLanguage = String(movie.original_language || \"\").toLowerCase();\n        return languages.includes(originalLanguage);\n    }\n\n    function isExistingResult(movie) {\n        if (!Array.isArray(currentResults)) return false;\n        const tmdbId = String(movie.id);\n        const title = normalizeTitle(movie.title || movie.original_title);\n        const year = getYear(movie.release_date);\n\n        return currentResults.some(function (existing) {\n            if (existing.tmdbId && String(existing.tmdbId) === tmdbId) return true;\n            const existingTitle = normalizeTitle(existing.title);\n            const existingYear = getYear(existing.date || existing.year || existing.release_date);\n            if (title && existingTitle && title === existingTitle && year && existingYear && year === existingYear) return true;\n            if (title && existingTitle && title === existingTitle && (!year || !existingYear)) return true;\n            return false;\n        });\n    }\n\n    function convertTMDBResult(movie) {\n        const title = movie.title || movie.original_title || \"Titre non renseign\u00e9\";\n        return {\n            uri: \"tmdb:\" + movie.id,\n            title: title,\n            poster: \"\",\n            image: \"\",\n            directors: \"\",\n            cast: \"\",\n            actors: \"\",\n            date: movie.release_date || \"\",\n            year: getYear(movie.release_date),\n            description: movie.overview || \"\",\n            imdb: \"\",\n            website: \"\",\n            primaryLink: \"https:\/\/www.themoviedb.org\/movie\/\" + movie.id,\n            tmdbId: String(movie.id),\n            source: \"TMDB\",\n            isTMDB: true,\n            useTitleAsPoster: true\n        };\n    }\n\n    async function collectTMDBResults() {\n\n        const selectedTypes = getTMDBType();\n        const languages = getSelectedLanguagesTMDB();\n        const maxRaw = maxResultsInputEl.value.trim();\n        const maxResults = maxRaw ? Math.max(1, Number(maxRaw)) : null;\n        const target = maxResults || Infinity;\n\n        const keywordIds = await getKeywordIds();\n        if (!keywordIds.length) {\n            throw new Error(\"Aucun mot-cl\u00e9 chr\u00e9tien reconnu par TMDB.\");\n        }\n\n        const unique = new Map();\n\n        for (const type of selectedTypes) {\n\n            const animationOnly = type === \"animation\";\n\n            for (const keywordId of keywordIds) {\n\n                let page = 1;\n                let totalPages = 1;\n\n                do {\n\n                    if (unique.size >= target) break;\n\n                    setTMDBStatus(\n                        \"\u23f3 TMDB : recherche \" + type + \" \u2014 mot-cl\u00e9 \" + keywordId\n                        + \" \u2014 page \" + page + \" \/ \" + totalPages\n                    );\n\n                    const data = await fetchTMDBPage(keywordId, page, animationOnly);\n\n                    totalPages = Math.min(Number(data.total_pages || 1), TMDB_MAX_PAGES);\n\n                    const results = data.results || [];\n\n                    results.forEach(function (movie) {\n                        if (!matchesLanguage(movie, languages)) return;\n                        const title = normalizeTitle(movie.title || movie.original_title);\n                        const year = getYear(movie.release_date);\n                        const key = title + \"|\" + year;\n                        if (!unique.has(key)) unique.set(key, movie);\n                    });\n\n                    page++;\n\n                    if (maxResults && unique.size >= maxResults) break;\n\n                }\n                while (page <= totalPages);\n            }\n        }\n\n        let movies = Array.from(unique.values());\n\n        if (sortSelectEl.value === \"oldest\") {\n            movies.sort(function (a, b) {\n                return String(a.release_date || \"\").localeCompare(String(b.release_date || \"\"));\n            });\n        }\n        else if (sortSelectEl.value === \"title\") {\n            movies.sort(function (a, b) {\n                return normalizeTitle(a.title).localeCompare(normalizeTitle(b.title));\n            });\n        }\n        else {\n            movies.sort(function (a, b) {\n                return String(b.release_date || \"\").localeCompare(String(a.release_date || \"\"));\n            });\n        }\n\n        if (maxResults) movies = movies.slice(0, maxResults);\n\n        return movies;\n    }\n\n    async function searchTMDB() {\n\n        const button = document.getElementById(\"wdfc-tmdbPlusButton\");\n        if (button.disabled) return;\n\n        if (religiousFilterEl && !religiousFilterEl.checked) {\n            const proceed = confirm(\n                \"Le filtre \u00ab Films chr\u00e9tiens \/ religieux \u00bb est d\u00e9sactiv\u00e9.\\n\\n\"\n                + \"La recherche TMDB de ce bouton est sp\u00e9cialement con\u00e7ue \"\n                + \"pour les films chr\u00e9tiens et religieux.\\n\\n\"\n                + \"Voulez-vous continuer ?\"\n            );\n            if (!proceed) return;\n        }\n\n        button.disabled = true;\n        if (progressBarEl) progressBarEl.style.width = \"10%\";\n\n        try {\n\n            setTMDBStatus(\"\u23f3 Pr\u00e9paration de la recherche TMDB...\");\n\n            const movies = await collectTMDBResults();\n\n            if (progressBarEl) progressBarEl.style.width = \"70%\";\n\n            let added = 0;\n            let duplicates = 0;\n\n            movies.forEach(function (movie) {\n                if (isExistingResult(movie)) {\n                    duplicates++;\n                    return;\n                }\n                const converted = convertTMDBResult(movie);\n                currentResults.push(converted);\n                added++;\n            });\n\n            if (sortSelectEl.value === \"title\") {\n                currentResults.sort(function (a, b) {\n                    return normalizeTitle(a.title).localeCompare(normalizeTitle(b.title));\n                });\n            }\n            else if (sortSelectEl.value === \"oldest\") {\n                currentResults.sort(function (a, b) {\n                    return String(a.date || \"\").localeCompare(String(b.date || \"\"));\n                });\n            }\n            else {\n                currentResults.sort(function (a, b) {\n                    return String(b.date || \"\").localeCompare(String(a.date || \"\"));\n                });\n            }\n\n            if (typeof renderResults === \"function\") renderResults();\n\n            if (progressBarEl) progressBarEl.style.width = \"100%\";\n\n            setTMDBStatus(\n                \"\u2705 TMDB termin\u00e9 : \" + added + \" nouveau(x) film(s) ajout\u00e9(s). \"\n                + duplicates + \" doublon(s) ignor\u00e9(s).\"\n            );\n\n        }\n        catch (error) {\n            console.error(\"TMDB Plus :\", error);\n            setTMDBStatus(\"\u274c Erreur TMDB : \" + error.message);\n            if (progressBarEl) progressBarEl.style.width = \"100%\";\n        }\n        finally {\n            button.disabled = false;\n        }\n    }\n\n    function enhanceTMDBCards() {\n\n        if (!resultsElementEl) return;\n\n        const cards = resultsElementEl.querySelectorAll(\n            \"[data-tmdb-id], .film-card, .movie-card, .result-card\"\n        );\n\n        cards.forEach(function (card) {\n\n            const tmdbId = card.dataset.tmdbId;\n            if (!tmdbId) return;\n\n            const film = currentResults.find(function (item) {\n                return String(item.tmdbId) === String(tmdbId);\n            });\n            if (!film) return;\n\n            const poster = card.querySelector(\"img\");\n            if (poster) {\n                const placeholder = document.createElement(\"div\");\n                placeholder.className = \"tmdb-title-poster-placeholder\";\n                placeholder.textContent = film.title;\n                poster.replaceWith(placeholder);\n            }\n        });\n    }\n\n    if (resultsElementEl) {\n        const observer = new MutationObserver(function () {\n            enhanceTMDBCards();\n        });\n        observer.observe(resultsElementEl, { childList: true, subtree: true });\n    }\n\n    createTMDBButton();\n\n    console.log(\"\u2705 Module TMDB \u00ab Plus de films \u00bb charg\u00e9.\");\n\n})();\n\n} \/\/ fin de init()\n\nif (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", init);\n}\nelse {\n    init();\n}\n\n})();\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-1acee22 e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"1acee22\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-8abc91f elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-heading\" data-id=\"8abc91f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">test 2<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-9cc7dd9 e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"9cc7dd9\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-99117e8 elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-spacer\" data-id=\"99117e8\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-d646ea8 e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"d646ea8\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-4970c28 e-con-full e-flex elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child\" data-id=\"4970c28\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-17b557c e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"17b557c\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1c4bd49 elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-html\" data-id=\"1c4bd49\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"fr\">\r\n<head>\r\n<meta charset=\"UTF-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n<title>Catalogue Films - Wikidata<\/title>\r\n\r\n<style>\r\n\r\n* { box-sizing: border-box; }\r\n\r\n:root { color-scheme: dark; }\r\n\r\n\/*\r\n   IMPORTANT : on ne stylise plus \"body\" directement.\r\n   Dans WordPress\/Elementor, \"body\" appartient au th\u00e8me du\r\n   site entier \u2014 le styliser ici entrait en conflit avec les\r\n   r\u00e8gles CSS du th\u00e8me (charg\u00e9es apr\u00e8s ou avec une sp\u00e9cificit\u00e9\r\n   diff\u00e9rente selon les pages), ce qui rendait le texte blanc\r\n   illisible une fois ins\u00e9r\u00e9 dans la page. Tout est maintenant\r\n   scop\u00e9 \u00e0 \".app\" avec !important sur les couleurs critiques,\r\n   pour ne d\u00e9pendre d'aucun style h\u00e9rit\u00e9 du site parent.\r\n*\/\r\n\r\n.app {\r\n    font-family: Arial, Helvetica, sans-serif !important;\r\n    background: #0b0b0d !important;\r\n    color: #fff !important;\r\n    margin: 0;\r\n\r\n    \/* Plein largeur, m\u00eame dans un conteneur WordPress\/Elementor\r\n       \"boxed\" (centr\u00e9 avec une largeur maximale) : on sort du\r\n       conteneur parent en s'alignant sur la largeur de la\r\n       fen\u00eatre plut\u00f4t que sur celle du bloc qui nous contient. *\/\r\n    width: 100vw;\r\n    max-width: 100vw;\r\n    position: relative;\r\n    left: 50%;\r\n    transform: translateX(-50%);\r\n    padding: 25px;\r\n}\r\n\r\n.app, .app * {\r\n    box-sizing: border-box;\r\n}\r\n\r\n.search-box {\r\n    background: #1d1d1d;\r\n    border-radius: 14px;\r\n    padding: 20px;\r\n    margin-bottom: 20px;\r\n}\r\n\r\n.field-row {\r\n    display: grid;\r\n    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\r\n    gap: 12px;\r\n    margin-bottom: 16px;\r\n}\r\n\r\n.field-row:last-child { margin-bottom: 0; }\r\n\r\n.field-row.search-row {\r\n    grid-template-columns: 3fr 1.4fr auto;\r\n}\r\n\r\n.field label {\r\n    display: block;\r\n    font-size: 11.5px;\r\n    text-transform: uppercase;\r\n    letter-spacing: .4px;\r\n    opacity: .55;\r\n    margin-bottom: 5px;\r\n    color: #fff !important;\r\n}\r\n\r\n.field.full-width { grid-column: 1 \/ -1; }\r\n\r\ninput, select {\r\n    width: 100%;\r\n    padding: 12px;\r\n    border-radius: 8px;\r\n    border: 1px solid #444;\r\n    background: #111 !important;\r\n    color: #fff !important;\r\n    font-size: 14px;\r\n}\r\n\r\nselect[multiple] { padding: 8px; }\r\n\r\nselect:disabled, input:disabled {\r\n    opacity: .4;\r\n    cursor: not-allowed;\r\n}\r\n\r\n.field-hint {\r\n    font-size: 11.5px;\r\n    opacity: .55;\r\n    margin-top: 6px;\r\n    color: #fff !important;\r\n}\r\n\r\nbutton {\r\n    padding: 12px 18px;\r\n    border: 0;\r\n    border-radius: 8px;\r\n    background: #f0c400 !important;\r\n    color: #111 !important;\r\n    font-weight: bold;\r\n    cursor: pointer;\r\n}\r\n\r\nbutton:hover { opacity: .85; }\r\nbutton:disabled { opacity: .4; cursor: not-allowed; }\r\n\r\n.youtube-button { background: #ff0000 !important; color: #fff !important; }\r\n.cinema-button { background: #2c2c30 !important; color: #fff !important; border: 1px solid #f0c400; }\r\n\r\n.toggle-filter {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 10px;\r\n    padding: 12px;\r\n    border-radius: 8px;\r\n    border: 1px solid #444;\r\n    background: #111;\r\n    cursor: pointer;\r\n    user-select: none;\r\n}\r\n\r\n.toggle-filter input {\r\n    width: 18px;\r\n    height: 18px;\r\n    accent-color: #f0c400;\r\n    cursor: pointer;\r\n}\r\n\r\n.toggle-filter span { font-size: 13.5px; color: #fff !important; }\r\n\r\n.lexical-box {\r\n    border: 1px solid #444;\r\n    border-radius: 8px;\r\n    padding: 12px;\r\n    background: #111;\r\n}\r\n\r\n.lexical-box legend {\r\n    font-size: 11.5px;\r\n    text-transform: uppercase;\r\n    letter-spacing: .4px;\r\n    opacity: .65;\r\n    padding: 0 6px;\r\n    color: #fff !important;\r\n}\r\n\r\n.lexical-options {\r\n    display: flex;\r\n    flex-wrap: wrap;\r\n    gap: 12px;\r\n    margin-top: 6px;\r\n}\r\n\r\n.lexical-options label {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 6px;\r\n    font-size: 13px;\r\n    cursor: pointer;\r\n    color: #fff !important;\r\n}\r\n\r\n.lexical-options input {\r\n    width: 16px;\r\n    height: 16px;\r\n    accent-color: #f0c400;\r\n    cursor: pointer;\r\n}\r\n\r\n.language-options {\r\n    max-height: 190px;\r\n    overflow-y: auto;\r\n    border: 1px solid #444;\r\n    border-radius: 8px;\r\n    padding: 12px;\r\n    background: #111;\r\n    display: flex;\r\n    flex-wrap: wrap;\r\n    gap: 10px 18px;\r\n}\r\n\r\n.language-options.is-disabled {\r\n    opacity: .4;\r\n    pointer-events: none;\r\n}\r\n\r\n.language-options label {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 6px;\r\n    font-size: 13px;\r\n    cursor: pointer;\r\n    color: #fff !important;\r\n}\r\n\r\n.language-options input {\r\n    width: 16px;\r\n    height: 16px;\r\n    accent-color: #f0c400;\r\n    cursor: pointer;\r\n}\r\n\r\n.status { margin: 20px 0; text-align: center; min-height: 25px; color: #fff !important; }\r\n\r\n.progress {\r\n    width: 100%;\r\n    height: 8px;\r\n    background: #333;\r\n    border-radius: 10px;\r\n    overflow: hidden;\r\n    margin-top: 15px;\r\n}\r\n\r\n.progress-bar {\r\n    width: 0%;\r\n    height: 100%;\r\n    background: #f0c400;\r\n    transition: width .2s;\r\n}\r\n\r\n.results {\r\n    display: grid;\r\n    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\r\n    gap: 24px;\r\n}\r\n\r\n.card {\r\n    position: relative;\r\n    background: #1a1a1d;\r\n    border-radius: 14px;\r\n    overflow: hidden;\r\n    box-shadow: 0 4px 14px rgba(0,0,0,.4);\r\n    transition: transform .25s ease, box-shadow .25s ease;\r\n    display: flex;\r\n    flex-direction: column;\r\n}\r\n\r\n.card:hover {\r\n    transform: translateY(-6px);\r\n    box-shadow: 0 14px 30px rgba(0,0,0,.6);\r\n}\r\n\r\n.poster-wrap {\r\n    position: relative;\r\n    width: 100%;\r\n    aspect-ratio: 2\/3;\r\n    background: #222;\r\n    overflow: hidden;\r\n}\r\n\r\n.poster {\r\n    width: 100%;\r\n    height: 100%;\r\n    object-fit: cover;\r\n    display: block;\r\n    transition: transform .35s ease;\r\n}\r\n\r\n.card:hover .poster { transform: scale(1.06); }\r\n\r\n\/* Nom du film affich\u00e9 SOUS le mot \"FILM\" du visuel de\r\n   remplacement (le mot \"FILM\" du SVG est positionn\u00e9 vers\r\n   68-70% de la hauteur de l'affiche). *\/\r\n.poster-fallback-title {\r\n    position: absolute;\r\n    left: 0;\r\n    right: 0;\r\n    top: 71%;\r\n    bottom: 6%;\r\n    display: flex;\r\n    align-items: flex-start;\r\n    justify-content: center;\r\n    text-align: center;\r\n    padding: 8px 14px 0;\r\n    font-weight: bold;\r\n    font-size: clamp(13px, 1.4vw + 8px, 18px);\r\n    line-height: 1.25;\r\n    color: var(--title-color, #00e5ff) !important;\r\n    text-shadow: 0 2px 8px rgba(0,0,0,.9);\r\n    pointer-events: none;\r\n    overflow: hidden;\r\n}\r\n\r\n.year-badge {\r\n    position: absolute;\r\n    top: 10px;\r\n    left: 10px;\r\n    background: rgba(0,0,0,.65);\r\n    backdrop-filter: blur(3px);\r\n    padding: 4px 10px;\r\n    border-radius: 20px;\r\n    font-size: 12px;\r\n    font-weight: bold;\r\n    color: #fff !important;\r\n}\r\n\r\n.religious-badge {\r\n    position: absolute;\r\n    top: 10px;\r\n    right: 10px;\r\n    background: rgba(240,196,0,.9);\r\n    color: #111 !important;\r\n    padding: 4px 10px;\r\n    border-radius: 20px;\r\n    font-size: 11px;\r\n    font-weight: bold;\r\n}\r\n\r\n.poster-overlay {\r\n    position: absolute;\r\n    inset: 0;\r\n    background: linear-gradient(to top, rgba(0,0,0,.92) 0%, rgba(0,0,0,.15) 55%, rgba(0,0,0,0) 100%);\r\n    opacity: 0;\r\n    transition: opacity .25s ease;\r\n    display: flex;\r\n    align-items: flex-end;\r\n    padding: 14px;\r\n}\r\n\r\n.card:hover .poster-overlay { opacity: 1; }\r\n\r\n.overlay-btn {\r\n    display: inline-block;\r\n    width: 100%;\r\n    text-align: center;\r\n    padding: 10px;\r\n    background: #f0c400;\r\n    color: #111 !important;\r\n    font-weight: bold;\r\n    text-decoration: none;\r\n    border-radius: 8px;\r\n    font-size: 13px;\r\n}\r\n\r\n.card-content {\r\n    padding: 14px 16px 16px;\r\n    flex: 1;\r\n    display: flex;\r\n    flex-direction: column;\r\n    gap: 6px;\r\n}\r\n\r\n\/*\r\n   Titre du film : rouge par d\u00e9faut, personnalisable via le\r\n   s\u00e9lecteur de couleur (bouton \"Couleur des titres\"), qui\r\n   modifie la variable --title-color. Taille de police\r\n   responsive (clamp) pour s'adapter \u00e0 la largeur de la carte\r\n   plut\u00f4t que d'\u00eatre coup\u00e9 sur 2 lignes fixes : le titre\r\n   s'affiche donc en entier, quelle que soit sa longueur.\r\n*\/\r\n.meta-line {\r\n    font-size: 12.5px;\r\n    color: rgba(255,255,255,.75) !important;\r\n    line-height: 1.5;\r\n    overflow-wrap: break-word;\r\n}\r\n\r\n.meta-line b { color: #fff !important; }\r\n\r\n.card-links {\r\n    margin-top: auto;\r\n    display: flex;\r\n    gap: 8px;\r\n    padding-top: 8px;\r\n}\r\n\r\n.link-primary, .link-secondary {\r\n    text-decoration: none;\r\n    text-align: center;\r\n    border-radius: 7px;\r\n    font-size: 12.5px;\r\n    font-weight: bold;\r\n    padding: 9px 6px;\r\n}\r\n\r\n.link-primary { flex: 2; background: #f0c400; color: #111 !important; }\r\n.link-secondary { flex: 1; background: #2c2c30; color: #fff !important; }\r\n.link-primary:hover, .link-secondary:hover { opacity: .85; }\r\n\r\n\/*\r\n   S\u00e9lecteur de couleur des titres : rond plut\u00f4t que\r\n   rectangulaire, taille r\u00e9duite de moiti\u00e9 environ.\r\n*\/\r\n#titleColorPicker {\r\n    -webkit-appearance: none;\r\n    appearance: none;\r\n    width: 22px;\r\n    height: 22px;\r\n    padding: 0;\r\n    border: 2px solid #444;\r\n    border-radius: 50%;\r\n    cursor: pointer;\r\n    background: none;\r\n    overflow: hidden;\r\n}\r\n\r\n#titleColorPicker::-webkit-color-swatch-wrapper {\r\n    padding: 0;\r\n    border-radius: 50%;\r\n}\r\n\r\n#titleColorPicker::-webkit-color-swatch {\r\n    border: none;\r\n    border-radius: 50%;\r\n}\r\n\r\n#titleColorPicker::-moz-color-swatch {\r\n    border: none;\r\n    border-radius: 50%;\r\n}\r\n\r\n.pagination {\r\n    display: flex;\r\n    justify-content: center;\r\n    align-items: center;\r\n    gap: 15px;\r\n    margin-top: 30px;\r\n    flex-wrap: wrap;\r\n    color: #fff !important;\r\n}\r\n\r\n\r\n@media (max-width: 700px) {\r\n    .field-row.search-row { grid-template-columns: 1fr; }\r\n}\r\n\r\n<\/style>\r\n<\/head>\r\n\r\n<body>\r\n<div class=\"app\">\r\n\r\n<section class=\"search-box\">\r\n\r\n<div class=\"field-row search-row\">\r\n\r\n<div class=\"field\">\r\n<label for=\"search\">Recherche libre<\/label>\r\n<input id=\"search\" type=\"search\" placeholder=\"Mot ou partie de mot...\">\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"searchCategory\">Rechercher dans<\/label>\r\n<select id=\"searchCategory\">\r\n<option value=\"title\" selected>Titre<\/option>\r\n<option value=\"director\">R\u00e9alisateur<\/option>\r\n<option value=\"cast\">Acteur<\/option>\r\n<option value=\"all\">Titre + r\u00e9alisateur + acteur<\/option>\r\n<\/select>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"searchButton\">\ud83d\udd0e Rechercher<\/button>\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"field-row\">\r\n\r\n<div class=\"field\">\r\n<label for=\"destinationSite\">Ouvrir les films sur<\/label>\r\n<select id=\"destinationSite\">\r\n<option value=\"justwatch\" selected>JustWatch<\/option>\r\n<option value=\"allocine\">AlloCin\u00e9<\/option>\r\n<option value=\"imdb\">IMDb<\/option>\r\n<option value=\"custom\">URL personnalis\u00e9e<\/option>\r\n<\/select>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"titleColorPicker\">Couleur des titres de films<\/label>\r\n<input id=\"titleColorPicker\" type=\"color\" value=\"#00e5ff\">\r\n<\/div>\r\n\r\n<div class=\"field full-width\">\r\n<label for=\"customSiteUrl\">URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus)<\/label>\r\n<input\r\n    id=\"customSiteUrl\"\r\n    type=\"text\"\r\n    placeholder=\"https:\/\/exemple.com\/recherche?q={titre}\"\r\n    disabled\r\n>\r\n<div class=\"field-hint\">\r\nUtilisez <code>{titre}<\/code> dans l'URL pour qu'il soit remplac\u00e9 par le titre du film \u00e0 chaque clic (ex : <code>https:\/\/www.google.com\/search?q={titre}+film<\/code>). Sans <code>{titre}<\/code>, le titre est ajout\u00e9 automatiquement en param\u00e8tre <code>q=<\/code> \u00e0 la fin de l'URL saisie, pour toujours atterrir sur le film cliqu\u00e9 plut\u00f4t que sur la page d'accueil du site.\r\n<\/div>\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"field-row\">\r\n\r\n<div class=\"field\">\r\n<label for=\"dateFilter\">Date de sortie<\/label>\r\n<select id=\"dateFilter\">\r\n<option value=\"all\">Toutes les dates<\/option>\r\n<option value=\"with\">Avec date de sortie<\/option>\r\n<option value=\"without\">Sans date de sortie<\/option>\r\n<\/select>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"day\">Derniers jours<\/label>\r\n<input id=\"day\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"month\">Derniers mois<\/label>\r\n<input id=\"month\" type=\"number\" placeholder=\"Ex : 7\" min=\"1\">\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"year\">Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3)<\/label>\r\n<input id=\"year\" type=\"number\" placeholder=\"Ex : 2026 ou 3\" min=\"1\">\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"yearFrom\">Ann\u00e9e de (fourchette)<\/label>\r\n<input id=\"yearFrom\" type=\"number\" placeholder=\"De\" min=\"1\">\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"yearTo\">Ann\u00e9e \u00e0 (fourchette)<\/label>\r\n<input id=\"yearTo\" type=\"number\" placeholder=\"\u00c0\" min=\"1\">\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"field-row\">\r\n\r\n<div class=\"field\">\r\n<label for=\"dateFrom\">Date pr\u00e9cise de d\u00e9but<\/label>\r\n<input id=\"dateFrom\" type=\"date\">\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"dateTo\">Date pr\u00e9cise de fin<\/label>\r\n<input id=\"dateTo\" type=\"date\">\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"field-row\">\r\n\r\n<div class=\"field\">\r\n<label for=\"titleFilter\">Titre<\/label>\r\n<select id=\"titleFilter\">\r\n<option value=\"all\">Tous les films<\/option>\r\n<option value=\"with\">Avec un titre<\/option>\r\n<option value=\"without\">Sans titre<\/option>\r\n<\/select>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"typeFilter\">Type<\/label>\r\n<select id=\"typeFilter\">\r\n<option value=\"film\">Films uniquement<\/option>\r\n<option value=\"animation\">Dessins anim\u00e9s uniquement<\/option>\r\n<option value=\"film_animation\" selected>Films + dessins anim\u00e9s<\/option>\r\n<option value=\"all\">Films et \u0153uvres audiovisuelles (large)<\/option>\r\n<\/select>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"sort\">Tri<\/label>\r\n<select id=\"sort\">\r\n<option value=\"newest\">Plus r\u00e9cents<\/option>\r\n<option value=\"oldest\">Plus anciens<\/option>\r\n<option value=\"title\">Titre A-Z<\/option>\r\n<\/select>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"pageSize\">R\u00e9sultats par page<\/label>\r\n<input id=\"pageSize\" type=\"number\" min=\"1\" max=\"500\" value=\"20\" list=\"pageSizePresets\">\r\n<datalist id=\"pageSizePresets\">\r\n<option value=\"20\">\r\n<option value=\"50\">\r\n<option value=\"100\">\r\n<option value=\"200\">\r\n<\/datalist>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label for=\"maxResults\">Nombre maxi de r\u00e9sultats<\/label>\r\n<input id=\"maxResults\" type=\"number\" placeholder=\"Illimit\u00e9\" min=\"1\">\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"field-row\">\r\n\r\n<div class=\"field full-width\">\r\n<label>Langue(s) du titre <span id=\"languageCount\" style=\"opacity:.5; text-transform:none;\"><\/span><\/label>\r\n<div id=\"languageOptions\" class=\"language-options\">\r\n<span style=\"opacity:.6; font-size:13px;\">Chargement des langues disponibles dans la base...<\/span>\r\n<\/div>\r\n\r\n<label class=\"toggle-filter\" style=\"margin-top:8px;\" for=\"allLanguages\">\r\n<input type=\"checkbox\" id=\"allLanguages\">\r\n<span>\ud83c\udf10 Toutes les langues (ignore la liste ci-dessus)<\/span>\r\n<\/label>\r\n\r\n<div class=\"field-hint\">\r\nLa liste ci-dessus est construite \u00e0 partir des langues r\u00e9ellement pr\u00e9sentes dans les libell\u00e9s de films sur Wikidata (\u00e9chantillon). Cochez une, plusieurs, ou toutes les langues souhait\u00e9es (aucune touche \u00e0 maintenir). Cochez \u00ab Toutes les langues \u00bb pour ne faire aucun tri par langue.\r\n<\/div>\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"field-row\">\r\n\r\n<label class=\"toggle-filter\" for=\"religiousFilter\">\r\n<input type=\"checkbox\" id=\"religiousFilter\" checked>\r\n<span>\u271d\ufe0f Films chr\u00e9tiens \/ religieux (genre \/ sujet Wikidata)<\/span>\r\n<\/label>\r\n\r\n<fieldset class=\"lexical-box\">\r\n<legend>\u00c9largir la recherche (mots-cl\u00e9s dans le titre)<\/legend>\r\n<div class=\"lexical-options\">\r\n<label><input type=\"checkbox\" class=\"lexicalKeyword\" value=\"chr\u00e9tien,christian\"> Chr\u00e9tien<\/label>\r\n<label><input type=\"checkbox\" class=\"lexicalKeyword\" value=\"religieux,religious\"> Religieux<\/label>\r\n<label><input type=\"checkbox\" class=\"lexicalKeyword\" value=\"biblique,biblical,bible\"> Biblique<\/label>\r\n<label><input type=\"checkbox\" class=\"lexicalKeyword\" value=\"spirituel,spiritual\"> Spirituel<\/label>\r\n<label><input type=\"checkbox\" class=\"lexicalKeyword\" value=\"croyance,belief,faith\"> Croyance<\/label>\r\n<label><input type=\"checkbox\" class=\"lexicalKeyword\" value=\"\u00e9vangile,gospel\"> \u00c9vangile<\/label>\r\n<label><input type=\"checkbox\" class=\"lexicalKeyword\" value=\"dieu,\u00e9ternel,god,eternal\"> Dieu \/ \u00c9ternel<\/label>\r\n<\/div>\r\n<input\r\n    id=\"lexicalCustom\"\r\n    type=\"text\"\r\n    placeholder=\"Autres mots-cl\u00e9s, s\u00e9par\u00e9s par une virgule (ex : messe, pri\u00e8re)\"\r\n    style=\"margin-top:8px;\"\r\n>\r\n<\/fieldset>\r\n\r\n<fieldset class=\"lexical-box\">\r\n<legend>Exclure les films contenant ces mots (titre)<\/legend>\r\n<input\r\n    id=\"excludeWords\"\r\n    type=\"text\"\r\n    placeholder=\"Mots \u00e0 exclure, s\u00e9par\u00e9s par une virgule (ex : horreur, zombie)\"\r\n>\r\n<\/fieldset>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"cancelButton\" disabled>\u23f9 Annuler<\/button>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"resetButton\">\ud83e\uddf9 R\u00e9initialiser<\/button>\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"field-row\">\r\n\r\n<div class=\"field\">\r\n<label for=\"pageInput\">Aller \u00e0 la page<\/label>\r\n<input id=\"pageInput\" type=\"number\" min=\"1\" value=\"1\">\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"pageButton\">\ud83d\udcc4 Aller \u00e0 la page<\/button>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"exportButton\">\u2b07\ufe0f Exporter CSV<\/button>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"reloadButton\">\ud83d\udd04 Rechercher<\/button>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"youtubeButton\" class=\"youtube-button\" type=\"button\">\u25b6\ufe0f S\u00e9lection sur YouTube<\/button>\r\n<\/div>\r\n\r\n<div class=\"field\">\r\n<label>&nbsp;<\/label>\r\n<button id=\"cinemaButton\" class=\"cinema-button\" type=\"button\">\ud83c\udfa6 Vue Cin\u00e9ma<\/button>\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<div class=\"progress\"><div id=\"progressBar\" class=\"progress-bar\"><\/div><\/div>\r\n\r\n<\/section>\r\n\r\n<div id=\"status\" class=\"status\">Pr\u00eat.<\/div>\r\n\r\n<div id=\"results\" class=\"results\"><\/div>\r\n\r\n<div class=\"pagination\">\r\n<button id=\"previousButton\">\u25c0 Pr\u00e9c\u00e9dent<\/button>\r\n<span id=\"pageInfo\">Page 1 \/ 1<\/span>\r\n<button id=\"nextButton\">Suivant \u25b6<\/button>\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n\r\n<script>\r\n\r\n\/* =========================================================\r\n   CONFIGURATION\r\n========================================================= *\/\r\n\r\nconst WIKIDATA_ENDPOINT = \"https:\/\/query.wikidata.org\/sparql\";\r\n\r\n\/\/ Genre \"Christian film\" \/ \"Bible film\" (correspondance directe,\r\n\/\/ sans remont\u00e9e transitive vers des sous-genres).\r\nconst RELIGIOUS_GENRE_IDS = [\"wd:Q115122793\", \"wd:Q7717463\"];\r\n\r\n\/\/ Sujet principal : liste explicite et cibl\u00e9e (PAS de recherche\r\n\/\/ transitive sur \"religion\" \u2014 celle-ci remontait automatiquement\r\n\/\/ islam, bouddhisme, hindouisme, catholicisme, etc., ce qui n'est\r\n\/\/ pas souhait\u00e9 ici).\r\n\/\/\r\n\/\/ Q5043    Christianisme\r\n\/\/ Q302     J\u00e9sus\r\n\/\/ Q766677  L'\u00c9ternel (Yahweh)\r\n\/\/ Q190     Dieu\r\n\/\/ Q194253  \u00c9vang\u00e9lisme\r\n\/\/ Q5410500 La foi\r\n\/\/ Q1845    Bible\r\n\/\/ Q34274   \u00c9vangile\r\n\/\/ Q34394   Croyance\r\n\/\/ Q9174    Religieux (religion, en tant que concept g\u00e9n\u00e9ral \u2014 pas\r\n\/\/          en mode transitif, donc sans les autres religions)\r\n\/\/ Q2728698 Croyance religieuse\r\n\/\/ Q51628   Nativit\u00e9 de J\u00e9sus\r\n\/\/ Q51636   Crucifixion de J\u00e9sus\r\n\/\/ Q51624   R\u00e9surrection de J\u00e9sus\r\n\/\/ Q9077    Mo\u00efse\r\n\/\/ Q81422   No\u00e9\r\n\/\/ Q41370   David (roi)\r\n\/\/ Q9200    Paul (ap\u00f4tre)\r\nconst RELIGIOUS_SUBJECT_IDS = [\r\n    \"wd:Q5043\", \"wd:Q302\", \"wd:Q766677\", \"wd:Q190\", \"wd:Q194253\",\r\n    \"wd:Q5410500\", \"wd:Q1845\", \"wd:Q34274\", \"wd:Q34394\", \"wd:Q9174\",\r\n    \"wd:Q2728698\", \"wd:Q51628\", \"wd:Q51636\", \"wd:Q51624\",\r\n    \"wd:Q9077\", \"wd:Q81422\", \"wd:Q41370\", \"wd:Q9200\"\r\n];\r\n\r\n\/\/ \"Film\" et \"film d'animation \/ dessin anim\u00e9\" sur Wikidata\r\nconst FILM_CLASS_ID = \"wd:Q11424\";\r\nconst ANIMATED_FILM_CLASS_ID = \"wd:Q202866\";\r\n\r\n\/\/ Plafond de comptage : au-del\u00e0, on affiche \"N+ r\u00e9sultats\"\r\n\/\/ plut\u00f4t que de lancer un COUNT potentiellement tr\u00e8s co\u00fbteux\r\n\/\/ sur toute la base.\r\nconst COUNT_CAP = 3000;\r\n\r\n\/\/ Nombre de films d'\u00e9chantillon utilis\u00e9 pour d\u00e9terminer quelles\r\n\/\/ langues sont r\u00e9ellement disponibles dans la base (\u00e9vite un\r\n\/\/ balayage complet de tout Wikidata, trop co\u00fbteux).\r\nconst LANGUAGE_SAMPLE_SIZE = 20000;\r\n\r\n\/\/ Nombre de films \"\u00e0 venir\" r\u00e9cup\u00e9r\u00e9s pour la vue Cin\u00e9ma.\r\nconst UPCOMING_LIMIT = 24;\r\n\r\n\/\/ Repli utilis\u00e9 si la d\u00e9tection dynamique des langues \u00e9choue\r\n\/\/ (Wikidata indisponible, requ\u00eate trop longue, etc.)\r\nconst FALLBACK_LANGUAGE_CODES = [\r\n    \"fr\",\"en\",\"es\",\"de\",\"it\",\"pt\",\"nl\",\"ar\",\"ru\",\"zh\",\"ja\",\"ko\",\r\n    \"hi\",\"tr\",\"pl\",\"sv\",\"no\",\"da\",\"fi\",\"el\",\"he\",\"cs\",\"hu\",\"ro\",\"uk\"\r\n];\r\n\r\n\/\/ Image utilis\u00e9e quand un film n'a pas d'affiche sur Wikidata.\r\n\/\/ G\u00e9n\u00e9r\u00e9e en SVG (pas de d\u00e9pendance r\u00e9seau, toujours disponible).\r\nconst DEFAULT_POSTER =\r\n    \"data:image\/svg+xml,\" +\r\n    encodeURIComponent(\r\n        '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 400 600\">' +\r\n        '<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">' +\r\n        '<stop offset=\"0\" stop-color=\"#34343e\"\/>' +\r\n        '<stop offset=\"1\" stop-color=\"#131316\"\/>' +\r\n        '<\/linearGradient><\/defs>' +\r\n        '<rect width=\"400\" height=\"600\" fill=\"url(#g)\"\/>' +\r\n        '<circle cx=\"200\" cy=\"248\" r=\"74\" fill=\"none\" stroke=\"#f0c400\" stroke-width=\"6\" opacity=\"0.9\"\/>' +\r\n        '<polygon points=\"184,210 184,286 250,248\" fill=\"#f0c400\" opacity=\"0.95\"\/>' +\r\n        '<text x=\"200\" y=\"412\" text-anchor=\"middle\" font-family=\"Arial, Helvetica, sans-serif\" ' +\r\n        'font-size=\"42\" font-weight=\"bold\" letter-spacing=\"10\" fill=\"#ffffff\" opacity=\"0.9\">FILM<\/text>' +\r\n        '<\/svg>'\r\n    );\r\n\r\n\/* =========================================================\r\n   VARIABLES\r\n========================================================= *\/\r\n\r\nlet currentPage = 1;\r\nlet totalPages = 1;\r\nlet totalKnown = true;\r\nlet hasNextPageUnknown = false;\r\nlet currentResults = [];\r\nlet abortController = null;\r\nlet isLoading = false;\r\n\r\n\/* =========================================================\r\n   ELEMENTS\r\n========================================================= *\/\r\n\r\nconst searchInput = document.getElementById(\"search\");\r\nconst searchCategory = document.getElementById(\"searchCategory\");\r\nconst destinationSiteSelect = document.getElementById(\"destinationSite\");\r\nconst titleColorPicker = document.getElementById(\"titleColorPicker\");\r\nconst customSiteUrlInput = document.getElementById(\"customSiteUrl\");\r\nconst dayInput = document.getElementById(\"day\");\r\nconst monthInput = document.getElementById(\"month\");\r\nconst yearInput = document.getElementById(\"year\");\r\nconst yearFromInput = document.getElementById(\"yearFrom\");\r\nconst yearToInput = document.getElementById(\"yearTo\");\r\nconst dateFromInput = document.getElementById(\"dateFrom\");\r\nconst dateToInput = document.getElementById(\"dateTo\");\r\nconst titleFilter = document.getElementById(\"titleFilter\");\r\nconst dateFilter = document.getElementById(\"dateFilter\");\r\nconst sortSelect = document.getElementById(\"sort\");\r\nconst typeFilter = document.getElementById(\"typeFilter\");\r\nconst religiousFilter = document.getElementById(\"religiousFilter\");\r\nconst lexicalCustomInput = document.getElementById(\"lexicalCustom\");\r\nconst excludeWordsInput = document.getElementById(\"excludeWords\");\r\nconst languageOptionsContainer = document.getElementById(\"languageOptions\");\r\nconst languageCountLabel = document.getElementById(\"languageCount\");\r\nconst allLanguagesCheckbox = document.getElementById(\"allLanguages\");\r\nconst pageSizeSelect = document.getElementById(\"pageSize\");\r\nconst maxResultsInput = document.getElementById(\"maxResults\");\r\nconst statusElement = document.getElementById(\"status\");\r\nconst resultsElement = document.getElementById(\"results\");\r\nconst progressBar = document.getElementById(\"progressBar\");\r\nconst cancelButton = document.getElementById(\"cancelButton\");\r\nconst pageInput = document.getElementById(\"pageInput\");\r\nconst previousButton = document.getElementById(\"previousButton\");\r\nconst nextButton = document.getElementById(\"nextButton\");\r\nconst pageInfo = document.getElementById(\"pageInfo\");\r\nconst youtubeButton = document.getElementById(\"youtubeButton\");\r\nconst cinemaButton = document.getElementById(\"cinemaButton\");\r\n\r\n\/* =========================================================\r\n   SITE D'OUVERTURE DES FILMS\r\n\r\n   D\u00e9termine, pour un film donn\u00e9, l'URL \u00e0 utiliser quand\r\n   l'internaute clique dessus \u2014 selon le site choisi dans le\r\n   s\u00e9lecteur \"Ouvrir les films sur\".\r\n\r\n   - JustWatch : utilise la page de r\u00e9sultats de recherche\r\n     officielle du site (fiable, s'ouvre directement, sans\r\n     d\u00e9tour ni page d'avertissement interm\u00e9diaire).\r\n   - AlloCin\u00e9 : utilise la vraie page de r\u00e9sultats de\r\n     recherche du site (fiable, quel que soit le film).\r\n   - IMDb : utilise la fiche exacte du film si Wikidata la\r\n     conna\u00eet (P345), sinon une recherche IMDb par titre.\r\n   - URL personnalis\u00e9e : gabarit fourni par l'internaute. Le\r\n     jeton {titre} est remplac\u00e9 par le titre du film ; sans ce\r\n     jeton, l'URL saisie s'ouvre telle quelle (identique pour\r\n     tous les films).\r\n\r\n   Dans tous les cas, si le titre est vide, on retombe sur la\r\n   fiche Wikidata du film (toujours disponible).\r\n========================================================= *\/\r\n\r\nfunction getFilmDestinationUrl(film) {\r\n\r\n    const title = film.title || \"\";\r\n    const encodedTitle = encodeURIComponent(title);\r\n    const site = destinationSiteSelect.value;\r\n\r\n    if (site === \"allocine\") {\r\n        return title\r\n            ? `https:\/\/www.allocine.fr\/rechercher\/?q=${encodedTitle}`\r\n            : film.uri;\r\n    }\r\n\r\n    if (site === \"imdb\") {\r\n        if (film.imdb) return film.imdb;\r\n        return title\r\n            ? `https:\/\/www.imdb.com\/find\/?q=${encodedTitle}`\r\n            : film.uri;\r\n    }\r\n\r\n    if (site === \"custom\") {\r\n\r\n        const template = customSiteUrlInput.value.trim();\r\n\r\n        if (!template) return film.primaryLink;\r\n\r\n        if (template.includes(\"{titre}\")) {\r\n            return template.split(\"{titre}\").join(encodedTitle);\r\n        }\r\n\r\n        \/*\r\n        -------------------------------------------------\r\n        Pas de {titre} dans le gabarit : avant, l'URL saisie\r\n        s'ouvrait telle quelle, IDENTIQUE pour tous les\r\n        films (donc jamais sur le film cliqu\u00e9). On ajoute\r\n        maintenant automatiquement le titre comme param\u00e8tre\r\n        de recherche (\"q=...\"), pour que chaque clic m\u00e8ne\r\n        bien vers CE film-l\u00e0 et non vers la page d'accueil\r\n        g\u00e9n\u00e9rique du site.\r\n        -------------------------------------------------\r\n        *\/\r\n        if (!title) return film.primaryLink;\r\n\r\n        const separator = template.includes(\"?\") ? \"&\" : \"?\";\r\n        return `${template}${separator}q=${encodedTitle}`;\r\n    }\r\n\r\n    \/\/ JustWatch dispose d'une vraie page de recherche publique\r\n    \/\/ (comme AlloCin\u00e9) : elle s'ouvre directement, sans d\u00e9tour\r\n    \/\/ ni page d'avertissement de redirection interm\u00e9diaire.\r\n    if (!title) return film.uri;\r\n\r\n    return `https:\/\/www.justwatch.com\/fr\/recherche?q=${encodedTitle}`;\r\n}\r\n\r\n\/* =========================================================\r\n   UTILITAIRES DATE (fen\u00eatres glissantes \"N derniers jours \/\r\n   mois \/ ann\u00e9es\")\r\n========================================================= *\/\r\n\r\nfunction daysAgo(n) {\r\n    const d = new Date();\r\n    d.setDate(d.getDate() - n);\r\n    return d;\r\n}\r\n\r\nfunction monthsAgo(n) {\r\n    const d = new Date();\r\n    d.setMonth(d.getMonth() - n);\r\n    return d;\r\n}\r\n\r\nfunction yearsAgo(n) {\r\n    const d = new Date();\r\n    d.setFullYear(d.getFullYear() - n);\r\n    return d;\r\n}\r\n\r\n\/* =========================================================\r\n   REQUETE SIMPLE (sans annulation, pour les appels ponctuels :\r\n   chargement des langues disponibles, vue Cin\u00e9ma)\r\n========================================================= *\/\r\n\r\nasync function fetchWikidataOnce(query) {\r\n\r\n    const response = await fetch(WIKIDATA_ENDPOINT, {\r\n        method: \"POST\",\r\n        headers: {\r\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\r\n            \"Accept\": \"application\/sparql-results+json\"\r\n        },\r\n        body: \"query=\" + encodeURIComponent(query)\r\n    });\r\n\r\n    if (!response.ok) {\r\n        throw new Error(\"Erreur HTTP \" + response.status);\r\n    }\r\n\r\n    return response.json();\r\n}\r\n\r\n\/* =========================================================\r\n   LANGUES DISPONIBLES DANS LA BASE\r\n\r\n   Interroge Wikidata pour ne proposer que les langues\r\n   r\u00e9ellement utilis\u00e9es dans les libell\u00e9s d'un \u00e9chantillon de\r\n   films (LANGUAGE_SAMPLE_SIZE), plut\u00f4t qu'une liste fig\u00e9e.\r\n   Les noms de langues affich\u00e9s sont g\u00e9n\u00e9r\u00e9s dynamiquement en\r\n   fran\u00e7ais via Intl.DisplayNames (aucune table \u00e0 maintenir).\r\n========================================================= *\/\r\n\r\nfunction languageDisplayName(code) {\r\n    try {\r\n        const displayNames = new Intl.DisplayNames([\"fr\"], { type: \"language\" });\r\n        const name = displayNames.of(code);\r\n        return name && name.toLowerCase() !== code.toLowerCase() ? name : code;\r\n    }\r\n    catch (error) {\r\n        return code;\r\n    }\r\n}\r\n\r\nfunction populateLanguageSelect(codes) {\r\n\r\n    const uniqueCodes = Array.from(new Set(codes)).filter(Boolean);\r\n\r\n    uniqueCodes.sort((a, b) =>\r\n        languageDisplayName(a).localeCompare(languageDisplayName(b), \"fr\")\r\n    );\r\n\r\n    languageOptionsContainer.innerHTML = \"\";\r\n\r\n    uniqueCodes.forEach(code => {\r\n\r\n        const label = document.createElement(\"label\");\r\n\r\n        const checkbox = document.createElement(\"input\");\r\n        checkbox.type = \"checkbox\";\r\n        checkbox.className = \"languageCheckbox\";\r\n        checkbox.value = code;\r\n        if (code === \"fr\") checkbox.checked = true;\r\n\r\n        const span = document.createElement(\"span\");\r\n        span.textContent = languageDisplayName(code) + \" (\" + code + \")\";\r\n\r\n        label.appendChild(checkbox);\r\n        label.appendChild(span);\r\n        languageOptionsContainer.appendChild(label);\r\n    });\r\n\r\n    if (!languageOptionsContainer.querySelector(\".languageCheckbox:checked\") && uniqueCodes.length) {\r\n        languageOptionsContainer.querySelector(\".languageCheckbox\").checked = true;\r\n    }\r\n\r\n    languageCountLabel.textContent = \"(\" + uniqueCodes.length + \" langues trouv\u00e9es)\";\r\n}\r\n\r\nasync function loadAvailableLanguages() {\r\n\r\n    try {\r\n\r\n        const query = `\r\n            SELECT DISTINCT ?lang WHERE {\r\n                {\r\n                    SELECT DISTINCT ?item WHERE {\r\n                        ?item wdt:P31 ${FILM_CLASS_ID} .\r\n                    }\r\n                    LIMIT ${LANGUAGE_SAMPLE_SIZE}\r\n                }\r\n                ?item rdfs:label ?itemLabel .\r\n                BIND(LANG(?itemLabel) AS ?lang)\r\n            }\r\n            ORDER BY ?lang\r\n        `;\r\n\r\n        const data = await fetchWikidataOnce(query);\r\n\r\n        const codes = data.results.bindings\r\n            .map(row => row.lang.value)\r\n            .filter(code => code && code.length <= 8);\r\n\r\n        if (codes.length === 0) {\r\n            throw new Error(\"Aucune langue retourn\u00e9e\");\r\n        }\r\n\r\n        populateLanguageSelect(codes);\r\n    }\r\n    catch (error) {\r\n        console.warn(\"Langues Wikidata indisponibles, repli sur une liste par d\u00e9faut :\", error);\r\n        populateLanguageSelect(FALLBACK_LANGUAGE_CODES);\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   LANGUES SELECTIONNEES\r\n   Renvoie null si \"Toutes les langues\" est coch\u00e9 (= aucun\r\n   filtre de langue), sinon la liste des codes coch\u00e9s dans le\r\n   multi-select (fr par d\u00e9faut si rien n'est s\u00e9lectionn\u00e9).\r\n========================================================= *\/\r\n\r\nfunction getSelectedLanguages() {\r\n\r\n    if (allLanguagesCheckbox.checked) {\r\n        return null;\r\n    }\r\n\r\n    const selected = Array.from(document.querySelectorAll(\".languageCheckbox:checked\"))\r\n        .map(cb => cb.value)\r\n        .filter(v => v);\r\n\r\n    return selected.length ? selected : [\"fr\"];\r\n}\r\n\r\n\/* =========================================================\r\n   CONDITIONS DE BASE (partag\u00e9es par la recherche courante et\r\n   par la requ\u00eate \"films \u00e0 venir\" de la vue Cin\u00e9ma) : type,\r\n   langue, th\u00e8me religieux, exclusion, recherche libre.\r\n========================================================= *\/\r\n\r\nfunction buildBaseConditions() {\r\n\r\n    const search = searchInput.value.trim();\r\n\r\n    \/\/ --- LABEL (une, plusieurs, ou toutes les langues) ---\r\n\r\n    const selectedLanguages = getSelectedLanguages();\r\n\r\n    let labelCondition;\r\n\r\n    if (selectedLanguages === null) {\r\n        \/\/ Toutes les langues : aucun filtre sur LANG(). Le\r\n        \/\/ regroupement (GROUP BY) dans la requ\u00eate de liste se\r\n        \/\/ charge ensuite de ne garder qu'un seul libell\u00e9 par film.\r\n        labelCondition = `?item rdfs:label ?itemLabel .`;\r\n    }\r\n    else {\r\n        const langList = selectedLanguages.map(l => `\"${l}\"`).join(\",\");\r\n        labelCondition = `\r\n            ?item rdfs:label ?itemLabel .\r\n            FILTER(LANG(?itemLabel) IN (${langList}))\r\n        `;\r\n    }\r\n\r\n    \/\/ --- TYPE (films \/ dessins anim\u00e9s \/ \u0153uvres audiovisuelles) ---\r\n\r\n    let typeCondition = `?item wdt:P31 ${FILM_CLASS_ID} .`;\r\n\r\n    if (typeFilter.value === \"animation\") {\r\n        typeCondition = `\r\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\r\n            UNION\r\n            { ?item wdt:P31 ?animType . ?animType wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\r\n        `;\r\n    }\r\n    else if (typeFilter.value === \"film_animation\") {\r\n        typeCondition = `\r\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\r\n            UNION\r\n            { ?item wdt:P31 ?filmType . ?filmType wdt:P279* ${FILM_CLASS_ID} . }\r\n            UNION\r\n            { ?item wdt:P31 ${ANIMATED_FILM_CLASS_ID} . }\r\n            UNION\r\n            { ?item wdt:P31 ?animType2 . ?animType2 wdt:P279* ${ANIMATED_FILM_CLASS_ID} . }\r\n        `;\r\n    }\r\n    else if (typeFilter.value === \"all\") {\r\n        typeCondition = `\r\n            { ?item wdt:P31 ${FILM_CLASS_ID} . }\r\n            UNION\r\n            { ?item wdt:P31 ?type . ?type wdt:P279* ${FILM_CLASS_ID} . }\r\n        `;\r\n    }\r\n\r\n    \/\/ --- THEME RELIGIEUX (filtre structur\u00e9 + mots-cl\u00e9s) ---\r\n    const lexicalKeywords =\r\n        Array.from(document.querySelectorAll(\".lexicalKeyword:checked\"))\r\n            .flatMap(cb => cb.value.split(\",\"));\r\n\r\n    const customKeywordsRaw = lexicalCustomInput.value.trim();\r\n\r\n    if (customKeywordsRaw) {\r\n        customKeywordsRaw\r\n            .split(\",\")\r\n            .map(word => word.trim())\r\n            .filter(Boolean)\r\n            .forEach(word => lexicalKeywords.push(word));\r\n    }\r\n\r\n    const hasLexicalFilter = lexicalKeywords.length > 0;\r\n\r\n    let religiousCondition = \"\";\r\n\r\n    if (religiousFilter.checked || hasLexicalFilter) {\r\n\r\n        const branches = [];\r\n\r\n        if (religiousFilter.checked) {\r\n\r\n            \/\/ Genre : Christian film \/ Bible film (correspondance directe).\r\n            branches.push(`\r\n                {\r\n                    ?item wdt:P136 ?religiousGenre .\r\n                    VALUES ?religiousGenre { ${RELIGIOUS_GENRE_IDS.join(\" \")} }\r\n                }\r\n            `);\r\n\r\n            \/\/ Sujet principal : Christianisme, J\u00e9sus, l'\u00c9ternel, Dieu,\r\n            \/\/ \u00e9vang\u00e9lisme, foi, Bible, croyance, religieux, croyance\r\n            \/\/ religieuse (correspondance directe, sans remonter vers\r\n            \/\/ d'autres religions comme l'islam, le bouddhisme, etc.)\r\n            branches.push(`\r\n                {\r\n                    ?item wdt:P921 ?religiousSubject .\r\n                    VALUES ?religiousSubject { ${RELIGIOUS_SUBJECT_IDS.join(\" \")} }\r\n                }\r\n            `);\r\n        }\r\n\r\n        if (hasLexicalFilter) {\r\n            const lexicalOr = lexicalKeywords\r\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\r\n                .join(\" || \");\r\n\r\n            branches.push(`{ FILTER(${lexicalOr}) }`);\r\n        }\r\n\r\n        religiousCondition = branches.join(\" UNION \");\r\n    }\r\n\r\n    \/\/ --- EXCLUSION PAR MOTS-CLES (ne pas afficher ces films) ---\r\n\r\n    const excludeWordsRaw = excludeWordsInput.value.trim();\r\n    let excludeCondition = \"\";\r\n\r\n    if (excludeWordsRaw) {\r\n\r\n        const excludeWords = excludeWordsRaw\r\n            .split(\",\")\r\n            .map(word => word.trim())\r\n            .filter(Boolean);\r\n\r\n        if (excludeWords.length) {\r\n            const excludeOr = excludeWords\r\n                .map(word => `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${word.replace(\/\"\/g, '\\\\\"')}\"))`)\r\n                .join(\" || \");\r\n\r\n            excludeCondition = `FILTER(!(${excludeOr}))`;\r\n        }\r\n    }\r\n\r\n    \/\/ --- RECHERCHE PAR CATEGORIE ---\r\n    let searchCondition = \"\";\r\n\r\n    if (search) {\r\n\r\n        const safeSearch = search.replace(\/\"\/g, '\\\\\"');\r\n        const category = searchCategory.value || \"title\";\r\n\r\n        const titleMatch = `CONTAINS(LCASE(STR(?itemLabel)), LCASE(\"${safeSearch}\"))`;\r\n\r\n        const directorBlock = `\r\n            ?item wdt:P57 ?searchDirector .\r\n            ?searchDirector rdfs:label ?searchDirectorLabel .\r\n            FILTER(LANG(?searchDirectorLabel) IN (\"fr\",\"en\"))\r\n            FILTER(CONTAINS(LCASE(STR(?searchDirectorLabel)), LCASE(\"${safeSearch}\")))\r\n        `;\r\n\r\n        const castBlock = `\r\n            ?item wdt:P161 ?searchCast .\r\n            ?searchCast rdfs:label ?searchCastLabel .\r\n            FILTER(LANG(?searchCastLabel) IN (\"fr\",\"en\"))\r\n            FILTER(CONTAINS(LCASE(STR(?searchCastLabel)), LCASE(\"${safeSearch}\")))\r\n        `;\r\n\r\n        if (category === \"title\") {\r\n            searchCondition = `FILTER(${titleMatch})`;\r\n        }\r\n        else if (category === \"director\") {\r\n            searchCondition = directorBlock;\r\n        }\r\n        else if (category === \"cast\") {\r\n            searchCondition = castBlock;\r\n        }\r\n        else {\r\n            searchCondition = `\r\n                { FILTER(${titleMatch}) }\r\n                UNION\r\n                { ${directorBlock} }\r\n                UNION\r\n                { ${castBlock} }\r\n            `;\r\n        }\r\n    }\r\n\r\n    return `\r\n        ${typeCondition}\r\n        ${labelCondition}\r\n        ${religiousCondition}\r\n        ${excludeCondition}\r\n        ${searchCondition}\r\n    `;\r\n}\r\n\r\n\/* =========================================================\r\n   CONDITIONS DE DATE (recherche courante)\r\n========================================================= *\/\r\n\r\nfunction buildWhereClause() {\r\n\r\n    const day = dayInput.value.trim();\r\n    const month = monthInput.value.trim();\r\n    const year = yearInput.value.trim();\r\n    const yearFrom = yearFromInput.value.trim();\r\n    const yearTo = yearToInput.value.trim();\r\n\r\n    const base = buildBaseConditions();\r\n\r\n    \/\/ --- DATE ---\r\n    \/\/ Priorit\u00e9 : fourchette pr\u00e9cise (jour+mois+ann\u00e9e exacts) >\r\n    \/\/            fourchette d'ann\u00e9es > champs jour\/mois\/ann\u00e9e.\r\n    const dateFrom = dateFromInput.value;\r\n    const dateTo = dateToInput.value;\r\n\r\n    const hasFullDateRange = Boolean(dateFrom || dateTo);\r\n    const hasYearRange = Boolean(yearFrom || yearTo);\r\n    const hasRollingFilter = Boolean(day || month || year);\r\n\r\n    const hasDateFilter = hasFullDateRange || hasYearRange || hasRollingFilter;\r\n\r\n    let dateVariable = `OPTIONAL{ ?item wdt:P577 ?date . }`;\r\n\r\n    if (dateFilter.value === \"with\" || hasDateFilter) {\r\n        dateVariable = `?item wdt:P577 ?date .`;\r\n    }\r\n\r\n    if (dateFilter.value === \"without\") {\r\n        dateVariable = `FILTER NOT EXISTS{ ?item wdt:P577 ?anyDate . }`;\r\n    }\r\n\r\n    let dateFilterCondition = \"\";\r\n\r\n    if (hasDateFilter && dateFilter.value !== \"without\") {\r\n\r\n        const conditions = [];\r\n\r\n        if (hasFullDateRange) {\r\n\r\n            \/\/ Fourchette de dates pr\u00e9cises (ex : 05\/05\/2024 -> 31\/07\/2026)\r\n            if (dateFrom) conditions.push(`?date >= \"${dateFrom}T00:00:00Z\"^^xsd:dateTime`);\r\n            if (dateTo) conditions.push(`?date <= \"${dateTo}T23:59:59Z\"^^xsd:dateTime`);\r\n        }\r\n        else if (hasYearRange) {\r\n\r\n            \/\/ Fourchette d'ann\u00e9es (ex : 2022 -> 2026)\r\n            if (yearFrom) conditions.push(`YEAR(?date) >= ${Number(yearFrom)}`);\r\n            if (yearTo) conditions.push(`YEAR(?date) <= ${Number(yearTo)}`);\r\n        }\r\n        else {\r\n\r\n            const now = new Date();\r\n            const nowIso = now.toISOString();\r\n\r\n            \/\/ \"Jour\" = N derniers jours (fen\u00eatre glissante)\r\n            if (day) {\r\n                conditions.push(`?date >= \"${daysAgo(Number(day)).toISOString()}\"^^xsd:dateTime`);\r\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\r\n            }\r\n\r\n            \/\/ \"Mois\" = N derniers mois (fen\u00eatre glissante)\r\n            if (month) {\r\n                conditions.push(`?date >= \"${monthsAgo(Number(month)).toISOString()}\"^^xsd:dateTime`);\r\n                conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\r\n            }\r\n\r\n            \/\/ \"Ann\u00e9e\" : un nombre qui ressemble \u00e0 une vraie ann\u00e9e\r\n            \/\/ (>= 100) est trait\u00e9 comme une ann\u00e9e pr\u00e9cise (ex : 2026).\r\n            \/\/ Un petit nombre (< 100) est trait\u00e9 comme \"N derni\u00e8res\r\n            \/\/ ann\u00e9es\" (ex : 3 -> 3 derni\u00e8res ann\u00e9es).\r\n            if (year) {\r\n\r\n                const yearNumber = Number(year);\r\n\r\n                if (yearNumber >= 100) {\r\n                    conditions.push(`YEAR(?date) = ${yearNumber}`);\r\n                }\r\n                else {\r\n                    conditions.push(`?date >= \"${yearsAgo(yearNumber).toISOString()}\"^^xsd:dateTime`);\r\n                    conditions.push(`?date <= \"${nowIso}\"^^xsd:dateTime`);\r\n                }\r\n            }\r\n        }\r\n\r\n        if (conditions.length) {\r\n            dateFilterCondition = `FILTER(${conditions.join(\" && \")})`;\r\n        }\r\n    }\r\n\r\n    return `\r\n        ${base}\r\n        ${dateVariable}\r\n        ${dateFilterCondition}\r\n    `;\r\n}\r\n\r\n\/* =========================================================\r\n   CONDITIONS \"A VENIR\" (vue Cin\u00e9ma) : m\u00eames filtres de type \/\r\n   langue \/ th\u00e8me \/ exclusion \/ recherche, mais date future.\r\n========================================================= *\/\r\n\r\nfunction buildUpcomingWhereClause() {\r\n\r\n    const base = buildBaseConditions();\r\n\r\n    return `\r\n        ${base}\r\n        ?item wdt:P577 ?date .\r\n        FILTER(?date > NOW())\r\n    `;\r\n}\r\n\r\n\/* =========================================================\r\n   REQUETE DE COMPTAGE\r\n\r\n   Compte les films correspondant aux filtres, plafonn\u00e9 \u00e0\r\n   COUNT_CAP pour rester rapide m\u00eame sur une recherche large.\r\n   (SELECT DISTINCT ?item : peu importe qu'un film ait\r\n   plusieurs libell\u00e9s dans plusieurs langues, il n'est compt\u00e9\r\n   qu'une seule fois.)\r\n========================================================= *\/\r\n\r\nfunction buildCountQuery() {\r\n\r\n    const where = buildWhereClause();\r\n\r\n    return `\r\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\r\n        SELECT (COUNT(*) AS ?count) WHERE {\r\n            SELECT DISTINCT ?item WHERE{ ${where} }\r\n            LIMIT ${COUNT_CAP}\r\n        }\r\n    `;\r\n}\r\n\r\n\/* =========================================================\r\n   REQUETE DE LISTE (l\u00e9g\u00e8re, pagination r\u00e9elle)\r\n\r\n   Regroup\u00e9e par ?item (GROUP BY) : quand plusieurs langues\r\n   (ou \"toutes les langues\") sont s\u00e9lectionn\u00e9es, un m\u00eame film\r\n   peut avoir plusieurs libell\u00e9s correspondants. On n'en garde\r\n   qu'un seul par film (SAMPLE) pour \u00e9viter les doublons de\r\n   cartes \u00e0 l'affichage.\r\n========================================================= *\/\r\n\r\nfunction buildListQuery(limit, offset) {\r\n\r\n    const where = buildWhereClause();\r\n\r\n    let order = \"DESC(?dateAgg)\";\r\n    if (sortSelect.value === \"oldest\") order = \"ASC(?dateAgg)\";\r\n    if (sortSelect.value === \"title\") order = \"LCASE(STR(?labelAgg))\";\r\n\r\n    return `\r\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\r\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\r\n        WHERE{ ${where} }\r\n        GROUP BY ?item\r\n        ORDER BY ${order}\r\n        LIMIT ${limit}\r\n        OFFSET ${offset}\r\n    `;\r\n}\r\n\r\n\/* =========================================================\r\n   REQUETE DE LISTE \"A VENIR\" (vue Cin\u00e9ma), tri\u00e9e par date de\r\n   sortie la plus proche.\r\n========================================================= *\/\r\n\r\nfunction buildUpcomingListQuery(limit) {\r\n\r\n    const where = buildUpcomingWhereClause();\r\n\r\n    return `\r\n        PREFIX xsd: <http:\/\/www.w3.org\/2001\/XMLSchema#>\r\n        SELECT ?item (SAMPLE(?itemLabel) AS ?labelAgg) (SAMPLE(?date) AS ?dateAgg)\r\n        WHERE{ ${where} }\r\n        GROUP BY ?item\r\n        ORDER BY ASC(?dateAgg)\r\n        LIMIT ${limit}\r\n    `;\r\n}\r\n\r\n\/* =========================================================\r\n   REQUETE D'ENRICHISSEMENT\r\n   (affiche, r\u00e9alisateur, casting, liens \u2014 uniquement pour\r\n   les films de la page affich\u00e9e)\r\n\r\n   Cha\u00eene de repli pour l'affiche : P3383 (affiche officielle)\r\n   en priorit\u00e9, puis P18 (image g\u00e9n\u00e9rique) si aucune affiche\r\n   n'est renseign\u00e9e sur Wikidata.\r\n========================================================= *\/\r\n\r\nfunction buildDetailsQuery(itemUris) {\r\n\r\n    const values = itemUris.map(uri => `<${uri}>`).join(\" \");\r\n\r\n    return `\r\n        SELECT ?item\r\n               (SAMPLE(?poster) AS ?poster)\r\n               (SAMPLE(?image) AS ?image)\r\n               (GROUP_CONCAT(DISTINCT ?directorLabel; separator=\", \") AS ?directors)\r\n               (GROUP_CONCAT(DISTINCT ?castLabel; separator=\", \") AS ?cast)\r\n               (SAMPLE(?website) AS ?website)\r\n               (SAMPLE(?imdb) AS ?imdb)\r\n        WHERE{\r\n            VALUES ?item { ${values} }\r\n            OPTIONAL{ ?item wdt:P3383 ?poster . }\r\n            OPTIONAL{ ?item wdt:P18 ?image . }\r\n            OPTIONAL{\r\n                ?item wdt:P57 ?director .\r\n                ?director rdfs:label ?directorLabel .\r\n                FILTER(LANG(?directorLabel) IN (\"fr\",\"en\"))\r\n            }\r\n            OPTIONAL{\r\n                ?item wdt:P161 ?cast .\r\n                ?cast rdfs:label ?castLabel .\r\n                FILTER(LANG(?castLabel) IN (\"fr\",\"en\"))\r\n            }\r\n            OPTIONAL{ ?item wdt:P856 ?website . }\r\n            OPTIONAL{\r\n                ?item wdt:P345 ?imdbId .\r\n                BIND(CONCAT(\"https:\/\/www.imdb.com\/title\/\", ?imdbId) AS ?imdb)\r\n            }\r\n        }\r\n        GROUP BY ?item\r\n    `;\r\n}\r\n\r\n\/* =========================================================\r\n   REQUETE AVEC RETRY (POST, pour \u00e9viter les URL trop longues)\r\n========================================================= *\/\r\n\r\nasync function fetchWikidata(query, attempt = 0) {\r\n\r\n    const response = await fetch(WIKIDATA_ENDPOINT, {\r\n        method: \"POST\",\r\n        headers: {\r\n            \"Content-Type\": \"application\/x-www-form-urlencoded\",\r\n            \"Accept\": \"application\/sparql-results+json\"\r\n        },\r\n        body: \"query=\" + encodeURIComponent(query),\r\n        signal: abortController.signal\r\n    });\r\n\r\n    if ([429, 502, 503, 504].includes(response.status)) {\r\n\r\n        if (attempt < 3) {\r\n\r\n            const delay = (attempt + 1) * 2500;\r\n\r\n            statusElement.textContent =\r\n                \"\u23f3 Wikidata est temporairement charg\u00e9. Nouvelle tentative dans \"\r\n                + (delay \/ 1000) + \" secondes...\";\r\n\r\n            await new Promise(resolve => setTimeout(resolve, delay));\r\n\r\n            return fetchWikidata(query, attempt + 1);\r\n        }\r\n    }\r\n\r\n    if (!response.ok) {\r\n        throw new Error(\"Erreur HTTP \" + response.status);\r\n    }\r\n\r\n    return response.json();\r\n}\r\n\r\n\/* =========================================================\r\n   RECHERCHE (comptage -> liste -> enrichissement)\r\n========================================================= *\/\r\n\r\nasync function searchWikidata() {\r\n\r\n    if (isLoading) return;\r\n\r\n    isLoading = true;\r\n    abortController = new AbortController();\r\n    cancelButton.disabled = false;\r\n    progressBar.style.width = \"10%\";\r\n    resultsElement.innerHTML = \"\";\r\n    statusElement.textContent = \"\u23f3 Pr\u00e9paration de la recherche...\";\r\n\r\n    try {\r\n\r\n        if (titleFilter.value === \"without\") {\r\n            currentResults = [];\r\n            totalPages = 1;\r\n            statusElement.textContent =\r\n                \"\u26a0\ufe0f Wikidata ne permet pas de r\u00e9cup\u00e9rer fiablement les films \"\r\n                + \"sans titre dans cette requ\u00eate l\u00e9g\u00e8re.\";\r\n            renderResults();\r\n            return;\r\n        }\r\n\r\n        const pageSize = Math.max(1, Number(pageSizeSelect.value) || 20);\r\n\r\n        const maxResultsRaw = maxResultsInput.value.trim();\r\n        const maxResults = maxResultsRaw ? Math.max(1, Number(maxResultsRaw)) : null;\r\n\r\n        \/* --- ETAPE 1 : comptage total (si possible) --- *\/\r\n\r\n        statusElement.textContent = \"\u23f3 Comptage des r\u00e9sultats...\";\r\n        progressBar.style.width = \"20%\";\r\n\r\n        let rawCount = null;\r\n        let cappedAtLimit = false;\r\n        let effectiveTotal = null;\r\n\r\n        try {\r\n            const countData = await fetchWikidata(buildCountQuery());\r\n            rawCount = Number(countData.results.bindings[0]?.count?.value || 0);\r\n            cappedAtLimit = rawCount >= COUNT_CAP;\r\n        }\r\n        catch (countError) {\r\n\r\n            if (countError.name === \"AbortError\") {\r\n                throw countError;\r\n            }\r\n\r\n            \/*\r\n            -------------------------------------------------\r\n            Le comptage peut \u00e9chouer (timeout, 500...) sur une\r\n            recherche trop large et peu s\u00e9lective (ex. \"tous\r\n            les films en fran\u00e7ais\", sans aucun filtre). Dans\r\n            ce cas on NE FAIT PAS \u00e9chouer toute la recherche :\r\n            on bascule sur l'ancien mode \"page par page sans\r\n            total connu\".\r\n            -------------------------------------------------\r\n            *\/\r\n\r\n            console.warn(\"Comptage indisponible, repli sans total exact :\", countError);\r\n\r\n            rawCount = null;\r\n        }\r\n\r\n        totalKnown = rawCount !== null;\r\n\r\n        if (totalKnown) {\r\n\r\n            effectiveTotal = maxResults ? Math.min(rawCount, maxResults) : rawCount;\r\n            totalPages = Math.max(1, Math.ceil(effectiveTotal \/ pageSize));\r\n\r\n            if (currentPage > totalPages) {\r\n                currentPage = totalPages;\r\n            }\r\n\r\n            if (effectiveTotal === 0) {\r\n                currentResults = [];\r\n                progressBar.style.width = \"100%\";\r\n                statusElement.textContent = \"\u274c Aucun r\u00e9sultat.\";\r\n                renderResults();\r\n                return;\r\n            }\r\n        }\r\n        else {\r\n            totalPages = Math.max(totalPages, currentPage);\r\n        }\r\n\r\n        \/* --- ETAPE 2 : liste des films de la page courante --- *\/\r\n\r\n        statusElement.textContent = \"\u23f3 Recherche des films...\";\r\n        progressBar.style.width = \"40%\";\r\n\r\n        const offset = (currentPage - 1) * pageSize;\r\n\r\n        let limit = pageSize;\r\n\r\n        if (maxResults) {\r\n            limit = Math.min(pageSize, Math.max(0, maxResults - offset));\r\n        }\r\n\r\n        \/\/ Si le total est inconnu, on demande un r\u00e9sultat de plus\r\n        \/\/ pour savoir s'il existe une page suivante.\r\n        const fetchLimit = totalKnown ? limit : limit + 1;\r\n\r\n        const listData = await fetchWikidata(buildListQuery(fetchLimit, offset));\r\n        let listRows = listData.results.bindings;\r\n\r\n        if (!totalKnown) {\r\n            hasNextPageUnknown = listRows.length > limit;\r\n            listRows = listRows.slice(0, limit);\r\n            totalPages = hasNextPageUnknown ? currentPage + 1 : currentPage;\r\n        }\r\n\r\n        progressBar.style.width = \"60%\";\r\n\r\n        if (listRows.length === 0) {\r\n            currentResults = [];\r\n            progressBar.style.width = \"100%\";\r\n            statusElement.textContent = \"\u274c Aucun r\u00e9sultat sur cette page.\";\r\n            renderResults();\r\n            return;\r\n        }\r\n\r\n        \/* --- ETAPE 3 : enrichissement (affiche, casting, liens) --- *\/\r\n\r\n        statusElement.textContent = \"\u23f3 R\u00e9cup\u00e9ration des affiches et informations...\";\r\n\r\n        const itemUris = listRows.map(row => row.item.value);\r\n        const detailsData = await fetchWikidata(buildDetailsQuery(itemUris));\r\n\r\n        const detailsByUri = {};\r\n        detailsData.results.bindings.forEach(row => {\r\n            detailsByUri[row.item.value] = row;\r\n        });\r\n\r\n        progressBar.style.width = \"90%\";\r\n\r\n        currentResults = listRows.map(row => convertResult(row, detailsByUri[row.item.value]));\r\n\r\n        progressBar.style.width = \"100%\";\r\n\r\n        let statusMessage;\r\n\r\n        if (totalKnown) {\r\n\r\n            statusMessage = \"\u2705 \" + effectiveTotal + \" r\u00e9sultat(s) trouv\u00e9(s)\";\r\n\r\n            if (cappedAtLimit && !maxResults) {\r\n                statusMessage += \" (au moins \" + COUNT_CAP + \" \u2014 affinez la recherche pour un compte exact)\";\r\n            }\r\n        }\r\n        else {\r\n            statusMessage =\r\n                \"\u26a0\ufe0f Total exact impossible \u00e0 calculer pour cette recherche \"\r\n                + \"(trop de crit\u00e8res combin\u00e9s). \"\r\n                + listRows.length + \" r\u00e9sultat(s) affich\u00e9(s) sur CETTE PAGE \"\r\n                + \"uniquement \u2014 ce n'est pas le total r\u00e9el, qui peut \u00eatre bien \"\r\n                + \"plus \u00e9lev\u00e9. Utilisez \u00ab Suivant \u00bb pour continuer \u00e0 parcourir, \"\r\n                + \"ou retirez un crit\u00e8re pour retrouver un compte exact.\";\r\n        }\r\n\r\n        statusElement.textContent = statusMessage + \".\";\r\n\r\n        renderResults();\r\n\r\n    }\r\n    catch (error) {\r\n\r\n        if (error.name === \"AbortError\") {\r\n            statusElement.textContent = \"\u23f9 Recherche annul\u00e9e.\";\r\n        }\r\n        else {\r\n            console.error(\"Wikidata :\", error);\r\n            statusElement.textContent = \"\u274c Impossible d'interroger Wikidata : \" + error.message;\r\n        }\r\n    }\r\n    finally {\r\n        isLoading = false;\r\n        cancelButton.disabled = true;\r\n        abortController = null;\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   CONVERSION (fusion ligne liste + ligne d\u00e9tails)\r\n========================================================= *\/\r\n\r\nfunction convertResult(listRow, detailsRow) {\r\n\r\n    const details = detailsRow || {};\r\n\r\n    const poster = details.poster\r\n        ? details.poster.value\r\n        : (details.image ? details.image.value : \"\");\r\n\r\n    const website = details.website ? details.website.value : \"\";\r\n    const imdb = details.imdb ? details.imdb.value : \"\";\r\n    const uri = listRow.item ? listRow.item.value : \"\";\r\n\r\n    \/\/ Priorit\u00e9 : site officiel > IMDb > fiche Wikidata\r\n    const primaryLink = website || imdb || uri;\r\n\r\n    return {\r\n        uri,\r\n        title: listRow.labelAgg ? listRow.labelAgg.value : \"\",\r\n        poster,\r\n        directors: details.directors ? details.directors.value : \"\",\r\n        cast: details.cast ? details.cast.value : \"\",\r\n        date: listRow.dateAgg ? listRow.dateAgg.value : \"\",\r\n        website,\r\n        imdb,\r\n        primaryLink\r\n    };\r\n}\r\n\r\n\/* =========================================================\r\n   AFFICHAGE\r\n========================================================= *\/\r\n\r\nfunction renderResults() {\r\n\r\n    resultsElement.innerHTML = \"\";\r\n\r\n    const isReligiousSearch =\r\n        religiousFilter.checked\r\n        || document.querySelectorAll(\".lexicalKeyword:checked\").length > 0;\r\n\r\n    currentResults.forEach(film => {\r\n\r\n        const card = document.createElement(\"article\");\r\n        card.className = \"card\";\r\n\r\n        const year = film.date ? new Date(film.date).getFullYear() : \"\";\r\n\r\n        const castList = film.cast\r\n            ? film.cast.split(\", \").slice(0, 4).join(\", \")\r\n            : \"\";\r\n\r\n        const hasRealPoster = Boolean(film.poster);\r\n        const filmTitle = film.title || \"Titre non renseign\u00e9\";\r\n        const destinationUrl = getFilmDestinationUrl(film);\r\n\r\n        card.innerHTML = `\r\n\r\n            <div class=\"poster-wrap\">\r\n\r\n                <img decoding=\"async\" class=\"poster\" src=\"${film.poster || DEFAULT_POSTER}\"\r\n                     alt=\"${escapeHTML(filmTitle)}\" loading=\"lazy\">\r\n\r\n                ${!hasRealPoster ? `<div class=\"poster-fallback-title\">${escapeHTML(filmTitle)}<\/div>` : \"\"}\r\n\r\n                ${year ? `<div class=\"year-badge\">${year}<\/div>` : \"\"}\r\n\r\n                ${isReligiousSearch ? `<div class=\"religious-badge\">\u271d\ufe0f Religieux<\/div>` : \"\"}\r\n\r\n                <a class=\"poster-overlay\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\r\n                    <span class=\"overlay-btn\">\u25b6 Voir le film<\/span>\r\n                <\/a>\r\n\r\n            <\/div>\r\n\r\n            <div class=\"card-content\">\r\n\r\n                ${film.directors ? `<div class=\"meta-line\"><b>R\u00e9alisation :<\/b> ${escapeHTML(film.directors)}<\/div>` : \"\"}\r\n\r\n                ${castList ? `<div class=\"meta-line\"><b>Avec :<\/b> ${escapeHTML(castList)}<\/div>` : \"\"}\r\n\r\n                <div class=\"meta-line\">\ud83d\udcc5 ${formatDate(film.date)}<\/div>\r\n\r\n                <div class=\"card-links\">\r\n                    <a class=\"link-primary\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\u25b6 Voir le film<\/a>\r\n                    <a class=\"link-secondary\" href=\"${film.uri}\" target=\"_blank\" rel=\"noopener\">Wikidata<\/a>\r\n                <\/div>\r\n\r\n            <\/div>\r\n        `;\r\n\r\n        resultsElement.appendChild(card);\r\n    });\r\n\r\n    updatePagination();\r\n}\r\n\r\n\/* =========================================================\r\n   PAGINATION\r\n========================================================= *\/\r\n\r\nfunction updatePagination() {\r\n\r\n    if (totalKnown) {\r\n        pageInfo.textContent = \"Page \" + currentPage + \" \/ \" + totalPages;\r\n        previousButton.disabled = currentPage <= 1;\r\n        nextButton.disabled = currentPage >= totalPages;\r\n        pageInput.max = totalPages;\r\n    }\r\n    else {\r\n        pageInfo.textContent = \"Page \" + currentPage + \" (total non calcul\u00e9)\";\r\n        previousButton.disabled = currentPage <= 1;\r\n        nextButton.disabled = !hasNextPageUnknown;\r\n        pageInput.removeAttribute(\"max\");\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   DATE \/ SECURITE\r\n========================================================= *\/\r\n\r\nfunction formatDate(date) {\r\n\r\n    if (!date) return \"Date non renseign\u00e9e\";\r\n\r\n    const d = new Date(date);\r\n\r\n    if (isNaN(d)) return date;\r\n\r\n    return d.toLocaleDateString(\"fr-FR\");\r\n}\r\n\r\nfunction escapeHTML(value) {\r\n    const div = document.createElement(\"div\");\r\n    div.textContent = value ? String(value) : \"\";\r\n    return div.innerHTML;\r\n}\r\n\r\n\/* =========================================================\r\n   VUE CINEMA\r\n   Pr\u00e9sente la s\u00e9lection courante sous forme d'affiches\r\n   cliquables (comme au cin\u00e9ma), et ajoute une section\r\n   \"Prochainement\" avec les films \u00e0 venir de la base (si\r\n   disponibles), tri\u00e9s par date de sortie la plus proche.\r\n   Cliquer sur une affiche envoie directement vers la page\r\n   d'acc\u00e8s au visionnage du film (site officiel > IMDb >\r\n   fiche Wikidata en dernier recours).\r\n========================================================= *\/\r\n\r\nfunction renderCinemaCard(film) {\r\n\r\n    const title = film.title || \"Titre non renseign\u00e9\";\r\n    const year = film.date ? new Date(film.date).getFullYear() : \"\";\r\n    const poster = film.poster || DEFAULT_POSTER;\r\n    const hasPoster = Boolean(film.poster);\r\n    const destinationUrl = getFilmDestinationUrl(film);\r\n\r\n    return `\r\n        <a class=\"cinema-card\" href=\"${destinationUrl}\" target=\"_blank\" rel=\"noopener\">\r\n            <div class=\"cinema-poster-wrap\">\r\n                <img decoding=\"async\" src=\"${poster}\" alt=\"${escapeHTML(title)}\" loading=\"lazy\">\r\n                ${!hasPoster ? `<div class=\"cinema-fallback-title\">${escapeHTML(title)}<\/div>` : \"\"}\r\n                ${year ? `<div class=\"cinema-year\">${year}<\/div>` : \"\"}\r\n            <\/div>\r\n            <div class=\"cinema-title\">${escapeHTML(title)}<\/div>\r\n        <\/a>\r\n    `;\r\n}\r\n\r\nfunction renderCinemaHtml(nowShowing, upcoming, loading) {\r\n\r\n    const nowCards = nowShowing.map(renderCinemaCard).join(\"\");\r\n\r\n    let upcomingSection = \"\";\r\n\r\n    if (loading) {\r\n        upcomingSection = `<p class=\"cinema-loading\">\u23f3 Recherche des films \u00e0 venir dans la base...<\/p>`;\r\n    }\r\n    else if (upcoming.length) {\r\n        upcomingSection = `\r\n            <h2 class=\"cinema-section-title\">\ud83d\udd1c Prochainement<\/h2>\r\n            <div class=\"cinema-grid\">${upcoming.map(renderCinemaCard).join(\"\")}<\/div>\r\n        `;\r\n    }\r\n\r\n    return `\r\n        <!DOCTYPE html>\r\n        <html lang=\"fr\">\r\n        <head>\r\n        <meta charset=\"UTF-8\">\r\n        <title>Au cin\u00e9ma \u2014 S\u00e9lection<\/title>\r\n        <style>\r\n            * { box-sizing: border-box; }\r\n            body { margin:0; font-family: Arial, Helvetica, sans-serif; background:#0b0b0d; color:#fff; padding:28px; }\r\n            h1 { text-align:center; margin-bottom: 6px; }\r\n            .cinema-subtitle { text-align:center; opacity:.6; margin-bottom:28px; font-size:13px; }\r\n            .cinema-section-title { margin: 34px 0 16px; font-size:18px; border-left:4px solid #f0c400; padding-left:10px; }\r\n            .cinema-loading { opacity:.6; margin-top:20px; }\r\n            .cinema-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(170px,1fr)); gap:20px; }\r\n            .cinema-card { color:#fff; text-decoration:none; display:block; background:#1a1a1d; border-radius:12px; overflow:hidden; transition: transform .2s ease; }\r\n            .cinema-card:hover { transform: translateY(-4px); }\r\n            .cinema-poster-wrap { position:relative; width:100%; aspect-ratio:2\/3; background:#222; overflow:hidden; }\r\n            .cinema-poster-wrap img { width:100%; height:100%; object-fit:cover; display:block; }\r\n            .cinema-fallback-title {\r\n                position:absolute; left:0; right:0; top:71%; bottom:6%;\r\n                display:flex; align-items:flex-start; justify-content:center;\r\n                text-align:center; padding:8px 12px 0; font-weight:bold;\r\n                font-size:13px; line-height:1.25; text-shadow:0 2px 6px rgba(0,0,0,.9);\r\n            }\r\n            .cinema-year { position:absolute; top:8px; left:8px; background:rgba(0,0,0,.65); padding:3px 8px; border-radius:14px; font-size:11px; font-weight:bold; }\r\n            .cinema-title { padding:10px 12px 14px; font-size:13.5px; font-weight:bold; line-height:1.3; }\r\n        <\/style>\r\n        <\/head>\r\n        <body>\r\n            <h1>\ud83c\udfac Au cin\u00e9ma<\/h1>\r\n            <div class=\"cinema-subtitle\">Cliquez sur une affiche pour acc\u00e9der directement \u00e0 la page de visionnage du film<\/div>\r\n            <div class=\"cinema-grid\">${nowCards || \"<p>Aucun film \u00e0 afficher.<\/p>\"}<\/div>\r\n            ${upcomingSection}\r\n        <\/body>\r\n        <\/html>\r\n    `;\r\n}\r\n\r\nasync function openCinemaView() {\r\n\r\n    if (currentResults.length === 0) {\r\n        alert(\"Aucun film \u00e0 afficher. Lancez d'abord une recherche.\");\r\n        return;\r\n    }\r\n\r\n    const win = window.open(\"\", \"_blank\");\r\n\r\n    if (!win) {\r\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la vue cin\u00e9ma.\");\r\n        return;\r\n    }\r\n\r\n    win.document.open();\r\n    win.document.write(renderCinemaHtml(currentResults, [], true));\r\n    win.document.close();\r\n\r\n    let upcomingFilms = [];\r\n\r\n    try {\r\n\r\n        const listData = await fetchWikidataOnce(buildUpcomingListQuery(UPCOMING_LIMIT));\r\n        const rows = listData.results.bindings;\r\n\r\n        if (rows.length) {\r\n\r\n            const uris = rows.map(row => row.item.value);\r\n            const detailsData = await fetchWikidataOnce(buildDetailsQuery(uris));\r\n\r\n            const detailsByUri = {};\r\n            detailsData.results.bindings.forEach(row => {\r\n                detailsByUri[row.item.value] = row;\r\n            });\r\n\r\n            upcomingFilms = rows.map(row => convertResult(row, detailsByUri[row.item.value]));\r\n        }\r\n    }\r\n    catch (error) {\r\n        console.warn(\"Films \u00e0 venir indisponibles :\", error);\r\n    }\r\n\r\n    if (!win.closed) {\r\n        win.document.open();\r\n        win.document.write(renderCinemaHtml(currentResults, upcomingFilms, false));\r\n        win.document.close();\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   EVENEMENTS\r\n========================================================= *\/\r\n\r\ncancelButton.addEventListener(\"click\", function () {\r\n    if (abortController) abortController.abort();\r\n});\r\n\r\ndocument.getElementById(\"searchButton\").addEventListener(\"click\", function () {\r\n    currentPage = 1;\r\n    searchWikidata();\r\n});\r\n\r\nsearchInput.addEventListener(\"keydown\", function (event) {\r\n    if (event.key === \"Enter\") {\r\n        currentPage = 1;\r\n        searchWikidata();\r\n    }\r\n});\r\n\r\nnextButton.addEventListener(\"click\", function () {\r\n    if (currentPage < totalPages) {\r\n        currentPage++;\r\n        searchWikidata();\r\n    }\r\n});\r\n\r\npreviousButton.addEventListener(\"click\", function () {\r\n    if (currentPage > 1) {\r\n        currentPage--;\r\n        searchWikidata();\r\n    }\r\n});\r\n\r\ndocument.getElementById(\"pageButton\").addEventListener(\"click\", function () {\r\n    let page = Number(pageInput.value);\r\n    if (page < 1) page = 1;\r\n    if (totalKnown && page > totalPages) page = totalPages;\r\n    currentPage = page;\r\n    searchWikidata();\r\n});\r\n\r\ndocument.getElementById(\"reloadButton\").addEventListener(\"click\", searchWikidata);\r\n\r\nallLanguagesCheckbox.addEventListener(\"change\", function () {\r\n    languageOptionsContainer.classList.toggle(\"is-disabled\", allLanguagesCheckbox.checked);\r\n});\r\n\r\ndestinationSiteSelect.addEventListener(\"change\", function () {\r\n    customSiteUrlInput.disabled = destinationSiteSelect.value !== \"custom\";\r\n    if (currentResults.length) renderResults();\r\n});\r\n\r\nconst TITLE_COLOR_STORAGE_KEY = \"filmCatalogTitleColor\";\r\n\r\ntitleColorPicker.addEventListener(\"input\", function () {\r\n    document.querySelector(\".app\").style.setProperty(\"--title-color\", titleColorPicker.value);\r\n\r\n    \/\/ Sauvegarde locale, dans le navigateur de l'internaute uniquement\r\n    \/\/ (aucune requ\u00eate, aucune place prise sur un serveur ou une base).\r\n    try {\r\n        localStorage.setItem(TITLE_COLOR_STORAGE_KEY, titleColorPicker.value);\r\n    }\r\n    catch (error) {\r\n        console.warn(\"Impossible de sauvegarder la couleur choisie :\", error);\r\n    }\r\n});\r\n\r\ncustomSiteUrlInput.addEventListener(\"input\", function () {\r\n    if (destinationSiteSelect.value === \"custom\" && currentResults.length) {\r\n        renderResults();\r\n    }\r\n});\r\n\r\nyoutubeButton.addEventListener(\"click\", function () {\r\n\r\n    if (currentResults.length === 0) {\r\n        alert(\"Aucun film \u00e0 afficher pour le moment. Lancez d'abord une recherche.\");\r\n        return;\r\n    }\r\n\r\n    const win = window.open(\"\", \"_blank\");\r\n\r\n    if (!win) {\r\n        alert(\"Merci d'autoriser les fen\u00eatres pop-up pour ce site afin d'ouvrir la s\u00e9lection YouTube.\");\r\n        return;\r\n    }\r\n\r\n    const linksHtml = currentResults.map(film => {\r\n        const title = film.title || \"Titre non renseign\u00e9\";\r\n        const query = encodeURIComponent(title + \" film complet\");\r\n        const url = \"https:\/\/www.youtube.com\/results?search_query=\" + query;\r\n        return \"<li><a href=\\\"\" + url + \"\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">\"\r\n            + escapeHTML(title) + \"<\/a><\/li>\";\r\n    }).join(\"\");\r\n\r\n    win.document.write(\r\n        \"<!DOCTYPE html><html lang=\\\"fr\\\"><head><meta charset=\\\"UTF-8\\\">\"\r\n        + \"<title>S\u00e9lection sur YouTube<\/title>\"\r\n        + \"<style>body{font-family:Arial,Helvetica,sans-serif;background:#0b0b0d;color:#fff;padding:24px;}\"\r\n        + \"h1{font-size:20px;} a{color:#f0c400;text-decoration:none;} a:hover{text-decoration:underline;}\"\r\n        + \"li{margin-bottom:10px;font-size:15px;}<\/style><\/head>\"\r\n        + \"<body><h1>\ud83c\udfac Recherche YouTube \u2014 s\u00e9lection actuelle (\" + currentResults.length + \" film(s))<\/h1>\"\r\n        + \"<ul>\" + linksHtml + \"<\/ul><\/body><\/html>\"\r\n    );\r\n\r\n    win.document.close();\r\n});\r\n\r\ncinemaButton.addEventListener(\"click\", openCinemaView);\r\n\r\ndocument.getElementById(\"resetButton\").addEventListener(\"click\", function () {\r\n\r\n    if (isLoading && abortController) abortController.abort();\r\n\r\n    searchInput.value = \"\";\r\n    searchCategory.value = \"title\";\r\n    destinationSiteSelect.value = \"justwatch\";\r\n    customSiteUrlInput.value = \"\";\r\n    customSiteUrlInput.disabled = true;\r\n    dayInput.value = \"\";\r\n    monthInput.value = \"\";\r\n    yearInput.value = \"\";\r\n    yearFromInput.value = \"\";\r\n    yearToInput.value = \"\";\r\n    dateFromInput.value = \"\";\r\n    dateToInput.value = \"\";\r\n    titleFilter.value = \"all\";\r\n    dateFilter.value = \"all\";\r\n    typeFilter.value = \"film_animation\";\r\n\r\n    document.querySelectorAll(\".languageCheckbox\").forEach(cb => {\r\n        cb.checked = (cb.value === \"fr\");\r\n    });\r\n    allLanguagesCheckbox.checked = false;\r\n    languageOptionsContainer.classList.remove(\"is-disabled\");\r\n\r\n    religiousFilter.checked = true;\r\n\r\n    document.querySelectorAll(\".lexicalKeyword\").forEach(cb => { cb.checked = false; });\r\n    lexicalCustomInput.value = \"\";\r\n    excludeWordsInput.value = \"\";\r\n\r\n    sortSelect.value = \"newest\";\r\n    pageSizeSelect.value = \"20\";\r\n    maxResultsInput.value = \"\";\r\n\r\n    currentPage = 1;\r\n    totalPages = 1;\r\n    totalKnown = true;\r\n    hasNextPageUnknown = false;\r\n    currentResults = [];\r\n\r\n    resultsElement.innerHTML = \"\";\r\n    progressBar.style.width = \"0%\";\r\n    pageInput.value = 1;\r\n    pageInfo.textContent = \"Page 1 \/ 1\";\r\n\r\n    statusElement.textContent = \"Filtres r\u00e9initialis\u00e9s.\";\r\n});\r\n\r\ndocument.getElementById(\"exportButton\").addEventListener(\"click\", function () {\r\n\r\n    if (currentResults.length === 0) {\r\n        alert(\"Aucun r\u00e9sultat \u00e0 exporter.\");\r\n        return;\r\n    }\r\n\r\n    const header = [\"Titre\", \"R\u00e9alisateur(s)\", \"Casting\", \"Date\", \"Lien\", \"Wikidata\"];\r\n\r\n    const rows = currentResults.map(film => [\r\n        film.title, film.directors, film.cast, film.date, film.primaryLink, film.uri\r\n    ]);\r\n\r\n    const csv = [header, ...rows]\r\n        .map(row => row.map(value =>\r\n            '\"' + String(value || \"\").replace(\/\"\/g, '\"\"') + '\"'\r\n        ).join(\";\"))\r\n        .join(\"\\n\");\r\n\r\n    const blob = new Blob([\"\\ufeff\" + csv], { type: \"text\/csv;charset=utf-8\" });\r\n    const url = URL.createObjectURL(blob);\r\n    const link = document.createElement(\"a\");\r\n    link.href = url;\r\n    link.download = \"films-wikidata.csv\";\r\n    link.click();\r\n    URL.revokeObjectURL(url);\r\n});\r\n\r\n\/* =========================================================\r\n   INITIALISATION\r\n   On ne lance pas de recherche automatique au chargement,\r\n   mais on r\u00e9cup\u00e8re tout de suite les langues disponibles\r\n   dans la base pour peupler le s\u00e9lecteur.\r\n========================================================= *\/\r\n\r\nstatusElement.textContent = \"Pr\u00eat. Entrez un terme ou lancez une recherche.\";\r\nloadAvailableLanguages();\r\n\r\ntry {\r\n    const savedTitleColor = localStorage.getItem(TITLE_COLOR_STORAGE_KEY);\r\n    if (savedTitleColor) {\r\n        titleColorPicker.value = savedTitleColor;\r\n        document.querySelector(\".app\").style.setProperty(\"--title-color\", savedTitleColor);\r\n    }\r\n}\r\ncatch (error) {\r\n    console.warn(\"Impossible de relire la couleur sauvegard\u00e9e :\", error);\r\n}\r\n\r\n<\/script>\r\n\r\nhtml\r\n<!-- =========================================================\r\n     TMDB - PLUS DE FILMS CHRETIENS\r\n     \r\n     A PLACER JUSTE AVANT <\/body>\r\n\r\n     Ce module :\r\n     - utilise l'API TMDB\r\n     - respecte #typeFilter\r\n     - respecte #pageSize\r\n     - respecte #maxResults\r\n     - respecte #sort\r\n     - respecte #religiousFilter\r\n     - respecte les langues s\u00e9lectionn\u00e9es\r\n     - recherche les films chr\u00e9tiens\/religieux\r\n     - recherche les animations chr\u00e9tiennes\r\n     - r\u00e9cup\u00e8re plusieurs pages TMDB\r\n     - d\u00e9doublonne avec currentResults\r\n     - n'affiche AUCUNE affiche TMDB\r\n     - ajoute les titres \u00e0 currentResults\r\n     - r\u00e9utilise renderResults()\r\n========================================================= -->\r\n\r\n<style>\r\n\r\n#tmdbPlusButton {\r\n    display: inline-flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    gap: 8px;\r\n    margin: 8px 0;\r\n    padding: 10px 18px;\r\n    border: 0;\r\n    border-radius: 8px;\r\n    background: #01b4e4;\r\n    color: #ffffff;\r\n    font-weight: 700;\r\n    cursor: pointer;\r\n    transition: opacity .2s, transform .2s;\r\n}\r\n\r\n#tmdbPlusButton:hover {\r\n    opacity: .88;\r\n    transform: translateY(-1px);\r\n}\r\n\r\n#tmdbPlusButton:disabled {\r\n    opacity: .5;\r\n    cursor: wait;\r\n    transform: none;\r\n}\r\n\r\n#tmdbPlusStatus {\r\n    display: none;\r\n    width: 100%;\r\n    box-sizing: border-box;\r\n    margin: 8px 0 14px;\r\n    padding: 10px 14px;\r\n    border-radius: 8px;\r\n    background: rgba(1,180,228,.12);\r\n    border: 1px solid rgba(1,180,228,.35);\r\n    color: inherit;\r\n    font-size: 13px;\r\n    line-height: 1.45;\r\n}\r\n\r\n\/*\r\n   Zone utilis\u00e9e uniquement si votre renderResults()\r\n   pr\u00e9voit un emplacement \"poster\" vide.\r\n\r\n   Le module ne charge aucune image TMDB.\r\n*\/\r\n.tmdb-title-poster-placeholder {\r\n    width: 100%;\r\n    height: 100%;\r\n    min-height: 260px;\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    box-sizing: border-box;\r\n    padding: 24px;\r\n    text-align: center;\r\n    background:\r\n        linear-gradient(\r\n            135deg,\r\n            #292932,\r\n            #111116\r\n        );\r\n    color: #ffffff;\r\n    font-size: clamp(18px, 2vw, 28px);\r\n    font-weight: 700;\r\n    line-height: 1.3;\r\n    overflow-wrap: anywhere;\r\n}\r\n\r\n<\/style>\r\n\r\n\r\n<script>\r\n\r\n(function () {\r\n\r\n    \"use strict\";\r\n\r\n\r\n    \/* =====================================================\r\n       CONFIGURATION\r\n    ===================================================== *\/\r\n\r\n    const TMDB_API_KEY =\r\n        \"af5576c58fe6fd7fab447fbb4dd2423a\";\r\n\r\n    const TMDB_API =\r\n        \"https:\/\/api.themoviedb.org\/3\";\r\n\r\n    const TMDB_LANGUAGE =\r\n        \"fr-FR\";\r\n\r\n    const TMDB_REGION =\r\n        \"FR\";\r\n\r\n\r\n    \/*\r\n     * TMDB impose une pagination.\r\n     * Le module parcourt les pages n\u00e9cessaires.\r\n     *\r\n     * En mode illimit\u00e9, on parcourt les pages disponibles\r\n     * jusqu'\u00e0 la limite maximale exploitable par TMDB.\r\n     *\/\r\n    const TMDB_MAX_PAGES =\r\n        500;\r\n\r\n\r\n    \/*\r\n     * Mots-cl\u00e9s chr\u00e9tiens\/religieux.\r\n     *\r\n     * IMPORTANT :\r\n     * chaque mot-cl\u00e9 est recherch\u00e9 s\u00e9par\u00e9ment.\r\n     * Les r\u00e9sultats sont ensuite fusionn\u00e9s.\r\n     *\r\n     * Cela \u00e9vite de demander \u00e0 TMDB qu'un film poss\u00e8de\r\n     * TOUS les mots-cl\u00e9s simultan\u00e9ment.\r\n     *\/\r\n    const CHRISTIAN_KEYWORDS = [\r\n\r\n        \"christianity\",\r\n        \"christian\",\r\n        \"christian faith\",\r\n        \"christian film\",\r\n        \"religious film\",\r\n        \"religion\",\r\n        \"faith\",\r\n        \"bible\",\r\n        \"biblical\",\r\n        \"jesus christ\",\r\n        \"jesus\",\r\n        \"gospel\",\r\n        \"resurrection\",\r\n        \"crucifixion\",\r\n        \"church\",\r\n        \"evangelical christianity\"\r\n\r\n    ];\r\n\r\n\r\n    \/* =====================================================\r\n       ELEMENTS DE L'INTERFACE EXISTANTE\r\n    ===================================================== *\/\r\n\r\n    const typeFilter =\r\n        document.getElementById(\r\n            \"typeFilter\"\r\n        );\r\n\r\n    const pageSizeSelect =\r\n        document.getElementById(\r\n            \"pageSize\"\r\n        );\r\n\r\n    const maxResultsInput =\r\n        document.getElementById(\r\n            \"maxResults\"\r\n        );\r\n\r\n    const sortSelect =\r\n        document.getElementById(\r\n            \"sort\"\r\n        );\r\n\r\n    const religiousFilter =\r\n        document.getElementById(\r\n            \"religiousFilter\"\r\n        );\r\n\r\n    const allLanguagesCheckbox =\r\n        document.getElementById(\r\n            \"allLanguages\"\r\n        );\r\n\r\n    const languageOptionsContainer =\r\n        document.getElementById(\r\n            \"languageOptions\"\r\n        );\r\n\r\n    const statusElement =\r\n        document.getElementById(\r\n            \"status\"\r\n        );\r\n\r\n    const progressBar =\r\n        document.getElementById(\r\n            \"progressBar\"\r\n        );\r\n\r\n    const resultsElement =\r\n        document.getElementById(\r\n            \"results\"\r\n        );\r\n\r\n\r\n    \/* =====================================================\r\n       VERIFICATION\r\n    ===================================================== *\/\r\n\r\n    if (\r\n        !typeFilter ||\r\n        !pageSizeSelect ||\r\n        !maxResultsInput ||\r\n        !sortSelect\r\n    ) {\r\n\r\n        console.warn(\r\n            \"TMDB Plus : un ou plusieurs filtres n\u00e9cessaires sont absents.\"\r\n        );\r\n\r\n        return;\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       CREER LE BOUTON\r\n    ===================================================== *\/\r\n\r\n    function createTMDBButton() {\r\n\r\n        if (\r\n            document.getElementById(\r\n                \"tmdbPlusButton\"\r\n            )\r\n        ) {\r\n\r\n            return;\r\n\r\n        }\r\n\r\n\r\n        const button =\r\n            document.createElement(\r\n                \"button\"\r\n            );\r\n\r\n\r\n        button.id =\r\n            \"tmdbPlusButton\";\r\n\r\n\r\n        button.type =\r\n            \"button\";\r\n\r\n\r\n        button.textContent =\r\n            \"\ud83c\udfac Plus de films\";\r\n\r\n\r\n        \/*\r\n         * On cherche les boutons existants.\r\n         * Le bouton TMDB est plac\u00e9 \u00e0 proximit\u00e9\r\n         * des commandes de recherche.\r\n         *\/\r\n\r\n        const searchButton =\r\n            document.querySelector(\r\n                '#searchButton, [data-action=\"search\"], button[type=\"submit\"]'\r\n            );\r\n\r\n\r\n        if (\r\n            searchButton &&\r\n            searchButton.parentNode\r\n        ) {\r\n\r\n            searchButton.parentNode.appendChild(\r\n                button\r\n            );\r\n\r\n        }\r\n        else if (\r\n            resultsElement &&\r\n            resultsElement.parentNode\r\n        ) {\r\n\r\n            resultsElement.parentNode.insertBefore(\r\n                button,\r\n                resultsElement\r\n            );\r\n\r\n        }\r\n        else {\r\n\r\n            document.body.appendChild(\r\n                button\r\n            );\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Zone de statut\r\n         *\/\r\n\r\n        const status =\r\n            document.createElement(\r\n                \"div\"\r\n            );\r\n\r\n\r\n        status.id =\r\n            \"tmdbPlusStatus\";\r\n\r\n\r\n        if (\r\n            button.parentNode\r\n        ) {\r\n\r\n            button.parentNode.insertBefore(\r\n                status,\r\n                button.nextSibling\r\n            );\r\n\r\n        }\r\n\r\n\r\n        button.addEventListener(\r\n            \"click\",\r\n            function () {\r\n\r\n                searchTMDB();\r\n\r\n            }\r\n        );\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       STATUT\r\n    ===================================================== *\/\r\n\r\n    function setTMDBStatus(\r\n        message\r\n    ) {\r\n\r\n        const element =\r\n            document.getElementById(\r\n                \"tmdbPlusStatus\"\r\n            );\r\n\r\n\r\n        if (\r\n            element\r\n        ) {\r\n\r\n            element.textContent =\r\n                message;\r\n\r\n            element.style.display =\r\n                \"block\";\r\n\r\n        }\r\n\r\n\r\n        if (\r\n            statusElement\r\n        ) {\r\n\r\n            statusElement.textContent =\r\n                message;\r\n\r\n        }\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       NORMALISATION DES TITRES\r\n    ===================================================== *\/\r\n\r\n    function normalizeTitle(\r\n        value\r\n    ) {\r\n\r\n        return String(\r\n            value || \"\"\r\n        )\r\n        .toLowerCase()\r\n        .normalize(\r\n            \"NFD\"\r\n        )\r\n        .replace(\r\n            \/[\\u0300-\\u036f]\/g,\r\n            \"\"\r\n        )\r\n        .replace(\r\n            \/[^\\p{L}\\p{N}]+\/gu,\r\n            \" \"\r\n        )\r\n        .trim();\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       ANNEE\r\n    ===================================================== *\/\r\n\r\n    function getYear(\r\n        value\r\n    ) {\r\n\r\n        if (\r\n            !value\r\n        ) {\r\n\r\n            return \"\";\r\n\r\n        }\r\n\r\n\r\n        const match =\r\n            String(\r\n                value\r\n            ).match(\r\n                \/\\b(18|19|20)\\d{2}\\b\/\r\n            );\r\n\r\n\r\n        return match\r\n            ? match[0]\r\n            : \"\";\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       TYPE SELECTIONNE\r\n    ===================================================== *\/\r\n\r\n    function getTMDBType() {\r\n\r\n        const value =\r\n            typeFilter.value;\r\n\r\n\r\n        \/*\r\n         * Films uniquement\r\n         *\/\r\n\r\n        if (\r\n            value ===\r\n            \"film\"\r\n        ) {\r\n\r\n            return [\r\n                \"movie\"\r\n            ];\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Dessins anim\u00e9s uniquement\r\n         *\/\r\n\r\n        if (\r\n            value ===\r\n            \"animation\"\r\n        ) {\r\n\r\n            return [\r\n                \"animation\"\r\n            ];\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Films + dessins anim\u00e9s\r\n         *\/\r\n\r\n        if (\r\n            value ===\r\n            \"film_animation\"\r\n        ) {\r\n\r\n            return [\r\n                \"movie\",\r\n                \"animation\"\r\n            ];\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Mode large.\r\n         *\r\n         * TMDB Discover Movie reste utilis\u00e9 ici\r\n         * car votre catalogue est orient\u00e9 films.\r\n         *\/\r\n\r\n        if (\r\n            value ===\r\n            \"all\"\r\n        ) {\r\n\r\n            return [\r\n                \"movie\",\r\n                \"animation\"\r\n            ];\r\n\r\n        }\r\n\r\n\r\n        return [\r\n            \"movie\"\r\n        ];\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       LANGUES SELECTIONNEES\r\n    ===================================================== *\/\r\n\r\n    function getSelectedLanguages() {\r\n\r\n        \/*\r\n         * \"Toutes les langues\"\r\n         *\/\r\n\r\n        if (\r\n            allLanguagesCheckbox &&\r\n            allLanguagesCheckbox.checked\r\n        ) {\r\n\r\n            return [];\r\n\r\n        }\r\n\r\n\r\n        if (\r\n            !languageOptionsContainer\r\n        ) {\r\n\r\n            return [];\r\n\r\n        }\r\n\r\n\r\n        const checked =\r\n            languageOptionsContainer\r\n                .querySelectorAll(\r\n                    'input[type=\"checkbox\"]:checked'\r\n                );\r\n\r\n\r\n        const languages =\r\n            [];\r\n\r\n\r\n        checked.forEach(\r\n            function (\r\n                checkbox\r\n            ) {\r\n\r\n                \/*\r\n                 * Le code de langue peut \u00eatre :\r\n                 * value=\"fr\"\r\n                 * value=\"en\"\r\n                 * ou data-lang\r\n                 *\/\r\n\r\n                const language =\r\n                    checkbox.value ||\r\n                    checkbox.dataset.lang ||\r\n                    checkbox.dataset.language ||\r\n                    \"\";\r\n\r\n\r\n                if (\r\n                    language\r\n                ) {\r\n\r\n                    languages.push(\r\n                        language.toLowerCase()\r\n                    );\r\n\r\n                }\r\n\r\n            }\r\n        );\r\n\r\n\r\n        return [\r\n            ...new Set(\r\n                languages\r\n            )\r\n        ];\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       MOTS-CLES TMDB -> IDENTIFIANTS\r\n    ===================================================== *\/\r\n\r\n    async function findKeywordId(\r\n        keyword\r\n    ) {\r\n\r\n        const params =\r\n            new URLSearchParams({\r\n\r\n                api_key:\r\n                    TMDB_API_KEY,\r\n\r\n                query:\r\n                    keyword,\r\n\r\n                page:\r\n                    \"1\"\r\n\r\n            });\r\n\r\n\r\n        const response =\r\n            await fetch(\r\n\r\n                TMDB_API +\r\n                \"\/search\/keyword?\" +\r\n                params.toString()\r\n\r\n            );\r\n\r\n\r\n        if (\r\n            !response.ok\r\n        ) {\r\n\r\n            throw new Error(\r\n                \"Recherche de mot-cl\u00e9 TMDB : HTTP \" +\r\n                response.status\r\n            );\r\n\r\n        }\r\n\r\n\r\n        const data =\r\n            await response.json();\r\n\r\n\r\n        if (\r\n            !data.results ||\r\n            !data.results.length\r\n        ) {\r\n\r\n            return null;\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Priorit\u00e9 au r\u00e9sultat exact.\r\n         *\/\r\n\r\n        const exact =\r\n            data.results.find(\r\n                function (\r\n                    item\r\n                ) {\r\n\r\n                    return normalizeTitle(\r\n                        item.name\r\n                    ) ===\r\n                    normalizeTitle(\r\n                        keyword\r\n                    );\r\n\r\n                }\r\n            );\r\n\r\n\r\n        return (\r\n            exact ||\r\n            data.results[0]\r\n        ).id;\r\n\r\n    }\r\n\r\n\r\n    async function getKeywordIds() {\r\n\r\n        const results =\r\n            await Promise.all(\r\n\r\n                CHRISTIAN_KEYWORDS.map(\r\n                    function (\r\n                        keyword\r\n                    ) {\r\n\r\n                        return findKeywordId(\r\n                            keyword\r\n                        )\r\n                        .catch(\r\n                            function (\r\n                                error\r\n                            ) {\r\n\r\n                                console.warn(\r\n                                    \"Mot-cl\u00e9 ignor\u00e9 :\",\r\n                                    keyword,\r\n                                    error\r\n                                );\r\n\r\n                                return null;\r\n\r\n                            }\r\n                        );\r\n\r\n                    }\r\n                )\r\n\r\n            );\r\n\r\n\r\n        return [\r\n            ...new Set(\r\n                results.filter(\r\n                    Boolean\r\n                )\r\n            )\r\n        ];\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       REQUETE TMDB\r\n    ===================================================== *\/\r\n\r\n    async function fetchTMDBPage(\r\n        keywordId,\r\n        page,\r\n        animationOnly\r\n    ) {\r\n\r\n        const params =\r\n            new URLSearchParams({\r\n\r\n                api_key:\r\n                    TMDB_API_KEY,\r\n\r\n                language:\r\n                    TMDB_LANGUAGE,\r\n\r\n                region:\r\n                    TMDB_REGION,\r\n\r\n                with_keywords:\r\n                    String(\r\n                        keywordId\r\n                    ),\r\n\r\n                sort_by:\r\n                    \"primary_release_date.desc\",\r\n\r\n                include_adult:\r\n                    \"false\",\r\n\r\n                page:\r\n                    String(\r\n                        page\r\n                    )\r\n\r\n            });\r\n\r\n\r\n        \/*\r\n         * Dessins anim\u00e9s :\r\n         * genre TMDB Animation = 16\r\n         *\/\r\n\r\n        if (\r\n            animationOnly\r\n        ) {\r\n\r\n            params.set(\r\n                \"with_genres\",\r\n                \"16\"\r\n            );\r\n\r\n        }\r\n\r\n\r\n        const response =\r\n            await fetch(\r\n\r\n                TMDB_API +\r\n                \"\/discover\/movie?\" +\r\n                params.toString()\r\n\r\n            );\r\n\r\n\r\n        if (\r\n            !response.ok\r\n        ) {\r\n\r\n            throw new Error(\r\n                \"TMDB Discover : HTTP \" +\r\n                response.status\r\n            );\r\n\r\n        }\r\n\r\n\r\n        return await response.json();\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       TEST DE LANGUE\r\n    ===================================================== *\/\r\n\r\n    function matchesLanguage(\r\n        movie,\r\n        languages\r\n    ) {\r\n\r\n        if (\r\n            !languages.length\r\n        ) {\r\n\r\n            return true;\r\n\r\n        }\r\n\r\n\r\n        const originalLanguage =\r\n            String(\r\n                movie.original_language ||\r\n                \"\"\r\n            )\r\n            .toLowerCase();\r\n\r\n\r\n        return languages.includes(\r\n            originalLanguage\r\n        );\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       TEST DOUBLON\r\n    ===================================================== *\/\r\n\r\n    function isExistingResult(\r\n        movie\r\n    ) {\r\n\r\n        if (\r\n            !Array.isArray(\r\n                currentResults\r\n            )\r\n        ) {\r\n\r\n            return false;\r\n\r\n        }\r\n\r\n\r\n        const tmdbId =\r\n            String(\r\n                movie.id\r\n            );\r\n\r\n\r\n        const title =\r\n            normalizeTitle(\r\n                movie.title ||\r\n                movie.original_title\r\n            );\r\n\r\n\r\n        const year =\r\n            getYear(\r\n                movie.release_date\r\n            );\r\n\r\n\r\n        return currentResults.some(\r\n            function (\r\n                existing\r\n            ) {\r\n\r\n                \/*\r\n                 * M\u00eame ID TMDB\r\n                 *\/\r\n\r\n                if (\r\n                    existing.tmdbId &&\r\n                    String(\r\n                        existing.tmdbId\r\n                    ) ===\r\n                    tmdbId\r\n                ) {\r\n\r\n                    return true;\r\n\r\n                }\r\n\r\n\r\n                \/*\r\n                 * M\u00eame titre + m\u00eame ann\u00e9e\r\n                 *\/\r\n\r\n                const existingTitle =\r\n                    normalizeTitle(\r\n                        existing.title\r\n                    );\r\n\r\n\r\n                const existingYear =\r\n                    getYear(\r\n                        existing.date ||\r\n                        existing.year ||\r\n                        existing.release_date\r\n                    );\r\n\r\n\r\n                if (\r\n                    title &&\r\n                    existingTitle &&\r\n                    title ===\r\n                    existingTitle &&\r\n                    year &&\r\n                    existingYear &&\r\n                    year ===\r\n                    existingYear\r\n                ) {\r\n\r\n                    return true;\r\n\r\n                }\r\n\r\n\r\n                \/*\r\n                 * M\u00eame titre sans ann\u00e9e.\r\n                 *\r\n                 * Utilis\u00e9 seulement si l'ann\u00e9e\r\n                 * manque d'un des deux c\u00f4t\u00e9s.\r\n                 *\/\r\n\r\n                if (\r\n                    title &&\r\n                    existingTitle &&\r\n                    title ===\r\n                    existingTitle &&\r\n                    (\r\n                        !year ||\r\n                        !existingYear\r\n                    )\r\n                ) {\r\n\r\n                    return true;\r\n\r\n                }\r\n\r\n\r\n                return false;\r\n\r\n            }\r\n        );\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       CONVERTIR TMDB -> FORMAT DE VOTRE CATALOGUE\r\n       \r\n       IMPORTANT :\r\n       poster = \"\"\r\n       \r\n       Aucune image TMDB n'est utilis\u00e9e.\r\n    ===================================================== *\/\r\n\r\n    function convertTMDBResult(\r\n        movie\r\n    ) {\r\n\r\n        const title =\r\n            movie.title ||\r\n            movie.original_title ||\r\n            \"Titre non renseign\u00e9\";\r\n\r\n\r\n        return {\r\n\r\n            \/*\r\n             * Champs compatibles avec votre catalogue\r\n             *\/\r\n\r\n            uri:\r\n                \"tmdb:\" +\r\n                movie.id,\r\n\r\n\r\n            title:\r\n                title,\r\n\r\n\r\n            \/*\r\n             * PAS D'AFFICHE TMDB\r\n             *\/\r\n\r\n            poster:\r\n                \"\",\r\n\r\n\r\n            image:\r\n                \"\",\r\n\r\n\r\n            directors:\r\n                \"\",\r\n\r\n\r\n            cast:\r\n                \"\",\r\n\r\n\r\n            actors:\r\n                \"\",\r\n\r\n\r\n            date:\r\n                movie.release_date ||\r\n                \"\",\r\n\r\n\r\n            year:\r\n                getYear(\r\n                    movie.release_date\r\n                ),\r\n\r\n\r\n            description:\r\n                movie.overview ||\r\n                \"\",\r\n\r\n\r\n            imdb:\r\n                \"\",\r\n\r\n\r\n            website:\r\n                \"\",\r\n\r\n\r\n            primaryLink:\r\n                \"https:\/\/www.themoviedb.org\/movie\/\" +\r\n                movie.id,\r\n\r\n\r\n            tmdbId:\r\n                String(\r\n                    movie.id\r\n                ),\r\n\r\n\r\n            source:\r\n                \"TMDB\",\r\n\r\n\r\n            isTMDB:\r\n                true,\r\n\r\n\r\n            \/*\r\n             * Indique au rendu qu'il faut\r\n             * afficher le titre \u00e0 la place\r\n             * de l'affiche.\r\n             *\/\r\n\r\n            useTitleAsPoster:\r\n                true\r\n\r\n        };\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       RECUPERATION DES RESULTATS\r\n    ===================================================== *\/\r\n\r\n    async function collectTMDBResults() {\r\n\r\n        const selectedTypes =\r\n            getTMDBType();\r\n\r\n\r\n        const languages =\r\n            getSelectedLanguages();\r\n\r\n\r\n        const maxRaw =\r\n            maxResultsInput.value.trim();\r\n\r\n\r\n        const maxResults =\r\n            maxRaw\r\n                ? Math.max(\r\n                    1,\r\n                    Number(\r\n                        maxRaw\r\n                    )\r\n                )\r\n                : null;\r\n\r\n\r\n        \/*\r\n         * Le filtre pageSize sert ici \u00e0 d\u00e9terminer\r\n         * combien de nouveaux r\u00e9sultats TMDB sont\r\n         * n\u00e9cessaires pour la page actuelle.\r\n         *\r\n         * En mode illimit\u00e9, on r\u00e9cup\u00e8re toutes les\r\n         * pages accessibles.\r\n         *\/\r\n\r\n        const pageSize =\r\n            Math.max(\r\n                1,\r\n                Number(\r\n                    pageSizeSelect.value\r\n                ) ||\r\n                20\r\n            );\r\n\r\n\r\n        \/*\r\n         * Nombre cible.\r\n         *\r\n         * Si maxResults est renseign\u00e9 :\r\n         * on ne d\u00e9passe pas cette limite.\r\n         *\r\n         * Sinon :\r\n         * mode illimit\u00e9.\r\n         *\/\r\n\r\n        const target =\r\n            maxResults ||\r\n            Infinity;\r\n\r\n\r\n        const keywordIds =\r\n            await getKeywordIds();\r\n\r\n\r\n        if (\r\n            !keywordIds.length\r\n        ) {\r\n\r\n            throw new Error(\r\n                \"Aucun mot-cl\u00e9 chr\u00e9tien reconnu par TMDB.\"\r\n            );\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Map globale pour d\u00e9doublonner\r\n         * les r\u00e9sultats provenant de plusieurs\r\n         * mots-cl\u00e9s.\r\n         *\/\r\n\r\n        const unique =\r\n            new Map();\r\n\r\n\r\n        \/*\r\n         * Pour chaque type :\r\n         *\r\n         * movie\r\n         * animation\r\n         *\/\r\n\r\n        for (\r\n            const type of\r\n            selectedTypes\r\n        ) {\r\n\r\n            const animationOnly =\r\n                type ===\r\n                \"animation\";\r\n\r\n\r\n            \/*\r\n             * Chaque mot-cl\u00e9 est recherch\u00e9 s\u00e9par\u00e9ment.\r\n             *\/\r\n\r\n            for (\r\n                const keywordId of\r\n                keywordIds\r\n            ) {\r\n\r\n                let page =\r\n                    1;\r\n\r\n\r\n                let totalPages =\r\n                    1;\r\n\r\n\r\n                do {\r\n\r\n                    \/*\r\n                     * Arr\u00eat si la limite utilisateur\r\n                     * est d\u00e9j\u00e0 atteinte.\r\n                     *\/\r\n\r\n                    if (\r\n                        unique.size >=\r\n                        target\r\n                    ) {\r\n\r\n                        break;\r\n\r\n                    }\r\n\r\n\r\n                    setTMDBStatus(\r\n\r\n                        \"\u23f3 TMDB : recherche \" +\r\n                        type +\r\n                        \" \u2014 mot-cl\u00e9 \" +\r\n                        keywordId +\r\n                        \" \u2014 page \" +\r\n                        page +\r\n                        \" \/ \" +\r\n                        totalPages\r\n\r\n                    );\r\n\r\n\r\n                    const data =\r\n                        await fetchTMDBPage(\r\n\r\n                            keywordId,\r\n\r\n                            page,\r\n\r\n                            animationOnly\r\n\r\n                        );\r\n\r\n\r\n                    totalPages =\r\n                        Math.min(\r\n\r\n                            Number(\r\n                                data.total_pages ||\r\n                                1\r\n                            ),\r\n\r\n                            TMDB_MAX_PAGES\r\n\r\n                        );\r\n\r\n\r\n                    const results =\r\n                        data.results ||\r\n                        [];\r\n\r\n\r\n                    results.forEach(\r\n                        function (\r\n                            movie\r\n                        ) {\r\n\r\n                            \/*\r\n                             * Filtre de langue.\r\n                             *\/\r\n\r\n                            if (\r\n                                !matchesLanguage(\r\n                                    movie,\r\n                                    languages\r\n                                )\r\n                            ) {\r\n\r\n                                return;\r\n\r\n                            }\r\n\r\n\r\n                            \/*\r\n                             * D\u00e9duplication TMDB.\r\n                             *\/\r\n\r\n                            const title =\r\n                                normalizeTitle(\r\n\r\n                                    movie.title ||\r\n                                    movie.original_title\r\n\r\n                                );\r\n\r\n\r\n                            const year =\r\n                                getYear(\r\n                                    movie.release_date\r\n                                );\r\n\r\n\r\n                            const key =\r\n                                title +\r\n                                \"|\" +\r\n                                year;\r\n\r\n\r\n                            if (\r\n                                !unique.has(\r\n                                    key\r\n                                )\r\n                            ) {\r\n\r\n                                unique.set(\r\n                                    key,\r\n                                    movie\r\n                                );\r\n\r\n                            }\r\n\r\n                        }\r\n                    );\r\n\r\n\r\n                    \/*\r\n                     * Page suivante.\r\n                     *\/\r\n\r\n                    page++;\r\n\r\n\r\n                    \/*\r\n                     * En mode limit\u00e9 :\r\n                     * on peut arr\u00eater d\u00e8s que le\r\n                     * nombre demand\u00e9 est atteint.\r\n                     *\/\r\n\r\n                    if (\r\n                        maxResults &&\r\n                        unique.size >=\r\n                        maxResults\r\n                    ) {\r\n\r\n                        break;\r\n\r\n                    }\r\n\r\n\r\n                }\r\n                while (\r\n                    page <=\r\n                    totalPages\r\n                );\r\n\r\n            }\r\n\r\n        }\r\n\r\n\r\n        let movies =\r\n            Array.from(\r\n                unique.values()\r\n            );\r\n\r\n\r\n        \/* =================================================\r\n           TRI\r\n        ================================================= *\/\r\n\r\n        if (\r\n            sortSelect.value ===\r\n            \"oldest\"\r\n        ) {\r\n\r\n            movies.sort(\r\n                function (\r\n                    a,\r\n                    b\r\n                ) {\r\n\r\n                    return String(\r\n                        a.release_date ||\r\n                        \"\"\r\n                    ).localeCompare(\r\n\r\n                        String(\r\n                            b.release_date ||\r\n                            \"\"\r\n                        )\r\n\r\n                    );\r\n\r\n                }\r\n            );\r\n\r\n        }\r\n        else if (\r\n            sortSelect.value ===\r\n            \"title\"\r\n        ) {\r\n\r\n            movies.sort(\r\n                function (\r\n                    a,\r\n                    b\r\n                ) {\r\n\r\n                    return normalizeTitle(\r\n                        a.title\r\n                    ).localeCompare(\r\n\r\n                        normalizeTitle(\r\n                            b.title\r\n                        )\r\n\r\n                    );\r\n\r\n                }\r\n            );\r\n\r\n        }\r\n        else {\r\n\r\n            \/*\r\n             * Plus r\u00e9cents\r\n             *\/\r\n\r\n            movies.sort(\r\n                function (\r\n                    a,\r\n                    b\r\n                ) {\r\n\r\n                    return String(\r\n                        b.release_date ||\r\n                        \"\"\r\n                    ).localeCompare(\r\n\r\n                        String(\r\n                            a.release_date ||\r\n                            \"\"\r\n                        )\r\n\r\n                    );\r\n\r\n                }\r\n            );\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Limitation finale.\r\n         *\/\r\n\r\n        if (\r\n            maxResults\r\n        ) {\r\n\r\n            movies =\r\n                movies.slice(\r\n                    0,\r\n                    maxResults\r\n                );\r\n\r\n        }\r\n\r\n\r\n        return movies;\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       RECHERCHE PRINCIPALE\r\n    ===================================================== *\/\r\n\r\n    async function searchTMDB() {\r\n\r\n        const button =\r\n            document.getElementById(\r\n                \"tmdbPlusButton\"\r\n            );\r\n\r\n\r\n        if (\r\n            button.disabled\r\n        ) {\r\n\r\n            return;\r\n\r\n        }\r\n\r\n\r\n        \/*\r\n         * Si le filtre religieux est d\u00e9sactiv\u00e9,\r\n         * le bouton reste disponible mais avertit\r\n         * l'utilisateur.\r\n         *\r\n         * TMDB est ici utilis\u00e9 sp\u00e9cifiquement\r\n         * pour rechercher le catalogue chr\u00e9tien.\r\n         *\/\r\n\r\n        if (\r\n            religiousFilter &&\r\n            !religiousFilter.checked\r\n        ) {\r\n\r\n            const proceed =\r\n                confirm(\r\n\r\n                    \"Le filtre \u00ab Films chr\u00e9tiens \/ religieux \u00bb est d\u00e9sactiv\u00e9.\\n\\n\" +\r\n                    \"La recherche TMDB de ce bouton est sp\u00e9cialement con\u00e7ue \" +\r\n                    \"pour les films chr\u00e9tiens et religieux.\\n\\n\" +\r\n                    \"Voulez-vous continuer ?\"\r\n\r\n                );\r\n\r\n\r\n            if (\r\n                !proceed\r\n            ) {\r\n\r\n                return;\r\n\r\n            }\r\n\r\n        }\r\n\r\n\r\n        button.disabled =\r\n            true;\r\n\r\n\r\n        if (\r\n            progressBar\r\n        ) {\r\n\r\n            progressBar.style.width =\r\n                \"10%\";\r\n\r\n        }\r\n\r\n\r\n        try {\r\n\r\n            setTMDBStatus(\r\n                \"\u23f3 Pr\u00e9paration de la recherche TMDB...\"\r\n            );\r\n\r\n\r\n            \/*\r\n             * R\u00e9cup\u00e9ration TMDB\r\n             *\/\r\n\r\n            const movies =\r\n                await collectTMDBResults();\r\n\r\n\r\n            if (\r\n                progressBar\r\n            ) {\r\n\r\n                progressBar.style.width =\r\n                    \"70%\";\r\n\r\n            }\r\n\r\n\r\n            let added =\r\n                0;\r\n\r\n\r\n            let duplicates =\r\n                0;\r\n\r\n\r\n            \/*\r\n             * Ajouter uniquement les films\r\n             * absents de Wikidata\/currentResults.\r\n             *\/\r\n\r\n            movies.forEach(\r\n                function (\r\n                    movie\r\n                ) {\r\n\r\n                    if (\r\n                        isExistingResult(\r\n                            movie\r\n                        )\r\n                    ) {\r\n\r\n                        duplicates++;\r\n\r\n                        return;\r\n\r\n                    }\r\n\r\n\r\n                    const converted =\r\n                        convertTMDBResult(\r\n                            movie\r\n                        );\r\n\r\n\r\n                    currentResults.push(\r\n                        converted\r\n                    );\r\n\r\n\r\n                    added++;\r\n\r\n                }\r\n            );\r\n\r\n\r\n            \/*\r\n             * Tri final global\r\n             *\/\r\n\r\n            if (\r\n                sortSelect.value ===\r\n                \"title\"\r\n            ) {\r\n\r\n                currentResults.sort(\r\n                    function (\r\n                        a,\r\n                        b\r\n                    ) {\r\n\r\n                        return normalizeTitle(\r\n                            a.title\r\n                        ).localeCompare(\r\n\r\n                            normalizeTitle(\r\n                                b.title\r\n                            )\r\n\r\n                        );\r\n\r\n                    }\r\n                );\r\n\r\n            }\r\n            else if (\r\n                sortSelect.value ===\r\n                \"oldest\"\r\n            ) {\r\n\r\n                currentResults.sort(\r\n                    function (\r\n                        a,\r\n                        b\r\n                    ) {\r\n\r\n                        return String(\r\n                            a.date ||\r\n                            \"\"\r\n                        ).localeCompare(\r\n\r\n                            String(\r\n                                b.date ||\r\n                                \"\"\r\n                            )\r\n\r\n                        );\r\n\r\n                    }\r\n                );\r\n\r\n            }\r\n            else {\r\n\r\n                currentResults.sort(\r\n                    function (\r\n                        a,\r\n                        b\r\n                    ) {\r\n\r\n                        return String(\r\n                            b.date ||\r\n                            \"\"\r\n                        ).localeCompare(\r\n\r\n                            String(\r\n                                a.date ||\r\n                                \"\"\r\n                            )\r\n\r\n                        );\r\n\r\n                    }\r\n                );\r\n\r\n            }\r\n\r\n\r\n            \/*\r\n             * IMPORTANT :\r\n             * on r\u00e9utilise le rendu de votre interface.\r\n             *\/\r\n\r\n            if (\r\n                typeof renderResults ===\r\n                \"function\"\r\n            ) {\r\n\r\n                renderResults();\r\n\r\n            }\r\n\r\n\r\n            if (\r\n                progressBar\r\n            ) {\r\n\r\n                progressBar.style.width =\r\n                    \"100%\";\r\n\r\n            }\r\n\r\n\r\n            setTMDBStatus(\r\n\r\n                \"\u2705 TMDB termin\u00e9 : \" +\r\n                added +\r\n                \" nouveau(x) film(s) ajout\u00e9(s). \" +\r\n                duplicates +\r\n                \" doublon(s) ignor\u00e9(s).\"\r\n\r\n            );\r\n\r\n\r\n        }\r\n        catch (\r\n            error\r\n        ) {\r\n\r\n            console.error(\r\n                \"TMDB Plus :\",\r\n                error\r\n            );\r\n\r\n\r\n            setTMDBStatus(\r\n\r\n                \"\u274c Erreur TMDB : \" +\r\n                error.message\r\n\r\n            );\r\n\r\n\r\n            if (\r\n                progressBar\r\n            ) {\r\n\r\n                progressBar.style.width =\r\n                    \"100%\";\r\n\r\n            }\r\n\r\n        }\r\n        finally {\r\n\r\n            button.disabled =\r\n                false;\r\n\r\n        }\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       AFFICHAGE DU TITRE A LA PLACE DE L'AFFICHE\r\n       \r\n       Cette fonction ajoute une r\u00e8gle au rendu existant.\r\n       \r\n       Comme votre renderResults() g\u00e9n\u00e8re les cartes,\r\n       on observe le DOM apr\u00e8s son ex\u00e9cution et on cherche\r\n       les cartes contenant un r\u00e9sultat TMDB.\r\n\r\n       IMPORTANT :\r\n       aucun poster TMDB n'est charg\u00e9.\r\n    ===================================================== *\/\r\n\r\n    function enhanceTMDBCards() {\r\n\r\n        \/*\r\n         * Cette partie est volontairement prudente :\r\n         * elle ne remplace pas les cartes Wikidata.\r\n         *\r\n         * Elle recherche les \u00e9l\u00e9ments image dont la source\r\n         * serait vide et les remplace uniquement si le\r\n         * r\u00e9sultat correspondant est TMDB.\r\n         *\/\r\n\r\n        if (\r\n            !resultsElement\r\n        ) {\r\n\r\n            return;\r\n\r\n        }\r\n\r\n\r\n        const cards =\r\n            resultsElement.querySelectorAll(\r\n                \"[data-tmdb-id], .film-card, .movie-card, .result-card\"\r\n            );\r\n\r\n\r\n        cards.forEach(\r\n            function (\r\n                card\r\n            ) {\r\n\r\n                const tmdbId =\r\n                    card.dataset.tmdbId;\r\n\r\n\r\n                if (\r\n                    !tmdbId\r\n                ) {\r\n\r\n                    return;\r\n\r\n                }\r\n\r\n\r\n                const film =\r\n                    currentResults.find(\r\n                        function (\r\n                            item\r\n                        ) {\r\n\r\n                            return String(\r\n                                item.tmdbId\r\n                            ) ===\r\n                            String(\r\n                                tmdbId\r\n                            );\r\n\r\n                        }\r\n                    );\r\n\r\n\r\n                if (\r\n                    !film\r\n                ) {\r\n\r\n                    return;\r\n\r\n                }\r\n\r\n\r\n                const poster =\r\n                    card.querySelector(\r\n                        \"img\"\r\n                    );\r\n\r\n\r\n                if (\r\n                    poster\r\n                ) {\r\n\r\n                    const placeholder =\r\n                        document.createElement(\r\n                            \"div\"\r\n                        );\r\n\r\n\r\n                    placeholder.className =\r\n                        \"tmdb-title-poster-placeholder\";\r\n\r\n\r\n                    placeholder.textContent =\r\n                        film.title;\r\n\r\n\r\n                    poster.replaceWith(\r\n                        placeholder\r\n                    );\r\n\r\n                }\r\n\r\n            }\r\n        );\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       OBSERVER DU RENDU\r\n       \r\n       Permet d'appliquer le remplacement titre\/affiche\r\n       apr\u00e8s renderResults().\r\n    ===================================================== *\/\r\n\r\n    if (\r\n        resultsElement\r\n    ) {\r\n\r\n        const observer =\r\n            new MutationObserver(\r\n                function () {\r\n\r\n                    enhanceTMDBCards();\r\n\r\n                }\r\n            );\r\n\r\n\r\n        observer.observe(\r\n            resultsElement,\r\n            {\r\n                childList:\r\n                    true,\r\n\r\n                subtree:\r\n                    true\r\n            }\r\n        );\r\n\r\n    }\r\n\r\n\r\n    \/* =====================================================\r\n       INITIALISATION\r\n    ===================================================== *\/\r\n\r\n    createTMDBButton();\r\n\r\n\r\n    console.log(\r\n        \"\u2705 Module TMDB \u00ab Plus de films \u00bb charg\u00e9.\"\r\n    );\r\n\r\n\r\n})();\r\n\r\n<\/script>\r\n\r\n\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-de404c0 e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"de404c0\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-00f7368 elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-spacer\" data-id=\"00f7368\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-66c6902 e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"66c6902\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-029a17c elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-spacer\" data-id=\"029a17c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-8852add e-flex e-con-boxed elementor-repeater-item-default_padding elementor-repeater-item-none elementor-repeater-item-none_hover wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent\" data-id=\"8852add\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ang_container_spacing_size&quot;:&quot;default_padding&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-07f884e elementor-repeater-item-none elementor-repeater-item-none_hover elementor-widget elementor-widget-spacer\" data-id=\"07f884e\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Recherche libre Rechercher dans TitreR\u00e9alisateurActeurTitre + r\u00e9alisateur + acteur &nbsp; \ud83d\udd0e Rechercher Ouvrir les films sur JustWatchAlloCin\u00e9IMDbURL personnalis\u00e9e Couleur des titres de films URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus) Utilisez {titre} dans l&rsquo;URL pour qu&rsquo;il soit remplac\u00e9 par le titre du film \u00e0 chaque clic. Sans {titre}, le titre est ajout\u00e9 automatiquement en param\u00e8tre q= \u00e0 la fin de l&rsquo;URL saisie. Les films venant de TMDB (bouton \u00ab\u00a0Plus de films\u00a0\u00bb) s&rsquo;ouvrent toujours directement sur TMDB, quel que soit ce choix \u2014 voir note plus bas. Date de sortie Toutes les datesAvec date de sortieSans date de sortie Derniers jours Derniers mois Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3) Ann\u00e9e de (fourchette) Ann\u00e9e \u00e0 (fourchette) Date pr\u00e9cise de d\u00e9but Date pr\u00e9cise de fin Titre Tous les filmsAvec un titreSans titre Type Films uniquementDessins anim\u00e9s uniquementFilms + dessins anim\u00e9sFilms et \u0153uvres audiovisuelles (large) Tri Plus r\u00e9centsPlus anciensTitre A-Z R\u00e9sultats par page Nombre maxi de r\u00e9sultats Langue(s) du titre Chargement des langues disponibles dans la base&#8230; \ud83c\udf10 Toutes les langues (ignore la liste ci-dessus) Cochez une, plusieurs, ou toutes les langues souhait\u00e9es. Cochez \u00ab Toutes les langues \u00bb pour ne faire aucun tri par langue. \u271d\ufe0f Films chr\u00e9tiens \/ religieux (genre \/ sujet Wikidata) \u00c9largir la recherche (mots-cl\u00e9s dans le titre) Chr\u00e9tien Religieux Biblique Spirituel Croyance \u00c9vangile Dieu \/ \u00c9ternel Exclure les films contenant ces mots (titre) &nbsp; \u23f9 Annuler &nbsp; \ud83e\uddf9 R\u00e9initialiser Aller \u00e0 la page &nbsp; \ud83d\udcc4 Aller \u00e0 la page &nbsp; \u2b07\ufe0f Exporter CSV &nbsp; \ud83d\udd04 Rechercher &nbsp; \u25b6\ufe0f S\u00e9lection sur YouTube &nbsp; \ud83c\udfa6 Vue Cin\u00e9ma Pr\u00eat. \u25c0 Pr\u00e9c\u00e9dent Page 1 \/ 1 Suivant \u25b6 Recherche libre Rechercher dans TitreR\u00e9alisateurActeurTitre + r\u00e9alisateur + acteur &nbsp; \ud83d\udd0e Rechercher Ouvrir les films sur JustWatchAlloCin\u00e9IMDbURL personnalis\u00e9e Couleur des titres de films URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus) Utilisez {titre} dans l&rsquo;URL pour qu&rsquo;il soit remplac\u00e9 par le titre du film \u00e0 chaque clic. Sans {titre}, le titre est ajout\u00e9 automatiquement en param\u00e8tre q= \u00e0 la fin de l&rsquo;URL saisie. Les films venant de TMDB (bouton \u00ab\u00a0Plus de films\u00a0\u00bb) s&rsquo;ouvrent toujours directement sur TMDB, quel que soit ce choix \u2014 voir note plus bas. Date de sortie Toutes les datesAvec date de sortieSans date de sortie Derniers jours Derniers mois Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3) Ann\u00e9e de (fourchette) Ann\u00e9e \u00e0 (fourchette) Date pr\u00e9cise de d\u00e9but Date pr\u00e9cise de fin Titre Tous les filmsAvec un titreSans titre Type Films uniquementDessins anim\u00e9s uniquementFilms + dessins anim\u00e9sFilms et \u0153uvres audiovisuelles (large) Tri Plus r\u00e9centsPlus anciensTitre A-Z R\u00e9sultats par page Nombre maxi de r\u00e9sultats Langue(s) du titre Chargement des langues disponibles dans la base&#8230; \ud83c\udf10 Toutes les langues (ignore la liste ci-dessus) Cochez une, plusieurs, ou toutes les langues souhait\u00e9es. Cochez \u00ab Toutes les langues \u00bb pour ne faire aucun tri par langue. \u271d\ufe0f Films chr\u00e9tiens \/ religieux (genre \/ sujet Wikidata) \u00c9largir la recherche (mots-cl\u00e9s dans le titre) Chr\u00e9tien Religieux Biblique Spirituel Croyance \u00c9vangile Dieu \/ \u00c9ternel Exclure les films contenant ces mots (titre) &nbsp; \u23f9 Annuler &nbsp; \ud83e\uddf9 R\u00e9initialiser Aller \u00e0 la page &nbsp; \ud83d\udcc4 Aller \u00e0 la page &nbsp; \u2b07\ufe0f Exporter CSV &nbsp; \ud83d\udd04 Rechercher &nbsp; \u25b6\ufe0f S\u00e9lection sur YouTube &nbsp; \ud83c\udfa6 Vue Cin\u00e9ma Pr\u00eat. \u25c0 Pr\u00e9c\u00e9dent Page 1 \/ 1 Suivant \u25b6 Recherche libre Rechercher dans TitreR\u00e9alisateurActeurTitre + r\u00e9alisateur + acteur &nbsp; \ud83d\udd0e Rechercher Ouvrir les films sur JustWatchAlloCin\u00e9IMDbURL personnalis\u00e9e Couleur des titres de films URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus) Utilisez {titre} dans l&rsquo;URL pour qu&rsquo;il soit remplac\u00e9 par le titre du film \u00e0 chaque clic. Sans {titre}, le titre est ajout\u00e9 automatiquement en param\u00e8tre q= \u00e0 la fin de l&rsquo;URL saisie. Les films venant de TMDB (bouton \u00ab\u00a0Plus de films\u00a0\u00bb) s&rsquo;ouvrent toujours directement sur TMDB, quel que soit ce choix \u2014 voir note plus bas. Date de sortie Toutes les datesAvec date de sortieSans date de sortie Derniers jours Derniers mois Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3) Ann\u00e9e de (fourchette) Ann\u00e9e \u00e0 (fourchette) Date pr\u00e9cise de d\u00e9but Date pr\u00e9cise de fin Titre Tous les filmsAvec un titreSans titre Type Films uniquementDessins anim\u00e9s uniquementFilms + dessins anim\u00e9sFilms et \u0153uvres audiovisuelles (large) Tri Plus r\u00e9centsPlus anciensTitre A-Z R\u00e9sultats par page Nombre maxi de r\u00e9sultats Langue(s) du titre Chargement des langues disponibles dans la base&#8230; \ud83c\udf10 Toutes les langues (ignore la liste ci-dessus) Cochez une, plusieurs, ou toutes les langues souhait\u00e9es. Cochez \u00ab Toutes les langues \u00bb pour ne faire aucun tri par langue. \u271d\ufe0f Films chr\u00e9tiens \/ religieux (genre \/ sujet Wikidata) \u00c9largir la recherche (mots-cl\u00e9s dans le titre) Chr\u00e9tien Religieux Biblique Spirituel Croyance \u00c9vangile Dieu \/ \u00c9ternel Exclure les films contenant ces mots (titre) &nbsp; \u23f9 Annuler &nbsp; \ud83e\uddf9 R\u00e9initialiser Aller \u00e0 la page &nbsp; \ud83d\udcc4 Aller \u00e0 la page &nbsp; \u2b07\ufe0f Exporter CSV &nbsp; \ud83d\udd04 Rechercher &nbsp; \u25b6\ufe0f S\u00e9lection sur YouTube &nbsp; \ud83c\udfa6 Vue Cin\u00e9ma Pr\u00eat. \u25c0 Pr\u00e9c\u00e9dent Page 1 \/ 1 Suivant \u25b6 test 2 Catalogue Films &#8211; Wikidata Recherche libre Rechercher dans TitreR\u00e9alisateurActeurTitre + r\u00e9alisateur + acteur &nbsp; \ud83d\udd0e Rechercher Ouvrir les films sur JustWatchAlloCin\u00e9IMDbURL personnalis\u00e9e Couleur des titres de films URL personnalis\u00e9e (si s\u00e9lectionn\u00e9e ci-dessus) Utilisez {titre} dans l&rsquo;URL pour qu&rsquo;il soit remplac\u00e9 par le titre du film \u00e0 chaque clic (ex : https:\/\/www.google.com\/search?q={titre}+film). Sans {titre}, le titre est ajout\u00e9 automatiquement en param\u00e8tre q= \u00e0 la fin de l&rsquo;URL saisie, pour toujours atterrir sur le film cliqu\u00e9 plut\u00f4t que sur la page d&rsquo;accueil du site. Date de sortie Toutes les datesAvec date de sortieSans date de sortie Derniers jours Derniers mois Ann\u00e9e (2026) ou N derni\u00e8res ann\u00e9es (3) Ann\u00e9e de (fourchette) Ann\u00e9e \u00e0 (fourchette) Date pr\u00e9cise de d\u00e9but Date pr\u00e9cise de fin Titre Tous les filmsAvec un titreSans titre Type Films uniquementDessins anim\u00e9s uniquementFilms + dessins anim\u00e9sFilms et \u0153uvres audiovisuelles (large) Tri Plus r\u00e9centsPlus anciensTitre A-Z R\u00e9sultats par page Nombre maxi de r\u00e9sultats Langue(s) du titre Chargement des langues disponibles dans la base&#8230; \ud83c\udf10 Toutes les langues (ignore la liste ci-dessus) La liste ci-dessus est construite \u00e0 partir des langues r\u00e9ellement pr\u00e9sentes dans les libell\u00e9s de films sur Wikidata (\u00e9chantillon). Cochez une, plusieurs, ou toutes les langues souhait\u00e9es (aucune touche \u00e0 maintenir). Cochez \u00ab Toutes les langues \u00bb<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"site-sidebar-layout":"no-sidebar","site-content-layout":"","ast-site-content-layout":"full-width-container","site-content-style":"unboxed","site-sidebar-style":"unboxed","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"disabled","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":"","_members_access_role":[],"_members_access_error":""},"class_list":["post-31705","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=\/wp\/v2\/pages\/31705","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=31705"}],"version-history":[{"count":134,"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=\/wp\/v2\/pages\/31705\/revisions"}],"predecessor-version":[{"id":32029,"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=\/wp\/v2\/pages\/31705\/revisions\/32029"}],"wp:attachment":[{"href":"https:\/\/allianceenjesus.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=31705"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}