{"id":14171,"date":"2026-08-02T13:32:38","date_gmt":"2026-08-02T11:32:38","guid":{"rendered":"https:\/\/pimpestudio.fr\/?page_id=14171"},"modified":"2026-08-24T18:25:35","modified_gmt":"2026-08-24T16:25:35","slug":"home-page","status":"publish","type":"page","link":"https:\/\/pimpestudio.fr\/en\/","title":{"rendered":"PIMP\u00c9 STUDIO"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"14171\" class=\"elementor elementor-14171\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1c7de43 e-con e-atomic-element e-flexbox-base e-b98e050 panel-infos \" data-id=\"1c7de43\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"1c7de43\" data-e-type=\"e-flexbox\" data-id=\"1c7de43\">\n    \t\t<div class=\"elementor-element elementor-element-b57e116 close-panel elementor-widget elementor-widget-heading\" data-id=\"b57e116\" 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\">Close<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-a4e1b04 e-con e-atomic-element e-flexbox-base e-de541e7 infos-content \" data-id=\"a4e1b04\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"a4e1b04\" data-e-type=\"e-flexbox\" data-id=\"a4e1b04\">\n    <div class=\"elementor-element elementor-element-a999c79 e-con e-atomic-element e-flexbox-base e-651a381 \" data-id=\"a999c79\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"a999c79\" data-e-type=\"e-flexbox\" data-id=\"a999c79\">\n    \t\t<div class=\"elementor-element elementor-element-9aebd13 elementor-widget elementor-widget-heading\" data-id=\"9aebd13\" 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\">Contact<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-7212f7f elementor-widget__width-initial elementor-widget elementor-widget-text-editor\" data-id=\"7212f7f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>Pimp\u00e9 Studio\u00a0est un studio de cr\u00e9ation fond\u00e9 par Louisa Errounda, sp\u00e9cialis\u00e9 en direction artistique, CGI et cr\u00e9ation d&rsquo;univers visuels. Le studio accompagne des marques des secteurs de la beaut\u00e9, de la mode, du luxe et de la culture dans la conception d&rsquo;images, de campagnes et d&rsquo;exp\u00e9riences visuelles.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n<div class=\"elementor-element elementor-element-09fc185 e-con e-atomic-element e-flexbox-base e-a9c013a \" data-id=\"09fc185\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"09fc185\" data-e-type=\"e-flexbox\" data-id=\"09fc185\">\n    \t\t<div class=\"elementor-element elementor-element-0937993 elementor-widget elementor-widget-heading\" data-id=\"0937993\" 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\">COMMISSIONS<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-40531b4 elementor-widget elementor-widget-text-editor\" data-id=\"40531b4\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><a href=\"mailto:louisa@pimpestudio.com\">louisa@pimpestudio.fr<\/a><\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-5e96f85 elementor-widget elementor-widget-spacer\" data-id=\"5e96f85\" 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<div class=\"elementor-element elementor-element-8dac948 elementor-widget elementor-widget-heading\" data-id=\"8dac948\" 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\">DEMANDES G\u00c9N\u00c9RALES<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-60e05c9 elementor-widget elementor-widget-text-editor\" data-id=\"60e05c9\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><a href=\"mailto:hello@pimpestudio.fr\">hello@pimpestudio.fr<\/a><\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-1ccd932 elementor-widget elementor-widget-spacer\" data-id=\"1ccd932\" 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<div class=\"elementor-element elementor-element-f15124e elementor-widget elementor-widget-heading\" data-id=\"f15124e\" 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\">SERVICES<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-0d7c92e elementor-widget elementor-widget-text-editor\" data-id=\"0d7c92e\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p class=\"p1\">Direction artistique <br \/>Direction cr\u00e9ative <br \/>CGI<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n\n<\/div>\n\n<\/div>\n<div class=\"elementor-element elementor-element-084e4cc e-con e-atomic-element e-flexbox-base e-47b6ef5 panel-projets \" data-id=\"084e4cc\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"084e4cc\" data-e-type=\"e-flexbox\" data-id=\"084e4cc\">\n    \t\t<div class=\"elementor-element elementor-element-ba25b52 elementor-widget elementor-widget-html\" data-id=\"ba25b52\" 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<!--\nPIMP\u00c9 STUDIO \u2014 HOME V3.24 \u2014 SCROLL SMOOTH + FOND CROSSFADE\nTRANSLATEPRESS FIX \u2014 CONSERVATION FR\/EN ENTRE LES PROJETS\n\nCette version remplace int\u00e9gralement le contenu du widget HTML actuel.\nElle conserve :\n- les cartes en boucle infinie ;\n- la navigation molette \/ trackpad \/ tactile \/ clavier ;\n- le fond flout\u00e9 synchronis\u00e9 avec la carte active ;\n- une roulette automatique des projets au chargement ;\n\nElle ajoute :\n- les cartes directement sur la Home ;\n- le stack recentr\u00e9 et l\u00e9g\u00e8rement remont\u00e9 ;\n- la suppression des l\u00e9gendes \u00e0 gauche ;\n- le nom du projet centr\u00e9 en bas de page avec une marge inf\u00e9rieure ;\n- le changement dynamique du titre au scroll ;\n- la Home appara\u00eet imm\u00e9diatement, puis les cartes d\u00e9filent automatiquement quelques secondes avant de ralentir et de s\u2019arr\u00eater. \n\nStructure Elementor attendue :\n- Header Elementor global : .pimpe-global-header\n- Logo de la Home : .pimpe-logo\n- Baseline globale : .pimpe-global-tagline\n- Menu global : .pimpe-global-menu\n- Lien Home global : .pimpe-global-home\n- Lien Infos global : .pimpe-global-infos\n- Zone projets\/Home : .panel-projets\n- Layout : .projects-layout\n- Ancienne colonne texte : .project-info (elle peut rester, elle sera masqu\u00e9e)\n- Stack : .project-stack > .card-stage > .project-card\n- Dans chaque carte : .card-project-title\n- Une carte peut contenir une image OU une vid\u00e9o HTML.\n- Lorsqu'une carte vid\u00e9o est active, la m\u00eame vid\u00e9o est jou\u00e9e en grand fond flout\u00e9.\n- Seule la vid\u00e9o de la carte active est lue ; les autres sont mises en pause.\n-->\n\n<style>\n:root {\n  --project-background-image: none;\n  --pimpe-logo-progress: 1;\n\n  \/* Le stack + le titre forment d\u00e9sormais un seul bloc visuel centr\u00e9. *\/\n  --pimpe-composition-shift: -50px;\n  --pimpe-stack-half-height: clamp(350px, 24vw, 390px);\n  --pimpe-title-gap: 32px;\n\n  \/* Bord droit exact du texte INFOS, recalcul\u00e9 en JavaScript. *\/\n  --pimpe-header-infos-right: 32px;\n}\n\n\/* =========================================================\n   BASE\n========================================================= *\/\n\nhtml,\nbody {\n  min-height: 100% !important;\n  overscroll-behavior: none;\n  background-color: #111 !important;\n}\n\n\/* =========================================================\n   \u00c9CRAN NOIR DE CHARGEMENT \u2014 AVANT LA ROULETTE\n========================================================= *\/\n\n.pimpe-home-loading-screen {\n  position: fixed !important;\n  inset: 0 !important;\n\n  z-index: 650 !important;\n\n  display: block !important;\n  width: 100% !important;\n  height: 100% !important;\n\n  margin: 0 !important;\n  padding: 0 !important;\n\n  background: #000 !important;\n\n  opacity: 1 !important;\n  visibility: visible !important;\n  pointer-events: auto !important;\n\n  transition:\n    opacity .82s cubic-bezier(.16, 1, .3, 1),\n    visibility 0s linear .82s !important;\n\n  will-change: opacity;\n}\n\n.pimpe-home-loading-screen.is-leaving {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n}\n\n\/* Ne masque jamais l\u2019\u00e9diteur Elementor. *\/\nbody.elementor-editor-active\n.pimpe-home-loading-screen,\nbody.elementor-editor-preview\n.pimpe-home-loading-screen,\nbody.elementor-edit-mode\n.pimpe-home-loading-screen {\n  display: none !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .pimpe-home-loading-screen {\n    transition-duration: .01ms !important;\n  }\n}\n\n\/*\n * Pendant le chargement, le header reste visible au-dessus du fond noir.\n * Les cards, le titre, l\u2019index et le fond projet restent totalement cach\u00e9s.\n *\/\nbody.pimpe-home-loading .pimpe-global-header {\n  z-index: 900 !important;\n  opacity: 1 !important;\n  visibility: visible !important;\n  pointer-events: auto !important;\n}\n\nbody.pimpe-home-loading .panel-projets {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n}\n\nbody.pimpe-home-loading .project-stack,\nbody.pimpe-home-loading .project-background-title,\nbody.pimpe-home-loading .pimpe-project-index {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n}\n\nbody.pimpe-home-ready {\n  overflow: hidden !important;\n}\n\n\/* Masque l'ancien slider d\u00e9coratif de la premi\u00e8re Home,\n   quel que soit le nom de classe actuellement utilis\u00e9. *\/\n.hero-slider,\n.legacy-image-slider {\n  display: none !important;\n}\n\n\/* =========================================================\n   HEADER ELEMENTOR GLOBAL \u2014 INT\u00c9GRATION UNIQUEMENT\n========================================================= *\/\n\n\/* La mise en page du header est d\u00e9sormais enti\u00e8rement g\u00e9r\u00e9e dans\n   Elementor. Ce widget ne lui impose plus aucune position ni dimension. *\/\n.pimpe-global-home,\n.pimpe-global-infos,\n.pimpe-global-logo,\n.pimpe-logo {\n  cursor: pointer !important;\n}\n\n\n\/* =========================================================\n   HEADER \/ LANGUES \u2014 LES WRAPPERS INVISIBLES NE BLOQUENT PAS LA HOME\n========================================================= *\/\n\n.pimpe-global-header {\n  pointer-events: none !important;\n}\n\n.pimpe-global-header .e-con,\n.pimpe-global-header .e-con-inner,\n.pimpe-global-header .elementor-element,\n.pimpe-global-header .elementor-widget,\n.pimpe-global-header .elementor-widget-container,\n.pimpe-global-header .pimpe-global-menu,\n.pimpe-global-header nav,\n.pimpe-global-header ul,\n.pimpe-global-header li {\n  pointer-events: none !important;\n}\n\n.pimpe-global-header a,\n.pimpe-global-header button,\n.pimpe-global-header [role=\"button\"],\n.pimpe-global-header [role=\"link\"] {\n  pointer-events: auto !important;\n}\n\n.pimpe-global-home:hover,\n.pimpe-global-infos:hover,\n.pimpe-global-infos.is-active {\n  opacity: .55 !important;\n}\n\n\/* Sur la Home, HOME indique discr\u00e8tement la page active. *\/\nbody.pimpe-home-ready .pimpe-global-home,\nbody.home .pimpe-global-home {\n  opacity: .55 !important;\n}\n\n\/* Le lien Projets n'est plus utile : les projets sont la Home. *\/\n.open-projets {\n  display: none !important;\n}\n\n\/* =========================================================\n   HOME PROJETS\n========================================================= *\/\n\n.panel-projets {\n  position: fixed !important;\n  inset: 0 !important;\n\n  \/*\n   * Ne pas imposer 100svh ici : sur mobile, la barre du navigateur\n   * change la hauteur visible. inset:0 \u00e9tire le panneau jusqu'au\n   * bord r\u00e9el du viewport sans laisser de bande inf\u00e9rieure.\n   *\/\n  width: auto !important;\n  height: auto !important;\n  min-height: 100vh !important;\n  min-height: 100dvh !important;\n\n  overflow: hidden !important;\n  isolation: isolate !important;\n  transform: none !important;\n  transition: none !important;\n  background: #111 !important;\n  z-index: 100 !important;\n}\n\n\/* Image flout\u00e9e synchronis\u00e9e avec la carte centrale. *\/\n.panel-projets::before {\n  content: \"\" !important;\n  position: absolute !important;\n\n  \/*\n   * D\u00e9bordement volontaire : le blur ne doit jamais r\u00e9v\u00e9ler son bord,\n   * m\u00eame pendant l'ouverture\/fermeture des barres Safari ou Chrome.\n   *\/\n  inset: -14dvh -14vw !important;\n\n  z-index: 0 !important;\n  pointer-events: none !important;\n\n  background-image: var(--project-background-image) !important;\n  background-position: center !important;\n  background-repeat: no-repeat !important;\n  background-size: cover !important;\n\n  opacity: 0 !important;\n  filter: blur(55px) saturate(1.25) !important;\n  transform: translateZ(0) scale(1.12) !important;\n  backface-visibility: hidden !important;\n  -webkit-backface-visibility: hidden !important;\n  transition: opacity .95s cubic-bezier(.22, .61, .36, 1) !important;\n  will-change: opacity, transform;\n}\n\n\n\n\/* =========================================================\n   FOND VID\u00c9O FLOUT\u00c9 SYNCHRONIS\u00c9 AVEC LA CARTE ACTIVE\n========================================================= *\/\n\n.pimpe-video-background {\n  position: absolute !important;\n  inset: -8% !important;\n  z-index: 0 !important;\n  overflow: hidden !important;\n  pointer-events: none !important;\n  opacity: 0 !important;\n  transition: opacity .95s cubic-bezier(.22, .61, .36, 1) !important;\n  will-change: opacity;\n}\n\n.pimpe-video-background.is-visible {\n  opacity: .6 !important;\n}\n\n.pimpe-video-background__layer {\n  position: absolute !important;\n  inset: 0 !important;\n  width: 100% !important;\n  height: 100% !important;\n  opacity: 0 !important;\n  transition: opacity .95s cubic-bezier(.22, .61, .36, 1) !important;\n}\n\n.pimpe-video-background__layer.is-active {\n  opacity: 1 !important;\n}\n\n.pimpe-video-background video {\n  display: block !important;\n  width: 100% !important;\n  height: 100% !important;\n  object-fit: cover !important;\n  object-position: center !important;\n  filter: blur(55px) saturate(1.25) !important;\n  transform: scale(1.08) !important;\n  pointer-events: none !important;\n}\n\n\/* Une vid\u00e9o active remplace le pseudo-\u00e9l\u00e9ment image, sans supprimer\n   le voile sombre plac\u00e9 au-dessus pour conserver la lisibilit\u00e9. *\/\n.panel-projets.has-project-video::before {\n  opacity: 0 !important;\n}\n\n.panel-projets.has-project-video::after {\n  opacity: 1 !important;\n}\n\n\/* Voile clair pour conserver la lisibilit\u00e9 du titre et du menu. *\/\n.panel-projets::after {\n  content: \"\" !important;\n  position: absolute !important;\n  inset: -2px !important;\n  z-index: 1 !important;\n  pointer-events: none !important;\n  background: rgba(0, 0, 0, .36) !important;\n  opacity: 0 !important;\n  transition: opacity .78s cubic-bezier(.16, 1, .3, 1) !important;\n}\n\n.panel-projets.has-project-background::before {\n  opacity: .6 !important;\n}\n\n.panel-projets.has-project-background::after {\n  opacity: 1 !important;\n}\n\n.panel-projets > * {\n  position: relative;\n  z-index: 2;\n}\n\n\/* La croix de l'ancien panneau Projets ne sert plus. *\/\n.panel-projets > .close-panel,\n.panel-projets .projects-close {\n  display: none !important;\n}\n\n\/* =========================================================\n   LAYOUT CENTR\u00c9\n========================================================= *\/\n\n.projects-layout {\n  position: relative !important;\n  display: flex !important;\n  align-items: center !important;\n  justify-content: center !important;\n  width: 100% !important;\n  height: 100% !important;\n  min-height: 100vh !important;\n  min-height: 100dvh !important;\n  margin: 0 !important;\n  padding: 72px 28px !important;\n  box-sizing: border-box !important;\n  overflow: hidden !important;\n}\n\n\/* L'ancienne colonne reste dans Elementor pour ne rien casser,\n   mais elle n'est plus affich\u00e9e. *\/\n.project-info {\n  display: none !important;\n}\n\n.project-stack {\n  position: relative !important;\n  flex: none !important;\n  width: min(68vw, 1080px) !important;\n  max-width: 1080px !important;\n  height: 100% !important;\n  min-height: 0 !important;\n  margin: 0 auto !important;\n  padding: 0 !important;\n  overflow: visible !important;\n  z-index: 4 !important;\n}\n\n.card-stage {\n  position: relative !important;\n  width: 100% !important;\n  height: 100% !important;\n  min-height: 0 !important;\n  margin: 0 !important;\n  padding: 0 !important;\n  overflow: visible !important;\n  z-index: 4 !important;\n}\n\n\/* =========================================================\n   TITRE DYNAMIQUE \u2014 SOUS LES CARDS, BLOC CENTR\u00c9\n========================================================= *\/\n\n.project-background-title {\n  position: absolute !important;\n  left: 50% !important;\n  \/* Niveau fixe dans le viewport : tous les titres gardent exactement\n     la m\u00eame ligne de base, ind\u00e9pendamment des cartes du stack. *\/\n  top: auto !important;\n  bottom: clamp(42px, 5vh, 64px) !important;\n\n  width: auto !important;\n  max-width: calc(100vw - 56px) !important;\n  margin: 0 !important;\n  padding: 0 !important;\n\n  transform: translateX(-50%) !important;\n  z-index: 3 !important;\n\n  color: #000 !important;\n  font-family:\n    \"Akira Expanded UltraBold\",\n    \"Akira Expanded Ultra Bold\",\n    \"Akira Expanded\",\n    \"Arial Black\",\n    sans-serif !important;\n  font-size: 84px;\n  font-weight: 900 !important;\n  font-style: normal !important;\n  line-height: .9 !important;\n  letter-spacing: -.04em !important;\n  text-align: center !important;\n  text-transform: uppercase !important;\n  white-space: nowrap !important;\n\n  opacity: 1 !important;\n  pointer-events: none !important;\n  user-select: none !important;\n\n  transition:\n    opacity .28s cubic-bezier(.22, .61, .36, 1) !important;\n\n  will-change: opacity;\n}\n\n.project-background-title.is-changing {\n  opacity: 0 !important;\n  transform: translateX(-50%) !important;\n  transition:\n    opacity .18s cubic-bezier(.4, 0, 1, 1) !important;\n}\n\n\/* \u00c9vite le petit saut visible au chargement :\n   le titre n\u2019appara\u00eet qu\u2019une fois sa position r\u00e9elle calcul\u00e9e. *\/\n.project-background-title:not(.pimpe-title-positioned) {\n  opacity: 0 !important;\n  visibility: hidden !important;\n}\n\n\/* Pendant le d\u00e9ploiement, les bounding boxes des cartes \u00e9voluent encore.\n   Le titre reste donc totalement masqu\u00e9 jusqu'au calcul de la position finale. *\/\nbody.pimpe-intro-running .project-background-title,\nbody.pimpe-title-finalizing .project-background-title {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  transition: none !important;\n}\n\n\/* Sur la Home finale, le titre reste lisible m\u00eame s\u2019il croise une image. *\/\n.panel-projets.has-project-background .project-background-title {\n  color: #fff !important;\n  mix-blend-mode: difference !important;\n}\n\n\/* Le stack est l\u00e9g\u00e8rement remont\u00e9 : la card active et son titre forment\n   alors un seul bloc visuel centr\u00e9 au milieu de l\u2019\u00e9cran. *\/\n.project-stack {\n  height: 100% !important;\n  transform: translateY(var(--pimpe-composition-shift)) !important;\n}\n\n\/* =========================================================\n   CARTES\n========================================================= *\/\n\n.card-stage > .project-card {\n  position: absolute !important;\n  top: 50% !important;\n  left: 50% !important;\n\n  width: min(40vw, 740px) !important;\n  height: auto !important;\n  aspect-ratio: 41 \/ 28 !important;\n  max-width: 740px !important;\n\n  margin: 0 !important;\n  padding: 0 !important;\n  border-radius: 22px !important;\n  overflow: hidden !important;\n  background: #fff !important;\n\n  box-shadow:\n    0 9px 20px rgba(0, 0, 0, .34),\n    0 3px 7px rgba(0, 0, 0, .20) !important;\n\n  opacity: 1 !important;\n  filter: none !important;\n  transform-origin: center !important;\n\n  transition:\n    transform .68s cubic-bezier(.22, .61, .36, 1),\n    box-shadow .45s ease !important;\n\n  will-change: transform;\n  pointer-events: none !important;\n}\n\n.card-stage > .project-card.is-active-card {\n  pointer-events: auto !important;\n  cursor: pointer !important;\n}\n\n.card-stage > .project-card,\n.card-stage > .project-card > *,\n.card-stage > .project-card .elementor-widget,\n.card-stage > .project-card .elementor-widget-container,\n.card-stage > .project-card .elementor-widget-image,\n.card-stage > .project-card picture,\n.card-stage > .project-card img,\n.card-stage > .project-card video {\n  opacity: 1 !important;\n  filter: none !important;\n}\n\n.card-stage > .project-card > .e-con-inner,\n.card-stage > .project-card .elementor-widget,\n.card-stage > .project-card .elementor-widget-container,\n.card-stage > .project-card .elementor-widget-image,\n.card-stage > .project-card .elementor-widget-html,\n.card-stage > .project-card picture {\n  width: 100% !important;\n  height: 100% !important;\n  max-width: none !important;\n}\n\n.card-stage > .project-card img,\n.card-stage > .project-card video {\n  display: block !important;\n  width: 100% !important;\n  height: 100% !important;\n  max-width: none !important;\n  object-fit: cover !important;\n  object-position: center !important;\n}\n\n.card-stage > .project-card video {\n  \/*\n   * Avant que la premi\u00e8re frame soit disponible, la carte conserve son\n   * propre fond plut\u00f4t que d'afficher un rectangle noir brutal.\n   *\/\n  background: transparent !important;\n  pointer-events: none !important;\n}\n\n\/* Donn\u00e9es Elementor invisibles stock\u00e9es dans les cartes. *\/\n.card-project-title,\n.card-project-description,\n.card-project-services-title,\n.card-project-services {\n  display: none !important;\n}\n\n\n\/* =========================================================\n   TABLETTE\n========================================================= *\/\n\n@media (max-width: 1100px) and (min-width: 768px) {\n  :root {\n    --pimpe-composition-shift: -44px;\n    --pimpe-stack-half-height: clamp(300px, 34vw, 345px);\n    --pimpe-title-gap: 28px;\n  }\n\n  .project-stack {\n    width: min(82vw, 880px) !important;\n    transform: translateY(var(--pimpe-composition-shift)) !important;\n  }\n\n  .card-stage > .project-card {\n    width: min(58vw, 620px) !important;\n    height: auto !important;\n    aspect-ratio: 41 \/ 28 !important;\n  }\n\n  .project-background-title {\n    top: auto !important;\n    bottom: clamp(34px, 4.2vh, 52px) !important;\n    font-size: clamp(50px, 7.8vw, 76px) !important;\n  }\n}\n\n\/* =========================================================\n   MOBILE\n========================================================= *\/\n\n@media (max-width: 767px) {\n  :root {\n    \/*\n     * G\u00e9om\u00e9trie mobile commune \u00e0 toutes les cartes.\n     * La largeur et la hauteur sont d\u00e9finies s\u00e9par\u00e9ment afin que chaque\n     * project-card poss\u00e8de strictement les m\u00eames dimensions, quel que soit\n     * le m\u00e9dia plac\u00e9 \u00e0 l'int\u00e9rieur.\n     *\/\n    --pimpe-mobile-card-width: min(86vw, 360px);\n    --pimpe-mobile-card-height: min(58.73vw, 246px);\n\n    \/*\n     * Le stack occupe uniquement la zone entre le header et le titre.\n     * Il est donc centr\u00e9 dans l'espace utile, et non dans tout le viewport.\n     *\/\n    --pimpe-mobile-header-space: 76px;\n    --pimpe-mobile-title-space: 202px;\n\n    --pimpe-composition-shift: 0px;\n    --pimpe-stack-half-height: clamp(180px, 52vw, 220px);\n    --pimpe-title-gap: 24px;\n  }\n\n  .pimpe-global-tagline {\n    display: none !important;\n  }\n\n  .pimpe-intro-logo {\n    left: 18px !important;\n    top: 14px !important;\n    bottom: auto !important;\n    width: 95px !important;\n    transform: none !important;\n  }\n\n  .projects-layout {\n    position: relative !important;\n    width: 100% !important;\n    height: 100% !important;\n    min-height: 100vh !important;\n    min-height: 100dvh !important;\n    padding: 0 !important;\n\n    \/*\n     * Ne jamais rogner les box-shadows des cards.\n     * Le recadrage g\u00e9n\u00e9ral du viewport reste assur\u00e9 par .panel-projets.\n     *\/\n    overflow: visible !important;\n  }\n\n  .projects-layout > .e-con-inner,\n  .projects-layout .project-stack,\n  .projects-layout .project-stack > .e-con-inner,\n  .projects-layout .card-stage,\n  .projects-layout .card-stage > .e-con-inner {\n    overflow: visible !important;\n  }\n\n  \/*\n   * Le stack est positionn\u00e9 dans une zone verticale ind\u00e9pendante.\n   * Cela \u00e9vite le chevauchement du header en haut et du titre en bas.\n   *\/\n  .project-stack {\n    position: absolute !important;\n    top: var(--pimpe-mobile-header-space) !important;\n    right: 16px !important;\n    bottom: var(--pimpe-mobile-title-space) !important;\n    left: 16px !important;\n\n    width: auto !important;\n    max-width: none !important;\n    height: auto !important;\n\n    margin: 0 !important;\n    transform: none !important;\n  }\n\n  .card-stage {\n    width: 100% !important;\n    height: 100% !important;\n    overflow: visible !important;\n  }\n\n  .project-stack,\n  .project-stack > .e-con-inner,\n  .card-stage,\n  .card-stage > .e-con-inner {\n    overflow: visible !important;\n  }\n\n  \/*\n   * Goutti\u00e8re lat\u00e9rale de s\u00e9curit\u00e9 pour que le flou de l'ombre\n   * reste enti\u00e8rement visible sur Safari mobile.\n   *\/\n  .project-stack {\n    right: 28px !important;\n    left: 28px !important;\n  }\n\n  .card-stage > .project-card {\n    width: var(--pimpe-mobile-card-width) !important;\n    min-width: var(--pimpe-mobile-card-width) !important;\n    max-width: var(--pimpe-mobile-card-width) !important;\n\n    height: var(--pimpe-mobile-card-height) !important;\n    min-height: var(--pimpe-mobile-card-height) !important;\n    max-height: var(--pimpe-mobile-card-height) !important;\n\n    aspect-ratio: auto !important;\n    flex: none !important;\n\n    border-radius: 17px !important;\n    box-shadow:\n      0 7px 16px rgba(0, 0, 0, .32),\n      0 2px 5px rgba(0, 0, 0, .18) !important;\n  }\n\n  \/*\n   * Tous les wrappers Elementor et tous les m\u00e9dias remplissent exactement\n   * la m\u00eame bo\u00eete. Aucune image ne peut imposer sa propre hauteur.\n   *\/\n  .card-stage > .project-card > .e-con-inner,\n  .card-stage > .project-card .elementor-widget,\n  .card-stage > .project-card .elementor-widget-container,\n  .card-stage > .project-card .elementor-widget-image,\n  .card-stage > .project-card .elementor-widget-html,\n  .card-stage > .project-card picture,\n  .card-stage > .project-card img,\n  .card-stage > .project-card video {\n    width: 100% !important;\n    min-width: 100% !important;\n    max-width: 100% !important;\n\n    height: 100% !important;\n    min-height: 100% !important;\n    max-height: 100% !important;\n\n    margin: 0 !important;\n    padding: 0 !important;\n  }\n\n  .card-stage > .project-card img,\n  .card-stage > .project-card video {\n    object-fit: cover !important;\n    object-position: center !important;\n  }\n\n  .project-background-title {\n    top: auto !important;\n    bottom: max(\n      92px,\n      calc(env(safe-area-inset-bottom, 0px) + 70px)\n    ) !important;\n\n    \/*\n     * Une m\u00eame zone et une m\u00eame taille de police sont utilis\u00e9es pour\n     * tous les projets. Les titres compos\u00e9s de plusieurs mots sont\n     * r\u00e9partis sur deux lignes par JavaScript.\n     *\/\n    width: calc(100vw - 56px) !important;\n    max-width: calc(100vw - 56px) !important;\n\n    margin: 0 !important;\n    padding: 0 !important;\n\n    font-size: 54px !important;\n    line-height: .82 !important;\n    letter-spacing: -.05em !important;\n\n    white-space: normal !important;\n    overflow: visible !important;\n    box-sizing: border-box !important;\n  }\n\n  .project-background-title__line {\n    display: block !important;\n    width: 100% !important;\n    white-space: nowrap !important;\n    text-align: center !important;\n    overflow: visible !important;\n  }\n}\n\n\/* T\u00e9l\u00e9phones courts : composition encore un peu plus compacte. *\/\n@media (max-width: 767px) and (max-height: 700px) {\n  :root {\n    --pimpe-mobile-card-width: min(80vw, 330px);\n    --pimpe-mobile-card-height: min(54.63vw, 225px);\n    --pimpe-mobile-header-space: 68px;\n    --pimpe-mobile-title-space: 174px;\n  }\n\n  .project-background-title {\n    bottom: max(\n      70px,\n      calc(env(safe-area-inset-bottom, 0px) + 50px)\n    ) !important;\n    font-size: 48px !important;\n  }\n}\n\n\/* Respecte les pr\u00e9f\u00e9rences d'accessibilit\u00e9. *\/\n@media (prefers-reduced-motion: reduce) {\n  .project-background-title,\n  .card-stage > .project-card {\n    transition-duration: .01ms !important;\n  }\n}\n\n\n\/* =========================================================\n   TRIPLE ROULETTE \u2014 INTRO AWWWARDS ULTRA FLUIDE\n========================================================= *\/\n\n\/* Apparition g\u00e9n\u00e9rale de la Home. *\/\nbody.pimpe-roulette-pending .panel-projets,\nbody.pimpe-roulette-pending .pimpe-global-header {\n  opacity: 0 !important;\n}\n\n.panel-projets,\n.pimpe-global-header {\n  transition:\n    opacity .68s cubic-bezier(.16, 1, .3, 1) !important;\n}\n\nbody.pimpe-page-visible .panel-projets,\nbody.pimpe-page-visible .pimpe-global-header {\n  opacity: 1 !important;\n}\n\n\/* Couche noire ind\u00e9pendante plac\u00e9e au-dessus du m\u00e9dia de fond,\n   mais sous les cartes et l'index. Son opacit\u00e9 est pilot\u00e9e directement\n   par requestAnimationFrame pendant l'agrandissement final. *\/\n.pimpe-roulette-curtain {\n  position: absolute !important;\n  inset: -2px !important;\n  z-index: 1 !important;\n  background: #000 !important;\n  opacity: var(--pimpe-curtain-opacity, 1) !important;\n  visibility: visible !important;\n  pointer-events: none !important;\n  transition: none !important;\n  will-change: opacity;\n}\n\nbody.pimpe-roulette-background-visible .pimpe-roulette-curtain {\n  opacity: 0 !important;\n  visibility: hidden !important;\n}\n\n\/* Aucun blur n'est calcul\u00e9 pendant le roulement rapide. *\/\nbody.pimpe-roulette-pending .panel-projets::before,\nbody.pimpe-roulette-running .panel-projets::before,\nbody.pimpe-roulette-pending .panel-projets::after,\nbody.pimpe-roulette-running .panel-projets::after,\nbody.pimpe-roulette-pending .pimpe-video-background,\nbody.pimpe-roulette-running .pimpe-video-background {\n  opacity: 0 !important;\n  transition: none !important;\n}\n\n\/* Pendant l'agrandissement, le blur et son voile apparaissent exactement\n   sur la m\u00eame progression que les cartes. *\/\nbody.pimpe-roulette-settling\n.panel-projets.has-project-background::before {\n  opacity: var(--pimpe-bg-image-opacity, 0) !important;\n  transition: none !important;\n}\n\nbody.pimpe-roulette-settling\n.panel-projets.has-project-background::after {\n  opacity: var(--pimpe-bg-overlay-opacity, 0) !important;\n  transition: none !important;\n}\n\nbody.pimpe-roulette-settling\n.pimpe-video-background.is-visible {\n  opacity: var(--pimpe-bg-video-opacity, 0) !important;\n  transition: none !important;\n}\n\n\/* Le titre est pr\u00e9par\u00e9 pendant l'agrandissement puis appara\u00eet en fondu. *\/\nbody.pimpe-roulette-pending .project-background-title,\nbody.pimpe-roulette-running .project-background-title,\nbody.pimpe-roulette-settling:not(.pimpe-roulette-title-visible)\n.project-background-title {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  transform: translateX(-50%) !important;\n  pointer-events: none !important;\n}\n\nbody.pimpe-roulette-title-visible .project-background-title,\nbody.pimpe-roulette-revealed .project-background-title {\n  opacity: 1 !important;\n  visibility: visible !important;\n  transform: translateX(-50%) !important;\n  transition:\n    opacity .42s cubic-bezier(.16, 1, .3, 1) !important;\n}\n\n\/* L'index reste visible pendant les deux passages. *\/\nbody.pimpe-roulette-pending .pimpe-project-index {\n  opacity: 0 !important;\n  visibility: visible !important;\n}\n\nbody.pimpe-page-visible .pimpe-project-index,\nbody.pimpe-roulette-running .pimpe-project-index,\nbody.pimpe-roulette-settling .pimpe-project-index {\n  opacity: 1 !important;\n  visibility: visible !important;\n}\n\nbody.pimpe-roulette-running .pimpe-project-index,\nbody.pimpe-roulette-settling .pimpe-project-index {\n  pointer-events: none !important;\n}\n\n\/* L'index r\u00e9agit plus rapidement pendant le passage des cartes. *\/\nbody.pimpe-roulette-running .pimpe-project-index__item {\n  transition:\n    color .11s linear,\n    opacity .11s linear !important;\n}\n\n\/* Seul transform est anim\u00e9 \u00e0 chaque frame. Les ombres restent statiques,\n   ce qui \u00e9vite les repaint co\u00fbteux et am\u00e9liore nettement la fluidit\u00e9. *\/\nbody.pimpe-roulette-pending .card-stage > .project-card,\nbody.pimpe-roulette-running .card-stage > .project-card,\nbody.pimpe-roulette-settling .card-stage > .project-card {\n  transition: none !important;\n  pointer-events: none !important;\n  backface-visibility: hidden !important;\n  transform-style: preserve-3d !important;\n  will-change: transform !important;\n  box-shadow:\n    0 8px 18px rgba(0, 0, 0, .27),\n    0 2px 6px rgba(0, 0, 0, .15) !important;\n}\n\nbody.pimpe-roulette-running .project-stack,\nbody.pimpe-roulette-settling .project-stack {\n  pointer-events: none !important;\n}\n\n\/* =========================================================\n   V3.23 \u2014 INTERACTION CONTINUE MOLETTE \/ TRACKPAD \/ TACTILE\n========================================================= *\/\n\n\/*\n * Pendant le geste, les cartes sont pilot\u00e9es directement par\n * requestAnimationFrame. La transition CSS de .68s est donc neutralis\u00e9e :\n * le stack suit r\u00e9ellement le mouvement au lieu de sauter d'une carte\n * enti\u00e8re \u00e0 chaque petit scroll.\n *\/\nbody.pimpe-user-scrolling\n.card-stage > .project-card,\nbody.pimpe-user-snapping\n.card-stage > .project-card {\n  transition: none !important;\n  pointer-events: none !important;\n  backface-visibility: hidden !important;\n  -webkit-backface-visibility: hidden !important;\n  transform-style: preserve-3d !important;\n  will-change: transform !important;\n}\n\n\/*\n * Un l\u00e9ger curseur visuel sur desktop : la zone enti\u00e8re du stack peut \u00eatre\n * scroll\u00e9e, mais seule la carte parfaitement centrale redevient cliquable\n * une fois le snap termin\u00e9.\n *\/\nbody.pimpe-user-scrolling .project-stack,\nbody.pimpe-user-snapping .project-stack {\n  pointer-events: none !important;\n}\n\n\/* =========================================================\n   INDEX PROJETS \u2014 RACCOURCI CLIQUABLE \u00c0 DROITE\n========================================================= *\/\n.pimpe-project-index {\n  position: fixed !important;\n  top: 50% !important;\n  right: var(--pimpe-header-infos-right) !important;\n  z-index: 700 !important;\n  display: flex !important;\n  flex-direction: column !important;\n  align-items: flex-end !important;\n  gap: 2px !important;\n  width: min(18vw, 250px) !important;\n  margin: 0 !important;\n  padding: 0 !important;\n  transform: translateY(-50%) !important;\n  opacity: 1 !important;\n  visibility: visible !important;\n  transition: opacity .45s ease, visibility .45s ease !important;\n}\n\n.pimpe-project-index__item {\n  appearance: none !important;\n  display: block !important;\n  width: 100% !important;\n  margin: 0 !important;\n  padding: 0 !important;\n  border: 0 !important;\n  outline: none !important;\n  box-shadow: none !important;\n  -webkit-tap-highlight-color: transparent !important;\n  background: transparent !important;\n  color: rgba(255,255,255,.58) !important;\n  font: inherit !important;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif !important;\n  font-size: clamp(16px, 1.45vw, 27px) !important;\n  font-weight: 700 !important;\n  line-height: 1.05 !important;\n  letter-spacing: -.045em !important;\n  text-align: right !important;\n  text-transform: none !important;\n  white-space: nowrap !important;\n  cursor: pointer !important;\n  opacity: 1 !important;\n  transform: translateX(0) !important;\n  transition: color .28s ease, opacity .28s ease !important;\n}\n\n.pimpe-project-index__item:hover,\n.pimpe-project-index__item:focus,\n.pimpe-project-index__item:focus-visible,\n.pimpe-project-index__item:active {\n  color: rgba(255,255,255,.88) !important;\n  transform: translateX(0) !important;\n  outline: none !important;\n  box-shadow: none !important;\n  border: 0 !important;\n}\n\n.pimpe-project-index__item.is-active {\n  color: #fff !important;\n  transform: translateX(0) !important;\n  outline: none !important;\n  box-shadow: none !important;\n}\n\nbody.pimpe-intro-running:not(.pimpe-intro-reveal) .pimpe-project-index,\nhtml.pimpe-infos-open .pimpe-project-index,\nbody.pimpe-infos-open .pimpe-project-index {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n}\n\n@media (max-width: 1100px) and (min-width: 768px) {\n  .pimpe-project-index {\n    right: var(--pimpe-header-infos-right) !important;\n    width: 190px !important;\n  }\n\n  .pimpe-project-index__item {\n    font-size: clamp(14px, 2vw, 20px) !important;\n  }\n}\n\n@media (max-width: 767px) {\n  .pimpe-project-index {\n    display: none !important;\n  }\n}\n\n\n\/* =========================================================\n   TRANSITION NOIRE ENTRE LES PAGES\n========================================================= *\/\n\n.pimpe-page-fade {\n  position: fixed !important;\n  inset: 0 !important;\n  z-index: 2147483600 !important;\n  background: #000 !important;\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n  transition:\n    opacity .34s cubic-bezier(.22, .61, .36, 1),\n    visibility .34s ease !important;\n}\n\n.pimpe-page-fade.is-visible {\n  opacity: 1 !important;\n  visibility: visible !important;\n  pointer-events: auto !important;\n}\n\nhtml.pimpe-page-entering .pimpe-page-fade {\n  opacity: 1 !important;\n  visibility: visible !important;\n}\n\nhtml.pimpe-logo-transition .pimpe-page-fade {\n  transition:\n    opacity .58s cubic-bezier(.16, 1, .3, 1),\n    visibility 0s linear .58s !important;\n}\n\nhtml.pimpe-logo-transition .pimpe-page-fade.is-visible {\n  transition:\n    opacity .58s cubic-bezier(.16, 1, .3, 1),\n    visibility 0s linear 0s !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .pimpe-page-fade {\n    transition-duration: .01ms !important;\n  }\n}\n\n\n\/* Le contenu de la Home dispara\u00eet pendant le fondu noir. *\/\n.project-stack,\n.project-background-title,\n.pimpe-project-index,\n.pimpe-global-header {\n  transition:\n    opacity .28s ease,\n    transform .34s cubic-bezier(.22, .61, .36, 1) !important;\n}\n\nbody.pimpe-page-leaving .project-stack,\nbody.pimpe-page-leaving .project-background-title,\nbody.pimpe-page-leaving .pimpe-project-index,\nbody.pimpe-page-leaving .pimpe-global-header {\n  opacity: 0 !important;\n  pointer-events: none !important;\n}\n\n\/*\n * Sortie Home :\n * le titre conserve imp\u00e9rativement son centrage horizontal.\n * Il dispara\u00eet uniquement en fondu, sans glisser vers la droite.\n *\/\nbody.pimpe-page-leaving .project-stack {\n  transform: translateY(10px) !important;\n}\n\nbody.pimpe-page-leaving .project-background-title {\n  transform: translateX(-50%) !important;\n}\n\n\/* =========================================================\n   ARRIV\u00c9E SUR LA HOME \u2014 POSITION FINALE AVANT AFFICHAGE\n========================================================= *\/\n\n\/*\n * Tant que renderCards() n'a pas calcul\u00e9 la g\u00e9om\u00e9trie finale,\n * le stack, le titre et l'index restent invisibles.\n * Cela supprime l'arriv\u00e9e lat\u00e9rale visible depuis une page Projet.\n *\/\nhtml:not(.pimpe-layout-ready) .project-stack,\nhtml:not(.pimpe-layout-ready) .project-background-title,\nhtml:not(.pimpe-layout-ready) .pimpe-project-index {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n  transition: none !important;\n}\n\n\/*\n * Les cartes re\u00e7oivent leur transform final sans interpolation.\n * Les transitions ne sont r\u00e9activ\u00e9es qu'une frame plus tard,\n * lorsque .pimpe-layout-settled est ajout\u00e9.\n *\/\nhtml:not(.pimpe-layout-settled) .card-stage > .project-card {\n  transition: none !important;\n}\n\n\/*\n * Une fois la position finale calcul\u00e9e, l'ensemble appara\u00eet par\n * simple fondu, sans d\u00e9placement horizontal.\n *\/\nhtml.pimpe-layout-ready .project-stack,\nhtml.pimpe-layout-ready .project-background-title,\nhtml.pimpe-layout-ready .pimpe-project-index {\n  visibility: visible !important;\n}\n\n\/* =========================================================\n   MASKED TEXT REVEAL \u2014 CHARGEMENT DE LA HOME\n========================================================= *\/\n\n\/*\n * L'\u00e9l\u00e9ment lui-m\u00eame sert de masque : le texte d\u00e9marre l\u00e9g\u00e8rement\n * sous sa ligne finale, puis est r\u00e9v\u00e9l\u00e9 verticalement.\n *\n * La propri\u00e9t\u00e9 CSS individuelle \"translate\" est utilis\u00e9e afin de ne\n * jamais \u00e9craser les transform d\u00e9j\u00e0 n\u00e9cessaires au centrage du titre\n * ou au positionnement de l'index.\n *\/\n.pimpe-text-reveal {\n  --pimpe-text-reveal-delay: 0ms;\n  --pimpe-text-reveal-duration: 880ms;\n  --pimpe-text-reveal-y: 20px;\n\n  -webkit-clip-path: inset(100% 0 0 0);\n  clip-path: inset(100% 0 0 0);\n  translate: 0 var(--pimpe-text-reveal-y);\n  opacity: 0 !important;\n\n  transition:\n    -webkit-clip-path var(--pimpe-text-reveal-duration)\n      cubic-bezier(.16, 1, .3, 1)\n      var(--pimpe-text-reveal-delay),\n    clip-path var(--pimpe-text-reveal-duration)\n      cubic-bezier(.16, 1, .3, 1)\n      var(--pimpe-text-reveal-delay),\n    translate var(--pimpe-text-reveal-duration)\n      cubic-bezier(.16, 1, .3, 1)\n      var(--pimpe-text-reveal-delay),\n    opacity 360ms ease\n      var(--pimpe-text-reveal-delay) !important;\n\n  will-change: clip-path, translate, opacity;\n}\n\n.pimpe-text-reveal.is-visible {\n  -webkit-clip-path: inset(0 0 0 0);\n  clip-path: inset(0 0 0 0);\n  translate: 0 0;\n  opacity: 1 !important;\n}\n\n\/* Le logo .pimpe-logo utilise exactement le m\u00eame moteur g\u00e9n\u00e9rique\n   .pimpe-text-reveal que les boutons et la baseline. *\/\n\n\/* Le grand titre reste compl\u00e8tement masqu\u00e9 jusqu'au d\u00e9clenchement final. *\/\nbody .project-background-title.pimpe-text-reveal:not(.is-visible) {\n  opacity: 0 !important;\n  visibility: hidden !important;\n}\n\nbody.pimpe-roulette-title-visible\n.project-background-title.pimpe-text-reveal.is-visible,\nbody.pimpe-roulette-revealed\n.project-background-title.pimpe-text-reveal.is-visible {\n  opacity: 1 !important;\n  visibility: visible !important;\n  -webkit-clip-path: inset(0 0 0 0) !important;\n  clip-path: inset(0 0 0 0) !important;\n  translate: 0 0 !important;\n}\n\n\/* Pendant le reveal, les lignes de l'index gardent leur couleur active. *\/\n.pimpe-project-index__item.pimpe-text-reveal {\n  transform: translateX(0) !important;\n}\n\n\/* Une fois l'animation termin\u00e9e, le JavaScript retire ces classes :\n   les comportements hover et les transitions ordinaires reprennent alors. *\/\n\n@media (prefers-reduced-motion: reduce) {\n  .pimpe-text-reveal,\n  .pimpe-text-reveal.is-visible {\n    -webkit-clip-path: none !important;\n    clip-path: none !important;\n    translate: none !important;\n    transition-duration: .01ms !important;\n  }\n}\n\n\n\n\/* =========================================================\n   V3.22 \u2014 FOND BLUR IND\u00c9PENDANT DU CONTENEUR ELEMENTOR\n========================================================= *\/\n\n\/*\n * Le panneau Elementor ne porte plus lui-m\u00eame le blur.\n * Il reste uniquement le calque de contenu : cards, titre et index.\n *\/\n.panel-projets {\n  background: transparent !important;\n}\n\n.panel-projets::before,\n.panel-projets::after {\n  content: none !important;\n  display: none !important;\n}\n\n\/*\n * Ce calque est d\u00e9plac\u00e9 directement sous <body> par JavaScript.\n * Il ne peut donc pas \u00eatre recadr\u00e9 par un parent Elementor ayant\n * transform, filter, perspective ou overflow.\n *\/\n.pimpe-home-viewport-background {\n  --pimpe-visual-width: 100vw;\n  --pimpe-visual-height: 100dvh;\n  --pimpe-viewport-overscan: 180px;\n\n  position: fixed !important;\n\n  top: calc(-1 * var(--pimpe-viewport-overscan)) !important;\n  left: calc(-1 * var(--pimpe-viewport-overscan)) !important;\n\n  width:\n    calc(\n      var(--pimpe-visual-width) +\n      (2 * var(--pimpe-viewport-overscan))\n    ) !important;\n\n  height:\n    calc(\n      var(--pimpe-visual-height) +\n      (2 * var(--pimpe-viewport-overscan))\n    ) !important;\n\n  min-width: 100vw !important;\n  min-height: 100dvh !important;\n\n  z-index: 50 !important;\n\n  overflow: hidden !important;\n  pointer-events: none !important;\n\n  background: #111 !important;\n\n  isolation: isolate !important;\n  contain: strict !important;\n\n  transform: translate3d(0, 0, 0) !important;\n  -webkit-transform: translate3d(0, 0, 0) !important;\n\n  backface-visibility: hidden !important;\n  -webkit-backface-visibility: hidden !important;\n\n  will-change: width, height;\n}\n\n\/* Image active flout\u00e9e. *\/\n.pimpe-home-viewport-background::before {\n  content: \"\" !important;\n\n  position: absolute !important;\n  inset: -24% !important;\n\n  z-index: 0 !important;\n  pointer-events: none !important;\n\n  background-image:\n    var(--project-background-image) !important;\n\n  background-position: center !important;\n  background-repeat: no-repeat !important;\n  background-size: cover !important;\n\n  opacity: 0 !important;\n\n  filter:\n    blur(58px)\n    saturate(1.25) !important;\n\n  transform:\n    translate3d(0, 0, 0)\n    scale(1.20) !important;\n\n  -webkit-transform:\n    translate3d(0, 0, 0)\n    scale(1.20) !important;\n\n  backface-visibility: hidden !important;\n  -webkit-backface-visibility: hidden !important;\n\n  transition:\n    opacity .95s\n    cubic-bezier(.22, .61, .36, 1) !important;\n\n  will-change: opacity, transform;\n}\n\n\/* Voile sombre au-dessus du blur. *\/\n.pimpe-home-viewport-background::after {\n  content: \"\" !important;\n\n  position: absolute !important;\n  inset: -4px !important;\n\n  z-index: 1 !important;\n  pointer-events: none !important;\n\n  background:\n    rgba(0, 0, 0, .36) !important;\n\n  opacity: 0 !important;\n\n  transition:\n    opacity .78s\n    cubic-bezier(.16, 1, .3, 1) !important;\n}\n\n.pimpe-home-viewport-background.has-project-background::before {\n  opacity: .60 !important;\n}\n\n.pimpe-home-viewport-background.has-project-background::after {\n  opacity: 1 !important;\n}\n\n\/* Aucun calcul de blur pendant les trois tours rapides. *\/\nbody.pimpe-roulette-pending\n.pimpe-home-viewport-background::before,\nbody.pimpe-roulette-running\n.pimpe-home-viewport-background::before,\nbody.pimpe-roulette-pending\n.pimpe-home-viewport-background::after,\nbody.pimpe-roulette-running\n.pimpe-home-viewport-background::after {\n  opacity: 0 !important;\n  transition: none !important;\n}\n\n\/* Apparition synchronis\u00e9e du blur pendant la mise en place finale. *\/\nbody.pimpe-roulette-settling\n.pimpe-home-viewport-background.has-project-background::before {\n  opacity:\n    var(--pimpe-bg-image-opacity, 0) !important;\n\n  transition: none !important;\n}\n\nbody.pimpe-roulette-settling\n.pimpe-home-viewport-background.has-project-background::after {\n  opacity:\n    var(--pimpe-bg-overlay-opacity, 0) !important;\n\n  transition: none !important;\n}\n\n\/*\n * En chargement, le fond noir V3.20 reste au-dessus du blur,\n * tandis que le header reste visible.\n *\/\nbody.pimpe-home-loading\n.pimpe-home-viewport-background::before,\nbody.pimpe-home-loading\n.pimpe-home-viewport-background::after {\n  opacity: 0 !important;\n}\n\n\/* S\u00e9curit\u00e9 suppl\u00e9mentaire sur les tr\u00e8s petits \u00e9crans. *\/\n@media (max-width: 767px) {\n  .pimpe-home-viewport-background {\n    --pimpe-viewport-overscan: 220px;\n  }\n\n  .pimpe-home-viewport-background::before {\n    inset: -30% !important;\n\n    filter:\n      blur(62px)\n      saturate(1.25) !important;\n\n    transform:\n      translate3d(0, 0, 0)\n      scale(1.24) !important;\n\n    -webkit-transform:\n      translate3d(0, 0, 0)\n      scale(1.24) !important;\n  }\n}\n\n\n\n\/* =========================================================\n   V3.24 \u2014 CROSSFADE FLUIDE ENTRE LES ARRI\u00c8RE-PLANS BLUR\n========================================================= *\/\n\n\/*\n * En V3.22\/V3.23, l'URL du m\u00eame pseudo-\u00e9l\u00e9ment changeait instantan\u00e9ment.\n * Une propri\u00e9t\u00e9 background-image ne s'interpole pas : m\u00eame avec une\n * transition d'opacit\u00e9, la nouvelle image apparaissait donc comme un\n * changement \"slide par slide\".\n *\n * V3.24 utilise trois vrais calques superpos\u00e9s. Le pr\u00e9c\u00e9dent fond peut\n * continuer \u00e0 dispara\u00eetre pendant que le nouveau appara\u00eet.\n *\/\n.pimpe-home-viewport-background::before {\n  content: none !important;\n  display: none !important;\n}\n\n.pimpe-home-bg-layer {\n  position: absolute !important;\n  inset: -24% !important;\n\n  z-index: 0 !important;\n  pointer-events: none !important;\n\n  background-position: center !important;\n  background-repeat: no-repeat !important;\n  background-size: cover !important;\n\n  opacity: 0 !important;\n\n  filter:\n    blur(58px)\n    saturate(1.25) !important;\n\n  transform:\n    translate3d(0, 0, 0)\n    scale(1.20) !important;\n\n  -webkit-transform:\n    translate3d(0, 0, 0)\n    scale(1.20) !important;\n\n  backface-visibility: hidden !important;\n  -webkit-backface-visibility: hidden !important;\n\n  transition:\n    opacity 820ms cubic-bezier(.22, .61, .36, 1) !important;\n\n  will-change: opacity, transform;\n}\n\n.pimpe-home-bg-layer.is-active {\n  opacity: .60 !important;\n}\n\n\/*\n * Tr\u00e8s l\u00e9ger assombrissement au milieu du fondu.\n * Ce n'est pas un flash noir : seulement ~9 %, pour fondre les \u00e9carts\n * importants de teinte entre deux projets sans casser le mouvement.\n *\/\n.pimpe-home-bg-crossfade-shade {\n  position: absolute !important;\n  inset: -4px !important;\n\n  z-index: .8 !important;\n  pointer-events: none !important;\n\n  background: #000 !important;\n  opacity: 0 !important;\n\n  will-change: opacity;\n}\n\n.pimpe-home-bg-crossfade-shade.is-pulsing {\n  animation:\n    pimpe-bg-crossfade-shade\n    620ms\n    cubic-bezier(.22, .61, .36, 1)\n    both !important;\n}\n\n@keyframes pimpe-bg-crossfade-shade {\n  0% {\n    opacity: 0;\n  }\n\n  34% {\n    opacity: .09;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n\n\/* Pendant l'intro rapide, aucun fond de projet n'est calcul\u00e9\/affich\u00e9. *\/\nbody.pimpe-roulette-pending\n.pimpe-home-bg-layer,\nbody.pimpe-roulette-running\n.pimpe-home-bg-layer {\n  opacity: 0 !important;\n  transition: none !important;\n}\n\nbody.pimpe-roulette-pending\n.pimpe-home-bg-crossfade-shade,\nbody.pimpe-roulette-running\n.pimpe-home-bg-crossfade-shade {\n  opacity: 0 !important;\n  animation: none !important;\n}\n\n\/*\n * Au settle final de l'intro, le premier blur conserve exactement la\n * progression d\u00e9j\u00e0 pilot\u00e9e par --pimpe-bg-image-opacity.\n *\/\nbody.pimpe-roulette-settling\n.pimpe-home-bg-layer.is-active {\n  opacity:\n    var(--pimpe-bg-image-opacity, 0) !important;\n\n  transition: none !important;\n}\n\n\/* Le loading noir reste prioritaire. *\/\nbody.pimpe-home-loading\n.pimpe-home-bg-layer,\nbody.pimpe-home-loading\n.pimpe-home-bg-crossfade-shade {\n  opacity: 0 !important;\n  animation: none !important;\n}\n\n\/* Le voile permanent ::after reste au-dessus de tous les blur. *\/\n.pimpe-home-viewport-background::after {\n  z-index: 1 !important;\n}\n\n\/* Mobile : m\u00eame overscan que le correctif viewport V3.22. *\/\n@media (max-width: 767px) {\n  .pimpe-home-bg-layer {\n    inset: -30% !important;\n\n    filter:\n      blur(62px)\n      saturate(1.25) !important;\n\n    transform:\n      translate3d(0, 0, 0)\n      scale(1.24) !important;\n\n    -webkit-transform:\n      translate3d(0, 0, 0)\n      scale(1.24) !important;\n\n    \/*\n     * Un peu plus long au doigt : le fond \"respire\" derri\u00e8re le stack\n     * alors que les cartes suivent imm\u00e9diatement le geste.\n     *\/\n    transition:\n      opacity 900ms cubic-bezier(.22, .61, .36, 1) !important;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .pimpe-home-bg-layer {\n    transition-duration: .01ms !important;\n  }\n\n  .pimpe-home-bg-crossfade-shade.is-pulsing {\n    animation-duration: .01ms !important;\n  }\n}\n\n<\/style>\n\n<div\n  class=\"pimpe-home-loading-screen\"\n  aria-hidden=\"true\"\n><\/div>\n\n<script>\n(function () {\n  \"use strict\";\n\n  let initialized = false;\n\n  function initializePimpeHomeV2() {\n    if (initialized) return;\n\n    const panelProjects = document.querySelector(\".panel-projets\");\n    const projectsLayout = document.querySelector(\".projects-layout\");\n    const cardStage = document.querySelector(\".card-stage\");\n    const homeLoadingScreen = document.querySelector(\n      \".pimpe-home-loading-screen\"\n    );\n    const cards = Array.from(\n      document.querySelectorAll(\".card-stage > .project-card\")\n    );\n\n    if (!panelProjects || !projectsLayout || !cardStage || !cards.length) {\n      window.setTimeout(initializePimpeHomeV2, 250);\n      return;\n    }\n\n    initialized = true;\n\n    function createViewportBackground() {\n      let background = document.querySelector(\n        \".pimpe-home-viewport-background\"\n      );\n\n      if (!background) {\n        background = document.createElement(\"div\");\n        background.className =\n          \"pimpe-home-viewport-background\";\n\n        background.setAttribute(\n          \"aria-hidden\",\n          \"true\"\n        );\n      }\n\n      \/*\n       * Point essentiel de la V3.22 :\n       * le fond est r\u00e9ellement d\u00e9plac\u00e9 sous <body>, hors de tout\n       * conteneur Elementor susceptible de recadrer un fixed.\n       *\/\n      if (background.parentElement !== document.body) {\n        document.body.prepend(background);\n      }\n\n      return background;\n    }\n\n    function createViewportBackgroundLayers(background) {\n      const existingLayers = Array.from(\n        background.querySelectorAll(\n          \".pimpe-home-bg-layer\"\n        )\n      );\n\n      \/*\n       * Trois couches plut\u00f4t que deux :\n       * si l'utilisateur fait d\u00e9filer rapidement plusieurs projets,\n       * un ancien blur peut finir son fade-out pendant que la nouvelle\n       * paire commence d\u00e9j\u00e0 son crossfade.\n       *\/\n      while (existingLayers.length < 3) {\n        const layer =\n          document.createElement(\"div\");\n\n        layer.className =\n          \"pimpe-home-bg-layer\";\n\n        layer.setAttribute(\n          \"aria-hidden\",\n          \"true\"\n        );\n\n        background.prepend(layer);\n        existingLayers.push(layer);\n      }\n\n      existingLayers\n        .slice(3)\n        .forEach(function (layer) {\n          layer.remove();\n        });\n\n      let shade =\n        background.querySelector(\n          \".pimpe-home-bg-crossfade-shade\"\n        );\n\n      if (!shade) {\n        shade =\n          document.createElement(\"div\");\n\n        shade.className =\n          \"pimpe-home-bg-crossfade-shade\";\n\n        shade.setAttribute(\n          \"aria-hidden\",\n          \"true\"\n        );\n\n        background.appendChild(shade);\n      }\n\n      return {\n        layers: existingLayers.slice(0, 3),\n        shade: shade\n      };\n    }\n\n    const viewportBackground =\n      createViewportBackground();\n\n    const viewportBackgroundParts =\n      createViewportBackgroundLayers(\n        viewportBackground\n      );\n\n    const viewportBackgroundLayers =\n      viewportBackgroundParts.layers;\n\n    const viewportBackgroundShade =\n      viewportBackgroundParts.shade;\n\n    let activeViewportBackgroundLayer = -1;\n    let currentViewportBackgroundUrl = \"\";\n    let backgroundShadeTimer = null;\n\n    const openHome = document.querySelector(\".pimpe-global-home\");\n\n    \/* Sur la Home, le widget Elementor HOME indique la page active. *\/\n    if (openHome && openHome.dataset.pimpeHomeReady !== \"true\") {\n      openHome.dataset.pimpeHomeReady = \"true\";\n      openHome.setAttribute(\"role\", \"link\");\n      openHome.setAttribute(\"tabindex\", \"0\");\n      openHome.setAttribute(\"aria-current\", \"page\");\n      openHome.setAttribute(\"aria-label\", \"Page d\u2019accueil\");\n\n      function refreshHomeFromMenu(event) {\n        event.preventDefault();\n        try { sessionStorage.setItem(\"pimpeLogoTransition\", \"1\"); } catch (error) {}\n        document.documentElement.classList.add(\"pimpe-logo-transition\");\n        window.pimpeNavigateWithBlackFade(getLocalizedHomeUrl());\n      }\n\n      openHome.addEventListener(\"click\", refreshHomeFromMenu);\n      openHome.addEventListener(\"keydown\", function (event) {\n        if (event.key === \"Enter\" || event.key === \" \") refreshHomeFromMenu(event);\n      });\n    }\n\n    let activeStep = 0;\n    let wheelAccumulator = 0;\n    let wheelLocked = false;\n\n    \/*\n     * V3.23 :\n     * - activeStep reste l'index entier de la carte r\u00e9ellement s\u00e9lectionn\u00e9e ;\n     * - interactionCurrentStep peut prendre des valeurs fractionnaires\n     *   (ex. 2.34), ce qui permet aux cartes de suivre le geste en continu ;\n     * - interactionTargetStep re\u00e7oit les deltas de molette \/ trackpad.\n     *\/\n    let interactionCurrentStep = 0;\n    let interactionTargetStep = 0;\n    let interactionFrame = null;\n    let interactionSnapTimer = null;\n    let interactionActive = false;\n    let interactionSnapping = false;\n    let interactionNearestIndex = -1;\n\n    let touchStartY = null;\n    let touchStartX = null;\n    let touchStartStep = 0;\n    let touchLastY = null;\n    let touchLastTime = 0;\n    let touchVelocityStepsPerMs = 0;\n\n    \/*\n     * Mobile tap fix:\n     * on ne d\u00e9marre PLUS le moteur de scroll d\u00e8s touchstart.\n     * Un doigt pos\u00e9 sur une carte reste d'abord un clic potentiel.\n     *\/\n    let touchGestureStarted = false;\n    let touchCandidateCard = null;\n    const TOUCH_DRAG_THRESHOLD = 14;\n\n    let currentTitle = \"\";\n    let titleChangeTimer = null;\n    let titleFadeFrame = null;\n    let backgroundVideoRoot = null;\n    let backgroundVideoLayers = [];\n    let activeBackgroundLayer = 0;\n    let backgroundVideoToken = 0;\n    let rouletteRunning = false;\n    let rouletteStarted = false;\n    let rouletteLaunchScheduled = false;\n    let loadingScreenReleased = false;\n    let rouletteNearestIndex = -1;\n    let viewportBackgroundFrame = null;\n\n    function syncViewportBackgroundSize() {\n      viewportBackgroundFrame = null;\n\n      const visualViewport =\n        window.visualViewport;\n\n      const width = Math.ceil(\n        visualViewport &&\n        Number.isFinite(visualViewport.width)\n          ? visualViewport.width\n          : window.innerWidth\n      );\n\n      const height = Math.ceil(\n        visualViewport &&\n        Number.isFinite(visualViewport.height)\n          ? visualViewport.height\n          : window.innerHeight\n      );\n\n      viewportBackground.style.setProperty(\n        \"--pimpe-visual-width\",\n        width + \"px\"\n      );\n\n      viewportBackground.style.setProperty(\n        \"--pimpe-visual-height\",\n        height + \"px\"\n      );\n\n      \/*\n       * Cette alternance imperceptible oblige WebKit \u00e0 repeindre\n       * le calque filtr\u00e9 apr\u00e8s un changement de barre d\u2019adresse.\n       *\/\n      viewportBackground.style.setProperty(\n        \"--pimpe-background-repaint\",\n        String(Date.now() % 2)\n      );\n    }\n\n    function requestViewportBackgroundSync() {\n      if (viewportBackgroundFrame !== null) {\n        return;\n      }\n\n      viewportBackgroundFrame =\n        window.requestAnimationFrame(\n          function () {\n            window.requestAnimationFrame(\n              syncViewportBackgroundSize\n            );\n          }\n        );\n    }\n\n    requestViewportBackgroundSync();\n\n    window.addEventListener(\n      \"resize\",\n      requestViewportBackgroundSync,\n      { passive: true }\n    );\n\n    window.addEventListener(\n      \"orientationchange\",\n      requestViewportBackgroundSync,\n      { passive: true }\n    );\n\n    window.addEventListener(\n      \"pageshow\",\n      requestViewportBackgroundSync\n    );\n\n    if (window.visualViewport) {\n      window.visualViewport.addEventListener(\n        \"resize\",\n        requestViewportBackgroundSync,\n        { passive: true }\n      );\n\n      window.visualViewport.addEventListener(\n        \"scroll\",\n        requestViewportBackgroundSync,\n        { passive: true }\n      );\n    }\n\n    \/* Elementor peut r\u00e9ex\u00e9cuter le widget dans l\u2019\u00e9diteur.\n       On supprime donc tout ancien titre avant d\u2019en cr\u00e9er un nouveau. *\/\n    projectsLayout\n      .querySelectorAll(\".project-background-title\")\n      .forEach(function (title) {\n        title.remove();\n      });\n\n    const backgroundTitle = document.createElement(\"div\");\n    backgroundTitle.className = \"project-background-title\";\n    backgroundTitle.setAttribute(\"aria-hidden\", \"true\");\n    backgroundTitle.classList.remove(\"pimpe-title-positioned\");\n    projectsLayout.prepend(backgroundTitle);\n\n    \/* Index cliquable g\u00e9n\u00e9r\u00e9 automatiquement depuis les titres des cartes. *\/\n    projectsLayout\n      .querySelectorAll(\".pimpe-project-index\")\n      .forEach(function (indexElement) {\n        indexElement.remove();\n      });\n\n    const projectIndex = document.createElement(\"nav\");\n    projectIndex.className = \"pimpe-project-index\";\n    projectIndex.setAttribute(\"aria-label\", \"Acc\u00e8s rapide aux projets\");\n\n    const projectIndexItems = cards.map(function (card, index) {\n      const button = document.createElement(\"button\");\n      button.type = \"button\";\n      button.className = \"pimpe-project-index__item\";\n      button.textContent = getCardTitle(card);\n      button.setAttribute(\"aria-label\", \"Afficher le projet \" + getCardTitle(card));\n      button.addEventListener(\"click\", function () {\n        if (rouletteRunning) return;\n        if (document.documentElement.classList.contains(\"pimpe-infos-open\")) return;\n        activeStep = index;\n\n        interactionCurrentStep =\n          activeStep;\n\n        interactionTargetStep =\n          activeStep;\n\n        interactionNearestIndex =\n          modulo(\n            activeStep,\n            cards.length\n          );\n\n        interactionActive = false;\n        interactionSnapping = false;\n\n        wheelAccumulator = 0;\n        wheelLocked = true;\n        renderCards();\n        window.setTimeout(function () {\n          wheelLocked = false;\n        }, 520);\n      });\n      projectIndex.appendChild(button);\n      return button;\n    });\n\n    projectsLayout.appendChild(projectIndex);\n\n    \/* =====================================================\n       MASKED TEXT REVEAL\n    ===================================================== *\/\n\n    const headerTextRevealEntries = [];\n    let titleTextRevealEntry = null;\n\n    function prepareTextRevealElement(element, options) {\n      if (!element) return null;\n\n      const settings = options || {};\n      const delay = Number.isFinite(settings.delay) ? settings.delay : 0;\n      const duration =\n        Number.isFinite(settings.duration) ? settings.duration : 880;\n      const translateY =\n        Number.isFinite(settings.translateY) ? settings.translateY : 20;\n\n      element.classList.remove(\"is-visible\");\n      element.classList.add(\"pimpe-text-reveal\");\n\n      element.style.setProperty(\n        \"--pimpe-text-reveal-delay\",\n        delay + \"ms\"\n      );\n      element.style.setProperty(\n        \"--pimpe-text-reveal-duration\",\n        duration + \"ms\"\n      );\n      element.style.setProperty(\n        \"--pimpe-text-reveal-y\",\n        translateY + \"px\"\n      );\n\n      return {\n        element: element,\n        delay: delay,\n        duration: duration\n      };\n    }\n\n    function cleanTextRevealEntry(entry) {\n      if (!entry || !entry.element) return;\n\n      entry.element.classList.remove(\n        \"pimpe-text-reveal\",\n        \"is-visible\"\n      );\n\n      entry.element.style.removeProperty(\n        \"--pimpe-text-reveal-delay\"\n      );\n      entry.element.style.removeProperty(\n        \"--pimpe-text-reveal-duration\"\n      );\n      entry.element.style.removeProperty(\n        \"--pimpe-text-reveal-y\"\n      );\n    }\n\n    function revealTextEntries(entries) {\n      const validEntries = entries.filter(function (entry) {\n        return Boolean(entry && entry.element);\n      });\n\n      if (!validEntries.length) return;\n\n      window.requestAnimationFrame(function () {\n        validEntries.forEach(function (entry) {\n          entry.element.classList.add(\"is-visible\");\n        });\n      });\n\n      const longestAnimation = validEntries.reduce(\n        function (maximum, entry) {\n          return Math.max(\n            maximum,\n            entry.delay + entry.duration\n          );\n        },\n        0\n      );\n\n      \/*\n       * Le nettoyage restaure ensuite les hover et transitions Elementor\n       * d'origine, sans modifier l'apparence obtenue.\n       *\/\n      window.setTimeout(function () {\n        validEntries.forEach(cleanTextRevealEntry);\n      }, longestAnimation + 180);\n    }\n\n    function prepareHomeTextReveals() {\n      const headerGroups = [\n        {\n          selector: \".pimpe-logo\",\n          delay: 20,\n          duration: 900,\n          translateY: 18\n        },\n        {\n          selector: \".pimpe-global-home\",\n          delay: 105\n        },\n        {\n          selector: \".pimpe-global-tagline\",\n          delay: 180\n        },\n        {\n          selector: \".pimpe-global-infos\",\n          delay: 255\n        }\n      ];\n\n      headerGroups.forEach(function (group) {\n        document\n          .querySelectorAll(group.selector)\n          .forEach(function (element) {\n            const entry = prepareTextRevealElement(\n              element,\n              {\n                delay: group.delay,\n                duration: Number.isFinite(group.duration)\n                  ? group.duration\n                  : 820,\n                translateY: Number.isFinite(group.translateY)\n                  ? group.translateY\n                  : 16\n              }\n            );\n\n            if (entry) headerTextRevealEntries.push(entry);\n          });\n      });\n\n      projectIndexItems.forEach(function (item, index) {\n        const entry = prepareTextRevealElement(\n          item,\n          {\n            delay: 285 + (index * 38),\n            duration: 760,\n            translateY: 15\n          }\n        );\n\n        if (entry) headerTextRevealEntries.push(entry);\n      });\n\n      titleTextRevealEntry = prepareTextRevealElement(\n        backgroundTitle,\n        {\n          delay: 0,\n          duration: 980,\n          translateY: 28\n        }\n      );\n    }\n\n    function revealHeaderAndIndexText() {\n      revealTextEntries(headerTextRevealEntries);\n    }\n\n    function revealFinalProjectTitle() {\n      if (!titleTextRevealEntry) return;\n      revealTextEntries([titleTextRevealEntry]);\n    }\n\n    prepareHomeTextReveals();\n\n    function showHeaderDuringImageLoading() {\n      const headerSelectors = [\n        \".pimpe-logo\",\n        \".pimpe-global-home\",\n        \".pimpe-global-tagline\",\n        \".pimpe-global-infos\"\n      ];\n\n      headerSelectors.forEach(function (selector) {\n        document\n          .querySelectorAll(selector)\n          .forEach(function (element) {\n            element.classList.remove(\n              \"pimpe-text-reveal\",\n              \"is-visible\"\n            );\n\n            element.style.removeProperty(\n              \"--pimpe-text-reveal-delay\"\n            );\n\n            element.style.removeProperty(\n              \"--pimpe-text-reveal-duration\"\n            );\n\n            element.style.removeProperty(\n              \"--pimpe-text-reveal-y\"\n            );\n          });\n      });\n    }\n\n    showHeaderDuringImageLoading();\n\n    \/* Rend le m\u00eame catalogue disponible aux pages Projet. *\/\n    saveProjectCatalog();\n\n    document.documentElement.style.setProperty(\n      \"--pimpe-logo-progress\",\n      \"1\"\n    );\n\n    document.body.classList.add(\n      \"pimpe-home-ready\",\n      \"pimpe-home-loading\",\n      \"pimpe-roulette-pending\"\n    );\n\n    primeCardImages();\n    prepareCardVideos();\n    prepareCardNavigation();\n    createVideoBackground();\n    createRouletteCurtain();\n\n    function revealStableLayout() {\n      \/* Les cartes sont d\u00e9j\u00e0 calcul\u00e9es par renderCards(). On attend deux\n         peintures compl\u00e8tes avant de r\u00e9v\u00e9ler le stack, puis encore une frame\n         avant de r\u00e9activer les transitions. Cela supprime le mouvement venant\n         de la droite visible lors d\u2019un refresh ou d\u2019un clic sur le logo. *\/\n      document.documentElement.classList.remove(\"pimpe-layout-settled\");\n\n      window.requestAnimationFrame(function () {\n        renderCards({ immediateTitle: true });\n\n        window.requestAnimationFrame(function () {\n          document.documentElement.classList.add(\"pimpe-layout-ready\");\n\n          window.requestAnimationFrame(function () {\n            document.documentElement.classList.add(\"pimpe-layout-settled\");\n          });\n        });\n      });\n    }\n\n    \/* Le logo r\u00e9duit agit comme un lien Home. Pendant l\u2019intro il reste\n       inactif ; une fois en haut, un clic recharge proprement la Home. *\/\n    const heroLogo = document.querySelector(\".pimpe-global-logo\");\n\n    if (heroLogo && !heroLogo.dataset.pimpeHomeLinkReady) {\n      heroLogo.dataset.pimpeHomeLinkReady = \"true\";\n      heroLogo.setAttribute(\"role\", \"link\");\n      heroLogo.setAttribute(\"aria-label\", \"Revenir \u00e0 la page d\u2019accueil\");\n      heroLogo.setAttribute(\"tabindex\", \"0\");\n\n      const goHome = function (event) {\n        event.preventDefault();\n        try { sessionStorage.setItem(\"pimpeLogoTransition\", \"1\"); } catch (error) {}\n        document.documentElement.classList.add(\"pimpe-logo-transition\");\n        window.pimpeNavigateWithBlackFade(getLocalizedHomeUrl());\n      };\n\n      heroLogo.addEventListener(\"click\", goHome);\n      heroLogo.addEventListener(\"keydown\", function (event) {\n        if (event.key === \"Enter\" || event.key === \" \") goHome(event);\n      });\n    }\n\n    \/* =====================================================\n       OUTILS\n    ===================================================== *\/\n\n    function modulo(number, divisor) {\n      return ((number % divisor) + divisor) % divisor;\n    }\n\n    function getCircularOffset(cardIndex, currentIndex, totalCards) {\n      let offset = cardIndex - currentIndex;\n      const half = totalCards \/ 2;\n\n      if (offset >= half) offset -= totalCards;\n      if (offset < -half) offset += totalCards;\n\n      return offset;\n    }\n\n    function extractText(element) {\n      if (!element) return \"\";\n\n      const heading = element.querySelector(\".elementor-heading-title\");\n      if (heading) return heading.textContent.trim();\n\n      const editor = element.querySelector(\".elementor-text-editor\");\n      if (editor) return editor.textContent.trim();\n\n      const container = element.querySelector(\".elementor-widget-container\");\n      if (container) return container.textContent.trim();\n\n      return element.textContent.trim();\n    }\n\n    function getCardTitle(card) {\n      if (!card) return \"\";\n\n      const titleSource = card.querySelector(\".card-project-title\");\n      const storedTitle = extractText(titleSource);\n\n      if (storedTitle) return storedTitle;\n\n      return (\n        card.getAttribute(\"data-project-title\") ||\n        card.getAttribute(\"aria-label\") ||\n        \"PROJECT\"\n      ).trim();\n    }\n\n    \/* =====================================================\n       TRANSLATEPRESS \u2014 CONSERVE LA LANGUE ACTIVE DANS LES URLS PROJET\n    ===================================================== *\/\n\n    function getPimpeCurrentLanguage() {\n      const path = window.location.pathname || \"\/\";\n\n      return \/^\\\/en(?:\\\/|$)\/i.test(path)\n        ? \"en\"\n        : \"fr\";\n    }\n\n    function localizePimpeInternalUrl(rawUrl) {\n      if (!rawUrl) return \"\";\n\n      let target;\n\n      try {\n        target = new URL(\n          rawUrl,\n          window.location.origin\n        );\n      } catch (error) {\n        return rawUrl;\n      }\n\n      \/* Ne touche jamais aux liens externes. *\/\n      if (\n        target.origin !==\n        window.location.origin\n      ) {\n        return rawUrl;\n      }\n\n      const currentLanguage =\n        getPimpeCurrentLanguage();\n\n      let pathname =\n        target.pathname || \"\/\";\n\n      \/*\n       * Version EN :\n       * TranslatePress attend \/en\/... .\n       *\n       * Version FR :\n       * la langue par d\u00e9faut reste sans pr\u00e9fixe.\n       *\/\n      if (currentLanguage === \"en\") {\n        if (\n          !\/^\\\/en(?:\\\/|$)\/i.test(pathname)\n        ) {\n          pathname =\n            \"\/en\" +\n            (\n              pathname.startsWith(\"\/\")\n                ? pathname\n                : \"\/\" + pathname\n            );\n        }\n      } else {\n        pathname =\n          pathname.replace(\n            \/^\\\/en(?=\\\/|$)\/i,\n            \"\"\n          );\n\n        if (!pathname) {\n          pathname = \"\/\";\n        }\n\n        if (!pathname.startsWith(\"\/\")) {\n          pathname = \"\/\" + pathname;\n        }\n      }\n\n      \/*\n       * Nettoyage des doubles slash \u00e9ventuels,\n       * sans toucher au protocole puisque seul pathname est trait\u00e9.\n       *\/\n      pathname =\n        pathname.replace(\/\\\/{2,}\/g, \"\/\");\n\n      target.pathname = pathname;\n\n      return target.href;\n    }\n\n    function getLocalizedHomeUrl() {\n      return localizePimpeInternalUrl(\n        window.location.origin + \"\/\"\n      );\n    }\n\n    \/* URL associ\u00e9e \u00e0 une carte.\n       Priorit\u00e9 : attribut data-project-url, puis lien Elementor pr\u00e9sent\n       sur ou dans la carte. *\/\n    function getCardUrl(card) {\n      if (!card) return \"\";\n\n      const explicitUrl =\n        card.getAttribute(\"data-project-url\") ||\n        card.dataset.projectUrl ||\n        \"\";\n\n      if (explicitUrl) return localizePimpeInternalUrl(explicitUrl.trim());\n\n      if (card.matches(\"a[href]\")) {\n        return localizePimpeInternalUrl(card.getAttribute(\"href\") || card.href || \"\");\n      }\n\n      const innerLink = card.querySelector(\"a[href]\");\n      if (innerLink) {\n        return localizePimpeInternalUrl(innerLink.getAttribute(\"href\") || innerLink.href || \"\");\n      }\n\n      \/* Certains conteneurs Elementor stockent le lien dans data-settings. *\/\n      const settingsNodes = [card].concat(\n        Array.from(card.querySelectorAll(\"[data-settings]\"))\n      );\n\n      for (const node of settingsNodes) {\n        const rawSettings = node.getAttribute(\"data-settings\");\n        if (!rawSettings) continue;\n\n        try {\n          const settings = JSON.parse(rawSettings);\n          const candidate =\n            settings?.link?.url ||\n            settings?.__globals__?.link ||\n            settings?.url ||\n            \"\";\n\n          if (typeof candidate === \"string\" && candidate.trim()) {\n            return localizePimpeInternalUrl(candidate.trim());\n          }\n        } catch (error) {}\n      }\n\n      return \"\";\n    }\n\n    function navigateToCardProject(card) {\n      if (!card) return;\n      if (rouletteRunning) return;\n      if (!card.classList.contains(\"is-active-card\")) return;\n\n      if (\n        document.documentElement.classList.contains(\n          \"pimpe-infos-open\"\n        )\n      ) {\n        return;\n      }\n\n      const url =\n        localizePimpeInternalUrl(\n          getCardUrl(card)\n        );\n\n      if (!url) return;\n\n      if (\n        typeof window.pimpeNavigateWithBlackFade ===\n        \"function\"\n      ) {\n        window.pimpeNavigateWithBlackFade(url);\n        return;\n      }\n\n      window.location.href = url;\n    }\n\n    function prepareCardNavigation() {\n      cards.forEach(function (card) {\n        if (\n          card.dataset.pimpeCardNavigationReady ===\n          \"true\"\n        ) {\n          return;\n        }\n\n        card.dataset.pimpeCardNavigationReady = \"true\";\n        card.setAttribute(\"role\", \"link\");\n        card.setAttribute(\"tabindex\", \"-1\");\n\n        card.addEventListener(\"click\", function (event) {\n          \/*\n           * Si Elementor a d\u00e9j\u00e0 g\u00e9n\u00e9r\u00e9 un v\u00e9ritable lien, le gestionnaire\n           * global de transition s'en charge. Sinon, le conteneur lui-m\u00eame\n           * devient le lien du projet.\n           *\/\n          if (event.target.closest(\"a[href]\")) return;\n\n          event.preventDefault();\n          navigateToCardProject(card);\n        });\n\n        card.addEventListener(\"keydown\", function (event) {\n          if (\n            event.key !== \"Enter\" &&\n            event.key !== \" \"\n          ) {\n            return;\n          }\n\n          event.preventDefault();\n          navigateToCardProject(card);\n        });\n      });\n    }\n\n    function saveProjectCatalog() {\n      const catalog = cards\n        .map(function (card) {\n          return {\n            title: getCardTitle(card),\n            url: localizePimpeInternalUrl(\n              getCardUrl(card)\n            ),\n            language: getPimpeCurrentLanguage()\n          };\n        })\n        .filter(function (project) {\n          return project.title && project.url;\n        });\n\n      if (!catalog.length) return;\n\n      try {\n        window.localStorage.setItem(\n          \"pimpeProjectCatalog\",\n          JSON.stringify(catalog)\n        );\n      } catch (error) {}\n    }\n\n    function getCardImage(card) {\n      if (!card) return \"\";\n\n      if (card.dataset.projectBackground) {\n        return card.dataset.projectBackground;\n      }\n\n      const image = card.querySelector(\"img\");\n      if (!image) return \"\";\n\n      return (\n        image.currentSrc ||\n        image.src ||\n        image.getAttribute(\"data-src\") ||\n        \"\"\n      );\n    }\n\n    function getCardVideo(card) {\n      return card ? card.querySelector(\"video\") : null;\n    }\n\n    function getVideoSource(video) {\n      if (!video) return \"\";\n\n      const sourceElement = video.querySelector(\"source\");\n\n      return (\n        video.currentSrc ||\n        video.getAttribute(\"src\") ||\n        video.getAttribute(\"data-src\") ||\n        video.getAttribute(\"data-lazy-src\") ||\n        video.src ||\n        sourceElement?.currentSrc ||\n        sourceElement?.getAttribute(\"src\") ||\n        sourceElement?.getAttribute(\"data-src\") ||\n        sourceElement?.getAttribute(\"data-lazy-src\") ||\n        sourceElement?.src ||\n        \"\"\n      );\n    }\n\n    function prepareCardVideos() {\n      cards.forEach(function (card) {\n        const video = getCardVideo(card);\n        if (!video) return;\n\n        video.muted = true;\n        video.defaultMuted = true;\n        video.loop = true;\n        video.playsInline = true;\n        video.preload = \"auto\";\n\n        video.setAttribute(\"muted\", \"\");\n        video.setAttribute(\"loop\", \"\");\n        video.setAttribute(\"playsinline\", \"\");\n        video.setAttribute(\"preload\", \"auto\");\n        video.removeAttribute(\"controls\");\n\n        \/*\n         * Force le navigateur \u00e0 demander le m\u00e9dia d\u00e8s l'initialisation,\n         * avant que la card vid\u00e9o n'arrive au centre de la roulette.\n         *\/\n        try {\n          video.load();\n        } catch (error) {}\n      });\n    }\n\n    function playCardVideosForIntro() {\n      cards.forEach(function (card) {\n        const video = getCardVideo(card);\n        if (!video) return;\n\n        const startPlayback = function () {\n          const playPromise = video.play();\n\n          if (\n            playPromise &&\n            typeof playPromise.catch === \"function\"\n          ) {\n            playPromise.catch(function () {});\n          }\n        };\n\n        if (video.readyState >= 2) {\n          startPlayback();\n          return;\n        }\n\n        video.addEventListener(\n          \"loadeddata\",\n          startPlayback,\n          { once: true }\n        );\n\n        video.addEventListener(\n          \"canplay\",\n          startPlayback,\n          { once: true }\n        );\n\n        try {\n          video.load();\n        } catch (error) {}\n      });\n    }\n\n    function createRouletteCurtain() {\n      let curtain = panelProjects.querySelector(\".pimpe-roulette-curtain\");\n\n      if (!curtain) {\n        curtain = document.createElement(\"div\");\n        curtain.className = \"pimpe-roulette-curtain\";\n        curtain.setAttribute(\"aria-hidden\", \"true\");\n        panelProjects.appendChild(curtain);\n      }\n\n      return curtain;\n    }\n\n    function createVideoBackground() {\n      if (backgroundVideoRoot) return backgroundVideoRoot;\n\n      backgroundVideoRoot = document.createElement(\"div\");\n      backgroundVideoRoot.className = \"pimpe-video-background\";\n      backgroundVideoRoot.setAttribute(\"aria-hidden\", \"true\");\n\n      backgroundVideoLayers = [0, 1].map(function () {\n        const layer = document.createElement(\"div\");\n        const video = document.createElement(\"video\");\n\n        layer.className = \"pimpe-video-background__layer\";\n        video.muted = true;\n        video.defaultMuted = true;\n        video.loop = true;\n        video.playsInline = true;\n        video.preload = \"auto\";\n        video.setAttribute(\"muted\", \"\");\n        video.setAttribute(\"loop\", \"\");\n        video.setAttribute(\"playsinline\", \"\");\n\n        layer.appendChild(video);\n        backgroundVideoRoot.appendChild(layer);\n\n        return { layer: layer, video: video, source: \"\" };\n      });\n\n      panelProjects.prepend(backgroundVideoRoot);\n      return backgroundVideoRoot;\n    }\n\n    function pauseInactiveCardVideos(activeCard) {\n      cards.forEach(function (card) {\n        const video = getCardVideo(card);\n        if (!video) return;\n\n        if (card === activeCard) {\n          const promise = video.play();\n          if (promise && typeof promise.catch === \"function\") {\n            promise.catch(function () {});\n          }\n        } else {\n          video.pause();\n        }\n      });\n    }\n\n    function scheduleActiveMediaSync() {\n      const sync = function () {\n        const currentIndex = modulo(activeStep, cards.length);\n        const activeCard = cards[currentIndex];\n\n        pauseInactiveCardVideos(activeCard);\n        updateBackground(activeCard);\n      };\n\n      \/* Deux frames : la premi\u00e8re applique la nouvelle carte active,\n         la seconde laisse Elementor et le navigateur actualiser la vid\u00e9o. *\/\n      window.requestAnimationFrame(function () {\n        window.requestAnimationFrame(sync);\n      });\n\n      \/* S\u00e9curit\u00e9s pour les vid\u00e9os charg\u00e9es paresseusement. *\/\n      [60, 160, 360].forEach(function (delay) {\n        window.setTimeout(sync, delay);\n      });\n    }\n\n    function hideVideoBackground() {\n      panelProjects.classList.remove(\"has-project-video\");\n      viewportBackground.classList.remove(\"has-project-video\");\n\n      if (backgroundVideoRoot) {\n        backgroundVideoRoot.classList.remove(\"is-visible\");\n      }\n\n      backgroundVideoLayers.forEach(function (entry) {\n        entry.layer.classList.remove(\"is-active\");\n        entry.video.pause();\n      });\n    }\n\n    function updateVideoBackground(card) {\n      const cardVideo = getCardVideo(card);\n      const source = getVideoSource(cardVideo);\n\n      if (!cardVideo) {\n        hideVideoBackground();\n        return false;\n      }\n\n      if (!source) {\n        \/* Elementor peut injecter l'URL r\u00e9elle une frame plus tard. *\/\n        window.setTimeout(function () {\n          const currentIndex = modulo(activeStep, cards.length);\n          if (cards[currentIndex] === card) updateBackground(card);\n        }, 80);\n        return true;\n      }\n\n      createVideoBackground();\n\n      const token = ++backgroundVideoToken;\n      const nextIndex = activeBackgroundLayer === 0 ? 1 : 0;\n      const next = backgroundVideoLayers[nextIndex];\n      const previous = backgroundVideoLayers[activeBackgroundLayer];\n\n      let hasRevealed = false;\n\n      const reveal = function () {\n        if (hasRevealed || token !== backgroundVideoToken) return;\n        hasRevealed = true;\n\n        \/* Le fond rejoint le timecode de la vid\u00e9o visible dans la carte. *\/\n        try {\n          if (Number.isFinite(cardVideo.currentTime)) {\n            next.video.currentTime = cardVideo.currentTime;\n          }\n        } catch (error) {}\n\n        const playPromise = next.video.play();\n        if (playPromise && typeof playPromise.catch === \"function\") {\n          playPromise.catch(function () {});\n        }\n\n        panelProjects.classList.add(\"has-project-video\");\n        viewportBackground.classList.add(\"has-project-video\");\n        backgroundVideoRoot.classList.add(\"is-visible\");\n        next.layer.classList.add(\"is-active\");\n        previous.layer.classList.remove(\"is-active\");\n        activeBackgroundLayer = nextIndex;\n\n        window.setTimeout(function () {\n          if (!previous.layer.classList.contains(\"is-active\")) {\n            previous.video.pause();\n          }\n        }, 760);\n      };\n\n      if (next.source !== source) {\n        next.source = source;\n        next.video.src = source;\n        next.video.load();\n\n        if (next.video.readyState >= 2) {\n          reveal();\n        } else {\n          next.video.addEventListener(\"loadeddata\", reveal, { once: true });\n          next.video.addEventListener(\"canplay\", reveal, { once: true });\n        }\n      } else {\n        reveal();\n      }\n\n      return true;\n    }\n\n\n    function refreshActiveVisual(options) {\n      const settings = options || {};\n      const currentIndex = modulo(activeStep, cards.length);\n      const activeCard = cards[currentIndex];\n      const image = activeCard ? activeCard.querySelector(\"img\") : null;\n      const video = activeCard ? getCardVideo(activeCard) : null;\n\n      pauseInactiveCardVideos(activeCard);\n      updateBackgroundTitle(activeCard, true);\n      updateBackground(activeCard);\n\n      const resync = function () {\n        updateBackground(activeCard);\n        updateBackgroundTitle(activeCard, true);\n        scheduleTitlePosition();\n      };\n\n      if (image) {\n        if (image.complete && (image.currentSrc || image.src)) {\n          resync();\n        } else {\n          image.addEventListener(\"load\", resync, { once: true });\n          image.addEventListener(\"error\", function () {\n            panelProjects.classList.remove(\"has-project-background\");\n          }, { once: true });\n        }\n\n        if (typeof image.decode === \"function\") {\n          image.decode().then(resync).catch(function () {});\n        }\n      }\n\n      if (video) {\n        video.addEventListener(\"loadedmetadata\", resync, { once: true });\n        video.addEventListener(\"loadeddata\", resync, { once: true });\n        video.addEventListener(\"canplay\", resync, { once: true });\n      }\n\n      if (settings.retry) {\n        [80, 220, 500, 900, 1500].forEach(function (delayMs) {\n          window.setTimeout(resync, delayMs);\n        });\n      }\n    }\n\n    \/* =====================================================\n       TITRE + FOND\n    ===================================================== *\/\n\n    function pulseViewportBackgroundShade() {\n      if (!viewportBackgroundShade) {\n        return;\n      }\n\n      if (backgroundShadeTimer !== null) {\n        window.clearTimeout(\n          backgroundShadeTimer\n        );\n\n        backgroundShadeTimer = null;\n      }\n\n      viewportBackgroundShade.classList.remove(\n        \"is-pulsing\"\n      );\n\n      \/*\n       * Force le red\u00e9marrage de l'animation m\u00eame lorsque deux changements\n       * de projet sont tr\u00e8s rapproch\u00e9s.\n       *\/\n      void viewportBackgroundShade.offsetWidth;\n\n      viewportBackgroundShade.classList.add(\n        \"is-pulsing\"\n      );\n\n      backgroundShadeTimer =\n        window.setTimeout(\n          function () {\n            backgroundShadeTimer = null;\n\n            viewportBackgroundShade.classList.remove(\n              \"is-pulsing\"\n            );\n          },\n          700\n        );\n    }\n\n    function getLeastVisibleBackgroundLayer() {\n      let bestIndex = -1;\n      let bestOpacity = Infinity;\n\n      viewportBackgroundLayers.forEach(\n        function (layer, index) {\n          if (\n            index ===\n            activeViewportBackgroundLayer\n          ) {\n            return;\n          }\n\n          const opacity =\n            Number.parseFloat(\n              window.getComputedStyle(layer).opacity\n            );\n\n          const safeOpacity =\n            Number.isFinite(opacity)\n              ? opacity\n              : 0;\n\n          if (safeOpacity < bestOpacity) {\n            bestOpacity = safeOpacity;\n            bestIndex = index;\n          }\n        }\n      );\n\n      if (bestIndex >= 0) {\n        return bestIndex;\n      }\n\n      return modulo(\n        activeViewportBackgroundLayer + 1,\n        viewportBackgroundLayers.length\n      );\n    }\n\n    function clearViewportBackgroundLayers() {\n      currentViewportBackgroundUrl = \"\";\n      activeViewportBackgroundLayer = -1;\n\n      viewportBackgroundLayers.forEach(\n        function (layer) {\n          layer.classList.remove(\n            \"is-active\"\n          );\n        }\n      );\n\n      if (viewportBackgroundShade) {\n        viewportBackgroundShade.classList.remove(\n          \"is-pulsing\"\n        );\n      }\n    }\n\n    function crossfadeViewportBackground(imageUrl) {\n      if (!imageUrl) {\n        clearViewportBackgroundLayers();\n        return;\n      }\n\n      \/*\n       * Les nombreux resync du m\u00e9dia actif ne doivent jamais relancer\n       * artificiellement un fondu si l'image n'a pas r\u00e9ellement chang\u00e9.\n       *\/\n      if (\n        imageUrl ===\n        currentViewportBackgroundUrl &&\n        activeViewportBackgroundLayer >= 0\n      ) {\n        return;\n      }\n\n      let nextLayerIndex =\n        viewportBackgroundLayers.findIndex(\n          function (layer, index) {\n            return (\n              index !==\n                activeViewportBackgroundLayer &&\n              layer.dataset.pimpeBackgroundUrl ===\n                imageUrl\n            );\n          }\n        );\n\n      if (nextLayerIndex < 0) {\n        nextLayerIndex =\n          getLeastVisibleBackgroundLayer();\n      }\n\n      const nextLayer =\n        viewportBackgroundLayers[\n          nextLayerIndex\n        ];\n\n      nextLayer.dataset.pimpeBackgroundUrl =\n        imageUrl;\n\n      nextLayer.style.backgroundImage =\n        'url(\"' +\n        imageUrl.replace(\/\"\/g, '\\\\\"') +\n        '\")';\n\n      \/*\n       * On ne retire pas l'ancien background-image :\n       * son calque garde l'image pr\u00e9c\u00e9dente pendant son fade-out.\n       *\/\n      viewportBackgroundLayers.forEach(\n        function (layer, index) {\n          layer.classList.toggle(\n            \"is-active\",\n            index === nextLayerIndex\n          );\n        }\n      );\n\n      activeViewportBackgroundLayer =\n        nextLayerIndex;\n\n      currentViewportBackgroundUrl =\n        imageUrl;\n\n      \/*\n       * Le premier fond de l'intro sort d\u00e9j\u00e0 du noir gr\u00e2ce au moteur\n       * pimpe-roulette-settling. Le mini-voile noir est r\u00e9serv\u00e9 aux\n       * changements volontaires apr\u00e8s l'intro.\n       *\/\n      if (\n        !rouletteRunning &&\n        !document.body.classList.contains(\n          \"pimpe-roulette-settling\"\n        ) &&\n        !document.body.classList.contains(\n          \"pimpe-home-loading\"\n        )\n      ) {\n        pulseViewportBackgroundShade();\n      }\n    }\n\n    function updateBackground(card) {\n      const usesVideo =\n        updateVideoBackground(card);\n\n      if (usesVideo) {\n        panelProjects.classList.add(\n          \"has-project-background\"\n        );\n\n        viewportBackground.classList.add(\n          \"has-project-background\"\n        );\n\n        clearViewportBackgroundLayers();\n\n        panelProjects.style.removeProperty(\n          \"--project-background-image\"\n        );\n\n        viewportBackground.style.removeProperty(\n          \"--project-background-image\"\n        );\n\n        return;\n      }\n\n      const imageUrl =\n        getCardImage(card);\n\n      if (!imageUrl) {\n        panelProjects.classList.remove(\n          \"has-project-background\",\n          \"has-project-video\"\n        );\n\n        viewportBackground.classList.remove(\n          \"has-project-background\",\n          \"has-project-video\"\n        );\n\n        clearViewportBackgroundLayers();\n\n        panelProjects.style.removeProperty(\n          \"--project-background-image\"\n        );\n\n        viewportBackground.style.removeProperty(\n          \"--project-background-image\"\n        );\n\n        return;\n      }\n\n      \/*\n       * La classe reste aussi sur .panel-projets pour conserver les r\u00e8gles\n       * existantes du titre. Le blur, lui, est rendu par les trois calques\n       * ind\u00e9pendants de viewportBackground.\n       *\/\n      panelProjects.classList.remove(\n        \"has-project-video\"\n      );\n\n      viewportBackground.classList.remove(\n        \"has-project-video\"\n      );\n\n      panelProjects.classList.add(\n        \"has-project-background\"\n      );\n\n      viewportBackground.classList.add(\n        \"has-project-background\"\n      );\n\n      crossfadeViewportBackground(\n        imageUrl\n      );\n\n      requestViewportBackgroundSync();\n    }\n\n    function normalizeProjectTitle(title) {\n      return String(title || \"\")\n        .trim()\n        .replace(\/\\s+\/g, \" \");\n    }\n\n    function createTitleMeasureElement() {\n      let measure = document.querySelector(\n        \".pimpe-title-measure\"\n      );\n\n      if (measure) return measure;\n\n      measure = document.createElement(\"span\");\n      measure.className = \"pimpe-title-measure\";\n      measure.setAttribute(\"aria-hidden\", \"true\");\n\n      Object.assign(measure.style, {\n        position: \"fixed\",\n        left: \"-10000px\",\n        top: \"-10000px\",\n        display: \"block\",\n        width: \"max-content\",\n        maxWidth: \"none\",\n        whiteSpace: \"nowrap\",\n        visibility: \"hidden\",\n        pointerEvents: \"none\",\n        textTransform: \"uppercase\",\n        fontFamily:\n          '\"Akira Expanded UltraBold\",' +\n          '\"Akira Expanded Ultra Bold\",' +\n          '\"Akira Expanded\",' +\n          '\"Arial Black\",sans-serif',\n        fontWeight: \"900\",\n        fontStyle: \"normal\",\n        lineHeight: \".82\",\n        letterSpacing: \"-.05em\"\n      });\n\n      document.body.appendChild(measure);\n      return measure;\n    }\n\n    function measureTitleTextAtSize(textValue, size) {\n      const measure = createTitleMeasureElement();\n\n      measure.style.fontSize = size + \"px\";\n      measure.textContent = textValue;\n\n      return measure.getBoundingClientRect().width;\n    }\n\n    function getBalancedMobileTitleLines(title) {\n      const normalizedTitle =\n        normalizeProjectTitle(title);\n\n      const words = normalizedTitle\n        .split(\" \")\n        .filter(Boolean);\n\n      if (words.length <= 1) {\n        return [normalizedTitle];\n      }\n\n      if (words.length === 2) {\n        return [words[0], words[1]];\n      }\n\n      const referenceSize = 100;\n      let bestLines = [\n        words.slice(0, 1).join(\" \"),\n        words.slice(1).join(\" \")\n      ];\n      let bestScore = Number.POSITIVE_INFINITY;\n\n      for (\n        let splitIndex = 1;\n        splitIndex < words.length;\n        splitIndex += 1\n      ) {\n        const firstLine = words\n          .slice(0, splitIndex)\n          .join(\" \");\n\n        const secondLine = words\n          .slice(splitIndex)\n          .join(\" \");\n\n        const firstWidth =\n          measureTitleTextAtSize(\n            firstLine,\n            referenceSize\n          );\n\n        const secondWidth =\n          measureTitleTextAtSize(\n            secondLine,\n            referenceSize\n          );\n\n        const maximumWidth = Math.max(\n          firstWidth,\n          secondWidth\n        );\n\n        const imbalance = Math.abs(\n          firstWidth - secondWidth\n        );\n\n        const score =\n          maximumWidth + (imbalance * .18);\n\n        if (score < bestScore) {\n          bestScore = score;\n          bestLines = [firstLine, secondLine];\n        }\n      }\n\n      return bestLines;\n    }\n\n    function getProjectTitleLines(title) {\n      if (window.innerWidth <= 767) {\n        return getBalancedMobileTitleLines(title);\n      }\n\n      return [normalizeProjectTitle(title)];\n    }\n\n    function renderBackgroundTitle(title) {\n      const lines = getProjectTitleLines(title);\n\n      backgroundTitle.replaceChildren();\n\n      lines.forEach(function (lineText) {\n        const line = document.createElement(\"span\");\n\n        line.className =\n          \"project-background-title__line\";\n\n        line.textContent = lineText;\n        backgroundTitle.appendChild(line);\n      });\n    }\n\n    function setBackgroundTitleFontSize(size) {\n      backgroundTitle.style.setProperty(\n        \"font-size\",\n        size.toFixed(2) + \"px\",\n        \"important\"\n      );\n    }\n\n    function calculateSharedMobileTitleSize() {\n      const availableWidth = Math.max(\n        210,\n        window.innerWidth - 72\n      );\n\n      const maximumSize =\n        window.innerHeight <= 700 ? 48 : 56;\n\n      const minimumSize = 28;\n      const referenceSize = 100;\n\n      let widestReferenceLine = 0;\n\n      cards.forEach(function (card) {\n        const title = getCardTitle(card);\n        const lines =\n          getBalancedMobileTitleLines(title);\n\n        lines.forEach(function (line) {\n          widestReferenceLine = Math.max(\n            widestReferenceLine,\n            measureTitleTextAtSize(\n              line,\n              referenceSize\n            )\n          );\n        });\n      });\n\n      if (widestReferenceLine <= 0) {\n        return minimumSize;\n      }\n\n      const widthBasedSize =\n        (\n          availableWidth \/\n          widestReferenceLine\n        ) * referenceSize;\n\n      return Math.max(\n        minimumSize,\n        Math.min(\n          maximumSize,\n          widthBasedSize\n        )\n      );\n    }\n\n    function getBackgroundTitleTextWidth() {\n      const lines = Array.from(\n        backgroundTitle.querySelectorAll(\n          \".project-background-title__line\"\n        )\n      );\n\n      if (!lines.length) {\n        return 0;\n      }\n\n      return lines.reduce(function (\n        maximumWidth,\n        line\n      ) {\n        return Math.max(\n          maximumWidth,\n          line.getBoundingClientRect().width\n        );\n      }, 0);\n    }\n\n    function fitBackgroundTitle() {\n      if (!backgroundTitle.textContent) return;\n\n      const isMobile = window.innerWidth <= 767;\n\n      if (isMobile) {\n        setBackgroundTitleFontSize(\n          calculateSharedMobileTitleSize()\n        );\n        return;\n      }\n\n      const viewportPadding = 72;\n      const availableWidth = Math.max(\n        220,\n        window.innerWidth - viewportPadding\n      );\n\n      const maximumSize = 84;\n      const minimumSize = 28;\n\n      let minimum = minimumSize;\n      let maximum = maximumSize;\n      let bestSize = minimumSize;\n\n      for (\n        let iteration = 0;\n        iteration < 12;\n        iteration += 1\n      ) {\n        const candidate =\n          (minimum + maximum) \/ 2;\n\n        setBackgroundTitleFontSize(candidate);\n\n        const textWidth =\n          getBackgroundTitleTextWidth();\n\n        if (textWidth <= availableWidth) {\n          bestSize = candidate;\n          minimum = candidate;\n        } else {\n          maximum = candidate;\n        }\n      }\n\n      setBackgroundTitleFontSize(bestSize);\n    }\n\n    function updateBackgroundTitle(card, immediate) {\n      const newTitle = getCardTitle(card);\n\n      if (!newTitle || newTitle === currentTitle) return;\n\n      if (titleChangeTimer !== null) {\n        window.clearTimeout(titleChangeTimer);\n        titleChangeTimer = null;\n      }\n\n      if (titleFadeFrame !== null) {\n        window.cancelAnimationFrame(titleFadeFrame);\n        titleFadeFrame = null;\n      }\n\n      if (immediate) {\n        backgroundTitle.classList.remove(\"is-changing\");\n        renderBackgroundTitle(newTitle);\n        currentTitle = newTitle;\n        fitBackgroundTitle();\n        return;\n      }\n\n      \/*\n       * Phase 1 : disparition rapide du titre actuel.\n       * Phase 2 : remplacement du texte lorsqu'il est invisible.\n       * Phase 3 : r\u00e9apparition douce du nouveau titre.\n       *\/\n      backgroundTitle.classList.add(\"is-changing\");\n\n      titleChangeTimer = window.setTimeout(function () {\n        titleChangeTimer = null;\n\n        renderBackgroundTitle(newTitle);\n        currentTitle = newTitle;\n        fitBackgroundTitle();\n\n        titleFadeFrame = window.requestAnimationFrame(function () {\n          titleFadeFrame = null;\n\n          window.requestAnimationFrame(function () {\n            backgroundTitle.classList.remove(\"is-changing\");\n          });\n        });\n      }, 190);\n    }\n\n    \/* =====================================================\n       POSITION DES CARTES\n    ===================================================== *\/\n\n    function positionTitleAfterCards() {\n      if (!backgroundTitle) return;\n\n      backgroundTitle.style.removeProperty(\"top\");\n      backgroundTitle.style.removeProperty(\"bottom\");\n      backgroundTitle.classList.add(\"pimpe-title-positioned\");\n\n      \/*\n       * En mobile, la zone du stack est d\u00e9j\u00e0 limit\u00e9e par\n       * --pimpe-mobile-title-space. On redemande n\u00e9anmoins une mesure apr\u00e8s\n       * le rendu pour tenir compte des polices et de la barre de navigation.\n       *\/\n      if (window.innerWidth <= 767) {\n        window.requestAnimationFrame(function () {\n          backgroundTitle.classList.add(\n            \"pimpe-mobile-title-positioned\"\n          );\n        });\n      }\n    }\n\n    function scheduleTitlePosition() {\n      window.requestAnimationFrame(function () {\n        window.requestAnimationFrame(positionTitleAfterCards);\n      });\n    }\n\n    function getResponsiveCardGeometry() {\n      const isMobile = window.matchMedia(\n        \"(max-width: 767px)\"\n      ).matches;\n\n      if (isMobile) {\n        const isShortViewport = window.innerHeight <= 700;\n\n        return {\n          firstOffset: isShortViewport ? 100 : 124,\n          additionalOffset: isShortViewport ? 34 : 44,\n\n          \/*\n           * Les cartes gardent toutes la m\u00eame bo\u00eete de base.\n           * La r\u00e9duction visuelle est volontairement plus douce qu'en V3.11,\n           * afin que le stack mobile reste homog\u00e8ne.\n           *\/\n          firstScale: .62,\n          additionalScaleLoss: .11,\n          minimumScale: .30,\n\n          \/*\n           * Une carte centrale + deux au-dessus + deux en dessous.\n           * Toute carte suppl\u00e9mentaire reste masqu\u00e9e jusqu'au scroll.\n           *\/\n          visibleCardLimit: 5\n        };\n      }\n\n      return {\n        firstOffset: 165,\n        additionalOffset: 72,\n        firstScale: .50,\n        additionalScaleLoss: .13,\n        minimumScale: .24,\n        visibleCardLimit: 5\n      };\n    }\n\n    function getVisibleCardIndexes(states, limit) {\n      if (\n        !Number.isFinite(limit) ||\n        limit <= 0 ||\n        states.length <= limit\n      ) {\n        return null;\n      }\n\n      const sortedStates = states\n        .slice()\n        .sort(function (a, b) {\n          const distanceDifference =\n            a.distance - b.distance;\n\n          if (Math.abs(distanceDifference) > .0001) {\n            return distanceDifference;\n          }\n\n          \/*\n           * En cas d'\u00e9galit\u00e9 parfaite pendant le mouvement,\n           * on garde une s\u00e9lection stable afin d'\u00e9viter un clignotement.\n           *\/\n          const offsetDifference =\n            Math.abs(a.offset) - Math.abs(b.offset);\n\n          if (Math.abs(offsetDifference) > .0001) {\n            return offsetDifference;\n          }\n\n          return a.index - b.index;\n        })\n        .slice(0, limit);\n\n      return new Set(\n        sortedStates.map(function (state) {\n          return state.index;\n        })\n      );\n    }\n\n    function setCardMobileVisibility(\n      card,\n      isVisible\n    ) {\n      if (isVisible) {\n        card.style.setProperty(\n          \"opacity\",\n          \"1\",\n          \"important\"\n        );\n        card.style.setProperty(\n          \"visibility\",\n          \"visible\",\n          \"important\"\n        );\n\n        \/*\n         * Une carte masqu\u00e9e avait re\u00e7u pointer-events:none en inline.\n         * Il faut imp\u00e9rativement retirer cette valeur lorsqu'elle revient,\n         * sinon la classe is-active-card ne peut plus la rendre cliquable.\n         *\/\n        card.style.removeProperty(\n          \"pointer-events\"\n        );\n        return;\n      }\n\n      card.style.setProperty(\n        \"opacity\",\n        \"0\",\n        \"important\"\n      );\n      card.style.setProperty(\n        \"visibility\",\n        \"hidden\",\n        \"important\"\n      );\n      card.style.setProperty(\n        \"pointer-events\",\n        \"none\",\n        \"important\"\n      );\n    }\n\n    function renderCards(options) {\n      const settings = options || {};\n      const totalCards = cards.length;\n      const currentIndex = modulo(activeStep, totalCards);\n      const activeCard = cards[currentIndex];\n      const geometry = getResponsiveCardGeometry();\n\n      updateBackground(activeCard);\n      updateBackgroundTitle(activeCard, Boolean(settings.immediateTitle));\n\n      const cardStates = cards.map(function (card, index) {\n        const offset = getCircularOffset(\n          index,\n          currentIndex,\n          totalCards\n        );\n\n        return {\n          card: card,\n          index: index,\n          offset: offset,\n          distance: Math.abs(offset)\n        };\n      });\n\n      const visibleCardIndexes = getVisibleCardIndexes(\n        cardStates,\n        geometry.visibleCardLimit\n      );\n\n      cardStates.forEach(function (state) {\n        const card = state.card;\n        const index = state.index;\n        const offset = state.offset;\n        const distance = state.distance;\n        const isVisible =\n          !visibleCardIndexes ||\n          visibleCardIndexes.has(index);\n\n        let translateY = 0;\n        let scale = 1;\n        let zIndex = 100;\n        let shadow =\n          \"0 9px 20px rgba(0, 0, 0, .34), \" +\n          \"0 3px 7px rgba(0, 0, 0, .20)\";\n\n        if (offset < 0) {\n          translateY =\n            -geometry.firstOffset -\n            ((distance - 1) * geometry.additionalOffset);\n          scale =\n            geometry.firstScale -\n            ((distance - 1) * geometry.additionalScaleLoss);\n          zIndex = 100 - distance;\n          shadow =\n            \"0 8px 17px rgba(0, 0, 0, .31), \" +\n            \"0 2px 6px rgba(0, 0, 0, .18)\";\n        }\n\n        if (offset > 0) {\n          translateY =\n            geometry.firstOffset +\n            ((distance - 1) * geometry.additionalOffset);\n          scale =\n            geometry.firstScale -\n            ((distance - 1) * geometry.additionalScaleLoss);\n          zIndex = 100 - distance;\n          shadow =\n            \"0 8px 17px rgba(0, 0, 0, .31), \" +\n            \"0 2px 6px rgba(0, 0, 0, .18)\";\n        }\n\n        const safeScale = Math.max(\n          scale,\n          geometry.minimumScale\n        );\n\n        card.style.transform =\n          \"translate(-50%, -50%) \" +\n          \"translateY(\" + translateY + \"px) \" +\n          \"scale(\" + safeScale + \")\";\n\n        setCardMobileVisibility(\n          card,\n          isVisible\n        );\n\n        card.style.setProperty(\n          \"filter\",\n          \"none\",\n          \"important\"\n        );\n        card.style.zIndex = isVisible\n          ? String(zIndex)\n          : \"0\";\n        card.style.setProperty(\n          \"box-shadow\",\n          shadow,\n          \"important\"\n        );\n\n        const isActiveCard =\n          offset === 0 && isVisible;\n\n        card.classList.toggle(\n          \"is-active-card\",\n          isActiveCard\n        );\n\n        card.style.setProperty(\n          \"pointer-events\",\n          isActiveCard ? \"auto\" : \"none\",\n          \"important\"\n        );\n\n        card.setAttribute(\n          \"aria-hidden\",\n          isActiveCard\n            ? \"false\"\n            : \"true\"\n        );\n\n        card.setAttribute(\n          \"tabindex\",\n          isActiveCard ? \"0\" : \"-1\"\n        );\n      });\n\n      pauseInactiveCardVideos(activeCard);\n\n      projectIndexItems.forEach(function (item, index) {\n        const isActive = index === currentIndex;\n        item.classList.toggle(\"is-active\", isActive);\n        item.setAttribute(\"aria-current\", isActive ? \"true\" : \"false\");\n      });\n\n      \/* Ordre visuel garanti :\n         1. sliding cards\n         2. espace\n         3. titre du projet *\/\n      scheduleTitlePosition();\n\n      \/* Le fond vid\u00e9o est recal\u00e9 juste apr\u00e8s que la nouvelle carte active\n         a r\u00e9ellement \u00e9t\u00e9 peinte. Cela \u00e9vite d'avoir \u00e0 scroller retour. *\/\n      scheduleActiveMediaSync();\n    }\n\n    function changeCard(direction) {\n      activeStep += direction;\n\n      interactionCurrentStep =\n        activeStep;\n\n      interactionTargetStep =\n        activeStep;\n\n      interactionNearestIndex =\n        modulo(\n          activeStep,\n          cards.length\n        );\n\n      interactionActive = false;\n      interactionSnapping = false;\n\n      document.body.classList.remove(\n        \"pimpe-user-scrolling\",\n        \"pimpe-user-snapping\"\n      );\n\n      renderCards();\n      scheduleActiveMediaSync();\n    }\n\n    \/* =====================================================\n       DOUBLE ROULETTE CONTINUE AU CHARGEMENT\n    ===================================================== *\/\n\n    function getContinuousCircularOffset(cardIndex, virtualStep, totalCards) {\n      let offset = cardIndex - virtualStep;\n      const half = totalCards \/ 2;\n\n      while (offset >= half) offset -= totalCards;\n      while (offset < -half) offset += totalCards;\n\n      return offset;\n    }\n\n    function easeInOutCubic(value) {\n      const x = Math.max(0, Math.min(1, value));\n\n      return x < .5\n        ? 4 * x * x * x\n        : 1 - Math.pow(-2 * x + 2, 3) \/ 2;\n    }\n\n    function easeOutCubic(value) {\n      const x = Math.max(0, Math.min(1, value));\n      return 1 - Math.pow(1 - x, 3);\n    }\n\n    function renderRouletteFrame(virtualStep, globalScale) {\n      const totalCards = cards.length;\n      const introScale =\n        typeof globalScale === \"number\" ? globalScale : .60;\n      const nearestIndex = modulo(Math.round(virtualStep), totalCards);\n      const geometry = getResponsiveCardGeometry();\n\n      const rouletteStates = cards.map(function (card, index) {\n        const offset = getContinuousCircularOffset(\n          index,\n          virtualStep,\n          totalCards\n        );\n\n        return {\n          card: card,\n          index: index,\n          offset: offset,\n          distance: Math.abs(offset)\n        };\n      });\n\n      const visibleCardIndexes = getVisibleCardIndexes(\n        rouletteStates,\n        geometry.visibleCardLimit\n      );\n\n      rouletteStates.forEach(function (state) {\n        const card = state.card;\n        const index = state.index;\n        const offset = state.offset;\n        const distance = state.distance;\n        const direction = offset < 0 ? -1 : 1;\n        const isVisible =\n          !visibleCardIndexes ||\n          visibleCardIndexes.has(index);\n\n        \/*\n         * Important : autour du centre, le d\u00e9placement reste lin\u00e9aire.\n         * La carte ne marque donc plus de micro-pause \u00e0 chaque passage.\n         * Aux positions enti\u00e8res, la g\u00e9om\u00e9trie correspond exactement \u00e0\n         * renderCards(), ce qui supprime aussi le petit snap final.\n         *\/\n        let translateY;\n        let localScale;\n\n        if (distance <= 1) {\n          translateY =\n            direction *\n            geometry.firstOffset *\n            distance;\n\n          localScale =\n            1 -\n            ((1 - geometry.firstScale) * distance);\n        } else {\n          translateY =\n            direction *\n            (\n              geometry.firstOffset +\n              (\n                (distance - 1) *\n                geometry.additionalOffset\n              )\n            );\n\n          localScale =\n            geometry.firstScale -\n            (\n              (distance - 1) *\n              geometry.additionalScaleLoss\n            );\n        }\n\n        const safeScale = Math.max(\n          localScale * introScale,\n          .16\n        );\n        const zIndex = Math.max(1, 100 - Math.round(distance * 10));\n\n        card.style.transform =\n          \"translate3d(-50%, -50%, 0) \" +\n          \"translate3d(0, \" + translateY.toFixed(3) + \"px, 0) \" +\n          \"scale3d(\" +\n          safeScale.toFixed(5) + \", \" +\n          safeScale.toFixed(5) + \", 1)\";\n\n        setCardMobileVisibility(\n          card,\n          isVisible\n        );\n\n        card.style.zIndex = isVisible\n          ? String(zIndex)\n          : \"0\";\n      });\n\n      \/* Mise \u00e0 jour de l'index uniquement lorsque la carte centrale change. *\/\n      if (nearestIndex !== rouletteNearestIndex) {\n        rouletteNearestIndex = nearestIndex;\n\n        projectIndexItems.forEach(function (item, index) {\n          const isActive = index === nearestIndex;\n          item.classList.toggle(\"is-active\", isActive);\n          item.setAttribute(\"aria-current\", isActive ? \"true\" : \"false\");\n        });\n      }\n    }\n\n    function setFinalBackgroundProgress(progress) {\n      const value = Math.max(0, Math.min(1, progress));\n      const curtain = createRouletteCurtain();\n\n      curtain.style.setProperty(\n        \"--pimpe-curtain-opacity\",\n        (1 - value).toFixed(5)\n      );\n\n      document.body.style.setProperty(\n        \"--pimpe-bg-image-opacity\",\n        (.60 * value).toFixed(5)\n      );\n\n      document.body.style.setProperty(\n        \"--pimpe-bg-overlay-opacity\",\n        value.toFixed(5)\n      );\n\n      document.body.style.setProperty(\n        \"--pimpe-bg-video-opacity\",\n        (.60 * value).toFixed(5)\n      );\n    }\n\n    function clearFinalBackgroundProgress() {\n      const curtain = createRouletteCurtain();\n\n      curtain.style.removeProperty(\"--pimpe-curtain-opacity\");\n      document.body.style.removeProperty(\"--pimpe-bg-image-opacity\");\n      document.body.style.removeProperty(\"--pimpe-bg-overlay-opacity\");\n      document.body.style.removeProperty(\"--pimpe-bg-video-opacity\");\n    }\n\n    function releaseHomeLoadingScreen() {\n      if (loadingScreenReleased) {\n        return Promise.resolve();\n      }\n\n      loadingScreenReleased = true;\n\n      if (!homeLoadingScreen) {\n        document.body.classList.remove(\n          \"pimpe-home-loading\"\n        );\n\n        return Promise.resolve();\n      }\n\n      return new Promise(function (resolve) {\n        homeLoadingScreen.classList.add(\n          \"is-leaving\"\n        );\n\n        window.setTimeout(function () {\n          homeLoadingScreen.remove();\n\n          document.body.classList.remove(\n            \"pimpe-home-loading\"\n          );\n\n          resolve();\n        }, 900);\n      });\n    }\n\n    function startProjectRoulette() {\n\n      if (rouletteStarted || cards.length < 2) {\n        document.body.classList.remove(\"pimpe-roulette-pending\");\n        document.body.classList.add(\n          \"pimpe-page-visible\",\n          \"pimpe-roulette-background-visible\",\n          \"pimpe-roulette-title-visible\",\n          \"pimpe-roulette-revealed\"\n        );\n        revealHeaderAndIndexText();\n        revealFinalProjectTitle();\n        return;\n      }\n\n      rouletteStarted = true;\n\n      const isElementorEditor = Boolean(\n        document.body.classList.contains(\"elementor-editor-active\") ||\n        document.documentElement.classList.contains(\"elementor-html\") ||\n        window.elementorFrontend?.isEditMode?.()\n      );\n\n      if (isElementorEditor) {\n        document.body.classList.remove(\n          \"pimpe-home-loading\"\n        );\n\n        if (homeLoadingScreen) {\n          homeLoadingScreen.remove();\n        }\n\n        document.body.classList.remove(\"pimpe-roulette-pending\");\n        document.body.classList.add(\n          \"pimpe-page-visible\",\n          \"pimpe-roulette-background-visible\",\n          \"pimpe-roulette-title-visible\",\n          \"pimpe-roulette-revealed\"\n        );\n        revealHeaderAndIndexText();\n        revealFinalProjectTitle();\n        return;\n      }\n\n      rouletteRunning = true;\n      rouletteNearestIndex = -1;\n      wheelLocked = true;\n      wheelAccumulator = 0;\n\n      document.body.classList.remove(\n        \"pimpe-roulette-settling\",\n        \"pimpe-roulette-title-visible\",\n        \"pimpe-roulette-revealed\",\n        \"pimpe-roulette-background-ready\",\n        \"pimpe-roulette-background-visible\"\n      );\n      document.body.classList.add(\"pimpe-roulette-running\");\n\n      panelProjects.classList.remove(\n        \"has-project-background\",\n        \"has-project-video\"\n      );\n\n      viewportBackground.classList.remove(\n        \"has-project-background\",\n        \"has-project-video\"\n      );\n\n      panelProjects.style.removeProperty(\n        \"--project-background-image\"\n      );\n\n      viewportBackground.style.removeProperty(\n        \"--project-background-image\"\n      );\n\n      clearViewportBackgroundLayers();\n\n      hideVideoBackground();\n\n      clearFinalBackgroundProgress();\n      setFinalBackgroundProgress(0);\n\n      \/*\n       * Les vid\u00e9os des cards jouent en sourdine pendant la roulette.\n       * Ainsi, leur premi\u00e8re frame est d\u00e9j\u00e0 d\u00e9cod\u00e9e lorsqu'elles passent\n       * au centre. Elles seront de nouveau mises en pause \u00e0 la fin.\n       *\/\n      playCardVideosForIntro();\n\n      \/* La roulette part toujours de la premi\u00e8re carte. *\/\n      activeStep = 0;\n\n      const totalCards = cards.length;\n      const startStep = 0;\n\n      \/* Trois tours complets, puis retour exact sur la carte 1. *\/\n      const finalVirtualStep = totalCards * 3;\n\n      \/* Plus rapide que la V3.4, tout en conservant un d\u00e9part et un arr\u00eat doux. *\/\n      const spinDuration = 3000;\n      const settleDuration = 820;\n      const delayBeforeMotion = 180;\n      const rollingScale = .60;\n\n      let animationStart = null;\n\n      cards.forEach(function (card) {\n        card.style.setProperty(\"opacity\", \"1\", \"important\");\n        card.style.setProperty(\"filter\", \"none\", \"important\");\n        card.classList.remove(\"is-active-card\");\n        card.setAttribute(\"aria-hidden\", \"true\");\n      });\n\n      renderRouletteFrame(startStep, rollingScale);\n\n      \/* La page, le header et l'index apparaissent ensemble en fondu. *\/\n      window.requestAnimationFrame(function () {\n        window.requestAnimationFrame(function () {\n          document.body.classList.add(\"pimpe-page-visible\");\n          revealHeaderAndIndexText();\n        });\n      });\n\n      function beginFinalSettle(timestamp) {\n        document.body.classList.remove(\"pimpe-roulette-running\");\n        document.body.classList.add(\n          \"pimpe-roulette-settling\",\n          \"pimpe-roulette-background-ready\"\n        );\n\n        \/*\n         * Le fond de la carte 1 est pr\u00e9par\u00e9 d\u00e8s le d\u00e9but de l'agrandissement.\n         * Il reste d'abord invisible, puis monte exactement avec la scale.\n         *\/\n        activeStep = 0;\n        const finalCard = cards[0];\n\n        updateBackgroundTitle(finalCard, true);\n        pauseInactiveCardVideos(finalCard);\n        updateBackground(finalCard);\n        refreshActiveVisual({ retry: true });\n        scheduleTitlePosition();\n\n        setFinalBackgroundProgress(0);\n\n        const settleStart = timestamp;\n        let titleWasRevealed = false;\n\n        function settleFrame(frameTimestamp) {\n          const elapsed = frameTimestamp - settleStart;\n          const progress = Math.min(1, elapsed \/ settleDuration);\n          const eased = easeInOutCubic(progress);\n\n          const scale =\n            rollingScale + ((1 - rollingScale) * eased);\n\n          \/* Scale et blur utilisent strictement la m\u00eame courbe. *\/\n          renderRouletteFrame(finalVirtualStep, scale);\n          setFinalBackgroundProgress(eased);\n\n          if (!titleWasRevealed && progress >= .42) {\n            titleWasRevealed = true;\n            document.body.classList.add(\"pimpe-roulette-title-visible\");\n            revealFinalProjectTitle();\n          }\n\n          if (progress < 1) {\n            window.requestAnimationFrame(settleFrame);\n            return;\n          }\n\n          \/*\n           * Les valeurs de la roulette correspondent maintenant exactement\n           * au slider final : renderCards() ne provoque plus de saut.\n           *\/\n          activeStep = 0;\n          renderCards({ immediateTitle: true });\n          scheduleActiveMediaSync();\n\n          document.body.classList.remove(\n            \"pimpe-roulette-pending\",\n            \"pimpe-roulette-settling\"\n          );\n          document.body.classList.add(\n            \"pimpe-roulette-background-visible\",\n            \"pimpe-roulette-title-visible\",\n            \"pimpe-roulette-revealed\"\n          );\n\n          rouletteRunning = false;\n          wheelLocked = false;\n          wheelAccumulator = 0;\n\n          interactionCurrentStep =\n            activeStep;\n\n          interactionTargetStep =\n            activeStep;\n\n          interactionNearestIndex =\n            modulo(\n              activeStep,\n              cards.length\n            );\n\n          interactionActive = false;\n          interactionSnapping = false;\n\n          document.body.classList.remove(\n            \"pimpe-user-scrolling\",\n            \"pimpe-user-snapping\"\n          );\n\n          window.requestAnimationFrame(function () {\n            clearFinalBackgroundProgress();\n          });\n        }\n\n        window.requestAnimationFrame(settleFrame);\n      }\n\n      function spinFrame(timestamp) {\n        if (animationStart === null) {\n          animationStart = timestamp + delayBeforeMotion;\n        }\n\n        if (timestamp < animationStart) {\n          window.requestAnimationFrame(spinFrame);\n          return;\n        }\n\n        const elapsed = timestamp - animationStart;\n        const progress = Math.min(1, elapsed \/ spinDuration);\n\n        \/*\n         * Ease-in-out : d\u00e9marrage doux, acc\u00e9l\u00e9ration franche,\n         * puis ralentissement sans longue tra\u00eene.\n         *\/\n        const motionProgress = easeInOutCubic(progress);\n        const virtualStep =\n          startStep +\n          ((finalVirtualStep - startStep) * motionProgress);\n\n        renderRouletteFrame(virtualStep, rollingScale);\n\n        if (progress < 1) {\n          window.requestAnimationFrame(spinFrame);\n          return;\n        }\n\n        window.requestAnimationFrame(beginFinalSettle);\n      }\n\n      window.requestAnimationFrame(spinFrame);\n    }\n\n    \/* =====================================================\n       V3.23 \u2014 MOLETTE \/ TRACKPAD \/ TACTILE CONTINUS\n    ===================================================== *\/\n\n    function interactionIsBlocked() {\n      return (\n        rouletteRunning ||\n        document.documentElement.classList.contains(\n          \"pimpe-infos-open\"\n        )\n      );\n    }\n\n    function getWheelPixelsPerCard() {\n      \/*\n       * Une petite impulsion de molette ne suffit plus \u00e0 changer de projet.\n       * Il faut environ 250 px cumul\u00e9s pour parcourir une carte compl\u00e8te.\n       * Un trackpad reste n\u00e9anmoins tr\u00e8s direct car ses deltas arrivent\n       * continuellement pendant tout le geste.\n       *\/\n      if (window.innerWidth <= 767) {\n        return 205;\n      }\n\n      if (window.innerWidth <= 1100) {\n        return 235;\n      }\n\n      return 255;\n    }\n\n    function getTouchPixelsPerCard() {\n      const geometry =\n        getResponsiveCardGeometry();\n\n      \/*\n       * Sur tactile, la distance physique du doigt correspond presque\n       * exactement \u00e0 l'espacement visuel de la premi\u00e8re carte :\n       * sensation de \"prendre\" r\u00e9ellement le stack.\n       *\/\n      return Math.max(\n        108,\n        geometry.firstOffset\n      );\n    }\n\n    function normalizeWheelMovement(event) {\n      let movement =\n        Math.abs(event.deltaY) >=\n        Math.abs(event.deltaX)\n          ? event.deltaY\n          : event.deltaX;\n\n      \/*\n       * deltaMode :\n       * 0 = pixels\n       * 1 = lignes\n       * 2 = pages\n       *\/\n      if (event.deltaMode === 1) {\n        movement *= 16;\n      } else if (event.deltaMode === 2) {\n        movement *= window.innerHeight;\n      }\n\n      \/*\n       * Certains p\u00e9riph\u00e9riques envoient un pic \u00e9norme sur une frame.\n       * On le limite sans supprimer le momentum des \u00e9v\u00e9nements suivants.\n       *\/\n      return Math.max(\n        -180,\n        Math.min(180, movement)\n      );\n    }\n\n    function beginContinuousInteraction() {\n      if (interactionActive) {\n        return;\n      }\n\n      interactionActive = true;\n      interactionSnapping = false;\n\n      interactionCurrentStep =\n        Number.isFinite(interactionCurrentStep)\n          ? interactionCurrentStep\n          : activeStep;\n\n      \/*\n       * Hors d'une interaction pr\u00e9c\u00e9dente, la position visible correspond\n       * exactement \u00e0 activeStep.\n       *\/\n      if (\n        Math.abs(\n          interactionCurrentStep -\n          activeStep\n        ) > .999\n      ) {\n        interactionCurrentStep =\n          activeStep;\n      }\n\n      interactionTargetStep =\n        interactionCurrentStep;\n\n      interactionNearestIndex =\n        modulo(\n          Math.round(interactionCurrentStep),\n          cards.length\n        );\n\n      document.body.classList.add(\n        \"pimpe-user-scrolling\"\n      );\n\n      document.body.classList.remove(\n        \"pimpe-user-snapping\"\n      );\n\n      \/*\n       * Pendant le drag, aucune carte n'est cliquable : on \u00e9vite qu'un\n       * rel\u00e2chement du doigt soit interpr\u00e9t\u00e9 comme un clic projet.\n       *\/\n      cards.forEach(function (card) {\n        card.classList.remove(\n          \"is-active-card\"\n        );\n\n        card.style.setProperty(\n          \"pointer-events\",\n          \"none\",\n          \"important\"\n        );\n      });\n    }\n\n    function syncInteractiveProject(\n      virtualStep,\n      immediateTitle\n    ) {\n      const nearestIndex =\n        modulo(\n          Math.round(virtualStep),\n          cards.length\n        );\n\n      if (\n        nearestIndex ===\n        interactionNearestIndex\n      ) {\n        return;\n      }\n\n      interactionNearestIndex =\n        nearestIndex;\n\n      const nearestCard =\n        cards[nearestIndex];\n\n      \/*\n       * Le titre et le blur changent uniquement quand une nouvelle carte\n       * devient r\u00e9ellement la plus proche du centre. Leur fondu existant\n       * est conserv\u00e9.\n       *\/\n      updateBackgroundTitle(\n        nearestCard,\n        Boolean(immediateTitle)\n      );\n\n      updateBackground(\n        nearestCard\n      );\n\n      projectIndexItems.forEach(\n        function (item, index) {\n          const isActive =\n            index === nearestIndex;\n\n          item.classList.toggle(\n            \"is-active\",\n            isActive\n          );\n\n          item.setAttribute(\n            \"aria-current\",\n            isActive\n              ? \"true\"\n              : \"false\"\n          );\n        }\n      );\n    }\n\n    function renderInteractivePosition(\n      virtualStep,\n      immediateTitle\n    ) {\n      \/*\n       * renderRouletteFrame() sait d\u00e9j\u00e0 interpoler la g\u00e9om\u00e9trie entre\n       * deux index entiers. On r\u00e9utilise donc exactement ce moteur,\n       * mais \u00e0 \u00e9chelle finale = 1.\n       *\/\n      renderRouletteFrame(\n        virtualStep,\n        1\n      );\n\n      syncInteractiveProject(\n        virtualStep,\n        immediateTitle\n      );\n    }\n\n    function finalizeContinuousInteraction() {\n      const snappedStep =\n        Math.round(\n          interactionTargetStep\n        );\n\n      activeStep = snappedStep;\n\n      interactionCurrentStep =\n        snappedStep;\n\n      interactionTargetStep =\n        snappedStep;\n\n      interactionActive = false;\n      interactionSnapping = false;\n\n      if (\n        interactionFrame !== null\n      ) {\n        window.cancelAnimationFrame(\n          interactionFrame\n        );\n\n        interactionFrame = null;\n      }\n\n      document.body.classList.remove(\n        \"pimpe-user-scrolling\",\n        \"pimpe-user-snapping\"\n      );\n\n      \/*\n       * Retour \u00e0 renderCards() uniquement \u00e0 la toute fin :\n       * la position est d\u00e9j\u00e0 exactement enti\u00e8re, donc aucun saut visuel.\n       * Cela restaure aussi la carte centrale cliquable.\n       *\/\n      renderCards({\n        immediateTitle: true\n      });\n\n      scheduleActiveMediaSync();\n      scheduleTitlePosition();\n\n      wheelAccumulator = 0;\n      wheelLocked = false;\n    }\n\n    function interactionAnimationFrame() {\n      interactionFrame = null;\n\n      if (!interactionActive) {\n        return;\n      }\n\n      const difference =\n        interactionTargetStep -\n        interactionCurrentStep;\n\n      \/*\n       * Pendant le scroll trackpad\/molette :\n       * forte accroche pour suivre la main.\n       *\n       * Pendant le snap :\n       * amortissement l\u00e9g\u00e8rement plus doux pour terminer naturellement.\n       *\/\n      const follow =\n        interactionSnapping\n          ? .24\n          : .42;\n\n      interactionCurrentStep +=\n        difference * follow;\n\n      if (\n        Math.abs(difference) < .0008\n      ) {\n        interactionCurrentStep =\n          interactionTargetStep;\n      }\n\n      renderInteractivePosition(\n        interactionCurrentStep,\n        false\n      );\n\n      if (\n        interactionSnapping &&\n        Math.abs(\n          interactionTargetStep -\n          interactionCurrentStep\n        ) < .002\n      ) {\n        finalizeContinuousInteraction();\n        return;\n      }\n\n      \/*\n       * Hors snap, on arr\u00eate la boucle lorsqu'on a rattrap\u00e9 le geste.\n       * Le prochain \u00e9v\u00e9nement wheel la relancera imm\u00e9diatement.\n       *\/\n      if (\n        !interactionSnapping &&\n        Math.abs(\n          interactionTargetStep -\n          interactionCurrentStep\n        ) < .001\n      ) {\n        return;\n      }\n\n      interactionFrame =\n        window.requestAnimationFrame(\n          interactionAnimationFrame\n        );\n    }\n\n    function requestInteractionFrame() {\n      if (\n        interactionFrame !== null\n      ) {\n        return;\n      }\n\n      interactionFrame =\n        window.requestAnimationFrame(\n          interactionAnimationFrame\n        );\n    }\n\n    function snapContinuousInteraction(\n      projectedStep\n    ) {\n      if (!interactionActive) {\n        return;\n      }\n\n      if (\n        interactionSnapTimer !== null\n      ) {\n        window.clearTimeout(\n          interactionSnapTimer\n        );\n\n        interactionSnapTimer = null;\n      }\n\n      const sourceStep =\n        Number.isFinite(projectedStep)\n          ? projectedStep\n          : interactionTargetStep;\n\n      interactionTargetStep =\n        Math.round(sourceStep);\n\n      interactionSnapping = true;\n\n      document.body.classList.remove(\n        \"pimpe-user-scrolling\"\n      );\n\n      document.body.classList.add(\n        \"pimpe-user-snapping\"\n      );\n\n      requestInteractionFrame();\n    }\n\n    function scheduleWheelSnap() {\n      if (\n        interactionSnapTimer !== null\n      ) {\n        window.clearTimeout(\n          interactionSnapTimer\n        );\n      }\n\n      \/*\n       * On attend la fin r\u00e9elle du geste trackpad \/ momentum avant\n       * de choisir la carte la plus proche.\n       *\/\n      interactionSnapTimer =\n        window.setTimeout(\n          function () {\n            interactionSnapTimer = null;\n            snapContinuousInteraction();\n          },\n          125\n        );\n    }\n\n    function handleWheel(event) {\n      if (interactionIsBlocked()) {\n        event.preventDefault();\n        event.stopPropagation();\n        return;\n      }\n\n      event.preventDefault();\n      event.stopPropagation();\n\n      \/*\n       * wheelLocked n'est plus utilis\u00e9 pour chaque mouvement utilisateur.\n       * Il reste seulement utile pendant l'intro et les sauts via l'index.\n       *\/\n      if (\n        wheelLocked &&\n        !interactionActive\n      ) {\n        return;\n      }\n\n      beginContinuousInteraction();\n\n      interactionSnapping = false;\n\n      document.body.classList.add(\n        \"pimpe-user-scrolling\"\n      );\n\n      document.body.classList.remove(\n        \"pimpe-user-snapping\"\n      );\n\n      const movement =\n        normalizeWheelMovement(event);\n\n      interactionTargetStep +=\n        movement \/\n        getWheelPixelsPerCard();\n\n      requestInteractionFrame();\n      scheduleWheelSnap();\n    }\n\n    window.addEventListener(\n      \"wheel\",\n      handleWheel,\n      {\n        passive: false,\n        capture: true\n      }\n    );\n\n    \/* =====================================================\n       TACTILE \u2014 TAP FIABLE + SWIPE CONTINU\n    ===================================================== *\/\n\n    function resetTouchState() {\n      touchStartY = null;\n      touchStartX = null;\n      touchLastY = null;\n      touchLastTime = 0;\n      touchVelocityStepsPerMs = 0;\n      touchGestureStarted = false;\n      touchCandidateCard = null;\n    }\n\n    window.addEventListener(\n      \"touchstart\",\n      function (event) {\n        if (interactionIsBlocked()) {\n          return;\n        }\n\n        if (!event.touches.length) {\n          return;\n        }\n\n        if (\n          interactionSnapTimer !== null\n        ) {\n          window.clearTimeout(\n            interactionSnapTimer\n          );\n\n          interactionSnapTimer = null;\n        }\n\n        const touch = event.touches[0];\n\n        \/*\n         * IMPORTANT :\n         * contrairement \u00e0 l'ancienne V3.24, on n'appelle PAS\n         * beginContinuousInteraction() ici.\n         *\n         * L'ancienne version supprimait imm\u00e9diatement is-active-card et\n         * pointer-events d\u00e8s que le doigt touchait l'\u00e9cran. Un simple tap\n         * pouvait donc perdre sa cible avant que le navigateur n'\u00e9mette click.\n         *\/\n        touchStartY = touch.clientY;\n        touchStartX = touch.clientX;\n\n        touchStartStep =\n          interactionCurrentStep;\n\n        touchLastY =\n          touchStartY;\n\n        touchLastTime =\n          performance.now();\n\n        touchVelocityStepsPerMs = 0;\n        touchGestureStarted = false;\n\n        touchCandidateCard =\n          event.target.closest(\n            \".card-stage > .project-card.is-active-card\"\n          );\n      },\n      {\n        passive: true,\n        capture: true\n      }\n    );\n\n    window.addEventListener(\n      \"touchmove\",\n      function (event) {\n        if (\n          document.documentElement\n            .classList.contains(\n              \"pimpe-infos-open\"\n            )\n        ) {\n          return;\n        }\n\n        if (\n          touchStartY === null ||\n          touchStartX === null ||\n          !event.touches.length\n        ) {\n          return;\n        }\n\n        const touch =\n          event.touches[0];\n\n        const currentY =\n          touch.clientY;\n\n        const currentX =\n          touch.clientX;\n\n        const totalMovementY =\n          touchStartY -\n          currentY;\n\n        const totalMovementX =\n          touchStartX -\n          currentX;\n\n        const distance =\n          Math.hypot(\n            totalMovementX,\n            totalMovementY\n          );\n\n        \/*\n         * Jusqu'\u00e0 14 px, le geste reste un TAP potentiel.\n         * On ne bloque rien, on ne retire pas la classe active et\n         * on ne lance aucun snap.\n         *\/\n        if (\n          !touchGestureStarted &&\n          distance < TOUCH_DRAG_THRESHOLD\n        ) {\n          return;\n        }\n\n        \/*\n         * Le seuil vient d'\u00eatre franchi : c'est maintenant un vrai swipe.\n         * Seulement \u00e0 ce moment-l\u00e0 on active le moteur de scroll continu.\n         *\/\n        if (!touchGestureStarted) {\n          touchGestureStarted = true;\n          touchCandidateCard = null;\n\n          beginContinuousInteraction();\n\n          touchStartStep =\n            interactionCurrentStep;\n\n          touchLastY =\n            touchStartY;\n\n          touchLastTime =\n            performance.now();\n\n          touchVelocityStepsPerMs = 0;\n        }\n\n        event.preventDefault();\n\n        const now =\n          performance.now();\n\n        const deltaTime =\n          Math.max(\n            8,\n            now - touchLastTime\n          );\n\n        const pixelDelta =\n          touchLastY -\n          currentY;\n\n        const instantVelocity =\n          (\n            pixelDelta \/\n            getTouchPixelsPerCard()\n          ) \/\n          deltaTime;\n\n        touchVelocityStepsPerMs =\n          (\n            touchVelocityStepsPerMs *\n            .72\n          ) +\n          (\n            instantVelocity *\n            .28\n          );\n\n        const draggedStep =\n          touchStartStep +\n          (\n            totalMovementY \/\n            getTouchPixelsPerCard()\n          );\n\n        interactionTargetStep =\n          draggedStep;\n\n        interactionCurrentStep =\n          draggedStep;\n\n        interactionSnapping = false;\n\n        renderInteractivePosition(\n          interactionCurrentStep,\n          false\n        );\n\n        touchLastY =\n          currentY;\n\n        touchLastTime =\n          now;\n      },\n      {\n        passive: false,\n        capture: true\n      }\n    );\n\n    function finishTouchSwipe() {\n      if (\n        touchStartY === null ||\n        !touchGestureStarted\n      ) {\n        return;\n      }\n\n      const projectedMomentum =\n        Math.max(\n          -.42,\n          Math.min(\n            .42,\n            touchVelocityStepsPerMs *\n            145\n          )\n        );\n\n      const projectedStep =\n        interactionCurrentStep +\n        projectedMomentum;\n\n      resetTouchState();\n\n      snapContinuousInteraction(\n        projectedStep\n      );\n    }\n\n    window.addEventListener(\n      \"touchend\",\n      function (event) {\n        if (\n          document.documentElement\n            .classList.contains(\n              \"pimpe-infos-open\"\n            )\n        ) {\n          resetTouchState();\n          return;\n        }\n\n        if (touchStartY === null) {\n          return;\n        }\n\n        \/*\n         * Aucun vrai drag n'a commenc\u00e9 :\n         * on traite explicitement le geste comme un TAP.\n         *\n         * On navigue directement depuis touchend au lieu d'attendre\n         * le click synth\u00e9tique de Safari\/Chrome mobile.\n         *\/\n        if (!touchGestureStarted) {\n          const card =\n            touchCandidateCard &&\n            touchCandidateCard.classList.contains(\n              \"is-active-card\"\n            )\n              ? touchCandidateCard\n              : null;\n\n          resetTouchState();\n\n          if (card) {\n            event.preventDefault();\n            navigateToCardProject(card);\n          }\n\n          return;\n        }\n\n        finishTouchSwipe();\n      },\n      {\n        passive: false,\n        capture: true\n      }\n    );\n\n    window.addEventListener(\n      \"touchcancel\",\n      function () {\n        if (touchGestureStarted) {\n          finishTouchSwipe();\n          return;\n        }\n\n        resetTouchState();\n      },\n      {\n        passive: true,\n        capture: true\n      }\n    );\n\n    \/* =====================================================\n       CLAVIER\n    ===================================================== *\/\n\n    document.addEventListener(\"keydown\", function (event) {\n      if (document.documentElement.classList.contains(\"pimpe-infos-open\")) return;\n\n      if (\n        event.key === \"ArrowDown\" ||\n        event.key === \"ArrowRight\" ||\n        event.key === \"PageDown\"\n      ) {\n        event.preventDefault();\n        changeCard(1);\n      }\n\n      if (\n        event.key === \"ArrowUp\" ||\n        event.key === \"ArrowLeft\" ||\n        event.key === \"PageUp\"\n      ) {\n        event.preventDefault();\n        changeCard(-1);\n      }\n    });\n\n\n    \/* =====================================================\n       INTRO OBLIGATOIRE \u2014 ATTENTE DU VRAI CHARGEMENT\n    ===================================================== *\/\n\n    function wait(milliseconds) {\n      return new Promise(function (resolve) {\n        window.setTimeout(resolve, milliseconds);\n      });\n    }\n\n    function waitForWindowLoad(maximumWait) {\n      if (document.readyState === \"complete\") {\n        return Promise.resolve();\n      }\n\n      return new Promise(function (resolve) {\n        let resolved = false;\n\n        function finish() {\n          if (resolved) return;\n          resolved = true;\n\n          window.removeEventListener(\n            \"load\",\n            finish\n          );\n\n          resolve();\n        }\n\n        window.addEventListener(\n          \"load\",\n          finish,\n          { once: true }\n        );\n\n        window.setTimeout(\n          finish,\n          maximumWait\n        );\n      });\n    }\n\n    function waitForVisibleDocument() {\n      if (\n        document.visibilityState === \"visible\"\n      ) {\n        return Promise.resolve();\n      }\n\n      return new Promise(function (resolve) {\n        function handleVisibility() {\n          if (\n            document.visibilityState !== \"visible\"\n          ) {\n            return;\n          }\n\n          document.removeEventListener(\n            \"visibilitychange\",\n            handleVisibility\n          );\n\n          resolve();\n        }\n\n        document.addEventListener(\n          \"visibilitychange\",\n          handleVisibility\n        );\n      });\n    }\n\n    function waitForFonts(maximumWait) {\n      if (\n        !document.fonts ||\n        !document.fonts.ready\n      ) {\n        return Promise.resolve();\n      }\n\n      return Promise.race([\n        document.fonts.ready.catch(\n          function () {}\n        ),\n        wait(maximumWait)\n      ]);\n    }\n\n    function promoteLazyAttribute(\n      element,\n      targetAttribute,\n      sourceAttributes\n    ) {\n      if (!element) return;\n\n      for (const sourceAttribute of sourceAttributes) {\n        const candidate =\n          element.getAttribute(sourceAttribute);\n\n        if (!candidate) continue;\n\n        element.setAttribute(\n          targetAttribute,\n          candidate\n        );\n\n        return;\n      }\n    }\n\n    function primeCardImages() {\n      cards.forEach(function (card) {\n        card\n          .querySelectorAll(\"picture source\")\n          .forEach(function (source) {\n            promoteLazyAttribute(\n              source,\n              \"srcset\",\n              [\n                \"data-srcset\",\n                \"data-lazy-srcset\"\n              ]\n            );\n          });\n\n        card\n          .querySelectorAll(\"img\")\n          .forEach(function (image) {\n            image.loading = \"eager\";\n            image.decoding = \"async\";\n\n            image.setAttribute(\n              \"loading\",\n              \"eager\"\n            );\n\n            image.setAttribute(\n              \"decoding\",\n              \"async\"\n            );\n\n            image.setAttribute(\n              \"fetchpriority\",\n              \"high\"\n            );\n\n            promoteLazyAttribute(\n              image,\n              \"srcset\",\n              [\n                \"data-srcset\",\n                \"data-lazy-srcset\"\n              ]\n            );\n\n            promoteLazyAttribute(\n              image,\n              \"sizes\",\n              [\n                \"data-sizes\",\n                \"data-lazy-sizes\"\n              ]\n            );\n\n            promoteLazyAttribute(\n              image,\n              \"src\",\n              [\n                \"data-src\",\n                \"data-lazy-src\"\n              ]\n            );\n\n            \/*\n             * Une r\u00e9affectation l\u00e9g\u00e8re oblige Safari\/Chrome \u00e0 reconsid\u00e9rer\n             * imm\u00e9diatement une image que le lazy loading avait laiss\u00e9e en attente.\n             *\/\n            const source =\n              image.currentSrc ||\n              image.getAttribute(\"src\") ||\n              \"\";\n\n            if (\n              source &&\n              !image.complete\n            ) {\n              image.src = source;\n            }\n          });\n      });\n    }\n\n    function waitForImageReady(image) {\n      if (!image) {\n        return Promise.resolve();\n      }\n\n      function decodeImage() {\n        if (\n          typeof image.decode !== \"function\"\n        ) {\n          return Promise.resolve();\n        }\n\n        return image.decode().catch(\n          function () {}\n        );\n      }\n\n      if (\n        image.complete &&\n        image.naturalWidth > 0\n      ) {\n        return decodeImage();\n      }\n\n      return new Promise(function (resolve) {\n        let resolved = false;\n\n        function finish() {\n          if (resolved) return;\n          resolved = true;\n\n          image.removeEventListener(\n            \"load\",\n            finish\n          );\n\n          image.removeEventListener(\n            \"error\",\n            finish\n          );\n\n          decodeImage().finally(resolve);\n        }\n\n        image.addEventListener(\n          \"load\",\n          finish,\n          { once: true }\n        );\n\n        image.addEventListener(\n          \"error\",\n          finish,\n          { once: true }\n        );\n      });\n    }\n\n    function waitForAllCardImages() {\n      primeCardImages();\n\n      const imagePromises = [];\n\n      cards.forEach(function (card) {\n        card\n          .querySelectorAll(\"img\")\n          .forEach(function (image) {\n            imagePromises.push(\n              waitForImageReady(image)\n            );\n          });\n      });\n\n      if (!imagePromises.length) {\n        return Promise.resolve();\n      }\n\n      return Promise.allSettled(\n        imagePromises\n      );\n    }\n\n    function waitForStablePaints(numberOfFrames) {\n      return new Promise(function (resolve) {\n        let remainingFrames =\n          Math.max(1, numberOfFrames);\n\n        function nextFrame() {\n          remainingFrames -= 1;\n\n          if (remainingFrames <= 0) {\n            resolve();\n            return;\n          }\n\n          window.requestAnimationFrame(\n            nextFrame\n          );\n        }\n\n        window.requestAnimationFrame(\n          nextFrame\n        );\n      });\n    }\n\n    async function launchMandatoryProjectRoulette() {\n      if (\n        rouletteLaunchScheduled ||\n        rouletteStarted\n      ) {\n        return;\n      }\n\n      rouletteLaunchScheduled = true;\n\n      \/*\n       * Le header est d\u00e9j\u00e0 visible sur un fond noir uni.\n       * La Home attend maintenant le chargement et le d\u00e9codage de TOUTES\n       * les images des cards, afin qu\u2019aucune carte blanche n\u2019apparaisse\n       * pendant les trois tours de roulette.\n       *\/\n      await Promise.all([\n        waitForWindowLoad(15000),\n        waitForFonts(6000),\n        waitForAllCardImages()\n      ]);\n\n      await waitForVisibleDocument();\n\n      refreshActiveVisual({ retry: true });\n      refitCurrentProjectTitle();\n\n      \/*\n       * Laisse au navigateur plusieurs peintures compl\u00e8tes pour transf\u00e9rer\n       * les images d\u00e9cod\u00e9es sur les calques des cards.\n       *\/\n      await waitForStablePaints(4);\n      await wait(120);\n\n      if (\n        document.visibilityState !== \"visible\"\n      ) {\n        rouletteLaunchScheduled = false;\n        launchMandatoryProjectRoulette();\n        return;\n      }\n\n      \/*\n       * Ordre visuel demand\u00e9 :\n       * fond noir + header\n       * \u2192 fondu du fond noir\n       * \u2192 roulette\n       * \u2192 mise en place finale des cards.\n       *\/\n      await releaseHomeLoadingScreen();\n      await waitForStablePaints(2);\n      await wait(70);\n\n      startProjectRoulette();\n    }\n\n    \/* =====================================================\n       INITIALISATION\n    ===================================================== *\/\n\n    window.addEventListener(\"resize\", function () {\n      window.requestAnimationFrame(function () {\n        fitBackgroundTitle();\n\n        if (!rouletteRunning) {\n          renderCards({ immediateTitle: true });\n        } else {\n          scheduleTitlePosition();\n        }\n      });\n    });\n\n    window.addEventListener(\"pimpe:refresh-active-visual\", function () {\n      refreshActiveVisual({ retry: true });\n    });\n\n    function refitCurrentProjectTitle() {\n      window.requestAnimationFrame(function () {\n        if (currentTitle) {\n          renderBackgroundTitle(currentTitle);\n        }\n\n        fitBackgroundTitle();\n        scheduleTitlePosition();\n      });\n    }\n\n    if (document.fonts && document.fonts.ready) {\n      document.fonts.ready.then(\n        refitCurrentProjectTitle\n      );\n\n      if (\n        typeof document.fonts.addEventListener ===\n        \"function\"\n      ) {\n        document.fonts.addEventListener(\n          \"loadingdone\",\n          refitCurrentProjectTitle\n        );\n      }\n    }\n\n    [120, 350, 800, 1500].forEach(function (delay) {\n      window.setTimeout(\n        refitCurrentProjectTitle,\n        delay\n      );\n    });\n\n    window.addEventListener(\n      \"pageshow\",\n      refitCurrentProjectTitle\n    );\n\n    renderCards({ immediateTitle: true });\n    refreshActiveVisual({ retry: true });\n    revealStableLayout();\n\n    \/*\n     * Ne lance plus la roulette imm\u00e9diatement apr\u00e8s DOMContentLoaded.\n     * Elle attend d\u00e9sormais que la page soit r\u00e9ellement charg\u00e9e, peinte\n     * et visible : elle ne peut donc plus se terminer derri\u00e8re l\u2019\u00e9cran\n     * noir ou pendant le t\u00e9l\u00e9chargement des m\u00e9dias.\n     *\/\n    launchMandatoryProjectRoulette();\n\n    window.addEventListener(\"load\", function () {\n      refreshActiveVisual({ retry: true });\n    }, { once: true });\n\n    window.addEventListener(\"pageshow\", function () {\n      refreshActiveVisual({ retry: true });\n    });\n\n    document.addEventListener(\"visibilitychange\", function () {\n      if (document.hidden) {\n        cards.forEach(function (card) {\n          const video = getCardVideo(card);\n          if (video) video.pause();\n        });\n\n        backgroundVideoLayers.forEach(function (entry) {\n          entry.video.pause();\n        });\n      } else {\n        refreshActiveVisual({ retry: true });\n\n        if (!rouletteStarted) {\n          launchMandatoryProjectRoulette();\n        }\n      }\n    });\n\n    window.dispatchEvent(new Event(\"resize\"));\n    window.setTimeout(function () {\n      window.dispatchEvent(new Event(\"resize\"));\n      refreshActiveVisual({ retry: true });\n    }, 500);\n  }\n\n  function startWhenReady() {\n    initializePimpeHomeV2();\n\n    if (!document.querySelector(\".card-stage > .project-card\")) {\n      window.setTimeout(startWhenReady, 300);\n    }\n  }\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", startWhenReady);\n  } else {\n    startWhenReady();\n  }\n})();\n<\/script>\n\n<div class=\"pimpe-page-fade is-visible\" aria-hidden=\"true\"><\/div>\n\n<script>\n(function () {\n  \"use strict\";\n\n  const FADE_DURATION = 340;\n  const LOGO_FADE_DURATION = 580;\n\n  function getFade() {\n    return document.querySelector(\".pimpe-page-fade\");\n  }\n\n  function restoreHomeState() {\n    const fade = getFade();\n\n    \/* Le navigateur peut restaurer la page depuis son cache avec l'\u00e9tat\n       de sortie encore actif. On nettoie toujours cet \u00e9tat. *\/\n    document.body.classList.remove(\"pimpe-page-leaving\");\n    document.documentElement.classList.remove(\"pimpe-page-entering\");\n\n    if (fade) {\n      fade.classList.remove(\"is-visible\");\n    }\n\n    window.setTimeout(function () {\n      document.documentElement.classList.remove(\"pimpe-logo-transition\");\n      try { sessionStorage.removeItem(\"pimpeLogoTransition\"); } catch (error) {}\n    }, 700);\n\n    window.requestAnimationFrame(function () {\n      window.requestAnimationFrame(function () {\n        window.dispatchEvent(new Event(\"resize\"));\n        window.dispatchEvent(new Event(\"pimpe:refresh-active-visual\"));\n      });\n    });\n  }\n\n  function revealPage() {\n    \/* Le noir est peint avant le contenu, puis retir\u00e9 apr\u00e8s deux frames :\n       l\u2019entr\u00e9e devient r\u00e9ellement progressive, y compris apr\u00e8s un clic logo. *\/\n    window.requestAnimationFrame(function () {\n      window.requestAnimationFrame(function () {\n        window.setTimeout(restoreHomeState, 80);\n      });\n    });\n  }\n\n  function navigateWithBlackFade(url) {\n    const fade = getFade();\n\n    if (!fade || !url) {\n      window.location.href = url;\n      return;\n    }\n\n    document.body.classList.add(\"pimpe-page-leaving\");\n    fade.classList.add(\"is-visible\");\n\n    const duration = document.documentElement.classList.contains(\"pimpe-logo-transition\")\n      ? LOGO_FADE_DURATION\n      : FADE_DURATION;\n\n    window.setTimeout(function () {\n      window.location.href = url;\n    }, duration);\n  }\n\n  window.pimpeNavigateWithBlackFade = navigateWithBlackFade;\n\n  \/* Permet aussi aux scripts existants du site de demander la m\u00eame transition. *\/\n  window.pimpeGoTo = navigateWithBlackFade;\n\n  \/* Fondu sur tous les liens internes du site :\n     logo, index, cartes, liens de navigation, etc. *\/\n  document.addEventListener(\"click\", function (event) {\n    const link = event.target.closest(\"a\");\n    if (!link) return;\n\n    if (event.defaultPrevented) return;\n    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n    if (link.target === \"_blank\") return;\n    if (link.hasAttribute(\"download\")) return;\n    if (link.dataset.pimpeNoFade === \"true\") return;\n\n    const rawHref = link.getAttribute(\"href\") || \"\";\n    if (\n      !rawHref ||\n      rawHref.startsWith(\"#\") ||\n      rawHref.startsWith(\"mailto:\") ||\n      rawHref.startsWith(\"tel:\") ||\n      rawHref.startsWith(\"javascript:\")\n    ) {\n      return;\n    }\n\n    const target = new URL(link.href, window.location.href);\n\n    if (target.origin !== window.location.origin) return;\n\n    \/* Un clic sur HOME ou le logo peut volontairement recharger la Home :\n       on conserve alors exactement le m\u00eame fondu. *\/\n    if (target.href === window.location.href) {\n      event.preventDefault();\n      navigateWithBlackFade(target.href);\n      return;\n    }\n\n    event.preventDefault();\n    event.stopImmediatePropagation();\n    navigateWithBlackFade(target.href);\n  }, true);\n\n  try {\n    if (sessionStorage.getItem(\"pimpeLogoTransition\") === \"1\") {\n      document.documentElement.classList.add(\"pimpe-logo-transition\");\n    }\n  } catch (error) {}\n\n  document.documentElement.classList.add(\"pimpe-page-entering\");\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", revealPage, { once: true });\n  } else {\n    revealPage();\n  }\n\n  window.addEventListener(\"pageshow\", function (event) {\n    const fade = getFade();\n\n    \/* Lors d'un retour arri\u00e8re\/avant, la page peut venir du bfcache.\n       On affiche d'abord le noir, puis on r\u00e9v\u00e8le la Home proprement. *\/\n    if (event.persisted && fade) {\n      fade.classList.add(\"is-visible\");\n      document.documentElement.classList.add(\"pimpe-page-entering\");\n\n      window.requestAnimationFrame(function () {\n        window.requestAnimationFrame(function () {\n          restoreHomeState();\n        });\n      });\n      return;\n    }\n\n    restoreHomeState();\n  });\n\n  window.addEventListener(\"pagehide\", function () {\n    const fade = getFade();\n\n    \/* Le navigateur n'autorise pas toujours une vraie animation avant un\n       clic sur sa fl\u00e8che Retour, mais ce noir est repris au frame restaur\u00e9. *\/\n    if (fade) fade.classList.add(\"is-visible\");\n\n    document.body.classList.remove(\"pimpe-page-leaving\");\n  });\n})();\n<\/script>\n\n\n<script>\n(function () {\n  \"use strict\";\n\n  let alignmentFrame = null;\n  let headerObserver = null;\n  let documentObserver = null;\n\n  function isUsableRect(rect) {\n    return Boolean(\n      rect &&\n      Number.isFinite(rect.right) &&\n      rect.width > 0 &&\n      rect.height > 0\n    );\n  }\n\n  \/*\n   * Mesure le bord droit du texte r\u00e9el, et non celui du widget Elementor.\n   * Cela fonctionne m\u00eame lorsque le widget ou son lien occupe une largeur\n   * sup\u00e9rieure au mot \u00ab INFOS \u00bb.\n   *\/\n  function getInfosTextRect(root) {\n    if (!root) return null;\n\n    const walker = document.createTreeWalker(\n      root,\n      NodeFilter.SHOW_TEXT,\n      {\n        acceptNode: function (node) {\n          const value = (node.nodeValue || \"\").trim();\n\n          return value\n            ? NodeFilter.FILTER_ACCEPT\n            : NodeFilter.FILTER_REJECT;\n        }\n      }\n    );\n\n    let node = walker.nextNode();\n    let fallbackRect = null;\n\n    while (node) {\n      const value = (node.nodeValue || \"\").trim();\n      const range = document.createRange();\n\n      range.selectNodeContents(node);\n\n      const rects = Array.from(range.getClientRects()).filter(\n        isUsableRect\n      );\n\n      if (rects.length) {\n        const rect = rects.reduce(function (best, current) {\n          return !best || current.right > best.right\n            ? current\n            : best;\n        }, null);\n\n        if (\/infos\/i.test(value)) {\n          range.detach();\n          return rect;\n        }\n\n        if (!fallbackRect || rect.right > fallbackRect.right) {\n          fallbackRect = rect;\n        }\n      }\n\n      range.detach();\n      node = walker.nextNode();\n    }\n\n    if (fallbackRect) return fallbackRect;\n\n    const candidates = [\n      root.querySelector(\".elementor-heading-title\"),\n      root.querySelector(\".elementor-button-text\"),\n      root.querySelector(\"a\"),\n      root.querySelector(\"span\"),\n      root\n    ].filter(Boolean);\n\n    for (const candidate of candidates) {\n      const rect = candidate.getBoundingClientRect();\n      const style = window.getComputedStyle(candidate);\n\n      if (\n        isUsableRect(rect) &&\n        style.display !== \"none\" &&\n        style.visibility !== \"hidden\"\n      ) {\n        return rect;\n      }\n    }\n\n    return null;\n  }\n\n  function applyHomeIndexAlignment() {\n    alignmentFrame = null;\n\n    const infos = document.querySelector(\".pimpe-global-infos\");\n    const indexes = Array.from(\n      document.querySelectorAll(\".pimpe-project-index\")\n    );\n\n    if (!infos || !indexes.length) return false;\n\n    const infosRect = getInfosTextRect(infos);\n    if (!infosRect) return false;\n\n    const viewportWidth =\n      document.documentElement.clientWidth ||\n      window.innerWidth;\n\n    const rightSpace = Math.max(\n      0,\n      viewportWidth - infosRect.right\n    );\n\n    const rightValue = rightSpace.toFixed(2) + \"px\";\n\n    document.documentElement.style.setProperty(\n      \"--pimpe-header-infos-right\",\n      rightValue\n    );\n\n    \/*\n     * Inline + priorit\u00e9 important : aucune r\u00e8gle Elementor ou ancienne\n     * media query ne peut reprendre la main apr\u00e8s le calcul.\n     *\/\n    indexes.forEach(function (index) {\n      index.style.setProperty(\n        \"right\",\n        rightValue,\n        \"important\"\n      );\n    });\n\n    return true;\n  }\n\n  function requestHomeIndexAlignment() {\n    if (alignmentFrame !== null) return;\n\n    alignmentFrame = window.requestAnimationFrame(\n      applyHomeIndexAlignment\n    );\n  }\n\n  function observeResolvedElements() {\n    const header = document.querySelector(\".pimpe-global-header\");\n    const infos = document.querySelector(\".pimpe-global-infos\");\n    const index = document.querySelector(\".pimpe-project-index\");\n\n    if (\n      \"ResizeObserver\" in window &&\n      !headerObserver &&\n      (header || infos || index)\n    ) {\n      headerObserver = new ResizeObserver(\n        requestHomeIndexAlignment\n      );\n\n      [header, infos, index].forEach(function (element) {\n        if (element) headerObserver.observe(element);\n      });\n    }\n\n    if (infos && index && documentObserver) {\n      documentObserver.disconnect();\n      documentObserver = null;\n    }\n  }\n\n  function initializeHomeIndexAlignmentFix() {\n    requestHomeIndexAlignment();\n    observeResolvedElements();\n\n    \/*\n     * L'index est cr\u00e9\u00e9 dynamiquement par le script principal de la Home.\n     * L'observer attend donc aussi bien le header que l'index.\n     *\/\n    documentObserver = new MutationObserver(function () {\n      requestHomeIndexAlignment();\n      observeResolvedElements();\n    });\n\n    documentObserver.observe(document.documentElement, {\n      childList: true,\n      subtree: true\n    });\n\n    [0, 80, 180, 350, 700, 1200, 2000, 3500].forEach(\n      function (delay) {\n        window.setTimeout(function () {\n          requestHomeIndexAlignment();\n          observeResolvedElements();\n        }, delay);\n      }\n    );\n\n    if (document.fonts && document.fonts.ready) {\n      document.fonts.ready.then(\n        requestHomeIndexAlignment\n      );\n    }\n\n    window.addEventListener(\n      \"load\",\n      requestHomeIndexAlignment\n    );\n\n    window.addEventListener(\n      \"resize\",\n      requestHomeIndexAlignment,\n      { passive: true }\n    );\n\n    window.addEventListener(\n      \"pageshow\",\n      requestHomeIndexAlignment\n    );\n  }\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\n      \"DOMContentLoaded\",\n      initializeHomeIndexAlignmentFix,\n      { once: true }\n    );\n  } else {\n    initializeHomeIndexAlignmentFix();\n  }\n})();\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-7bc30d5 e-con e-atomic-element e-flexbox-base e-d3cede8 projects-layout \" data-id=\"7bc30d5\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"7bc30d5\" data-e-type=\"e-flexbox\" data-id=\"7bc30d5\">\n    <div class=\"elementor-element elementor-element-3556d57 e-con e-atomic-element e-flexbox-base e-27f63a6 project-stack \" data-id=\"3556d57\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"3556d57\" data-e-type=\"e-flexbox\" data-id=\"3556d57\">\n    <div class=\"elementor-element elementor-element-f8f248a e-con e-atomic-element e-flexbox-base e-c2c57c1 card-stage \" data-id=\"f8f248a\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"f8f248a\" data-e-type=\"e-flexbox\" data-id=\"f8f248a\">\n    <a class=\"elementor-element elementor-element-6fdf0e6 e-con e-atomic-element e-flexbox-base e-6fdf0e6-1210ebd project-card \" data-id=\"6fdf0e6\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"6fdf0e6\" href=\"\/glitch-beauty\/\" target=\"_self\" data-e-type=\"e-flexbox\" data-id=\"6fdf0e6\">\n    \t\t\t<img fetchpriority=\"high\" decoding=\"async\" class=\"e-image-base \" data-interaction-id=\"8adee84\" data-e-type=\"widget\" data-id=\"8adee84\" id=\"14311\" src=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1229x1536.png\" width=\"1229\" height=\"1536\" srcset=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1229x1536.png 1229w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-768x960.png 768w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1638x2048.png 1638w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-scaled.png 2048w\" alt=\"\"\/>\t\t\t\t<div class=\"elementor-element elementor-element-5177323 card-project-title elementor-widget elementor-widget-heading\" data-id=\"5177323\" 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\">Glitch Beauty<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/a>\n<a class=\"elementor-element elementor-element-862347a e-con e-atomic-element e-flexbox-base e-862347a-03f1475 project-card \" data-id=\"862347a\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"862347a\" href=\"\/le-rem\/\" target=\"_self\" data-e-type=\"e-flexbox\" data-id=\"862347a\">\n    \t\t\t<img decoding=\"async\" class=\"e-image-base \" data-interaction-id=\"35aeddd\" data-e-type=\"widget\" data-id=\"35aeddd\" id=\"14471\" src=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-3-1536x864.png\" width=\"1536\" height=\"864\" srcset=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-3-1536x864.png 1536w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-3-768x432.png 768w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-3-2048x1152.png 2048w\" alt=\"\"\/>\t\t\t\t<div class=\"elementor-element elementor-element-c5bbd19 card-project-title elementor-widget elementor-widget-heading\" data-id=\"c5bbd19\" 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\">Le Rem<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/a>\n<a class=\"elementor-element elementor-element-59394d2 e-con e-atomic-element e-flexbox-base e-5095a13 project-card \" data-id=\"59394d2\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"59394d2\" href=\"\/hotel-mahfouf\/\" target=\"_self\" data-e-type=\"e-flexbox\" data-id=\"59394d2\">\n    \t\t\t<img decoding=\"async\" class=\"e-image-base \" data-interaction-id=\"43cb65c\" data-e-type=\"widget\" data-id=\"43cb65c\" id=\"14278\" src=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/PIERCING-Artwork-1228x1536.png\" width=\"1228\" height=\"1536\" srcset=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/PIERCING-Artwork-1228x1536.png 1228w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/PIERCING-Artwork-768x961.png 768w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/PIERCING-Artwork-1637x2048.png 1637w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/PIERCING-Artwork-scaled.png 2046w\" alt=\"\"\/>\t\t\t\t<div class=\"elementor-element elementor-element-059e3ba card-project-title elementor-widget elementor-widget-heading\" data-id=\"059e3ba\" 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\">Hotel Mahfouf<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/a>\n<a class=\"elementor-element elementor-element-5c74af8 e-con e-atomic-element e-flexbox-base e-5c74af8-9c5cc83 project-card \" data-id=\"5c74af8\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"5c74af8\" href=\"\/sorbe\/\" target=\"_self\" data-e-type=\"e-flexbox\" data-id=\"5c74af8\">\n    \t\t\t<img loading=\"lazy\" decoding=\"async\" class=\"e-image-base \" data-interaction-id=\"34b43fd\" data-e-type=\"widget\" data-id=\"34b43fd\" id=\"14281\" src=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/BUNDLE-1-1229x1536.png\" width=\"1229\" height=\"1536\" srcset=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/BUNDLE-1-1229x1536.png 1229w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/BUNDLE-1-768x960.png 768w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/BUNDLE-1-1638x2048.png 1638w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/BUNDLE-1.png 2000w\" alt=\"\"\/>\t\t\t\t<div class=\"elementor-element elementor-element-2338123 card-project-title elementor-widget elementor-widget-heading\" data-id=\"2338123\" 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\">Sorbe<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/a>\n<a class=\"elementor-element elementor-element-4dce85a e-con e-atomic-element e-flexbox-base e-4dce85a-8b2a21d project-card \" data-id=\"4dce85a\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"4dce85a\" href=\"\/le-patchouli\/\" target=\"_self\" data-e-type=\"e-flexbox\" data-id=\"4dce85a\">\n    \t\t\t<img loading=\"lazy\" decoding=\"async\" class=\"e-image-base \" data-interaction-id=\"f61c1a0\" data-e-type=\"widget\" data-id=\"f61c1a0\" id=\"14542\" src=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-4-864x1536.png\" width=\"864\" height=\"1536\" srcset=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-4-864x1536.png 864w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-4-768x1365.png 768w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-4-1152x2048.png 1152w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/Le-REM-4-scaled.png 1440w\" alt=\"\"\/>\t\t\t\t<div class=\"elementor-element elementor-element-77e0764 card-project-title elementor-widget elementor-widget-heading\" data-id=\"77e0764\" 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\">Le Patchouli<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/a>\n<a class=\"elementor-element elementor-element-451bbb9 e-con e-atomic-element e-flexbox-base e-451bbb9-e31a006 project-card \" data-id=\"451bbb9\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"451bbb9\" href=\"\/cider\/\" target=\"_self\" data-e-type=\"e-flexbox\" data-id=\"451bbb9\">\n    \t\t\t<img loading=\"lazy\" decoding=\"async\" class=\"e-image-base \" data-interaction-id=\"fa899f2\" data-e-type=\"widget\" data-id=\"fa899f2\" id=\"14389\" src=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/VISUEL-1-1229x1536.png\" width=\"1229\" height=\"1536\" srcset=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/VISUEL-1-1229x1536.png 1229w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/VISUEL-1-768x960.png 768w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/VISUEL-1-1638x2048.png 1638w, https:\/\/pimpestudio.fr\/wp-content\/uploads\/VISUEL-1-scaled.png 2048w\" alt=\"\"\/>\t\t\t\t<div class=\"elementor-element elementor-element-69dc10e card-project-title elementor-widget elementor-widget-heading\" data-id=\"69dc10e\" 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\">Cider<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/a>\n\n<\/div>\n\n<\/div>\n\n<\/div>\n\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Close Contact Pimp\u00e9 Studio\u00a0est un studio de cr\u00e9ation fond\u00e9 par Louisa Errounda, sp\u00e9cialis\u00e9 en direction artistique, CGI et cr\u00e9ation d&rsquo;univers visuels. Le studio accompagne des marques des secteurs de la beaut\u00e9, de la mode, du luxe et de la culture dans la conception d&rsquo;images, de campagnes et d&rsquo;exp\u00e9riences visuelles. COMMISSIONS louisa@pimpestudio.fr DEMANDES G\u00c9N\u00c9RALES hello@pimpestudio.fr SERVICES [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-14171","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.3 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>PIMP\u00c9 STUDIO<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/pimpestudio.fr\/en\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"PIMP\u00c9 STUDIO\" \/>\n<meta property=\"og:description\" content=\"Close Contact Pimp\u00e9 Studio\u00a0est un studio de cr\u00e9ation fond\u00e9 par Louisa Errounda, sp\u00e9cialis\u00e9 en direction artistique, CGI et cr\u00e9ation d&rsquo;univers visuels. Le studio accompagne des marques des secteurs de la beaut\u00e9, de la mode, du luxe et de la culture dans la conception d&rsquo;images, de campagnes et d&rsquo;exp\u00e9riences visuelles. COMMISSIONS louisa@pimpestudio.fr DEMANDES G\u00c9N\u00c9RALES hello@pimpestudio.fr SERVICES [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/pimpestudio.fr\/en\/\" \/>\n<meta property=\"og:site_name\" content=\"Pimp\u00e9 Studio\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-24T16:25:35+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1229x1536.png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"2 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/\",\"url\":\"https:\\\/\\\/pimpestudio.fr\\\/\",\"name\":\"PIMP\u00c9 STUDIO\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/pimpestudio.fr\\\/wp-content\\\/uploads\\\/COMPO-Palettes-1229x1536.png\",\"datePublished\":\"2026-08-02T11:32:38+00:00\",\"dateModified\":\"2026-08-24T16:25:35+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/pimpestudio.fr\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#primaryimage\",\"url\":\"https:\\\/\\\/pimpestudio.fr\\\/wp-content\\\/uploads\\\/COMPO-Palettes-1229x1536.png\",\"contentUrl\":\"https:\\\/\\\/pimpestudio.fr\\\/wp-content\\\/uploads\\\/COMPO-Palettes-1229x1536.png\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/pimpestudio.fr\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"PIMP\u00c9 STUDIO\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#website\",\"url\":\"https:\\\/\\\/pimpestudio.fr\\\/\",\"name\":\"Pimp\u00e9 Studio\",\"description\":\"Studio cr\u00e9atif\",\"publisher\":{\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#\\\/schema\\\/person\\\/f01b954736bb16a50ed6447f2a231615\"},\"alternateName\":\"Pimp\u00e9\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/pimpestudio.fr\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/#\\\/schema\\\/person\\\/f01b954736bb16a50ed6447f2a231615\",\"name\":\"admin2823\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/wp-content\\\/uploads\\\/DTS_EDEN_Marlen_Stahlhuth_10129.webp\",\"url\":\"https:\\\/\\\/pimpestudio.fr\\\/wp-content\\\/uploads\\\/DTS_EDEN_Marlen_Stahlhuth_10129.webp\",\"contentUrl\":\"https:\\\/\\\/pimpestudio.fr\\\/wp-content\\\/uploads\\\/DTS_EDEN_Marlen_Stahlhuth_10129.webp\",\"width\":1280,\"height\":1217,\"caption\":\"admin2823\"},\"logo\":{\"@id\":\"https:\\\/\\\/pimpestudio.fr\\\/wp-content\\\/uploads\\\/DTS_EDEN_Marlen_Stahlhuth_10129.webp\"},\"sameAs\":[\"https:\\\/\\\/pimpestudio.fr\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"PIMP\u00c9 STUDIO","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/pimpestudio.fr\/en\/","og_locale":"en_US","og_type":"article","og_title":"PIMP\u00c9 STUDIO","og_description":"Close Contact Pimp\u00e9 Studio\u00a0est un studio de cr\u00e9ation fond\u00e9 par Louisa Errounda, sp\u00e9cialis\u00e9 en direction artistique, CGI et cr\u00e9ation d&rsquo;univers visuels. Le studio accompagne des marques des secteurs de la beaut\u00e9, de la mode, du luxe et de la culture dans la conception d&rsquo;images, de campagnes et d&rsquo;exp\u00e9riences visuelles. COMMISSIONS louisa@pimpestudio.fr DEMANDES G\u00c9N\u00c9RALES hello@pimpestudio.fr SERVICES [&hellip;]","og_url":"https:\/\/pimpestudio.fr\/en\/","og_site_name":"Pimp\u00e9 Studio","article_modified_time":"2026-08-24T16:25:35+00:00","og_image":[{"url":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1229x1536.png","type":"","width":"","height":""}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"2 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/pimpestudio.fr\/","url":"https:\/\/pimpestudio.fr\/","name":"PIMP\u00c9 STUDIO","isPartOf":{"@id":"https:\/\/pimpestudio.fr\/#website"},"primaryImageOfPage":{"@id":"https:\/\/pimpestudio.fr\/#primaryimage"},"image":{"@id":"https:\/\/pimpestudio.fr\/#primaryimage"},"thumbnailUrl":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1229x1536.png","datePublished":"2026-08-02T11:32:38+00:00","dateModified":"2026-08-24T16:25:35+00:00","breadcrumb":{"@id":"https:\/\/pimpestudio.fr\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/pimpestudio.fr\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/pimpestudio.fr\/#primaryimage","url":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1229x1536.png","contentUrl":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/COMPO-Palettes-1229x1536.png"},{"@type":"BreadcrumbList","@id":"https:\/\/pimpestudio.fr\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/pimpestudio.fr\/"},{"@type":"ListItem","position":2,"name":"PIMP\u00c9 STUDIO"}]},{"@type":"WebSite","@id":"https:\/\/pimpestudio.fr\/#website","url":"https:\/\/pimpestudio.fr\/","name":"Pimp\u00e9 Studio","description":"Studio cr\u00e9atif","publisher":{"@id":"https:\/\/pimpestudio.fr\/#\/schema\/person\/f01b954736bb16a50ed6447f2a231615"},"alternateName":"Pimp\u00e9","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/pimpestudio.fr\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/pimpestudio.fr\/#\/schema\/person\/f01b954736bb16a50ed6447f2a231615","name":"admin2823","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/DTS_EDEN_Marlen_Stahlhuth_10129.webp","url":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/DTS_EDEN_Marlen_Stahlhuth_10129.webp","contentUrl":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/DTS_EDEN_Marlen_Stahlhuth_10129.webp","width":1280,"height":1217,"caption":"admin2823"},"logo":{"@id":"https:\/\/pimpestudio.fr\/wp-content\/uploads\/DTS_EDEN_Marlen_Stahlhuth_10129.webp"},"sameAs":["https:\/\/pimpestudio.fr"]}]}},"_links":{"self":[{"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/pages\/14171","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/comments?post=14171"}],"version-history":[{"count":166,"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/pages\/14171\/revisions"}],"predecessor-version":[{"id":15149,"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/pages\/14171\/revisions\/15149"}],"wp:attachment":[{"href":"https:\/\/pimpestudio.fr\/en\/wp-json\/wp\/v2\/media?parent=14171"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}