2026-04-05 20:49:52 +02:00
// Home page specific JS: team carousel + modal + portfolio preview + partners + konami
2026-02-06 18:36:03 +01:00
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
2026-04-05 20:49:52 +02:00
// ── Portfolio preview — même cartes que la page réalisations ──────────────────
const homePortfolioGrid = document . getElementById ( 'home-portfolio-grid' ) ;
if ( homePortfolioGrid ) {
fetch ( 'assets/data/projects.json' )
. then ( res => res . json ( ) )
. then ( projects => {
const first3 = projects . slice ( 0 , 3 ) ;
const delayClasses = [ '' , ' reveal-delay-1' , ' reveal-delay-2' ] ;
const categoryLabels = { bot : 'Bot Discord' , map : 'Fortnite Map' , graphisme : 'Graphisme' } ;
homePortfolioGrid . innerHTML = first3 . map ( ( p , i ) => {
2026-04-07 01:51:50 +02:00
const firstFile = Array . isArray ( p . files ) && p . files . length > 0 ? p . files [ 0 ] : null ;
const imgSrc = firstFile ? ( p . images || '' ) + firstFile : null ;
2026-04-05 20:49:52 +02:00
return `
< div class = "portfolio-item reveal${delayClasses[i]}" data - project - id = "${p.id}" role = "button" tabindex = "0" aria - label = "Voir le projet ${p.title}" >
< div class = "portfolio-bg" style = "background: ${p.gradient};" >
2026-04-07 01:51:50 +02:00
$ { imgSrc ? ` <img src=" ${ imgSrc } " alt=" ${ p . title } " class="portfolio-cover-img" onerror="this.style.display='none'"> ` : '' }
2026-04-05 20:49:52 +02:00
< / d i v >
< div class = "portfolio-content" >
< span class = "portfolio-tag" > $ { categoryLabels [ p . category ] || p . category } < / s p a n >
< h4 > $ { p . title } < / h 4 >
< / d i v >
< / d i v >
` ;
} ) . join ( '' ) ;
setTimeout ( ( ) => {
homePortfolioGrid . querySelectorAll ( '.reveal' ) . forEach ( el => el . classList . add ( 'active' ) ) ;
if ( window . feather && feather . replace ) try { feather . replace ( ) ; } catch ( e ) { }
} , 80 ) ;
2026-04-07 01:51:50 +02:00
// Délégation globale sur document — fonctionne même si realisations.js
// se charge après home.js (ordre des scripts defer non garanti)
2026-04-05 20:49:52 +02:00
homePortfolioGrid . addEventListener ( 'click' , e => {
const item = e . target . closest ( '.portfolio-item[data-project-id]' ) ;
if ( ! item ) return ;
const id = item . getAttribute ( 'data-project-id' ) ;
if ( typeof window . openProjectModal === 'function' ) {
window . openProjectModal ( id ) ;
2026-04-07 01:51:50 +02:00
} else {
// Retry : realisations.js pas encore initialisé, on attend
const wait = setInterval ( ( ) => {
if ( typeof window . openProjectModal === 'function' ) {
clearInterval ( wait ) ;
window . openProjectModal ( id ) ;
}
} , 50 ) ;
// Abandon après 3 secondes
setTimeout ( ( ) => clearInterval ( wait ) , 3000 ) ;
2026-04-05 20:49:52 +02:00
}
} ) ;
} )
. catch ( err => console . error ( 'Failed to load projects for home preview' , err ) ) ;
}
// ── Partners — carousel + modal ─────────────────────────────────────────────
const partnersTrack = document . getElementById ( 'partners-track' ) ;
const partnerModalOverlay = document . getElementById ( 'partner-modal-overlay' ) ;
if ( partnersTrack ) {
fetch ( 'assets/data/partners.json' )
. then ( res => res . json ( ) )
. then ( partners => {
if ( ! partners . length ) {
const section = document . getElementById ( 'partenaires' ) ;
if ( section ) section . style . display = 'none' ;
return ;
}
// Générer les slides (tripler pour garantir boucle sans trou)
const copies = [ ... partners , ... partners , ... partners ] ;
partnersTrack . innerHTML = copies . map ( ( p , i ) => {
2026-04-08 00:57:37 +02:00
const imgSrc = p . logo || ` assets/images/partners/ ${ p . name } .png ` ;
2026-04-05 20:49:52 +02:00
return `
< div class = "partner-slide" data - partner - idx = "${i % partners.length}" role = "button" tabindex = "0" aria - label = "Voir ${p.displayName}" >
< div class = "partner-slide-inner" >
< img src = "${imgSrc}" alt = "${p.displayName}" class = "partner-logo-img"
onerror = "this.style.display='none';this.nextElementSibling.style.display='flex';" >
< div class = "partner-logo-fallback" style = "background:${p.gradient};display:none;" >
$ { p . displayName . substring ( 0 , 2 ) . toUpperCase ( ) }
< / d i v >
< span class = "partner-slide-name" > $ { p . displayName } < / s p a n >
< / d i v >
< / d i v >
` ;
} ) . join ( '' ) ;
// Défilement infini — loopWidth mesuré une seule fois après rendu complet
const track = partnersTrack ;
let isPaused = false ;
let scrollPos = 0 ;
let loopWidth = 0 ;
let animFrame ;
const SPEED = 0.6 ;
function measureLoop ( ) {
// Mesure la largeur exacte d'un bloc (1/3 des slides)
// On utilise offsetWidth de chaque slide réel + gap pour éviter les arrondis de scrollWidth
const slides = Array . from ( track . querySelectorAll ( '.partner-slide' ) ) ;
const perGroup = partners . length ;
if ( slides . length < perGroup ) return 0 ;
// Largeur d'un groupe = somme des (offsetWidth + gap) des premiers slides
const style = window . getComputedStyle ( track ) ;
const gap = parseFloat ( style . gap ) || 24 ;
let w = 0 ;
for ( let i = 0 ; i < perGroup ; i ++ ) {
w += slides [ i ] . offsetWidth + gap ;
}
return Math . round ( w ) ;
}
function autoScroll ( ) {
if ( ! isPaused ) {
scrollPos += SPEED ;
if ( loopWidth > 0 && scrollPos >= loopWidth ) {
scrollPos -= loopWidth ;
}
track . style . transform = ` translateX(- ${ scrollPos } px) ` ;
}
animFrame = requestAnimationFrame ( autoScroll ) ;
}
// Attendre que les slides soient rendues et mesurées
requestAnimationFrame ( ( ) => {
requestAnimationFrame ( ( ) => {
loopWidth = measureLoop ( ) ;
animFrame = requestAnimationFrame ( autoScroll ) ;
} ) ;
} ) ;
// Re-mesurer au resize pour rester précis
let resizeTO ;
window . addEventListener ( 'resize' , ( ) => {
clearTimeout ( resizeTO ) ;
resizeTO = setTimeout ( ( ) => { loopWidth = measureLoop ( ) ; } , 150 ) ;
} ) ;
track . parentElement . addEventListener ( 'mouseenter' , ( ) => { isPaused = true ; } ) ;
track . parentElement . addEventListener ( 'mouseleave' , ( ) => { isPaused = false ; } ) ;
// Click → modal
track . querySelectorAll ( '.partner-slide' ) . forEach ( slide => {
const open = ( ) => {
const idx = parseInt ( slide . dataset . partnerIdx , 10 ) ;
openPartnerModal ( partners [ idx ] ) ;
} ;
slide . addEventListener ( 'click' , open ) ;
slide . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' || e . key === ' ' ) { e . preventDefault ( ) ; open ( ) ; } } ) ;
} ) ;
function openPartnerModal ( p ) {
isPaused = true ;
2026-04-08 00:57:37 +02:00
const imgSrc = p . logo || ` assets/images/partners/ ${ p . name } .png ` ;
const logoImg = document . getElementById ( 'partner-modal-logo-img' ) ;
const logoInner = document . getElementById ( 'partner-modal-logo-fallback' ) ;
logoImg . src = imgSrc ;
logoImg . alt = p . displayName ;
logoImg . style . display = 'block' ;
logoInner . style . background = '' ;
logoInner . style . justifyContent = '' ;
// Si l'image échoue → fallback initiales dans l'octogone
logoImg . onerror = function ( ) {
2026-04-05 20:49:52 +02:00
this . style . display = 'none' ;
2026-04-08 00:57:37 +02:00
logoInner . style . background = p . gradient ;
logoInner . style . display = 'flex' ;
logoInner . style . alignItems = 'center' ;
logoInner . style . justifyContent = 'center' ;
logoInner . style . fontSize = '2rem' ;
logoInner . style . fontFamily = "'OmegaLarge', sans-serif" ;
logoInner . style . color = 'white' ;
logoInner . textContent = p . displayName . substring ( 0 , 2 ) . toUpperCase ( ) ;
2026-04-05 20:49:52 +02:00
} ;
document . getElementById ( 'partner-modal-name' ) . textContent = p . displayName ;
document . getElementById ( 'partner-modal-desc' ) . textContent = p . longDesc ;
document . getElementById ( 'partner-modal-since' ) . textContent = p . since ;
document . getElementById ( 'partner-modal-services' ) . innerHTML = ( p . services || [ ] ) . map ( s => ` <span class="team-modal-skill"> ${ s } </span> ` ) . join ( '' ) ;
document . getElementById ( 'partner-modal-stats' ) . innerHTML = ( p . stats || [ ] ) . map ( s => `
< div class = "team-modal-stat" >
< div class = "team-modal-stat-value" > $ { s . value } < / d i v >
< div class = "team-modal-stat-label" > $ { s . label } < / d i v >
< / d i v >
` ).join('');
const links = document . getElementById ( 'partner-modal-links' ) ;
2026-04-07 01:51:50 +02:00
// Réseaux sociaux depuis le champ social du partenaire
links . innerHTML = typeof window . renderSocialLinks === 'function'
? window . renderSocialLinks ( p . social )
: '' ;
2026-04-05 20:49:52 +02:00
if ( window . feather ) feather . replace ( ) ;
partnerModalOverlay . classList . add ( 'active' ) ;
document . body . style . overflow = 'hidden' ;
}
if ( partnerModalOverlay ) {
const closeBtn = document . getElementById ( 'partner-modal-close' ) ;
closeBtn && closeBtn . addEventListener ( 'click' , closePartnerModal ) ;
partnerModalOverlay . addEventListener ( 'click' , e => { if ( e . target === partnerModalOverlay ) closePartnerModal ( ) ; } ) ;
document . addEventListener ( 'keydown' , e => {
if ( e . key === 'Escape' && partnerModalOverlay . classList . contains ( 'active' ) ) closePartnerModal ( ) ;
} ) ;
}
function closePartnerModal ( ) {
partnerModalOverlay . classList . remove ( 'active' ) ;
document . body . style . overflow = '' ;
isPaused = false ;
}
} )
. catch ( err => {
const section = document . getElementById ( 'partenaires' ) ;
if ( section ) section . style . display = 'none' ;
} ) ;
}
// ── Team section ─────────────────────────────────────────────────────────────
2026-02-06 18:36:03 +01:00
fetch ( 'assets/data/team.json' )
. then ( res => res . json ( ) )
2026-04-05 20:49:52 +02:00
. then ( members => { initTeamSection ( members ) ; } )
. catch ( err => { console . error ( 'Failed to load team data' , err ) ; } ) ;
2026-02-06 18:36:03 +01:00
2026-04-05 20:49:52 +02:00
// Konami + easter egg → définis au niveau top-level du script (voir bas de fichier)
// ── initTeamSection ──────────────────────────────────────────────────────────
2026-02-06 18:36:03 +01:00
function initTeamSection ( members ) {
const carousel = document . getElementById ( 'team-carousel' ) ;
const dotsContainer = document . getElementById ( 'carousel-dots' ) ;
const prevBtn = document . getElementById ( 'carousel-prev' ) ;
const nextBtn = document . getElementById ( 'carousel-next' ) ;
2026-04-05 20:49:52 +02:00
carousel . innerHTML = members . map ( ( m , i ) => {
const imgSrc = ` assets/images/team/ ${ m . id } .png ` ;
return `
< div class = "team-card" data - member = "${i}" role = "button" tabindex = "0" aria - label = "Voir le profil de ${m.name}" >
< div class = "team-avatar" >
< div class = "team-avatar-inner" id = "team-avatar-inner-${i}" >
< img src = "${imgSrc}" alt = "${m.name}" class = "team-avatar-photo"
2026-04-13 23:05:47 +02:00
onerror = "const s=this.src;if(s.endsWith('.png')){this.src=s.replace('.png','.jpg');}else if(s.endsWith('.jpg')){this.src=s.replace('.jpg','.gif');}else{this.style.display='none';document.getElementById('team-avatar-fallback-${i}').style.display='flex';}" >
2026-04-05 20:49:52 +02:00
< span class = "team-avatar-fallback" id = "team-avatar-fallback-${i}" style = "display:none;" > $ { m . avatar } < / s p a n >
< / d i v >
< / d i v >
< h4 > $ { m . name } < / h 4 >
< p class = "team-role" > $ { m . role } < / p >
< p class = "team-desc" > $ { ( m . bio || '' ) . split ( '.' ) . shift ( ) } . < / p >
2026-02-06 18:36:03 +01:00
< / d i v >
2026-04-05 20:49:52 +02:00
` ;
} ) . join ( '' ) ;
let cards = Array . from ( carousel . querySelectorAll ( '.team-card' ) ) ;
2026-02-06 18:36:03 +01:00
let currentIndex = 0 ;
let cardsPerView = 3 ;
let autoPlayInterval ;
function updateCardsPerView ( ) {
if ( window . innerWidth <= 768 ) cardsPerView = 1 ;
else if ( window . innerWidth <= 1024 ) cardsPerView = 2 ;
else cardsPerView = 3 ;
}
2026-04-05 20:49:52 +02:00
function getTotalDots ( ) { return Math . max ( 1 , cards . length - cardsPerView + 1 ) ; }
2026-02-06 18:36:03 +01:00
function createDots ( ) {
dotsContainer . innerHTML = '' ;
2026-04-05 20:49:52 +02:00
for ( let i = 0 ; i < getTotalDots ( ) ; i ++ ) {
2026-02-06 18:36:03 +01:00
const dot = document . createElement ( 'div' ) ;
dot . classList . add ( 'carousel-dot' ) ;
if ( i === 0 ) dot . classList . add ( 'active' ) ;
dot . addEventListener ( 'click' , ( ) => goToSlide ( i ) ) ;
dotsContainer . appendChild ( dot ) ;
}
}
function updateCarousel ( ) {
2026-04-05 20:49:52 +02:00
cards = Array . from ( carousel . querySelectorAll ( '.team-card' ) ) ;
if ( ! cards . length ) return ;
const gap = 32 ;
2026-02-06 18:36:03 +01:00
const cardWidth = cards [ 0 ] . offsetWidth + gap ;
carousel . style . transform = ` translateX(- ${ currentIndex * cardWidth } px) ` ;
2026-04-05 20:49:52 +02:00
Array . from ( dotsContainer . querySelectorAll ( '.carousel-dot' ) ) . forEach ( ( d , i ) => d . classList . toggle ( 'active' , i === currentIndex ) ) ;
2026-02-06 18:36:03 +01:00
}
2026-04-05 20:49:52 +02:00
function goToSlide ( index ) {
currentIndex = Math . min ( index , Math . max ( 0 , cards . length - cardsPerView ) ) ;
updateCarousel ( ) ; resetAutoPlay ( ) ;
2026-02-06 18:36:03 +01:00
}
function nextSlide ( ) {
2026-04-05 20:49:52 +02:00
const max = Math . max ( 0 , cards . length - cardsPerView ) ;
currentIndex = currentIndex >= max ? 0 : currentIndex + 1 ;
2026-02-06 18:36:03 +01:00
updateCarousel ( ) ;
}
function prevSlide ( ) {
2026-04-05 20:49:52 +02:00
const max = Math . max ( 0 , cards . length - cardsPerView ) ;
currentIndex = currentIndex <= 0 ? max : currentIndex - 1 ;
2026-02-06 18:36:03 +01:00
updateCarousel ( ) ;
}
2026-04-05 20:49:52 +02:00
function startAutoPlay ( ) { clearInterval ( autoPlayInterval ) ; autoPlayInterval = setInterval ( nextSlide , 4000 ) ; }
function resetAutoPlay ( ) { clearInterval ( autoPlayInterval ) ; startAutoPlay ( ) ; }
2026-02-06 18:36:03 +01:00
prevBtn && prevBtn . addEventListener ( 'click' , ( ) => { prevSlide ( ) ; resetAutoPlay ( ) ; } ) ;
nextBtn && nextBtn . addEventListener ( 'click' , ( ) => { nextSlide ( ) ; resetAutoPlay ( ) ; } ) ;
carousel . addEventListener ( 'mouseenter' , ( ) => clearInterval ( autoPlayInterval ) ) ;
carousel . addEventListener ( 'mouseleave' , startAutoPlay ) ;
2026-04-05 20:49:52 +02:00
let resizeTimer ;
const ro = new ResizeObserver ( ( ) => {
clearTimeout ( resizeTimer ) ;
resizeTimer = setTimeout ( ( ) => { updateCardsPerView ( ) ; currentIndex = 0 ; createDots ( ) ; updateCarousel ( ) ; } , 100 ) ;
2026-02-06 18:36:03 +01:00
} ) ;
2026-04-05 20:49:52 +02:00
ro . observe ( carousel . parentElement || document . body ) ;
updateCardsPerView ( ) ; createDots ( ) ; updateCarousel ( ) ; startAutoPlay ( ) ;
2026-02-06 18:36:03 +01:00
2026-04-05 20:49:52 +02:00
// Modal
2026-02-06 18:36:03 +01:00
const modalOverlay = document . getElementById ( 'team-modal-overlay' ) ;
const modalClose = document . getElementById ( 'team-modal-close' ) ;
2026-04-07 01:51:50 +02:00
const modalAvatar = document . getElementById ( 'team-modal-avatar' ) ;
const modalName = document . getElementById ( 'team-modal-name' ) ;
const modalRole = document . getElementById ( 'team-modal-role' ) ;
const modalBio = document . getElementById ( 'team-modal-bio' ) ;
const modalStats = document . getElementById ( 'team-modal-stats' ) ;
const modalSkills = document . getElementById ( 'team-modal-skills' ) ;
const modalSocial = document . getElementById ( 'team-modal-social' ) ;
2026-02-06 18:36:03 +01:00
function openMemberModal ( index ) {
2026-04-05 20:49:52 +02:00
const m = members [ index ] ;
if ( ! m ) return ;
// Avatar avec image si dispo
const imgSrc = ` assets/images/team/ ${ m . id } .png ` ;
modalAvatar . innerHTML = `
< img src = "${imgSrc}" alt = "${m.name}" class = "team-modal-avatar-photo"
2026-04-13 23:05:47 +02:00
onerror = "const s=this.src;if(s.endsWith('.png')){this.src=s.replace('.png','.jpg');}else if(s.endsWith('.jpg')){this.src=s.replace('.jpg','.gif');}else{this.style.display='none';this.nextElementSibling.style.display='flex';}" >
2026-04-05 20:49:52 +02:00
< span class = "team-modal-avatar-fallback" > $ { m . avatar || m . name . charAt ( 0 ) } < / s p a n >
` ;
modalName . textContent = m . name ;
modalRole . textContent = m . role ;
modalBio . textContent = m . bio ;
modalStats . innerHTML = ( m . stats || [ ] ) . map ( s => `
2026-02-06 18:36:03 +01:00
< div class = "team-modal-stat" >
2026-04-05 20:49:52 +02:00
< div class = "team-modal-stat-value" > $ { s . value } < / d i v >
< div class = "team-modal-stat-label" > $ { s . label } < / d i v >
2026-02-06 18:36:03 +01:00
< / d i v >
` ).join('');
2026-04-05 20:49:52 +02:00
modalSkills . innerHTML = ( m . skills || [ ] ) . map ( s => ` <span class="team-modal-skill"> ${ s } </span> ` ) . join ( '' ) ;
2026-04-07 01:51:50 +02:00
// Réseaux sociaux depuis le JSON du membre (+ fallback discord global)
const socialHtml = typeof window . renderSocialLinks === 'function'
? window . renderSocialLinks ( m . social , ( window . OK _SOCIAL || { } ) . discord )
: '' ;
modalSocial . innerHTML = socialHtml ;
2026-02-06 18:36:03 +01:00
modalOverlay . classList . add ( 'active' ) ;
document . body . style . overflow = 'hidden' ;
}
function closeMemberModal ( ) {
modalOverlay . classList . remove ( 'active' ) ;
document . body . style . overflow = '' ;
}
carousel . querySelectorAll ( '.team-card' ) . forEach ( card => {
2026-04-05 20:49:52 +02:00
const openCard = ( ) => { const idx = parseInt ( card . dataset . member , 10 ) ; openMemberModal ( idx ) ; } ;
card . addEventListener ( 'click' , openCard ) ;
card . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' || e . key === ' ' ) { e . preventDefault ( ) ; openCard ( ) ; } } ) ;
2026-02-06 18:36:03 +01:00
} ) ;
modalClose && modalClose . addEventListener ( 'click' , closeMemberModal ) ;
2026-04-05 20:49:52 +02:00
modalOverlay && modalOverlay . addEventListener ( 'click' , e => { if ( e . target === modalOverlay ) closeMemberModal ( ) ; } ) ;
document . addEventListener ( 'keydown' , e => { if ( e . key === 'Escape' && modalOverlay && modalOverlay . classList . contains ( 'active' ) ) closeMemberModal ( ) ; } ) ;
}
} ) ;
// ═══════════════════════════════════════════════════════════════════════════
// EASTER EGG — top-level (hors DOMContentLoaded pour garantir l'attachement)
// ═══════════════════════════════════════════════════════════════════════════
( function ( ) {
var KONAMI = [ 'ArrowUp' , 'ArrowUp' , 'ArrowDown' , 'ArrowDown' , 'ArrowLeft' , 'ArrowRight' , 'ArrowLeft' , 'ArrowRight' , 'b' , 'a' ] ;
var progress = 0 ;
// PC : Konami Code — capture:true pour passer avant tout autre listener
window . addEventListener ( 'keydown' , function ( e ) {
var key = e . key . length === 1 ? e . key . toLowerCase ( ) : e . key ;
if ( key === KONAMI [ progress ] ) {
progress ++ ;
if ( progress === KONAMI . length ) {
progress = 0 ;
triggerEasterEgg ( ) ;
}
} else {
progress = ( key === KONAMI [ 0 ] ) ? 1 : 0 ;
}
} , true ) ; // capture phase — reçu avant tout stopPropagation
// Mobile : 10 taps sur le logo navbar OU logo footer
var tapCount = 0 ;
var tapTimer = null ;
document . addEventListener ( 'click' , function ( e ) {
if ( ! e . target . closest ( '.logo' ) && ! e . target . closest ( '.logo-mini' ) && ! e . target . closest ( '.omega-symbol' ) ) return ;
if ( ! ( 'ontouchstart' in window ) && navigator . maxTouchPoints === 0 ) return ;
tapCount ++ ;
clearTimeout ( tapTimer ) ;
tapTimer = setTimeout ( function ( ) { tapCount = 0 ; } , 600 ) ;
if ( tapCount >= 10 ) { tapCount = 0 ; triggerEasterEgg ( ) ; }
} ) ;
function triggerEasterEgg ( ) {
if ( document . getElementById ( 'easter-egg-overlay' ) ) return ;
var egg = document . createElement ( 'div' ) ;
egg . id = 'easter-egg-overlay' ;
egg . setAttribute ( 'role' , 'dialog' ) ;
egg . setAttribute ( 'aria-modal' , 'true' ) ;
egg . innerHTML = '<div class="easter-egg-box"><div class="easter-egg-icon">🎮</div><h2>GG\u00a0! Tu as trouvé l\u2019easter egg\u00a0!</h2><p>La vraie question c\u2019est\u00a0: combien de temps tu as mis\u00a0?</p><button id="easter-egg-close" class="btn btn-primary">Fermer</button></div>' ;
document . body . appendChild ( egg ) ;
var closeBtn = document . getElementById ( 'easter-egg-close' ) ;
closeBtn && closeBtn . focus ( ) ;
var remove = function ( ) { egg . remove ( ) ; } ;
closeBtn && closeBtn . addEventListener ( 'click' , remove ) ;
egg . addEventListener ( 'click' , function ( e ) { if ( e . target === egg ) remove ( ) ; } ) ;
window . addEventListener ( 'keydown' , function esc ( e ) {
if ( e . key === 'Escape' ) { remove ( ) ; window . removeEventListener ( 'keydown' , esc ) ; }
} , { once : true } ) ;
2026-02-06 18:36:03 +01:00
}
2026-04-05 20:49:52 +02:00
} ) ( ) ;