diff --git a/assets/css/components/_lightbox.scss b/assets/css/components/_lightbox.scss
new file mode 100644
index 0000000..d4dcb5b
--- /dev/null
+++ b/assets/css/components/_lightbox.scss
@@ -0,0 +1,81 @@
+#lightbox {
+ position: fixed;
+ inset: 0;
+ z-index: calc(var(--z-header) + 10);
+ background-color: rgba(0, 0, 0, 0.92);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transition: opacity 0.3s ease, visibility 0.3s ease;
+
+ body.lightbox-open & {
+ opacity: 1;
+ visibility: visible;
+ pointer-events: auto;
+ }
+}
+
+#lightbox-close {
+ position: absolute;
+ top: var(--padding-body);
+ right: var(--padding-body);
+ background: none;
+ border: none;
+ color: var(--color-txt);
+ cursor: pointer;
+ padding: 8px;
+ z-index: 1;
+ line-height: 0;
+
+ svg {
+ width: 30px;
+ fill: var(--color-txt);
+ transition: fill 0.2s ease;
+ }
+
+ &:hover svg {
+ fill: var(--grey-400);
+ }
+}
+
+#lightbox-swiper {
+ width: 90vw;
+
+ .swiper-slide {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ figure {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: default;
+
+ img {
+ max-width: 90vw;
+ max-height: calc(90dvh - 100px);
+ width: auto;
+ height: auto;
+ object-fit: contain;
+ }
+
+ figcaption {
+ margin-top: calc(var(--spacing) * 0.5);
+ color: var(--color-txt-light);
+ font-size: var(--fs-small);
+ text-align: center;
+ }
+ }
+ }
+}
+
+// Cursor zoom-in sur les figures éligibles — desktop uniquement
+@media (min-width: 1080px) {
+ figure[data-lightbox] {
+ cursor: zoom-in;
+ }
+}
diff --git a/assets/css/style.scss b/assets/css/style.scss
index 974c0d9..3a46dd1 100644
--- a/assets/css/style.scss
+++ b/assets/css/style.scss
@@ -27,6 +27,7 @@
@import "components/card-block-small";
@import "components/card-open-graph";
@import "components/swiper";
+@import "components/lightbox";
@import "components/slider-before-after";
@import "components/dl-table";
@import "components/bottom-bar";
diff --git a/assets/js/lightbox.js b/assets/js/lightbox.js
new file mode 100644
index 0000000..eed3f3e
--- /dev/null
+++ b/assets/js/lightbox.js
@@ -0,0 +1,162 @@
+import Swiper from 'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.mjs';
+
+const DESKTOP = window.matchMedia('(min-width: 1080px)');
+
+// Inline du close.svg (assets/icons/close.svg)
+const CLOSE_SVG = ``;
+
+let lightboxEl = null;
+let lightboxSwiper = null;
+
+function createLightboxDOM() {
+ const el = document.createElement('div');
+ el.id = 'lightbox';
+ el.setAttribute('aria-modal', 'true');
+ el.setAttribute('role', 'dialog');
+ el.setAttribute('aria-label', 'Image agrandie');
+ el.setAttribute('inert', '');
+
+ el.innerHTML = `
+
+
+ `;
+
+ document.body.appendChild(el);
+
+ el.querySelector('#lightbox-close').addEventListener('click', closeLightbox);
+
+ // Fermeture au clic sur le fond (pas sur le swiper)
+ el.addEventListener('click', (e) => {
+ if (e.target === el) closeLightbox();
+ });
+
+ return el;
+}
+
+function ensureLightboxDOM() {
+ if (!lightboxEl) {
+ lightboxEl = createLightboxDOM();
+ }
+}
+
+function getImageData(figure) {
+ const img = figure.querySelector('img');
+
+ // 1. figcaption dans la figure
+ // 2. p.caption dans la figure (horizontal-gallery)
+ // 3. p.caption sibling de la figure (covers : resource, news-item, impact…)
+ const captionEl =
+ figure.querySelector('figcaption') ||
+ figure.querySelector('p.caption') ||
+ (figure.nextElementSibling?.matches('p.caption') ? figure.nextElementSibling : null);
+
+ return {
+ src: img?.src || '',
+ alt: img?.alt || '',
+ caption: captionEl?.innerHTML || '',
+ };
+}
+
+function buildSlides(images) {
+ const wrapper = lightboxEl.querySelector('.swiper-wrapper');
+ wrapper.innerHTML = '';
+
+ images.forEach(({ src, alt, caption }) => {
+ const slide = document.createElement('div');
+ slide.className = 'swiper-slide';
+ slide.innerHTML = `
+
+
+ ${caption ? `${caption}` : ''}
+
+ `;
+ wrapper.appendChild(slide);
+ });
+}
+
+function openLightbox(images, startIndex = 0) {
+ ensureLightboxDOM();
+ buildSlides(images);
+
+ const swiperEl = lightboxEl.querySelector('#lightbox-swiper');
+
+ // Détruire l'instance précédente si elle existe
+ if (swiperEl.swiper) {
+ swiperEl.swiper.destroy(true, true);
+ }
+
+ lightboxSwiper = new Swiper(swiperEl, {
+ slidesPerView: 1,
+ initialSlide: startIndex,
+ speed: 400,
+ watchOverflow: true,
+ keyboard: { enabled: true },
+ navigation: {
+ nextEl: swiperEl.querySelector('.swiper-button-next'),
+ prevEl: swiperEl.querySelector('.swiper-button-prev'),
+ },
+ pagination: {
+ el: swiperEl.querySelector('.swiper-pagination'),
+ clickable: true,
+ },
+ a11y: {
+ prevSlideMessage: 'Image précédente',
+ nextSlideMessage: 'Image suivante',
+ },
+ });
+
+ lightboxEl.removeAttribute('inert');
+ document.body.classList.add('lightbox-open');
+ lightboxEl.querySelector('#lightbox-close').focus();
+}
+
+function closeLightbox() {
+ document.body.classList.remove('lightbox-open');
+ lightboxEl.setAttribute('inert', '');
+}
+
+function isEligible(figure) {
+ // Exclure les figures dans un
+ if (figure.closest('a')) return false;
+ // Exclure si le parent direct contient un .link-block (card navigable)
+ if (figure.parentElement.querySelector(':scope > .link-block')) return false;
+ return true;
+}
+
+export function initLightbox() {
+ // Fermeture à la touche Echap
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && document.body.classList.contains('lightbox-open')) {
+ closeLightbox();
+ }
+ });
+
+ document.querySelectorAll('figure').forEach((figure) => {
+ if (!isEligible(figure)) return;
+
+ // Marquer les figures éligibles (pour le cursor CSS)
+ figure.setAttribute('data-lightbox', '');
+
+ figure.addEventListener('click', () => {
+ if (!DESKTOP.matches) return;
+
+ const slide = figure.closest('.swiper-slide');
+
+ if (slide) {
+ // Galerie : ouvrir toutes les images du swiper parent
+ const swiperEl = slide.closest('.swiper');
+ const allFigures = [...swiperEl.querySelectorAll('.swiper-slide > figure')];
+ const startIndex = allFigures.indexOf(figure);
+ openLightbox(allFigures.map(getImageData), startIndex);
+ } else {
+ // Image standalone
+ openLightbox([getImageData(figure)], 0);
+ }
+ });
+ });
+}
diff --git a/assets/js/script.js b/assets/js/script.js
index a568a39..31198a9 100644
--- a/assets/js/script.js
+++ b/assets/js/script.js
@@ -9,6 +9,7 @@ import { initSliderBeforeAfter} from './sliderBeforeAfter.js';
import { navInvestigation } from './investigation.js';
import { progressBar, scrollBack} from './bottom-bar.js';
import { initSort } from './sort.js';
+import { initLightbox } from './lightbox.js';
const responsiveMedium = 1080;
const responsiveSmall = 768;
@@ -27,6 +28,7 @@ window.onload = async function () {
playVideo();
initDropdowns(responsiveSmall, responsiveSmallX);
initSwipers();
+ initLightbox();
progressBar();