{"id":7604,"date":"2026-08-25T15:14:01","date_gmt":"2026-08-25T13:14:01","guid":{"rendered":"https:\/\/www.infinigate.com\/be\/?page_id=7604"},"modified":"2026-08-27T12:16:35","modified_gmt":"2026-08-27T10:16:35","slug":"meet-our-team","status":"publish","type":"page","link":"https:\/\/www.infinigate.com\/be\/meet-our-team\/","title":{"rendered":"Meet our Team"},"content":{"rendered":"\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<!--\n  Infinigate Belgium \u2014 Team Directory\n\n  HOW TO INSTALL\n  Paste this whole file into the HTML tab of a single \"Custom HTML\" block, and\n  leave that block's CSS and JS tabs EMPTY. Everything needed is in here, in\n  the order it has to be in. Splitting it across the three tabs would reorder\n  it \u2014 the block puts CSS first, then JS, then markup \u2014 and the code would run\n  before the page it drives exists.\n\n  DYNAMIC: nothing about the team is hardcoded here. On load the block:\n    1. finds the \"Team\" media folder via the WordPress REST API,\n    2. downloads team.xlsx from that folder and parses it in the browser,\n    3. matches each person's Photo column to an image in the same folder.\n\n  So to add, remove or update a colleague: edit team.xlsx and re-upload it to\n  the Team folder, and drop their photo in the same folder. No code changes.\n\n  No external libraries, no theme or plugin changes. The .xlsx is a ZIP, and it\n  is unpacked with the browser's native DecompressionStream.\n\n  WHY THERE ARE TWO SCRIPTS\n  WordPress edits code it finds in post content \u2014 see the loader's own comment\n  below. The short first script repairs what WordPress changed and runs the\n  second one, which holds the directory itself and is deliberately inert until\n  then. Keep them in this order, and keep them in this block.\n-->\n\n<div class=\"ig-team\" id=\"ig-team\">\n\n  <!-- Print only. Becomes page 1 of the PDF; never shown on screen. -->\n  <section class=\"ig-team__cover\" id=\"ig-team-cover\" aria-hidden=\"true\">\n    <img class=\"ig-team__cover-logo\" id=\"ig-team-cover-logo\" alt=\"\">\n    <p class=\"ig-team__cover-title\" id=\"ig-team-cover-title\"><\/p>\n    <span class=\"ig-team__cover-rule\"><\/span>\n    <p class=\"ig-team__cover-sub\" id=\"ig-team-cover-sub\"><\/p>\n    <p class=\"ig-team__cover-meta\" id=\"ig-team-cover-meta\"><\/p>\n    <p class=\"ig-team__cover-filters\" id=\"ig-team-cover-filters\"><\/p>\n    <p class=\"ig-team__cover-addr\" id=\"ig-team-cover-addr\"><\/p>\n  <\/section>\n\n  <div class=\"ig-team__bar\" id=\"ig-team-bar\" hidden>\n    <div class=\"ig-team__bar-inner\">\n\n      <div class=\"ig-team__search\">\n        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"7\"\/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"\/><\/svg>\n        <label class=\"ig-team__sr\" for=\"ig-team-search\">Search team<\/label>\n        <input type=\"search\" id=\"ig-team-search\" placeholder=\"Search by name or job title\u2026\" autocomplete=\"off\">\n      <\/div>\n\n      <div class=\"ig-team__msel\" data-facet=\"departments\">\n        <button type=\"button\" class=\"ig-team__msel-btn\" aria-expanded=\"false\">Department <span class=\"ig-team__count\" hidden><\/span><\/button>\n        <div class=\"ig-team__msel-panel\" hidden><\/div>\n      <\/div>\n\n      <div class=\"ig-team__msel\" data-facet=\"products\">\n        <button type=\"button\" class=\"ig-team__msel-btn\" aria-expanded=\"false\">Product <span class=\"ig-team__count\" hidden><\/span><\/button>\n        <div class=\"ig-team__msel-panel\" hidden><\/div>\n      <\/div>\n\n      <div class=\"ig-team__msel\" data-facet=\"jobTitles\">\n        <button type=\"button\" class=\"ig-team__msel-btn\" aria-expanded=\"false\">Job title <span class=\"ig-team__count\" hidden><\/span><\/button>\n        <div class=\"ig-team__msel-panel\" hidden><\/div>\n      <\/div>\n\n      <button type=\"button\" class=\"ig-team__btn ig-team__btn--ghost\" id=\"ig-team-clear\">Clear filters<\/button>\n\n      <button type=\"button\" class=\"ig-team__btn ig-team__btn--coral\" id=\"ig-team-print\">\n        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/><polyline points=\"7 10 12 15 17 10\"\/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"\/><\/svg>\n        Save as PDF\n      <\/button>\n\n    <\/div>\n    <p class=\"ig-team__result\" id=\"ig-team-result\" aria-live=\"polite\"><\/p>\n  <\/div>\n\n  <div class=\"ig-team__content\" id=\"ig-team-content\">\n    <div class=\"ig-team__status\" id=\"ig-team-status\">\n      <div class=\"ig-team__spinner\" aria-hidden=\"true\"><\/div>\n      <p>Loading the team\u2026<\/p>\n    <\/div>\n  <\/div>\n\n<\/div>\n\n<style>\n.ig-team {\n    --igt-navy: var(--color--blue, #153447);\n    --igt-coral: var(--color--coral, #ed5740);\n    --igt-coral-dark: #d4432c;\n    --igt-tint: #e5edf4;\n    --igt-grey: var(--color--gray-25, #f4f4f6);\n    --igt-line: var(--color--gray-200, #e5edf4);\n    --igt-text: var(--color--gray-800, #1d2939);\n    --igt-muted: var(--color--gray-600, #475467);\n    --igt-radius: var(--border-radius--m, 10px);\n    --igt-shadow: 0 3px 9px hsla(207, 53%, 18%, 0.12);\n    --igt-shadow-hover: 0 3px 20px 3px hsla(207, 53%, 18%, 0.16);\n\n    color: var(--igt-text);\n}\n\n.ig-team *,\n.ig-team *::before,\n.ig-team *::after { box-sizing: border-box; }\n\n\/* Filtering toggles the [hidden] attribute. Class rules below set display on\n   several of these elements, which would beat the browser default for\n   [hidden], so hiding is enforced here. *\/\n.ig-team [hidden] { display: none !important; }\n\n\/* The theme sets a global `svg, table { width: 100% }`. Inside any flex\n   parent with a definite width that resolves and blows the icons up, so\n   every icon in this block gets an explicit size. *\/\n.ig-team svg {\n    width: auto;\n    height: auto;\n    max-width: none;\n    flex: none;\n}\n\n.ig-team__search svg { width: 16px; height: 16px; }\n.ig-team__btn svg   { width: 15px; height: 15px; }\n.ig-team__email svg { width: 13px; height: 13px; }\n\n.ig-team__sr {\n    position: absolute;\n    width: 1px; height: 1px;\n    padding: 0; margin: -1px;\n    overflow: hidden;\n    clip: rect(0 0 0 0);\n    white-space: nowrap;\n}\n\n\/* ---------- Loading \/ error ---------- *\/\n.ig-team__status { text-align: center; padding: var(--space--xxl, 64px) var(--space--s, 16px); color: var(--igt-muted); }\n.ig-team__status p { font-size: 20px; margin: var(--space--xs, 12px) 0 0; }\n.ig-team__status--error { color: var(--igt-coral-dark); }\n.ig-team__status--error code {\n    display: block;\n    margin-top: var(--space--xxs, 8px);\n    color: var(--igt-muted);\n    word-break: break-word;\n    font-size: 16px;\n}\n\n\/* Old browser that cannot unpack the .xlsx. Reads as information, not as an\n   error, because there is nothing wrong with the page or the data. *\/\n.ig-team__status--notice { color: var(--igt-text); max-width: 46em; margin: 0 auto; }\n.ig-team__status--notice a { color: var(--igt-coral-dark); }\n\n.ig-team__spinner {\n    width: 28px;\n    height: 28px;\n    margin: 0 auto;\n    border: 3px solid var(--igt-line);\n    border-top-color: var(--igt-coral);\n    border-radius: 50%;\n    animation: ig-team-spin 700ms linear infinite;\n}\n\n@keyframes ig-team-spin { to { transform: rotate(360deg); } }\n\n@media (prefers-reduced-motion: reduce) {\n    .ig-team__spinner { animation: none; }\n}\n\n\/* ---------- Filter bar ---------- *\/\n\/* `top` cannot be var(--site-header--bottom): the theme defines that property\n   as `auto`, and a var() fallback only applies when the property is undefined,\n   not when it holds an unusable value \u2014 so the bar would resolve to\n   `top: auto` and silently never stick. The site header is not fixed on\n   desktop, so 0 is the correct offset. *\/\n.ig-team__bar {\n    position: sticky;\n    top: 0;\n    z-index: 20;\n    background: #fff;\n    border-bottom: 1px solid var(--igt-line);\n    padding-bottom: var(--space--xxs, 8px);\n    margin-bottom: var(--space--l, 32px);\n}\n\n.ig-team__bar-inner {\n    display: flex;\n    flex-wrap: wrap;\n    gap: var(--space--xxs, 8px);\n    align-items: center;\n    padding: var(--space--xs, 12px) 0;\n}\n\n.ig-team__search {\n    flex: 1 1 220px;\n    display: flex;\n    align-items: center;\n    gap: var(--space--xxs, 8px);\n    background: var(--igt-grey);\n    border: 1px solid var(--igt-line);\n    border-radius: var(--igt-radius);\n    padding: var(--space--xxs, 8px) var(--space--xs, 12px);\n}\n\n.ig-team__search svg { color: var(--igt-muted); }\n\n.ig-team__search input {\n    border: 0;\n    background: none;\n    outline: none;\n    width: 100%;\n    min-width: 0;\n    font: inherit;\n    color: inherit;\n    -webkit-appearance: none;\n}\n\n.ig-team__msel { position: relative; }\n\n.ig-team__msel-btn,\n.ig-team__btn {\n    display: flex;\n    align-items: center;\n    gap: 7px;\n    font: inherit;\n    font-weight: 600;\n    line-height: 1.2;\n    border-radius: var(--igt-radius);\n    padding: 9px 14px;\n    cursor: pointer;\n    white-space: nowrap;\n    transition: background-color 200ms, border-color 200ms, color 200ms;\n}\n\n.ig-team__msel-btn,\n.ig-team__btn--ghost {\n    background: var(--igt-grey);\n    border: 1px solid var(--igt-line);\n    color: var(--igt-text);\n}\n\n.ig-team__msel-btn:hover,\n.ig-team__btn--ghost:hover { border-color: var(--igt-navy); }\n\n.ig-team__msel-btn[aria-expanded=\"true\"],\n.ig-team__msel-btn.is-active {\n    border-color: var(--igt-navy);\n    background: var(--igt-tint);\n    color: var(--igt-navy);\n}\n\n.ig-team__count {\n    background: var(--igt-coral);\n    color: #fff;\n    font-size: 15px;\n    font-weight: 700;\n    border-radius: 20px;\n    padding: 1px 7px;\n}\n\n.ig-team__btn--coral {\n    background: var(--igt-coral);\n    border: 1px solid var(--igt-coral);\n    color: #fff;\n    margin-left: auto;\n}\n\n.ig-team__btn--coral:hover { background: var(--igt-coral-dark); border-color: var(--igt-coral-dark); }\n\n.ig-team__msel-panel {\n    position: absolute;\n    top: calc(100% + var(--space--xxs, 8px));\n    left: 0;\n    min-width: 240px;\n    max-height: 320px;\n    overflow-y: auto;\n    background: #fff;\n    border: 1px solid var(--igt-line);\n    border-radius: var(--border-radius--l, 16px);\n    box-shadow: var(--igt-shadow-hover);\n    padding: var(--space--xs, 12px);\n    z-index: 30;\n}\n\n.ig-team__msel-option {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    padding: 6px;\n    border-radius: 6px;\n    cursor: pointer;\n}\n\n.ig-team__msel-option:hover { background: var(--igt-grey); }\n.ig-team__msel-option input { accent-color: var(--igt-navy); margin: 0; }\n\n.ig-team__result {\n    color: var(--igt-muted);\n    margin: 0;\n}\n\n.ig-team__result b { color: var(--igt-navy); }\n\n.ig-team__chip {\n    display: inline-flex;\n    align-items: center;\n    gap: 5px;\n    background: var(--igt-tint);\n    color: var(--igt-navy);\n    border-radius: 20px;\n    padding: 2px 6px 2px 12px;\n    font-weight: 600;\n    margin: 2px 4px 2px 0;\n}\n\n.ig-team__chip button {\n    border: 0;\n    background: none;\n    cursor: pointer;\n    color: inherit;\n    line-height: 1;\n    padding: 0 4px;\n}\n\n\/* ---------- Sections + cards ---------- *\/\n.ig-team__section { margin-bottom: var(--space--xl, 48px); }\n\n.ig-team__section-head {\n    display: flex;\n    align-items: baseline;\n    gap: var(--space--xs, 12px);\n    margin-bottom: var(--space--m, 24px);\n    padding-bottom: var(--space--xs, 12px);\n    border-bottom: 2px solid var(--igt-line);\n}\n\n.ig-team__section-head h3 {\n    margin: 0;\n    font-size: clamp(20px, 2.2vw, 26px);\n    color: var(--igt-navy);\n    font-weight: 600;\n}\n\n.ig-team__section-count { color: var(--igt-muted); font-weight: 600; }\n\n.ig-team__grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n    gap: var(--space--m, 24px);\n}\n\n.ig-team__card {\n    display: flex;\n    flex-direction: column;\n    background: #fff;\n    border: 1px solid var(--igt-line);\n    border-radius: var(--border-radius--l, 16px);\n    box-shadow: var(--igt-shadow);\n    overflow: hidden;\n    transition: transform 200ms, box-shadow 200ms;\n}\n\n.ig-team__card:hover { transform: translateY(-4px); box-shadow: var(--igt-shadow-hover); }\n\n.ig-team__photo {\n    aspect-ratio: 3 \/ 2;\n    background: var(--igt-tint);\n}\n\n.ig-team__photo img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    display: block;\n}\n\n.ig-team__body {\n    display: flex;\n    flex-direction: column;\n    gap: var(--space--xxxs, 4px);\n    flex: 1;\n    padding: var(--space--s, 16px);\n}\n\n\/* The theme lets paragraphs keep the browser's default margins, which would\n   stack on top of the flex gap below and space the card out unevenly. The\n   card controls its own rhythm. *\/\n.ig-team__body p { margin: 0; }\n\n.ig-team__name { font-size: 17px; font-weight: 700; color: var(--igt-navy); }\n.ig-team__name { font-size: 17px; font-weight: 700; color: var(--igt-navy); }\np.ig-team__job { font-size: 16px; color: var(--igt-muted); line-height: 1.2; margin-bottom: 8px; }\n\n.ig-team__email {\n    display: flex;\n    align-items: center;\n    font-size: 14px;\n    gap: 6px;\n    color: var(--igt-muted);\n    text-decoration: none;\n    word-break: break-all;\n}\n\n.ig-team__email:hover { color: var(--igt-coral); }\n\n.ig-team__tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: var(--space--xs, 12px); }\n\n.ig-team__tag {\n    font-size: 16px;\n    font-weight: 600;\n    border-radius: 20px;\n    padding: 3px 10px;\n}\n\n.ig-team__tag--dept { background: var(--igt-tint); color: var(--igt-navy); }\n.ig-team__tag--product { background: #fdece9; color: var(--igt-coral-dark); }\n\n.ig-team__empty { text-align: center; padding: var(--space--xxl, 64px) var(--space--s, 16px); color: var(--igt-muted); }\n.ig-team__empty h3 { color: var(--igt-navy); margin: 0 0 var(--space--xxs, 8px); }\n\n\/* ---------- Print \/ Save as PDF ---------- *\/\n\n\/* The cover is page one of the PDF and nothing at all on screen. *\/\n.ig-team__cover { display: none; }\n\n\/* Page geometry. There are no left and right page margins: the running header\n   band has to reach the paper edge, and a margin box can only ever be as wide\n   as the margin band it sits in. The side gutters are given back to the\n   content as horizontal padding further down, which \u2014 unlike vertical padding\n   \u2014 is applied again on every page a block breaks across.\n\n   The header band itself is injected as an @page rule by the script, because\n   it is drawn as an SVG carrying the logo and the document title. It has to be\n   an image rather than a background colour: Chrome's print dialog has\n   \"Background graphics\" switched off by default, and margin boxes ignore\n   print-color-adjust, so a coloured band would simply not print for most\n   people. An image always does. *\/\n@page {\n    size: A4;\n    margin: 26mm 0 18mm;\n}\n\n\/* Page one: no margins, no running header, no footer. *\/\n@page igteamcover {\n    margin: 0;\n\n    @top-center   { content: none; }\n    @bottom-left  { content: none; }\n    @bottom-right { content: none; }\n}\n\n\/* The previous approach hid the page with `visibility: hidden` and pulled the\n   block to the top with `position: absolute`. Both backfired. Hidden elements\n   keep their layout, so the first page opened with a page-height gap where the\n   site header and hero used to be; and an absolutely positioned box does not\n   paginate, so everything after the first section was silently dropped \u2014 a\n   PDF of eleven people came out with four.\n\n   Instead, JavaScript marks the block's ancestor chain on beforeprint, and\n   everything off that path is taken out of the layout completely. The\n   directory is left as an ordinary block that flows and breaks across pages. *\/\n@media print {\n    .ig-team-print-ancestor > *:not(.ig-team-print-ancestor):not(.ig-team) {\n        display: none !important;\n    }\n\n    \/* The page box now owns the margins, so the theme's wrappers must not add\n       their own gutters, widths or chrome on top. *\/\n    .ig-team-print-ancestor {\n        margin: 0 !important;\n        padding: 0 !important;\n        max-width: none !important;\n        width: auto !important;\n        background: none !important;\n        box-shadow: none !important;\n        border: 0 !important;\n    }\n\n    \/* Backgrounds and tints are dropped by default when printing. Asking for\n       them explicitly is what keeps the cover navy, the section bars tinted\n       and the tags coloured without anyone touching the print dialog. *\/\n    .ig-team,\n    .ig-team * {\n        -webkit-print-color-adjust: exact;\n        print-color-adjust: exact;\n    }\n\n    .ig-team { font-size: 10pt; }\n\n    .ig-team__bar,\n    .ig-team__empty { display: none !important; }\n\n    \/* ---- Page 1: cover ---- *\/\n\n    \/* Deliberately plain block flow. A flex column with `margin-top: auto`\n       laid out correctly on screen but came out empty once the named page\n       gave this box its own geometry \u2014 the auto margin and the page's\n       fragmentation disagreed about how tall the container was. Fixed\n       margins down the page have no such argument with the paginator. *\/\n    .ig-team__cover {\n        page: igteamcover;\n        break-after: page;\n        display: block;\n        height: 296mm;\n        margin: 0;\n        padding: 82mm 20mm 0;\n        background: var(--igt-navy);\n        color: #fff;\n        text-align: center;\n    }\n\n    .ig-team__cover-logo { display: inline-block; width: 62mm; height: auto; }\n\n    .ig-team__cover-title {\n        margin: 40mm 0 0;\n        font-size: 30pt;\n        font-weight: 700;\n        line-height: 1.1;\n        color: #fff;\n    }\n\n    .ig-team__cover-rule {\n        display: block;\n        width: 28mm;\n        height: 1.2mm;\n        margin: 6mm auto 0;\n        border-radius: 1mm;\n        background: var(--igt-coral);\n    }\n\n    .ig-team__cover-sub { margin: 8mm 0 0; font-size: 12pt; color: #b9c7d4; }\n    .ig-team__cover-meta { margin: 78mm 0 0; font-size: 9.5pt; color: #8fa3b5; }\n    .ig-team__cover-filters { margin: 2mm 0 0; font-size: 9.5pt; color: #8fa3b5; }\n    .ig-team__cover-addr { margin: 22mm 0 0; font-size: 8pt; color: #6f8395; }\n\n    \/* ---- Following pages: the directory ---- *\/\n\n    \/* The gutters the page box gave up, handed back so they repeat per page. *\/\n    .ig-team__content { padding: 0 14mm; }\n\n    .ig-team__section { margin-bottom: 6mm; }\n\n    .ig-team__section-head {\n        justify-content: space-between;\n        align-items: center;\n        gap: var(--space--xs, 12px);\n        margin: 0 0 4mm;\n        padding: 3mm 4mm;\n        border: 0;\n        border-radius: 2mm;\n        background: var(--igt-tint);\n        break-after: avoid;\n    }\n\n    .ig-team__section-head h3 { font-size: 13pt; }\n    .ig-team__section-count { font-size: 9pt; }\n\n    \/* On screen the cards are a grid of portraits. On paper they read far\n       better as a list of rows with a round headshot, which is also what lets\n       a card sit whole on one page instead of splitting. *\/\n    .ig-team__grid { display: block; }\n\n    .ig-team__card {\n        flex-direction: row;\n        align-items: center;\n        gap: 5mm;\n        margin: 0 0 3mm;\n        padding: 4mm;\n        border: 1px solid #e9edf2;\n        border-radius: 3mm;\n        background: #f7f8fa;\n        box-shadow: none;\n        break-inside: avoid;\n    }\n\n    .ig-team__photo {\n        width: 18mm;\n        height: 18mm;\n        aspect-ratio: auto;\n        border-radius: 50%;\n        overflow: hidden;\n        flex: none;\n    }\n\n    .ig-team__body { flex: 1; gap: 0.8mm; padding: 0; }\n    .ig-team__name { font-size: 12pt; }\n    .ig-team__job { margin: 0; font-size: 10pt; color: #3d6a8a; }\n\n    \/* Three narrow print columns used to snap addresses mid-domain\n       (\"\u2026infinigate.co \/ m\"); on a full-width row they fit outright. *\/\n    .ig-team__email {\n        font-size: 9pt;\n        word-break: normal;\n        overflow-wrap: anywhere;\n    }\n\n    .ig-team__tags { gap: 2mm; margin-top: 2mm; }\n    .ig-team__tag { font-size: 7.5pt; }\n}\n<\/style>\n\n<script>\n\/* ---------------------------------------------------------------------------\n   Loader.\n\n   WordPress runs post content through wptexturize(). That function rewrites a\n   bare ampersand into its numeric HTML entity inside anything its tokeniser\n   reads as a tag \u2014 and JavaScript is full of angle brackets that are not\n   markup, both in comparisons and inside strings of HTML. Stretches of the\n   directory code below therefore get mistaken for tags, and every logical-AND\n   inside those stretches arrives as a pair of entities. That is a syntax\n   error, and a single syntax error kills an entire script: the spinner would\n   spin for ever.\n\n   So the directory is parked in the inert script element below, where the\n   browser will not execute it, and this loader restores the ampersands before\n   running the code itself.\n\n   Two things keep this loader intact where the directory is not. It is placed\n   before the code it repairs, so it is parsed while the tokeniser is still in\n   a known-good state. And no part of it, comment included, contains an angle\n   bracket or a literal ampersand \u2014 the one ampersand it needs is built from\n   its character code. Please keep it that way when editing.\n\n   The element below is deliberately not typed \"text slash plain\". Consent\n   managers park blocked scripts under that type and switch them to JavaScript\n   once consent is given; this site runs one that does exactly that, and it\n   would hand the unrepaired code straight to the parser.\n\n   Restoring is a no-op when nothing was rewritten, so this stays correct if\n   the content is ever served unfiltered.\n   --------------------------------------------------------------------------- *\/\n(function () {\n    'use strict';\n\n    function start() {\n        var node = document.getElementById('ig-team-code');\n        if (!node) return;\n\n        var amp = String.fromCharCode(38);\n        var code = node.textContent.split(amp + '#038;').join(amp);\n\n        try {\n            new Function(code)();\n        } catch (e) {\n            var status = document.getElementById('ig-team-status');\n\n            if (status) {\n                status.className = 'ig-team__status ig-team__status--error';\n                status.textContent = 'The team directory could not start.';\n            }\n\n            if (window.console) console.error('[ig-team] loader', e);\n        }\n    }\n\n    \/\/ The code island sits after this loader, so wait for the parser either way.\n    if (document.readyState === 'loading') {\n        document.addEventListener('DOMContentLoaded', start, { once: true });\n    } else {\n        start();\n    }\n}());\n<\/script>\n\n<script type=\"text\/ig-team-source\" id=\"ig-team-code\">\n(function () {\n    'use strict';\n\n    \/\/ The loader is what runs this code. If anything else on the page manages\n    \/\/ to run it as well \u2014 a consent manager unblocking scripts, an optimiser\n    \/\/ rewriting them \u2014 the second run has to do nothing at all: a second set\n    \/\/ of listeners on the same elements would double every filter click.\n    if (window.igTeamStarted) return;\n    window.igTeamStarted = true;\n\n    \/* =================================================================\n       CONFIG\n       ================================================================= *\/\n\n    \/\/ The Media Library folder holding team.xlsx and the team photos, matched\n    \/\/ on the NAME shown in Media > Folders \u2014 not the slug.\n    \/\/\n    \/\/ Folders Pro stamps a timestamp into every folder's slug\n    \/\/ (team-1787329895-13), and that timestamp differs on every site. Matching\n    \/\/ the name instead means this same block works unchanged on local, staging\n    \/\/ and production, and on any other country site that has a folder of this\n    \/\/ name. Nothing here needs editing per environment.\n    var FOLDER_NAME = 'Team';\n\n    \/\/ Worksheet inside team.xlsx, matched case-insensitively so \"Team\" and\n    \/\/ \"team\" both work. Falls back to the first sheet if there is no match,\n    \/\/ which is why a single-sheet workbook always loads whatever it is called.\n    var SHEET_NAME = 'Team';\n\n    \/\/ Section order. Departments not listed here are appended alphabetically.\n    var DEPARTMENT_ORDER = [\n        'Management',\n        'External Sales Team',\n        'Internal Account Management Team',\n        'Pre-Sales & Engineering',\n        'Marketing',\n        'Back Office & Logistics',\n        'Finance',\n        'HR'\n    ];\n\n    var PLACEHOLDER_KEY = 'placeholder';\n\n    \/\/ The PDF's wording. These, and CONTACT_EMAIL below, are the only lines\n    \/\/ that differ per country \u2014 the directory itself adapts to the site it\n    \/\/ runs on. Leave `title` empty to use the page's own H1.\n    var PRINT = {\n        title: '',\n        subtitle: 'Infinigate Belgium \u2014 Team Directory',\n        company: 'Infinigate Belgium',\n        address: 'Blarenberglaan 3b, 2800 Mechelen \u00b7 BE 0452.826.187'\n    };\n\n    \/\/ The Infinigate mark, white on navy, for the cover and the running header.\n    var LOGO_SVG = '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 678.61 137.76\"><g><path d=\"M59.28 84.43v34.13H19.2V78.48h34.13c.55 0 1-.45 1-1v-17.2c0-.55-.45-1-1-1H15c-8.28 0-15 6.72-15 15v48.48c0 8.28 6.72 15 15 15h48.48c8.28 0 15-6.72 15-15V84.43c0-.55-.45-1-1-1h-17.2c-.55 0-1 .45-1 1Z\" fill=\"#ed5740\"\/><path d=\"M78.48 53.38V19.2h40.08v40.08H84.43c-.55 0-1 .45-1 1v17.2c0 .55.45 1 1 1h38.33c8.28 0 15-6.72 15-15V15c0-8.28-6.72-15-15-15H74.28c-8.28 0-15 6.72-15 15v38.38c0 .55.45 1 1 1h17.2c.55 0 1-.45 1-1Z\" fill=\"#ffffff\"\/><rect x=\"59.28\" y=\"59.28\" width=\"19.2\" height=\"19.2\" rx=\"1\" ry=\"1\" fill=\"#153447\"\/><path d=\"M177.84 14.74h15.6V31h-15.6V14.74Zm15.2 27.75v64.63h-14.8V42.49h14.8Zm9.51 0h14.41v6.61c4.89-5.15 11.5-8.33 19.03-8.33 15.2 0 25.64 11.1 25.64 29.21v37.14h-14.8V70.38c0-9.12-5.15-15.99-14.27-15.99s-15.33 6.48-15.33 16.78v35.95h-14.67V42.49Zm73.62 12.82h-12.42V42.49h12.42v-5.42c0-15.07 7-22.47 22.47-22.47h21.81v12.03h-20.09c-6.21 0-9.38 2.91-9.38 9.25v6.61h38.46v64.63h-14.8V55.31h-23.66v51.81h-14.8V55.31Zm63.17-12.82h14.41v6.61c4.89-5.15 11.5-8.33 19.03-8.33 15.2 0 25.64 11.1 25.64 29.21v37.14h-14.8V70.38c0-9.12-5.15-15.99-14.27-15.99s-15.33 6.48-15.33 16.78v35.95h-14.67V42.49Zm68.6-27.75h15.6V31h-15.6V14.74Zm15.2 27.75v64.63h-14.8V42.49h14.8Zm9.38 67.41v-1.85h14.67v1.72c0 5.82 4.1 9.65 12.42 9.65h11.37c8.46 0 12.42-4.23 12.42-13.22v-8.72c-5.42 4.76-12.42 7.67-20.35 7.67-18.37 0-32.38-13.08-32.38-32.12s14.01-32.38 32.38-32.38c8.33 0 15.6 3.17 21.15 8.46V42.5h14.01v63.31c0 16.12-8.99 26.17-25.51 26.17h-15.99c-16.52 0-24.19-8.86-24.19-22.07Zm32.25-17.31c11.1 0 19.43-8.19 19.43-19.56s-8.33-19.43-19.43-19.43-19.16 8.59-19.16 19.43 8.19 19.56 19.16 19.56Zm41.5-3.58c0-12.69 10.57-21.54 30.4-21.54h11.63v-1.59c0-8.06-4.89-13.08-13.22-13.08-7.14 0-13.08 4.49-13.61 11.1H506.8c.66-13.61 12.42-23.13 28.55-23.13 17.18 0 27.75 10.44 27.75 26.43v39.91h-14.41v-6.74c-4.23 5.02-11.37 8.46-18.37 8.46-14.54 0-24.05-7.8-24.05-19.82Zm25.24 8.33c9.65 0 17.18-7.8 17.18-17.97v-1.06h-11.63c-10.7 0-16.39 3.57-16.39 10.31 0 5.15 3.83 8.72 10.84 8.72Zm34.5-54.85h12.16V25.31h14.8v17.18h17.31v12.82h-17.31v29.08c0 6.61 3.3 9.91 9.52 9.91h7.8v12.82h-9.65c-14.8 0-22.47-6.87-22.47-21.67V55.32h-12.16V42.5Zm47.18 33.04c0-19.82 15.73-34.76 33.83-34.76s31.59 13.74 31.59 31.32c0 1.59-.13 3.7-.53 6.61h-50.49c1.45 10.84 9.65 17.58 19.82 17.58 7.67 0 14.14-3.83 15.86-9.65h14.54c-3.17 12.69-15.07 22.2-31.32 22.2-19.3 0-33.31-14.8-33.31-33.3Zm50.35-8.86c-.53-7.4-7.4-13.35-16.52-13.35-8.33 0-15.46 5.15-18.37 13.35h34.89Z\" fill=\"#ffffff\"\/><\/g><\/svg>';\n\n    \/\/ Offered whenever the directory cannot be shown. Per country, this is the\n    \/\/ one line that changes \u2014 everything else adapts to the site it runs on.\n    var CONTACT_EMAIL = 'sales-be@infinigate.com';\n\n    \/* =================================================================\n       Boot when the document is ready\n       ================================================================= *\/\n\n    \/\/ The Custom HTML block serialises its three tabs in a fixed order \u2014 CSS,\n    \/\/ then JS, then markup \u2014 so this script is parsed before #ig-team exists.\n    \/\/ Looking the elements up now would find nothing and leave the spinner\n    \/\/ running forever, so everything below waits for the parser to finish.\n    if (document.readyState === 'loading') {\n        document.addEventListener('DOMContentLoaded', boot, { once: true });\n    } else {\n        boot();\n    }\n\n    function boot() {\n\n        \/* =================================================================\n           DOM\n           ================================================================= *\/\n\n        var root = document.getElementById('ig-team');\n        if (!root) return;\n\n        var bar = document.getElementById('ig-team-bar');\n        var content = document.getElementById('ig-team-content');\n        var statusEl = document.getElementById('ig-team-status');\n        var resultLine = document.getElementById('ig-team-result');\n        var searchInput = document.getElementById('ig-team-search');\n\n        var state = { search: '', departments: [], products: [], jobTitles: [] };\n        var people = [];\n        var options = { departments: [], products: [], jobTitles: [] };\n\n        function esc(s) {\n            return String(s == null ? '' : s).replace(\/[&<>\"']\/g, function (c) {\n                return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c];\n            });\n        }\n\n        \/\/ The status element is removed once the cards render. If anything\n        \/\/ throws after that point it still needs somewhere to say so, so put\n        \/\/ it back rather than writing into a detached node nobody can see.\n        function showStatus(className, html) {\n            statusEl.className = 'ig-team__status ' + className;\n            statusEl.innerHTML = html;\n            statusEl.hidden = false;\n            if (!statusEl.isConnected) content.appendChild(statusEl);\n        }\n\n        function fail(message, detail) {\n            showStatus('ig-team__status--error',\n                '<p><strong>' + esc(message) + '<\/strong><\/p>' +\n                (detail ? '<code>' + esc(detail) + '<\/code>' : ''));\n        }\n\n        \/\/ A visitor-facing dead end rather than a fault to report: it says what\n        \/\/ is going on and offers the mail address instead of an error code.\n        function notice(headline, explanation) {\n            showStatus('ig-team__status--notice',\n                '<p><strong>' + headline + '<\/strong><\/p>' + explanation +\n                '<p>In the meantime you can reach us at ' +\n                '<a href=\"mailto:' + esc(CONTACT_EMAIL) + '\">' + esc(CONTACT_EMAIL) + '<\/a>.<\/p>');\n            if (bar) bar.hidden = true;\n        }\n\n        function unsupportedBrowser() {\n            notice('This page needs a more recent browser.',\n                '<p>The team directory works in Chrome or Edge 103 and above, ' +\n                'Firefox 113 and above, and Safari 16.4 and above. Updating your ' +\n                'browser will bring it up.<\/p>');\n        }\n\n        \/* =================================================================\n           WordPress REST API\n           ================================================================= *\/\n\n        \/\/ Read the REST root from the <link rel=\"https:\/\/api.w.org\/\"> tag WordPress\n        \/\/ prints in <head>, so this works on any subsite without hardcoding \/be\/.\n        function restRoot() {\n            var link = document.querySelector('link[rel=\"https:\/\/api.w.org\/\"]');\n            return (link && link.href) || (window.location.origin + '\/wp-json\/');\n        }\n\n        function getJson(url) {\n            return fetch(url, { credentials: 'same-origin' }).then(function (res) {\n                if (!res.ok) {\n                    \/\/ Carry the status so the caller can tell \"the site is\n                    \/\/ refusing anonymous access\" apart from a real fault.\n                    var err = new Error('HTTP ' + res.status + ' for ' + url);\n                    err.status = res.status;\n                    throw err;\n                }\n                return res.json();\n            });\n        }\n\n        function findFolder() {\n            \/\/ `search` is a partial, case-insensitive match, so \"Team\" would also\n            \/\/ return \"Marketing Team\" or \"Team Photos\". Narrow with it, then take\n            \/\/ only an exact name match so the wrong folder can never be picked up.\n            var url = restRoot() + 'wp\/v2\/media_folder?per_page=100&search=' + encodeURIComponent(FOLDER_NAME);\n\n            return getJson(url).then(function (terms) {\n                var wanted = FOLDER_NAME.trim().toLowerCase();\n\n                var exact = terms.filter(function (t) {\n                    return String(t.name || '').trim().toLowerCase() === wanted;\n                });\n\n                if (exact.length === 1) return exact[0].id;\n\n                if (exact.length > 1) {\n                    throw new Error(\n                        'More than one media folder is named \"' + FOLDER_NAME + '\". ' +\n                        'Rename the ones you do not want used.'\n                    );\n                }\n\n                \/\/ Nothing matched. Name what does exist \u2014 on a fresh site this is\n                \/\/ almost always \"the folder has not been created yet\", and the\n                \/\/ list makes that obvious to whoever is setting the page up.\n                var near = terms.map(function (t) { return t.name; }).join(', ');\n                throw new Error(\n                    'No media folder named \"' + FOLDER_NAME + '\" on this site.' +\n                    (near ? ' Folders with similar names: ' + near + '.' : '')\n                );\n            });\n        }\n\n        function listFolderMedia(termId) {\n            var out = [];\n\n            function page(n) {\n                var url = restRoot() + 'wp\/v2\/media?media_folder=' + termId +\n                          '&per_page=100&page=' + n + '&_fields=id,source_url,mime_type,alt_text,media_details';\n\n                return getJson(url).then(function (items) {\n                    out = out.concat(items);\n                    \/\/ Keep paging while the folder is bigger than one page.\n                    return items.length === 100 && n < 10 ? page(n + 1) : out;\n                });\n            }\n\n            return page(1);\n        }\n\n        \/* =================================================================\n           .xlsx reader \u2014 a ZIP of XML files, unpacked natively.\n           ================================================================= *\/\n\n        var decoder = new TextDecoder();\n\n        function unzip(buffer) {\n            var dv = new DataView(buffer);\n            var u8 = new Uint8Array(buffer);\n\n            \/\/ The End Of Central Directory record sits in the last 64KB.\n            var eocd = -1;\n            var floor = Math.max(0, buffer.byteLength - 22 - 65535);\n            for (var i = buffer.byteLength - 22; i >= floor; i--) {\n                if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }\n            }\n            if (eocd < 0) throw new Error('team.xlsx is not a valid ZIP archive');\n\n            var count = dv.getUint16(eocd + 10, true);\n            var entries = {};\n            var p = dv.getUint32(eocd + 16, true);\n\n            for (var n = 0; n < count; n++) {\n                if (dv.getUint32(p, true) !== 0x02014b50) break;\n\n                var nameLen = dv.getUint16(p + 28, true);\n                var name = decoder.decode(u8.subarray(p + 46, p + 46 + nameLen));\n\n                entries[name] = {\n                    method: dv.getUint16(p + 10, true),\n                    compSize: dv.getUint32(p + 20, true),\n                    localOffset: dv.getUint32(p + 42, true)\n                };\n\n                p += 46 + nameLen + dv.getUint16(p + 30, true) + dv.getUint16(p + 32, true);\n            }\n\n            return {\n                has: function (name) { return !!entries[name]; },\n\n                text: function (name) {\n                    var entry = entries[name];\n                    if (!entry) return Promise.resolve('');\n\n                    var o = entry.localOffset;\n                    if (dv.getUint32(o, true) !== 0x04034b50) {\n                        return Promise.reject(new Error('Corrupt ZIP entry: ' + name));\n                    }\n\n                    var start = o + 30 + dv.getUint16(o + 26, true) + dv.getUint16(o + 28, true);\n                    var raw = new Uint8Array(buffer, start, entry.compSize);\n\n                    if (entry.method === 0) return Promise.resolve(decoder.decode(raw));\n\n                    if (typeof DecompressionStream !== 'function') {\n                        return Promise.reject(new Error('This browser cannot unpack .xlsx (no DecompressionStream)'));\n                    }\n\n                    var stream = new Blob([raw]).stream().pipeThrough(new DecompressionStream('deflate-raw'));\n                    return new Response(stream).text();\n                }\n            };\n        }\n\n        function parseXml(text) {\n            var doc = new DOMParser().parseFromString(text, 'application\/xml');\n            if (doc.getElementsByTagName('parsererror').length) throw new Error('Malformed XML inside team.xlsx');\n            return doc;\n        }\n\n        function colOf(ref) { return (ref || '').replace(\/[0-9]\/g, ''); }\n\n        function readSheet(zip) {\n            \/\/ workbook.xml names the sheets; the .rels file maps each to a file path.\n            return zip.text('xl\/workbook.xml').then(function (wbText) {\n                return zip.text('xl\/_rels\/workbook.xml.rels').then(function (relsText) {\n                    var rels = {};\n                    Array.prototype.forEach.call(parseXml(relsText).getElementsByTagName('Relationship'), function (r) {\n                        rels[r.getAttribute('Id')] = r.getAttribute('Target').replace(\/^\\\/?xl\\\/\/, '');\n                    });\n\n                    var sheets = parseXml(wbText).getElementsByTagName('sheet');\n                    var target = null;\n\n                    var wanted = String(SHEET_NAME).trim().toLowerCase();\n\n                    Array.prototype.forEach.call(sheets, function (s) {\n                        \/\/ Case-insensitive: Excel keeps whatever capitalisation the\n                        \/\/ tab was given, so \"team\" and \"Team\" are the same sheet to\n                        \/\/ whoever is editing the file.\n                        if (!target &#038;&#038; String(s.getAttribute('name') || '').trim().toLowerCase() === wanted) {\n                            target = rels[s.getAttribute('r:id')];\n                        }\n                    });\n\n                    if (!target &#038;&#038; sheets.length) target = rels[sheets[0].getAttribute('r:id')];\n                    if (!target) throw new Error('No worksheet found in team.xlsx');\n\n                    return 'xl\/' + target;\n                });\n            });\n        }\n\n        function readSharedStrings(zip) {\n            if (!zip.has('xl\/sharedStrings.xml')) return Promise.resolve([]);\n\n            return zip.text('xl\/sharedStrings.xml').then(function (text) {\n                return Array.prototype.map.call(parseXml(text).getElementsByTagName('si'), function (si) {\n                    return Array.prototype.map.call(si.getElementsByTagName('t'), function (t) {\n                        return t.textContent;\n                    }).join('');\n                });\n            });\n        }\n\n        function readRows(zip) {\n            return readSheet(zip).then(function (sheetPath) {\n                return readSharedStrings(zip).then(function (shared) {\n                    return zip.text(sheetPath).then(function (text) {\n                        return Array.prototype.map.call(parseXml(text).getElementsByTagName('row'), function (row) {\n                            var cells = {};\n\n                            Array.prototype.forEach.call(row.getElementsByTagName('c'), function (c) {\n                                var type = c.getAttribute('t');\n                                var v = c.getElementsByTagName('v')[0];\n                                var value;\n\n                                if (type === 'inlineStr') {\n                                    value = Array.prototype.map.call(c.getElementsByTagName('t'), function (t) {\n                                        return t.textContent;\n                                    }).join('');\n                                } else if (!v) {\n                                    value = '';\n                                } else if (type === 's') {\n                                    value = shared[parseInt(v.textContent, 10)] || '';\n                                } else {\n                                    value = v.textContent;\n                                }\n\n                                cells[colOf(c.getAttribute('r'))] = value;\n                            });\n\n                            return cells;\n                        });\n                    });\n                });\n            });\n        }\n\n        \/* =================================================================\n           Spreadsheet -> people\n           ================================================================= *\/\n\n        \/\/ \"Peter B.png\" -> \"peter-b\", which matches the media filename \"Peter-B.jpg\".\n        \/\/\n        \/\/ WordPress does not always store a photo under the name it was given.\n        \/\/ Anything wider than 2560px is re-saved as \"Peter-B-scaled.jpg\", and\n        \/\/ the generated thumbnails carry their dimensions. Strip both, or the\n        \/\/ spreadsheet's \"Peter B.png\" quietly falls through to the placeholder.\n        function photoKey(value) {\n            return String(value || '')\n                .replace(\/\\.[a-z0-9]+$\/i, '')\n                .replace(\/-scaled$\/i, '')\n                .replace(\/-\\d+x\\d+$\/i, '')\n                .trim()\n                .toLowerCase()\n                .replace(\/[\\s_]+\/g, '-')\n                .replace(\/-+\/g, '-');\n        }\n\n        function splitMulti(value) {\n            \/\/ The spreadsheet's Instructions sheet allows \";\" or \",\" as the separator.\n            return String(value || '').split(\/[;,]\/).map(function (s) { return s.trim(); }).filter(Boolean);\n        }\n\n        function addUnique(target, values) {\n            values.forEach(function (value) {\n                if (target.indexOf(value) === -1) target.push(value);\n            });\n        }\n\n        \/\/ One row per person is the normal case, but a colleague who handles\n        \/\/ more than one vendor gets a row each \u2014 same name, same job, same\n        \/\/ department, a different mail address and product on every line.\n        \/\/ Those have to arrive as a single card carrying all of them, so rows\n        \/\/ are folded together by name as they are read.\n        \/\/\n        \/\/ Name is the whole of the identity: matching on name plus job title\n        \/\/ would split anyone whose role was typed differently on the second\n        \/\/ row. The flip side is that two different colleagues who genuinely\n        \/\/ share a full name would merge into one card \u2014 that has to be fixed\n        \/\/ in the spreadsheet, there is nothing here that could tell them apart.\n        function buildPeople(rows, photos) {\n            if (!rows.length) throw new Error('The Team sheet is empty');\n\n            \/\/ Row 1 holds the column headers; map header name -> column letter.\n            var headers = {};\n            Object.keys(rows[0]).forEach(function (col) {\n                headers[String(rows[0][col]).trim().toLowerCase()] = col;\n            });\n\n            function field(row, names) {\n                for (var i = 0; i < names.length; i++) {\n                    var col = headers[names[i].toLowerCase()];\n                    if (col &#038;&#038; row[col] != null) return String(row[col]).trim();\n                }\n                return '';\n            }\n\n            var byName = {};\n            var order = [];\n\n            rows.slice(1).forEach(function (row) {\n                var first = field(row, ['First Name', 'FirstName']);\n                var last = field(row, ['Last Name', 'LastName']);\n                var name = (first + ' ' + last).trim();\n\n                if (!name) return;\n\n                \/\/ Accent-insensitive, so \"Melanie\" on one row and \"M\u00e9lanie\" on\n                \/\/ the next are still recognised as the same colleague.\n                var key = norm(name);\n                var person = byName[key];\n\n                if (!person) {\n                    person = byName[key] = {\n                        name: name,\n                        job: '',\n                        emails: [],\n                        photo: null,\n                        departments: [],\n                        products: []\n                    };\n\n                    order.push(person);\n                }\n\n                \/\/ First non-empty wins. The job title doubles as a filter\n                \/\/ value, compared verbatim against the spreadsheet, so it has\n                \/\/ to stay a single string.\n                if (!person.job) person.job = field(row, ['Job Title', 'JobTitle']);\n\n                var email = field(row, ['Email']);\n                if (email &#038;&#038; person.emails.indexOf(email) === -1) person.emails.push(email);\n\n                if (!person.photo) {\n                    person.photo = photos[photoKey(field(row, ['Photo', 'Photo Filename']))] || null;\n                }\n\n                addUnique(person.departments, splitMulti(field(row, ['Departments', 'Department'])));\n                addUnique(person.products, splitMulti(field(row, ['Products', 'Product'])));\n            });\n\n            \/\/ Only once every row has had its say, so a later row naming a\n            \/\/ photo still beats the placeholder an earlier one would have got.\n            order.forEach(function (person) {\n                if (!person.photo) person.photo = photos[PLACEHOLDER_KEY] || null;\n            });\n\n            return order;\n        }\n\n        function buildPhotoMap(media) {\n            var map = {};\n\n            media.forEach(function (item) {\n                if (!item.mime_type || item.mime_type.indexOf('image\/') !== 0) return;\n\n                var filename = decodeURIComponent(item.source_url.split('\/').pop().split('?')[0]);\n                var sizes = (item.media_details &#038;&#038; item.media_details.sizes) || {};\n\n                function pick() {\n                    for (var i = 0; i < arguments.length; i++) {\n                        if (sizes[arguments[i]]) return sizes[arguments[i]];\n                    }\n                    return null;\n                }\n\n                var display = pick('medium_large', 'large', 'full');\n                var small = pick('medium', 'thumbnail');\n\n                var srcset = [];\n                if (small) srcset.push(small.source_url + ' ' + small.width + 'w');\n                if (display) srcset.push(display.source_url + ' ' + display.width + 'w');\n\n                var key = photoKey(filename);\n\n                var record = {\n                    src: display ? display.source_url : item.source_url,\n                    srcset: srcset.join(', '),\n                    width: display ? display.width : (item.media_details &#038;&#038; item.media_details.width) || 768,\n                    height: display ? display.height : (item.media_details &#038;&#038; item.media_details.height) || 512,\n                    alt: item.alt_text || ''\n                };\n\n                map[key] = record;\n\n                \/\/ Re-uploading a photo that already exists gets it stored as\n                \/\/ \"Peter-B-1.jpg\". Register the un-numbered name as an alias so\n                \/\/ the spreadsheet still finds it \u2014 but never let the alias\n                \/\/ overwrite a genuine exact match.\n                var base = key.replace(\/-\\d+$\/, '');\n                if (base !== key &#038;&#038; !map[base]) map[base] = record;\n            });\n\n            return map;\n        }\n\n        function findSpreadsheet(media) {\n            var sheet = media.filter(function (item) {\n                return \/spreadsheet|excel\/.test(item.mime_type || '') || \/\\.xlsx?($|\\?)\/i.test(item.source_url);\n            })[0];\n\n            if (!sheet) throw new Error('No .xlsx file in the \"' + FOLDER_NAME + '\" media folder');\n            return sheet.source_url;\n        }\n\n        \/* =================================================================\n           Render\n           ================================================================= *\/\n\n        \/\/ Lower-cased and stripped of accents, so searching \"Melanie\" finds\n        \/\/ \"M\u00e9lanie\" and searching \"M\u00e9lanie\" still finds her too.\n        function norm(value) {\n            var s = String(value || '').toLowerCase();\n            return s.normalize ? s.normalize('NFD').replace(\/[\\u0300-\\u036f]\/g, '') : s;\n        }\n\n        function uniqueSorted(values) {\n            return values.filter(function (v, i) { return v &#038;&#038; values.indexOf(v) === i; })\n                         .sort(function (a, b) { return a.localeCompare(b); });\n        }\n\n        function cardHtml(person, dept, personIndex) {\n            var tags = person.departments.map(function (d) {\n                return '<span class=\"ig-team__tag ig-team__tag--dept\">' + esc(d) + '<\/span>';\n            }).concat(person.products.map(function (pr) {\n                return '<span class=\"ig-team__tag ig-team__tag--product\">' + esc(pr) + '<\/span>';\n            })).join('');\n\n            \/\/ A colleague can hold more than one address \u2014 one per vendor\n            \/\/ they look after \u2014 and each is its own mailto link.\n            var emails = person.emails.map(function (address) {\n                return '<a class=\"ig-team__email\" href=\"mailto:' + esc(address) + '\">' +\n                       '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><path d=\"M22 6l-10 7L2 6\"\/><path d=\"M2 6h20v12H2z\"\/><\/svg>' +\n                       esc(address) + '<\/a>';\n            }).join('');\n\n            var photo = person.photo\n                ? '<img decoding=\"async\" src=\"' + esc(person.photo.src) + '\"' +\n                  (person.photo.srcset ? ' srcset=\"' + esc(person.photo.srcset) + '\" sizes=\"auto, (max-width: 600px) 100vw, 280px\"' : '') +\n                  ' alt=\"' + esc(person.photo.alt || person.name) + '\" loading=\"lazy\"' +\n                  ' width=\"' + person.photo.width + '\" height=\"' + person.photo.height + '\">'\n                : '';\n\n            return '<article class=\"ig-team__card\"' +\n                       ' data-person=\"' + personIndex + '\"' +\n                       ' data-dept=\"' + esc(dept) + '\"' +\n                       \/\/ What the search box matches against. data-job stays\n                       \/\/ exact, because the Job title filter compares it to the\n                       \/\/ spreadsheet value verbatim.\n                       ' data-search=\"' + esc(norm(person.name + ' ' + person.job)) + '\"' +\n                       ' data-job=\"' + esc(person.job) + '\"' +\n                       ' data-products=\"' + esc(person.products.join('|')) + '\">' +\n                       '<div class=\"ig-team__photo\">' + photo + '<\/div>' +\n                       '<div class=\"ig-team__body\">' +\n                           '<p class=\"ig-team__name\">' + esc(person.name) + '<\/p>' +\n                           '<p class=\"ig-team__job\">' + esc(person.job) + '<\/p>' +\n                           emails +\n                           (tags ? '<div class=\"ig-team__tags\">' + tags + '<\/div>' : '') +\n                       '<\/div>' +\n                   '<\/article>';\n        }\n\n        function render() {\n            content.innerHTML = options.departments.map(function (dept) {\n                var members = people.filter(function (p) { return p.departments.indexOf(dept) !== -1; });\n                if (!members.length) return '';\n\n                return '<section class=\"ig-team__section\" data-dept=\"' + esc(dept) + '\">' +\n                           '<div class=\"ig-team__section-head\">' +\n                               '<h3>' + esc(dept) + '<\/h3>' +\n                               '<span class=\"ig-team__section-count\">(' + members.length + ')<\/span>' +\n                           '<\/div>' +\n                           '<div class=\"ig-team__grid\">' + members.map(function (p) {\n                               return cardHtml(p, dept, people.indexOf(p));\n                           }).join('') + '<\/div>' +\n                       '<\/section>';\n            }).join('') +\n            '<div class=\"ig-team__empty\" id=\"ig-team-empty\" hidden>' +\n                '<h3>No colleagues match those filters<\/h3>' +\n                '<p>Try removing a filter or clearing your search.<\/p>' +\n            '<\/div>';\n        }\n\n        \/* =================================================================\n           Filtering\n           ================================================================= *\/\n\n        function applyFilters() {\n            var cards = content.querySelectorAll('.ig-team__card');\n            var visiblePeople = [];\n\n            Array.prototype.forEach.call(cards, function (card) {\n                var products = card.dataset.products ? card.dataset.products.split('|') : [];\n\n                var show =\n                    (!state.search || card.dataset.search.indexOf(state.search) !== -1) &&\n                    (!state.departments.length || state.departments.indexOf(card.dataset.dept) !== -1) &&\n                    (!state.jobTitles.length || state.jobTitles.indexOf(card.dataset.job) !== -1) &&\n                    (!state.products.length || products.some(function (pr) { return state.products.indexOf(pr) !== -1; }));\n\n                card.hidden = !show;\n                if (show && visiblePeople.indexOf(card.dataset.person) === -1) {\n                    visiblePeople.push(card.dataset.person);\n                }\n            });\n\n            Array.prototype.forEach.call(content.querySelectorAll('.ig-team__section'), function (section) {\n                var shown = section.querySelectorAll('.ig-team__card:not([hidden])').length;\n                section.hidden = shown === 0;\n                section.querySelector('.ig-team__section-count').textContent = '(' + shown + ')';\n            });\n\n            document.getElementById('ig-team-empty').hidden = visiblePeople.length > 0;\n\n            resultLine.innerHTML = 'Showing <b>' + visiblePeople.length + '<\/b> of <b>' + people.length + '<\/b> ' +\n                                   (people.length === 1 ? 'colleague' : 'colleagues') + chipsHtml();\n        }\n\n        function chipsHtml() {\n            return ['departments', 'products', 'jobTitles'].reduce(function (html, facet) {\n                return html + state[facet].map(function (val) {\n                    return ' <span class=\"ig-team__chip\">' + esc(val) +\n                           '<button type=\"button\" data-facet=\"' + facet + '\" data-value=\"' + esc(val) + '\" aria-label=\"Remove ' + esc(val) + ' filter\">&times;<\/button><\/span>';\n                }).join('');\n            }, '');\n        }\n\n        \/* =================================================================\n           Multi-select dropdowns\n           ================================================================= *\/\n\n        function renderPanel(msel) {\n            var facet = msel.dataset.facet;\n\n            msel.querySelector('.ig-team__msel-panel').innerHTML = options[facet].map(function (opt) {\n                return '<label class=\"ig-team__msel-option\">' +\n                           '<input type=\"checkbox\" value=\"' + esc(opt) + '\"' + (state[facet].indexOf(opt) !== -1 ? ' checked' : '') + '>' +\n                           '<span>' + esc(opt) + '<\/span>' +\n                       '<\/label>';\n            }).join('');\n        }\n\n        function syncButtons() {\n            Array.prototype.forEach.call(root.querySelectorAll('.ig-team__msel'), function (msel) {\n                var selected = state[msel.dataset.facet];\n                var count = msel.querySelector('.ig-team__count');\n\n                count.hidden = selected.length === 0;\n                count.textContent = selected.length;\n                msel.querySelector('.ig-team__msel-btn').classList.toggle('is-active', selected.length > 0);\n\n                renderPanel(msel);\n            });\n        }\n\n        function closeAllPanels() {\n            Array.prototype.forEach.call(root.querySelectorAll('.ig-team__msel'), function (msel) {\n                msel.querySelector('.ig-team__msel-panel').hidden = true;\n                msel.querySelector('.ig-team__msel-btn').setAttribute('aria-expanded', 'false');\n            });\n        }\n\n        root.addEventListener('click', function (e) {\n            var toggle = e.target.closest('.ig-team__msel-btn');\n            if (toggle) {\n                var panel = toggle.nextElementSibling;\n                var wasOpen = !panel.hidden;\n                closeAllPanels();\n                if (!wasOpen) {\n                    panel.hidden = false;\n                    toggle.setAttribute('aria-expanded', 'true');\n                }\n                return;\n            }\n\n            var chipBtn = e.target.closest('.ig-team__chip button');\n            if (chipBtn) {\n                var facet = chipBtn.dataset.facet;\n                state[facet] = state[facet].filter(function (v) { return v !== chipBtn.dataset.value; });\n                syncButtons();\n                applyFilters();\n            }\n        });\n\n        root.addEventListener('change', function (e) {\n            var box = e.target.closest('.ig-team__msel-option input');\n            if (!box) return;\n\n            var facet = box.closest('.ig-team__msel').dataset.facet;\n            state[facet] = box.checked\n                ? state[facet].concat([box.value])\n                : state[facet].filter(function (v) { return v !== box.value; });\n\n            syncButtons();\n            applyFilters();\n        });\n\n        document.addEventListener('click', function (e) {\n            if (!e.target.closest('.ig-team__msel')) closeAllPanels();\n        });\n\n        searchInput.addEventListener('input', function () {\n            state.search = norm(searchInput.value.trim());\n            applyFilters();\n        });\n\n        \/\/ Escape closes an open dropdown and hands focus back to its button,\n        \/\/ so the filter bar can be driven without a mouse.\n        root.addEventListener('keydown', function (e) {\n            if (e.key !== 'Escape') return;\n\n            var open = root.querySelector('.ig-team__msel-panel:not([hidden])');\n            if (!open) return;\n\n            var btn = open.previousElementSibling;\n            closeAllPanels();\n            if (btn) btn.focus();\n        });\n\n        document.getElementById('ig-team-clear').addEventListener('click', function () {\n            state = { search: '', departments: [], products: [], jobTitles: [] };\n            searchInput.value = '';\n            closeAllPanels();\n            syncButtons();\n            applyFilters();\n        });\n\n        \/* =================================================================\n           Print \/ Save as PDF\n           ================================================================= *\/\n\n        function logoDataUri() {\n            return 'data:image\/svg+xml,' + encodeURIComponent(LOGO_SVG);\n        }\n\n        \/\/ The running header band, drawn as one image so it survives the print\n        \/\/ dialog's \"Background graphics\" setting being off. 210mm x 26mm is the\n        \/\/ full width of an A4 page and the height of the top margin band.\n        function bandDataUri() {\n            var band =\n                '<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"210mm\" height=\"26mm\" viewBox=\"0 0 210 26\">' +\n                    '<rect width=\"210\" height=\"26\" fill=\"#153447\"\/>' +\n                    '<svg viewBox=\"0 0 678.61 137.76\" x=\"14\" y=\"9.4\" width=\"34.5\" height=\"7\">' +\n                        LOGO_SVG.replace(\/^<svg[^>]*>\/, '').replace(\/<\\\/svg>$\/, '') +\n                    '<\/svg>' +\n                    '<text x=\"196\" y=\"15.1\" text-anchor=\"end\" font-family=\"Arial, Helvetica, sans-serif\" ' +\n                        'font-size=\"3.6\" font-weight=\"bold\" fill=\"#ffffff\">' + esc(PRINT.subtitle) + '<\/text>' +\n                '<\/svg>';\n\n            return 'data:image\/svg+xml,' + encodeURIComponent(band);\n        }\n\n        \/\/ The @page rules that carry the header band and the footer. They are\n        \/\/ built here rather than written into the stylesheet because the band\n        \/\/ is an image built from CONFIG, and because counter(page) only works\n        \/\/ inside a margin box \u2014 it is the one way a browser will number pages.\n        function installPageRules() {\n            if (document.getElementById('ig-team-page-rules')) return;\n\n            var foot = esc(PRINT.company + ' \u00b7 ' + PRINT.address);\n\n            var style = document.createElement('style');\n            style.id = 'ig-team-page-rules';\n            style.media = 'print';\n            style.textContent =\n                '@page {' +\n                    '@top-center { content: url(\"' + bandDataUri() + '\"); }' +\n                    '@bottom-left { content: \"' + foot + '\"; padding: 0 14mm;' +\n                        'font-family: Arial, Helvetica, sans-serif; font-size: 8pt; color: #667c8c; }' +\n                    '@bottom-right { content: \"Page \" counter(page) \" of \" counter(pages); padding: 0 14mm;' +\n                        'font-family: Arial, Helvetica, sans-serif; font-size: 8pt; color: #667c8c; }' +\n                '}';\n\n            document.head.appendChild(style);\n        }\n\n        \/\/ Everything on the cover that depends on what is on screen right now:\n        \/\/ the date, how many people the export covers, and which filters were\n        \/\/ left applied when it was taken.\n        function fillCover() {\n            var h1 = document.querySelector('h1');\n            var pageTitle = PRINT.title || (h1 ? h1.textContent.trim() : document.title);\n\n            var shown = content.querySelectorAll('.ig-team__card:not([hidden])');\n            var people_ = [];\n\n            Array.prototype.forEach.call(shown, function (card) {\n                if (people_.indexOf(card.dataset.person) === -1) people_.push(card.dataset.person);\n            });\n\n            var labels = { departments: 'Department', products: 'Product', jobTitles: 'Job title' };\n            var filters = [];\n\n            ['departments', 'products', 'jobTitles'].forEach(function (facet) {\n                if (state[facet].length) filters.push(labels[facet] + ': ' + state[facet].join(', '));\n            });\n\n            if (state.search) filters.push('Search: ' + searchInput.value.trim());\n\n            var generated = new Date().toLocaleDateString('en-GB', {\n                day: 'numeric', month: 'long', year: 'numeric'\n            });\n\n            document.getElementById('ig-team-cover-logo').src = logoDataUri();\n            document.getElementById('ig-team-cover-title').textContent = pageTitle;\n            document.getElementById('ig-team-cover-sub').textContent = PRINT.subtitle;\n            document.getElementById('ig-team-cover-meta').textContent =\n                'Generated ' + generated + ' \u00b7 ' + people_.length +\n                (people_.length === 1 ? ' team profile' : ' team profiles');\n            document.getElementById('ig-team-cover-filters').textContent = filters.join(' \u00b7 ');\n            document.getElementById('ig-team-cover-addr').textContent = PRINT.address;\n        }\n\n        \/\/ Everything that is not on the path from <body> down to this block is\n        \/\/ removed from the layout while printing. Marking the path in the DOM\n        \/\/ is the only way to express \"hide my aunts and uncles, at every\n        \/\/ level\" \u2014 CSS on its own cannot walk upwards.\n        function markPrintAncestors() {\n            var node = root.parentNode;\n\n            while (node && node.nodeType === 1 && node !== document.documentElement) {\n                node.classList.add('ig-team-print-ancestor');\n                node = node.parentNode;\n            }\n        }\n\n        function unmarkPrintAncestors() {\n            Array.prototype.forEach.call(\n                document.querySelectorAll('.ig-team-print-ancestor'),\n                function (el) { el.classList.remove('ig-team-print-ancestor'); }\n            );\n        }\n\n        function preparePrint() {\n            installPageRules();\n            fillCover();\n            markPrintAncestors();\n        }\n\n        \/\/ Covers Ctrl\/Cmd-P and the browser's own print menu, not just the\n        \/\/ button below.\n        window.addEventListener('beforeprint', preparePrint);\n        window.addEventListener('afterprint', unmarkPrintAncestors);\n\n        \/\/ Cards are lazy-loaded, and only Chrome reliably forces pending lazy\n        \/\/ images to load before opening the print dialog \u2014 elsewhere the PDF\n        \/\/ comes out with empty photo panels below the fold. Load them first,\n        \/\/ but never let one slow or broken image hold the dialog hostage.\n        document.getElementById('ig-team-print').addEventListener('click', function () {\n            var images = Array.prototype.slice.call(content.querySelectorAll('img'));\n            var pending = images.filter(function (img) { return !img.complete; });\n\n            images.forEach(function (img) { img.loading = 'eager'; });\n\n            if (!pending.length) {\n                window.print();\n                return;\n            }\n\n            var settled = pending.map(function (img) {\n                return new Promise(function (resolve) {\n                    img.addEventListener('load', resolve, { once: true });\n                    img.addEventListener('error', resolve, { once: true });\n                });\n            });\n\n            var timeout = new Promise(function (resolve) { setTimeout(resolve, 3000); });\n\n            Promise.race([Promise.all(settled), timeout]).then(function () { window.print(); });\n        });\n\n        \/* =================================================================\n           Boot\n           ================================================================= *\/\n\n        \/\/ The .xlsx is unpacked with the browser's native DecompressionStream.\n        \/\/ Testing for the constructor alone is not enough: Chrome 80-102 has\n        \/\/ DecompressionStream but not the 'deflate-raw' format that ZIP entries\n        \/\/ use, so it would pass a typeof check and then throw from inside the ZIP\n        \/\/ reader, giving a visitor a technical error instead of an explanation.\n        \/\/ Constructing one is the only honest test.\n        function supportsRawDeflate() {\n            if (typeof DecompressionStream !== 'function') return false;\n            try {\n                new DecompressionStream('deflate-raw');\n                return true;\n            } catch (e) {\n                return false;\n            }\n        }\n\n        if (!supportsRawDeflate()) {\n            unsupportedBrowser();\n            return;\n        }\n\n        findFolder()\n            .then(listFolderMedia)\n            .then(function (media) {\n                var photos = buildPhotoMap(media);\n                var xlsxUrl = findSpreadsheet(media);\n\n                return fetch(xlsxUrl, { credentials: 'same-origin' })\n                    .then(function (res) {\n                        if (!res.ok) throw new Error('HTTP ' + res.status + ' fetching team.xlsx');\n                        return res.arrayBuffer();\n                    })\n                    .then(function (buffer) { return readRows(unzip(buffer)); })\n                    .then(function (rows) { return buildPeople(rows, photos); });\n            })\n            .then(function (loaded) {\n                people = loaded;\n                if (!people.length) throw new Error('No people found in team.xlsx');\n\n                var found = uniqueSorted(people.reduce(function (acc, p) { return acc.concat(p.departments); }, []));\n\n                options.departments = DEPARTMENT_ORDER.filter(function (d) { return found.indexOf(d) !== -1; })\n                                                      .concat(found.filter(function (d) { return DEPARTMENT_ORDER.indexOf(d) === -1; }));\n                options.products = uniqueSorted(people.reduce(function (acc, p) { return acc.concat(p.products); }, []));\n                options.jobTitles = uniqueSorted(people.map(function (p) { return p.job; }));\n\n                statusEl.remove();\n                bar.hidden = false;\n\n\n                render();\n                syncButtons();\n                applyFilters();\n            })\n            .catch(function (err) {\n                \/\/ 401\/403 means the REST endpoints are readable for an editor\n                \/\/ but not for the public \u2014 the page works when you are logged\n                \/\/ in and fails for everyone else. That is a site configuration\n                \/\/ problem, not something a visitor should be shown a code for.\n                if (err && (err.status === 401 || err.status === 403)) {\n                    notice('The team directory is temporarily unavailable.',\n                        '<p>We could not load it just now. Please try again shortly.<\/p>');\n                } else {\n                    fail('The team directory could not load.', err && err.message);\n                }\n\n                if (window.console) console.error('[ig-team]', err);\n            });\n\n    }\n})();\n<\/script>\n\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Search team Department Product Job title Clear filters Save as PDF Loading the team\u2026<\/p>\n","protected":false},"author":13,"featured_media":7657,"parent":0,"menu_order":41,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"folder":[],"class_list":["post-7604","page","type-page","status-publish","has-post-thumbnail","hentry"],"acf":[],"distributor_meta":false,"distributor_terms":false,"distributor_media":false,"distributor_original_site_name":"Infinigate | Belgium","distributor_original_site_url":"https:\/\/www.infinigate.com\/be","push-errors":false,"_links":{"self":[{"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/pages\/7604","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/users\/13"}],"replies":[{"embeddable":true,"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/comments?post=7604"}],"version-history":[{"count":5,"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/pages\/7604\/revisions"}],"predecessor-version":[{"id":7667,"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/pages\/7604\/revisions\/7667"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/media\/7657"}],"wp:attachment":[{"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/media?parent=7604"}],"wp:term":[{"taxonomy":"folder","embeddable":true,"href":"https:\/\/www.infinigate.com\/be\/wp-json\/wp\/v2\/folder?post=7604"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}