{"id":398,"date":"2025-10-08T19:42:58","date_gmt":"2025-10-08T19:42:58","guid":{"rendered":"https:\/\/greensolv.com\/?page_id=398"},"modified":"2026-04-02T01:22:06","modified_gmt":"2026-04-02T01:22:06","slug":"galerie-greensolv","status":"publish","type":"page","link":"https:\/\/greensolv.com\/fr\/gallery-greensolv\/","title":{"rendered":"Galerie | Greensolv"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"398\" class=\"elementor elementor-398\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f896283 e-con-full e-flex e-con e-parent\" data-id=\"f896283\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2e8e293 elementor-widget elementor-widget-html\" data-id=\"2e8e293\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<div class=\"gsx-wrap gsx-theme-light\" data-contact-url=\"https:\/\/greensolv.com\/contact-us\/\">\r\n  \r\n  <div class=\"gsx-hero\">\r\n    <span class=\"gsx-hero-badge\">\r\n      <span class=\"gsx-hero-dot\"><\/span>\r\n      OUR MEDIA\r\n    <\/span>\r\n    <h1 class=\"gsx-hero-title\">GREENSOLV GALLERY<\/h1>\r\n    <p class=\"gsx-hero-subtext\">Discover our latest installations, equipment, and work environments across all our divisions.<\/p>\r\n  <\/div>\r\n\r\n  <div class=\"gsx-top\">\r\n    <button class=\"gsx-filter-toggle\" type=\"button\" aria-expanded=\"false\" aria-controls=\"gsxFilterPanel\">\r\n      <span class=\"gsx-filter-toggle-icon\">\u203a<\/span>\r\n      <span class=\"gsx-filter-toggle-text\">FILTER<\/span>\r\n    <\/button>\r\n\r\n    <aside id=\"gsxFilterPanel\" class=\"gsx-filter-panel\" aria-hidden=\"true\">\r\n      <div class=\"gsx-filter-head\">\r\n        <div class=\"gsx-filter-title\">FILTER<\/div>\r\n        <button class=\"gsx-filter-close\" type=\"button\" aria-label=\"Close\">\u2715<\/button>\r\n      <\/div>\r\n      <div class=\"gsx-filter-list\" id=\"gsxFilterList\"><\/div>\r\n    <\/aside>\r\n  <\/div>\r\n\r\n  <div class=\"gsx-gallery\" id=\"gsxGallery\"><\/div>\r\n\r\n  <div class=\"gsx-actions\" id=\"gsxActions\">\r\n    <div id=\"gsxScrollSentinel\" style=\"height: 20px; width: 100%; margin-top: 40px;\"><\/div>\r\n  <\/div>\r\n\r\n  <div id=\"gsxLightbox\" class=\"gsx-lightbox\" aria-hidden=\"true\">\r\n    <div class=\"gsx-lightbox-content\">\r\n      <button class=\"gsx-lightbox-close\" aria-label=\"Close modal\">\u2715<\/button>\r\n      <img decoding=\"async\" id=\"gsxLightboxImg\" src=\"\" alt=\"\">\r\n    <\/div>\r\n  <\/div>\r\n\r\n<\/div>\r\n\r\n<style>\r\n  @import url('https:\/\/fonts.googleapis.com\/css2?family=Roboto:wght@300;400;500;600;700;900&display=swap');\r\n  @import url('https:\/\/fonts.googleapis.com\/css2?family=Oswald:wght@400;500;600;700&display=swap');\r\n\r\n  .gsx-wrap {\r\n    --gsx-radius: 16px; \r\n    --gsx-radius-sm: 8px;\r\n    --gsx-gap: clamp(18px, 2.6vw, 30px); \r\n    --gsx-green: #0CBC41;\r\n    --gsx-green-light: #e6f7ea;\r\n    --gsx-green-hover: #0a9d36;\r\n    --bg-block: #ffffff;\r\n    --ink: #111827;\r\n    --muted: #6b7280;\r\n    --soft: rgba(0,0,0,0.03);\r\n    --line: rgba(0,0,0,0.06);\r\n    --card-bg: #FFFFFF;\r\n    --card-border-color: rgba(0,0,0,0.08); \r\n    --card-shadow: 0 4px 20px rgba(0,0,0,0.05);\r\n    --shadowHover: 0 10px 30px rgba(0,0,0,0.1);\r\n    position: relative !important;\r\n    max-width: 1280px !important;\r\n    margin: 0 auto !important;\r\n    padding: 0 clamp(20px, 5vw, 64px) !important; \r\n    background-color: transparent !important; \r\n    font-family: Roboto, system-ui, -apple-system, \"Segoe UI\", Arial, sans-serif !important;\r\n    color: var(--ink) !important;\r\n    line-height: 1.6 !important;\r\n  }\r\n  .gsx-wrap *, .gsx-wrap *::before, .gsx-wrap *::after { box-sizing: border-box !important; }\r\n  .gsx-wrap h1, .gsx-wrap h2, .gsx-wrap h3, .gsx-filter-title {\r\n    font-family: \"DIN Condensed\", Oswald, \"Arial Narrow\", sans-serif !important;\r\n    font-weight: 700 !important; text-transform: uppercase !important;\r\n    letter-spacing: 0.02em !important; line-height: 1.1 !important;\r\n    color: var(--ink) !important; margin: 0 !important; padding: 0 !important;\r\n  }\r\n  .gsx-wrap p, .gsx-wrap span {\r\n    font-family: Roboto, system-ui, -apple-system, \"Segoe UI\", Arial, sans-serif !important;\r\n    line-height: 1.6 !important; margin: 0 !important;\r\n  }\r\n  .gsx-hero { margin-bottom: 40px !important; padding-top: 10px !important; }\r\n  .gsx-hero-badge {\r\n    display: inline-flex !important; align-items: center !important; gap: 8px !important;\r\n    background: var(--soft) !important; border: 1px solid var(--line) !important;\r\n    color: var(--muted) !important; padding: 6px 16px !important; \r\n    border-radius: 999px !important; font-size: 11px !important; font-weight: 700 !important;\r\n    letter-spacing: 0.1em !important; text-transform: uppercase !important; margin-bottom: 16px !important;\r\n  }\r\n  .gsx-hero-dot {\r\n    width: 6px !important; height: 6px !important; border-radius: 50% !important;\r\n    background: var(--gsx-green) !important; box-shadow: 0 0 8px rgba(12,188,65,0.6) !important;\r\n  }\r\n  .gsx-hero-title { font-size: clamp(32px, 4vw, 44px) !important; color: var(--ink) !important; margin-bottom: 12px !important; }\r\n  .gsx-hero-subtext {\r\n    max-width: 80ch !important; color: var(--muted) !important;\r\n    line-height: 1.75 !important; font-size: clamp(15px, 1.6vw, 17px) !important;\r\n  }\r\n  .gsx-top {\r\n    position: sticky !important; top: 140px !important; \r\n    z-index: 100 !important; width: 0 !important; height: 0 !important; \r\n  }\r\n  .gsx-filter-toggle {\r\n    position: absolute !important; left: -75px !important; top: 0 !important; \r\n    width: 56px !important; height: 160px !important; border: 1px solid var(--line) !important; \r\n    border-radius: var(--gsx-radius-sm) !important; background: var(--card-bg) !important;\r\n    color: var(--ink) !important; box-shadow: 0 10px 30px rgba(0,0,0,0.15) !important;\r\n    cursor: pointer !important; display: flex !important; align-items: center !important;\r\n    justify-content: center !important; gap: 10px !important; padding: 10px !important;\r\n    transition: background 0.3s ease, transform 0.3s ease !important;\r\n  }\r\n  .gsx-filter-toggle:hover { background: var(--soft) !important; transform: translateY(-2px) !important; }\r\n  .gsx-filter-toggle-text {\r\n    writing-mode: vertical-rl !important; transform: rotate(180deg) !important;\r\n    font-size: 11px !important; font-weight: 700 !important; letter-spacing: 0.1em !important; \r\n    text-transform: uppercase !important; color: var(--ink) !important;\r\n  }\r\n  .gsx-filter-toggle-icon { font-size: 20px !important; color: var(--ink) !important; }\r\n  .gsx-filter-panel {\r\n    position: absolute !important; left: -10px !important; top: 0 !important; \r\n    width: 320px !important; background: var(--card-bg) !important; \r\n    border: 1px solid var(--card-border-color) !important; border-radius: var(--gsx-radius) !important; \r\n    box-shadow: 0 20px 50px rgba(0,0,0,0.15) !important; padding: 24px !important; display: none;\r\n  }\r\n  .gsx-filter-panel.is-open { display: block !important; animation: gsxPanelFade 0.3s ease forwards; }\r\n  @keyframes gsxPanelFade {\r\n    from { opacity: 0; transform: translateX(-10px); }\r\n    to { opacity: 1; transform: translateX(0); }\r\n  }\r\n  .gsx-filter-head {\r\n    display: flex !important; align-items: center !important; justify-content: space-between !important;\r\n    margin-bottom: 20px !important; padding-bottom: 12px !important; border-bottom: 1px solid var(--line) !important;\r\n  }\r\n  .gsx-filter-title { font-size: clamp(20px, 2.5vw, 26px) !important; }\r\n  .gsx-filter-close {\r\n    background: transparent !important; border: none !important; color: var(--muted) !important;\r\n    cursor: pointer !important; font-size: 18px !important; padding: 5px !important;\r\n  }\r\n  .gsx-filter-list { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; }\r\n  .gsx-pill {\r\n    background: var(--card-bg) !important; color: var(--ink) !important;\r\n    border: 1px solid var(--line) !important; border-radius: 999px !important;\r\n    padding: 8px 16px !important; cursor: pointer !important; transition: all 0.2s ease !important;\r\n    font-size: 12px !important; font-weight: 700 !important; letter-spacing: 0.08em !important;\r\n    text-transform: uppercase !important; font-family: Roboto, sans-serif !important;\r\n  }\r\n  .gsx-pill:hover { background: var(--gsx-green-light) !important; border-color: rgba(12,188,65,0.3) !important; color: var(--gsx-green) !important; }\r\n  .gsx-pill.is-active { background: var(--ink) !important; color: #fff !important; border-color: var(--ink) !important; }\r\n  \r\n  \/* --- CHANGEMENT CL\u00c9 : Masonry Flexbox --- *\/\r\n  .gsx-gallery {\r\n    display: flex !important;\r\n    align-items: flex-start !important;\r\n    gap: var(--gsx-gap) !important;\r\n    width: 100% !important;\r\n  }\r\n  .gsx-masonry-column {\r\n    display: flex !important;\r\n    flex-direction: column !important;\r\n    gap: var(--gsx-gap) !important;\r\n    flex: 1 !important;\r\n    min-width: 0 !important; \/* Emp\u00eache l'overflow *\/\r\n  }\r\n  \r\n  @media (max-width: 980px){\r\n    .gsx-wrap { padding: 0 20px !important; } \r\n    .gsx-top { position: static !important; width: 100% !important; height: auto !important; margin-bottom: 24px !important; }\r\n    .gsx-filter-toggle { display: none !important; }\r\n    .gsx-filter-panel { position: static !important; width: 100% !important; display: block !important; border: none !important; box-shadow: none !important; background: transparent !important; padding: 0 !important; }\r\n    .gsx-filter-head { display: none !important; }\r\n  }\r\n  \r\n  .gsx-wrap .gsx-animate-in {\r\n    opacity: 0; transform: translateY(15px);\r\n    animation: gsxFadeUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\r\n  }\r\n  @keyframes gsxFadeUp { to { opacity: 1; transform: translateY(0); } }\r\n  \r\n  .gsx-skeleton {\r\n    background: linear-gradient(90deg, var(--soft) 25%, rgba(0,0,0,0.06) 50%, var(--soft) 75%) !important;\r\n    background-size: 400% 100% !important;\r\n    animation: gsxSkeletonPulse 1.5s ease-in-out infinite !important;\r\n    width: 100% !important; border-radius: var(--gsx-radius) !important;\r\n    aspect-ratio: var(--gsx-ratio, 4 \/ 3);\r\n  }\r\n  @keyframes gsxSkeletonPulse {\r\n    0% { background-position: 200% 0; }\r\n    100% { background-position: -200% 0; }\r\n  }\r\n  .gsx-empty-state {\r\n    width: 100% !important; text-align: center !important; padding: 80px 20px !important;\r\n    font-size: clamp(16px, 2vw, 20px) !important; color: var(--muted) !important; font-weight: 500 !important;\r\n  }\r\n  .gsx-actions { display: flex !important; justify-content: center !important; padding: 20px 0 !important; width: 100%; }\r\n  \r\n  .gsx-tile {\r\n    border-radius: var(--gsx-radius) !important; overflow: hidden !important;\r\n    background: var(--card-bg) !important; border: 1px solid var(--card-border-color) !important;\r\n    box-shadow: var(--card-shadow) !important; \r\n    transition: box-shadow 0.4s ease, transform 0.4s ease !important;\r\n    width: 100% !important;\r\n  }\r\n  .gsx-tile-img:hover { transform: translateY(-4px) !important; box-shadow: var(--shadowHover) !important; border-color: rgba(12,188,65,0.2) !important; }\r\n  .gsx-media {\r\n    width: 100% !important; aspect-ratio: var(--gsx-ratio, 4 \/ 3) !important;\r\n    background: var(--soft) !important; position: relative !important; cursor: pointer !important;\r\n  }\r\n  .gsx-media img {\r\n    position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important;\r\n    object-fit: cover !important; display: block !important;\r\n    transition: transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\r\n  }\r\n  .gsx-media:hover img { transform: scale(1.05) !important; }\r\n  \r\n  .gsx-cta {\r\n    background: #ffffff !important; border: 1px solid var(--card-border-color) !important; \r\n    border-radius: var(--gsx-radius) !important; box-shadow: var(--card-shadow) !important; \r\n    padding: clamp(24px, 3vw, 40px) clamp(20px, 2.5vw, 32px) !important; \r\n    display: flex !important; flex-direction: column !important; justify-content: space-between !important;\r\n    text-align: left !important; width: 100% !important; overflow: hidden !important; \r\n    aspect-ratio: auto !important; \/* Le CTA d\u00e9finit sa propre hauteur *\/\r\n  }\r\n  .gsx-wrap .gsx-cta-badge {\r\n    display: inline-block !important; background: var(--gsx-green-light) !important; \r\n    color: var(--gsx-green) !important; padding: 8px 16px !important;\r\n    border-radius: 999px !important; border: 1px solid rgba(12,188,65,0.15) !important;\r\n    font-size: 11px !important; font-weight: 800 !important;\r\n    letter-spacing: 0.08em !important; text-transform: uppercase !important;\r\n    margin-bottom: clamp(16px, 3vh, 24px) !important; align-self: flex-start !important;\r\n  }\r\n  .gsx-wrap h3.gsx-cta-title { \r\n    font-size: clamp(22px, 2.5vw, 32px) !important; color: var(--ink) !important; \r\n    margin: 0 0 clamp(12px, 2vh, 16px) 0 !important; line-height: 1.2 !important;\r\n    word-wrap: break-word !important; \r\n  }\r\n  .gsx-wrap p.gsx-cta-sub { \r\n    color: var(--muted) !important; font-size: clamp(14px, 1.5vw, 15px) !important; \r\n    line-height: 1.5 !important; margin: 0 0 clamp(24px, 4vh, 40px) 0 !important; \r\n  }\r\n  .gsx-cta-btn {\r\n    display: flex !important; align-items: center !important; justify-content: center !important; \r\n    width: 100% !important; max-width: 100% !important; \r\n    padding: 14px 48px 14px 16px !important; border-radius: var(--gsx-radius-sm) !important; \r\n    background: var(--gsx-green) !important; color: #ffffff !important; \r\n    text-decoration: none !important; font-family: Roboto, sans-serif !important;\r\n    font-size: clamp(12px, 1.2vw, 14px) !important; font-weight: 800 !important;\r\n    text-transform: uppercase !important; transition: all 0.2s ease !important;\r\n    position: relative !important; margin: 0 !important; line-height: 1.3 !important;\r\n  }\r\n  .gsx-cta-btn:hover { \r\n    background: var(--gsx-green-hover) !important; transform: translateY(-2px) !important;\r\n    box-shadow: 0 8px 20px rgba(12,188,65,0.25) !important;\r\n  }\r\n  .gsx-cta-btn-icon {\r\n    position: absolute !important; right: 8px !important; top: 50% !important; \r\n    transform: translateY(-50%) !important; background: rgba(255,255,255,0.25) !important; \r\n    border-radius: 6px !important; width: 32px !important; height: 32px !important;\r\n    display: flex !important; align-items: center !important; justify-content: center !important;\r\n    flex-shrink: 0 !important; \r\n  }\r\n  \r\n  \/* Lightbox *\/\r\n  .gsx-lightbox {\r\n    position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important;\r\n    background: rgba(8, 8, 8, 0.95) !important; backdrop-filter: blur(8px) !important;\r\n    z-index: 99999 !important; display: flex !important; align-items: center !important; justify-content: center !important;\r\n    opacity: 0 !important; pointer-events: none !important; transition: opacity 0.3s ease !important;\r\n  }\r\n  .gsx-lightbox.is-open { opacity: 1 !important; pointer-events: auto !important; }\r\n  .gsx-lightbox-content {\r\n    position: relative !important; max-width: 90vw !important; max-height: 90vh !important;\r\n    transform: scale(0.95) translateY(10px) !important;\r\n    transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1) !important;\r\n  }\r\n  .gsx-lightbox.is-open .gsx-lightbox-content { transform: scale(1) translateY(0) !important; }\r\n  .gsx-lightbox img {\r\n    max-width: 100% !important; max-height: 90vh !important; border-radius: 8px !important;\r\n    box-shadow: 0 30px 60px rgba(0,0,0,0.6) !important; object-fit: contain !important; display: block !important;\r\n  }\r\n  .gsx-lightbox-close {\r\n    position: absolute !important; top: -40px !important; right: 0 !important; background: none !important;\r\n    border: none !important; color: #fff !important; font-size: 32px !important; font-weight: 300 !important;\r\n    cursor: pointer !important; padding: 5px !important; transition: transform 0.2s ease !important;\r\n    font-family: sans-serif !important;\r\n  }\r\n  .gsx-lightbox-close:hover { transform: scale(1.1) !important; }\r\n<\/style>\r\n\r\n<script>\r\n(function(){\r\n  const wrap = document.querySelector('.gsx-wrap');\r\n  if(!wrap) return;\r\n\r\n  const galleryEl = document.getElementById('gsxGallery');\r\n  const filterListEl = document.getElementById('gsxFilterList');\r\n  const sentinel = document.getElementById('gsxScrollSentinel');\r\n  \r\n  const lightbox = document.getElementById('gsxLightbox');\r\n  const lightboxImg = document.getElementById('gsxLightboxImg');\r\n  const lightboxClose = document.querySelector('.gsx-lightbox-close');\r\n\r\n  const CONFIG = {\r\n    siteUrl: \"https:\/\/greensolv.com\",\r\n    contactUrl: wrap.getAttribute('data-contact-url') || \"https:\/\/greensolv.com\/contact-us\/\",\r\n    itemsPerPage: 16, \r\n    firstCtaAt: 8,\r\n    ctaEvery: 24,\r\n    cta: {\r\n      badge: \"CONTACT US\",\r\n      title: \"GET STARTED\",\r\n      sub: \"Talk to one of our specialists today to find the perfect solution for your needs.\",\r\n      button: \"REQUEST A QUOTE\" \r\n    },\r\n    ratios: [\"4 \/ 3\",\"1 \/ 1\",\"3 \/ 4\",\"16 \/ 9\",\"4 \/ 3\",\"3 \/ 4\"]\r\n  };\r\n  \r\n  let allItems = [];\r\n  let currentFilteredItems = [];\r\n  \r\n  \/\/ Nouveau syst\u00e8me de flux (Feed)\r\n  let layoutFeed = []; \r\n  let renderedCount = 0; \r\n  let isFetching = false; \r\n  \r\n  \/\/ Gestion DOM Masonry\r\n  let domColumns = [];\r\n  let virtualColHeights = []; \/\/ Track la hauteur relative pour toujours remplir la plus courte\r\n\r\n  const toggleBtn = wrap.querySelector('.gsx-filter-toggle');\r\n  const panel = document.getElementById('gsxFilterPanel');\r\n  const closeBtn = wrap.querySelector('.gsx-filter-close');\r\n\r\n  function openPanel(){ panel.classList.add('is-open'); panel.setAttribute('aria-hidden','false'); toggleBtn.setAttribute('aria-expanded','true'); }\r\n  function closePanel(){ panel.classList.remove('is-open'); panel.setAttribute('aria-hidden','true'); toggleBtn.setAttribute('aria-expanded','false'); }\r\n  if(toggleBtn) toggleBtn.addEventListener('click', () => panel.classList.contains('is-open') ? closePanel() : openPanel());\r\n  if(closeBtn) closeBtn.addEventListener('click', closePanel);\r\n\r\n  function openLightbox(src, alt) {\r\n    lightboxImg.src = src; lightboxImg.alt = alt || \"\";\r\n    lightbox.classList.add('is-open'); lightbox.setAttribute('aria-hidden', 'false');\r\n  }\r\n  function closeLightbox() {\r\n    lightbox.classList.remove('is-open'); lightbox.setAttribute('aria-hidden', 'true');\r\n    setTimeout(() => lightboxImg.src = \"\", 300);\r\n  }\r\n  if(lightboxClose) lightboxClose.addEventListener('click', closeLightbox);\r\n  if(lightbox) lightbox.addEventListener('click', (e) => { if(e.target === lightbox || e.target === lightbox.querySelector('.gsx-lightbox-content')) closeLightbox(); });\r\n  document.addEventListener('keydown', (e) => { if(e.key === \"Escape\") closeLightbox(); });\r\n\r\n  function escapeHtml(str){ return String(str || \"\").replaceAll(\"&\",\"&amp;\").replaceAll(\"<\",\"&lt;\").replaceAll(\">\",\"&gt;\").replaceAll('\"',\"&quot;\").replaceAll(\"'\",\"&#039;\"); }\r\n\r\n  function buildFiltersFromItems(items){\r\n    filterListEl.innerHTML = \"\";\r\n    const termSet = new Set();\r\n    items.forEach(item => { if(item.terms && Array.isArray(item.terms)) item.terms.forEach(t => termSet.add(t)); });\r\n    const sortedTerms = Array.from(termSet).sort();\r\n\r\n    const allPill = makePill(\"ALL\", \"all\");\r\n    allPill.classList.add('is-active');\r\n    filterListEl.appendChild(allPill);\r\n\r\n    sortedTerms.forEach(term => {\r\n      const label = term.replace(\/-\/g, ' ').toUpperCase();\r\n      filterListEl.appendChild(makePill(label, term));\r\n    });\r\n  }\r\n\r\n  function makePill(label, term){\r\n    const b = document.createElement('button');\r\n    b.type = \"button\"; b.className = \"gsx-pill\"; b.textContent = label; b.dataset.term = term;\r\n    b.addEventListener('click', () => applyFilter(term));\r\n    return b;\r\n  }\r\n\r\n  \/\/ --- NOUVELLE LOGIQUE MASONRY ---\r\n  \r\n  function getColumnCount() {\r\n    const w = window.innerWidth;\r\n    if (w <= 620) return 1;\r\n    if (w <= 980) return 2;\r\n    if (w <= 1280) return 3;\r\n    return 4;\r\n  }\r\n\r\n  function initColumns() {\r\n    galleryEl.innerHTML = \"\";\r\n    domColumns = [];\r\n    virtualColHeights = [];\r\n    const count = getColumnCount();\r\n    for(let i=0; i<count; i++) {\r\n      const col = document.createElement('div');\r\n      col.className = 'gsx-masonry-column';\r\n      domColumns.push(col);\r\n      virtualColHeights.push(0);\r\n      galleryEl.appendChild(col);\r\n    }\r\n  }\r\n\r\n  \/\/ Pr\u00e9pare un tableau \"Feed\" d\u00e9terministe garantissant la distance entre les CTAs\r\n  function prepareLayoutFeed(items) {\r\n    const feed = [];\r\n    let itemsSinceLastCta = 0;\r\n    let totalImages = 0;\r\n\r\n    for (let i = 0; i < items.length; i++) {\r\n      feed.push({ type: 'image', data: items[i], imageIndex: totalImages });\r\n      totalImages++;\r\n      itemsSinceLastCta++;\r\n\r\n      \/\/ S\u00e9curit\u00e9 anti-collision: Minimum 14 tuiles entre deux CTA (~3.5 rang\u00e9es d'\u00e9cart garanties)\r\n      if (\r\n        (totalImages === CONFIG.firstCtaAt || itemsSinceLastCta >= CONFIG.ctaEvery) \r\n        && itemsSinceLastCta >= 14 \r\n      ) {\r\n        feed.push({ type: 'cta' });\r\n        itemsSinceLastCta = 0;\r\n      }\r\n    }\r\n    return feed;\r\n  }\r\n\r\n  \/\/ Calcule la \"hauteur relative\" d'une tuile pour toujours remplir la colonne la plus courte\r\n  function getRelativeHeight(ratioString) {\r\n    const parts = ratioString.split('\/');\r\n    if(parts.length === 2) {\r\n       const ratio = parseFloat(parts[0]) \/ parseFloat(parts[1]);\r\n       return 1 \/ ratio; \/\/ Hauteur relative par rapport \u00e0 la largeur\r\n    }\r\n    return 1;\r\n  }\r\n\r\n  function createImageTile(item, idx){\r\n    const tile = document.createElement('article'); \r\n    tile.className = \"gsx-tile gsx-tile-img gsx-animate-in\";\r\n    const ratioStr = CONFIG.ratios[idx % CONFIG.ratios.length];\r\n    \r\n    const media = document.createElement('div'); \r\n    media.className = \"gsx-media\";\r\n    media.style.setProperty(\"--gsx-ratio\", ratioStr);\r\n\r\n    const img = document.createElement('img');\r\n    img.loading = \"lazy\"; img.src = item.src; img.alt = item.alt || \"\";\r\n    media.appendChild(img); tile.appendChild(media);\r\n    tile.addEventListener('click', () => openLightbox(item.src, item.alt));\r\n    \r\n    \/\/ On attache la hauteur relative estim\u00e9e pour le calcul de Masonry\r\n    tile.dataset.relHeight = getRelativeHeight(ratioStr);\r\n    return tile;\r\n  }\r\n\r\n  function createCtaTile(){\r\n    const tile = document.createElement('article'); \r\n    tile.className = \"gsx-tile gsx-cta gsx-animate-in\";\r\n    tile.innerHTML = `\r\n      <div>\r\n        <span class=\"gsx-cta-badge\">${escapeHtml(CONFIG.cta.badge)}<\/span>\r\n        <h3 class=\"gsx-cta-title\">${escapeHtml(CONFIG.cta.title)}<\/h3>\r\n        <p class=\"gsx-cta-sub\">${escapeHtml(CONFIG.cta.sub)}<\/p>\r\n      <\/div>\r\n      <a class=\"gsx-cta-btn\" href=\"${CONFIG.contactUrl}\">\r\n        ${escapeHtml(CONFIG.cta.button)}\r\n        <span class=\"gsx-cta-btn-icon\">\r\n          <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 18l6-6-6-6\"\/><\/svg>\r\n        <\/span>\r\n      <\/a>\r\n    `;\r\n    tile.dataset.relHeight = 1.3; \/\/ Hauteur relative arbitraire pour le CTA\r\n    return tile;\r\n  }\r\n\r\n  function renderNextBatch(){\r\n    if(layoutFeed.length === 0){\r\n      galleryEl.innerHTML = `<div class=\"gsx-empty-state gsx-animate-in\">No images found.<\/div>`;\r\n      if(sentinel) sentinel.style.display = 'none';\r\n      return;\r\n    }\r\n\r\n    const batch = layoutFeed.slice(renderedCount, renderedCount + CONFIG.itemsPerPage);\r\n    if(batch.length === 0) {\r\n      if(sentinel) sentinel.style.display = 'none';\r\n      return;\r\n    }\r\n\r\n    batch.forEach((feedItem, batchIdx) => {\r\n      \/\/ Trouver la colonne la moins haute virtuellement\r\n      let shortestColIdx = 0;\r\n      let minHeight = virtualColHeights[0];\r\n      for(let i = 1; i < virtualColHeights.length; i++) {\r\n        if(virtualColHeights[i] < minHeight) {\r\n          minHeight = virtualColHeights[i];\r\n          shortestColIdx = i;\r\n        }\r\n      }\r\n\r\n      let el;\r\n      if(feedItem.type === 'image') el = createImageTile(feedItem.data, feedItem.imageIndex);\r\n      else el = createCtaTile();\r\n      \r\n      \/\/ Petit d\u00e9lai fluide pour l'apparition\r\n      el.style.animationDelay = `${(batchIdx % 8) * 0.08}s`;\r\n\r\n      domColumns[shortestColIdx].appendChild(el);\r\n      virtualColHeights[shortestColIdx] += parseFloat(el.dataset.relHeight);\r\n      renderedCount++;\r\n    });\r\n\r\n    if (sentinel) {\r\n      sentinel.style.display = renderedCount >= layoutFeed.length ? 'none' : 'block';\r\n    }\r\n    isFetching = false;\r\n  }\r\n\r\n  function applyFilter(term){\r\n    const activeTerm = term || \"all\";\r\n    currentFilteredItems = activeTerm === \"all\" \r\n      ? allItems \r\n      : allItems.filter(item => item.terms && item.terms.includes(activeTerm));\r\n      \r\n    \/\/ Reset complet\r\n    layoutFeed = prepareLayoutFeed(currentFilteredItems);\r\n    renderedCount = 0;\r\n    initColumns();\r\n    renderNextBatch();\r\n    \r\n    filterListEl.querySelectorAll('.gsx-pill').forEach(p => p.classList.toggle('is-active', p.dataset.term === activeTerm));\r\n\r\n    if (wrap) {\r\n      const topOffset = wrap.getBoundingClientRect().top + window.scrollY - 80; \r\n      window.scrollTo({ top: topOffset, behavior: 'smooth' });\r\n    }\r\n  }\r\n\r\n  if (sentinel) {\r\n    const observer = new IntersectionObserver((entries) => {\r\n      if (entries[0].isIntersecting && !isFetching && renderedCount < layoutFeed.length) {\r\n        isFetching = true;\r\n        requestAnimationFrame(() => renderNextBatch());\r\n      }\r\n    }, { rootMargin: \"600px\" }); \r\n    observer.observe(sentinel);\r\n  }\r\n\r\n  \/\/ Re-calcul du layout si redimensionnement (Debounc\u00e9 pour les perfs)\r\n  let resizeTimer;\r\n  window.addEventListener('resize', () => {\r\n    clearTimeout(resizeTimer);\r\n    resizeTimer = setTimeout(() => {\r\n      if(domColumns.length !== getColumnCount() && layoutFeed.length > 0) {\r\n        const tempCount = renderedCount;\r\n        renderedCount = 0;\r\n        initColumns();\r\n        \/\/ On force le re-rendu de ce qui \u00e9tait d\u00e9j\u00e0 affich\u00e9\r\n        while(renderedCount < tempCount) {\r\n           const batchSize = Math.min(CONFIG.itemsPerPage, tempCount - renderedCount);\r\n           const prevItemsPerPage = CONFIG.itemsPerPage;\r\n           CONFIG.itemsPerPage = batchSize;\r\n           renderNextBatch();\r\n           CONFIG.itemsPerPage = prevItemsPerPage;\r\n        }\r\n      }\r\n    }, 250);\r\n  });\r\n\r\n  async function fetchAllMedia(){\r\n    const base = CONFIG.siteUrl.replace(\/\\\/$\/,\"\");\r\n    const url = `${base}\/wp-json\/gsx\/v1\/gallery`; \r\n    try {\r\n      const r = await fetch(url);\r\n      if(!r.ok) throw new Error(\"HTTP \" + r.status);\r\n      return await r.json();\r\n    } catch (error) {\r\n      console.warn(\"API request failed \u2014 using demo data.\");\r\n      const mockData = [];\r\n      const termsList = [\"equipment\", \"laboratory\", \"powder-coating\", \"wheels\"];\r\n      for(let i=1; i<=42; i++) {\r\n        mockData.push({\r\n          src: `https:\/\/picsum.photos\/seed\/greensolv${i}\/800\/600`,\r\n          alt: `Demo Image ${i}`,\r\n          terms: [termsList[Math.floor(Math.random() * termsList.length)]]\r\n        });\r\n      }\r\n      return mockData;\r\n    }\r\n  }\r\n\r\n  (async function init(){\r\n    initColumns();\r\n    \/\/ Skeleton temp (juste sur la premi\u00e8re colonne)\r\n    domColumns[0].innerHTML = `<div class=\"gsx-skeleton\" style=\"--gsx-ratio: 4\/3\"><\/div><div class=\"gsx-skeleton\" style=\"--gsx-ratio: 16\/9\"><\/div>`;\r\n    domColumns[1]?.insertAdjacentHTML('beforeend', `<div class=\"gsx-skeleton\" style=\"--gsx-ratio: 1\/1\"><\/div>`);\r\n    \r\n    try {\r\n      allItems = await fetchAllMedia(); \r\n      currentFilteredItems = allItems;\r\n      buildFiltersFromItems(allItems);\r\n      \r\n      layoutFeed = prepareLayoutFeed(currentFilteredItems);\r\n      renderedCount = 0;\r\n      initColumns(); \/\/ Efface les skeletons\r\n      renderNextBatch();\r\n    } catch(err) {\r\n      galleryEl.innerHTML = `<div class=\"gsx-empty-state gsx-animate-in\">Something went wrong while loading the gallery.<\/div>`;\r\n    }\r\n  })();\r\n})();\r\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Le syst\u00e8me de d\u00e9capage id\u00e9al pour l'industrie a\u00e9rospatiale !<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"_angie_page":false,"footnotes":""},"class_list":["post-398","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>Gallery | Greensolv<\/title>\n<meta name=\"description\" content=\"Explore Greensolv stripping systems, equipment, facilities and industrial solutions designed for cleaner and more efficient operations.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/greensolv.com\/fr\/galerie-greensolv\/\" \/>\n<meta property=\"og:locale\" content=\"fr_CA\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Gallery | Greensolv\" \/>\n<meta property=\"og:description\" content=\"Explore Greensolv stripping systems, equipment, facilities and industrial solutions designed for cleaner and more efficient operations.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/greensolv.com\/fr\/galerie-greensolv\/\" \/>\n<meta property=\"og:site_name\" content=\"Greensolv\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/greensolv\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-04-02T01:22:06+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Estimation du temps de lecture\" \/>\n\t<meta name=\"twitter:data1\" content=\"16 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/greensolv.com\\\/gallery-greensolv\\\/\",\"url\":\"https:\\\/\\\/greensolv.com\\\/gallery-greensolv\\\/\",\"name\":\"Gallery | Greensolv\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/greensolv.com\\\/#website\"},\"datePublished\":\"2025-10-08T19:42:58+00:00\",\"dateModified\":\"2026-04-02T01:22:06+00:00\",\"description\":\"Explore Greensolv stripping systems, equipment, facilities and industrial solutions designed for cleaner and more efficient operations.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/greensolv.com\\\/gallery-greensolv\\\/#breadcrumb\"},\"inLanguage\":\"fr-CA\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/greensolv.com\\\/gallery-greensolv\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/greensolv.com\\\/gallery-greensolv\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/greensolv.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Gallery | Greensolv\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/greensolv.com\\\/#website\",\"url\":\"https:\\\/\\\/greensolv.com\\\/\",\"name\":\"Greensolv\",\"description\":\"Greensolv\",\"publisher\":{\"@id\":\"https:\\\/\\\/greensolv.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/greensolv.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"fr-CA\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/greensolv.com\\\/#organization\",\"name\":\"Greensolv\",\"url\":\"https:\\\/\\\/greensolv.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-CA\",\"@id\":\"https:\\\/\\\/greensolv.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/greensolv.com\\\/wp-content\\\/uploads\\\/2025\\\/09\\\/greensolv.png\",\"contentUrl\":\"https:\\\/\\\/greensolv.com\\\/wp-content\\\/uploads\\\/2025\\\/09\\\/greensolv.png\",\"width\":400,\"height\":95,\"caption\":\"Greensolv\"},\"image\":{\"@id\":\"https:\\\/\\\/greensolv.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/greensolv\\\/\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/greensolv-inc.\\\/?originalSubdomain=fr\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UC2QinAkroLYi9IWiI535img\",\"https:\\\/\\\/www.instagram.com\\\/greensolv_inc\\\/\",\"https:\\\/\\\/www.tiktok.com\\\/@greensolv?lang=en\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Galerie | Greensolv","description":"D\u00e9couvrez les syst\u00e8mes de d\u00e9capage, les \u00e9quipements, les installations et les solutions industrielles de Greensolv, con\u00e7us pour des op\u00e9rations plus propres et plus efficaces.","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:\/\/greensolv.com\/fr\/galerie-greensolv\/","og_locale":"fr_CA","og_type":"article","og_title":"Gallery | Greensolv","og_description":"Explore Greensolv stripping systems, equipment, facilities and industrial solutions designed for cleaner and more efficient operations.","og_url":"https:\/\/greensolv.com\/fr\/galerie-greensolv\/","og_site_name":"Greensolv","article_publisher":"https:\/\/www.facebook.com\/greensolv\/","article_modified_time":"2026-04-02T01:22:06+00:00","twitter_card":"summary_large_image","twitter_misc":{"Estimation du temps de lecture":"16 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/greensolv.com\/gallery-greensolv\/","url":"https:\/\/greensolv.com\/gallery-greensolv\/","name":"Galerie | Greensolv","isPartOf":{"@id":"https:\/\/greensolv.com\/#website"},"datePublished":"2025-10-08T19:42:58+00:00","dateModified":"2026-04-02T01:22:06+00:00","description":"D\u00e9couvrez les syst\u00e8mes de d\u00e9capage, les \u00e9quipements, les installations et les solutions industrielles de Greensolv, con\u00e7us pour des op\u00e9rations plus propres et plus efficaces.","breadcrumb":{"@id":"https:\/\/greensolv.com\/gallery-greensolv\/#breadcrumb"},"inLanguage":"fr-CA","potentialAction":[{"@type":"ReadAction","target":["https:\/\/greensolv.com\/gallery-greensolv\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/greensolv.com\/gallery-greensolv\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/greensolv.com\/"},{"@type":"ListItem","position":2,"name":"Gallery | Greensolv"}]},{"@type":"WebSite","@id":"https:\/\/greensolv.com\/#website","url":"https:\/\/greensolv.com\/","name":"Greensolv","description":"Greensolv","publisher":{"@id":"https:\/\/greensolv.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/greensolv.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"fr-CA"},{"@type":"Organization","@id":"https:\/\/greensolv.com\/#organization","name":"Greensolv","url":"https:\/\/greensolv.com\/","logo":{"@type":"ImageObject","inLanguage":"fr-CA","@id":"https:\/\/greensolv.com\/#\/schema\/logo\/image\/","url":"https:\/\/greensolv.com\/wp-content\/uploads\/2025\/09\/greensolv.png","contentUrl":"https:\/\/greensolv.com\/wp-content\/uploads\/2025\/09\/greensolv.png","width":400,"height":95,"caption":"Greensolv"},"image":{"@id":"https:\/\/greensolv.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/greensolv\/","https:\/\/www.linkedin.com\/company\/greensolv-inc.\/?originalSubdomain=fr","https:\/\/www.youtube.com\/channel\/UC2QinAkroLYi9IWiI535img","https:\/\/www.instagram.com\/greensolv_inc\/","https:\/\/www.tiktok.com\/@greensolv?lang=en"]}]}},"_links":{"self":[{"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/pages\/398","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/comments?post=398"}],"version-history":[{"count":155,"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/pages\/398\/revisions"}],"predecessor-version":[{"id":3582,"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/pages\/398\/revisions\/3582"}],"wp:attachment":[{"href":"https:\/\/greensolv.com\/fr\/wp-json\/wp\/v2\/media?parent=398"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}