1 2"use strict"; 3 4const LOGO = { 5 desktop: "/assets/images/logo-2026/IMNT-WebsiteAssets-10LayoutLogo2-Desktop.svg", 6 tablet: "/assets/images/logo-2026/IMNT-WebsiteAssets-10LayoutLogo2-Tablet.svg", 7 mobile: "/assets/images/logo-2026/IMNT-WebsiteAssets-10LayoutLogo2-Mobile.svg" 8}; 9 10const SPOTIFY_PLAYLIST = "4LUeXfUtL55p3AbszRmOPZ"; 11 12const NEWSLETTER_ENDPOINT = "https://hook.eu1.make.com/qvearhjtb5zta0stbqbc2pj9ungh5mpr"; 13const SIG = { pt: "/assets/images/signature-2026/IMNT2026-Signature-PT.png", en: "/assets/images/signature-2026/IMNT2026-Signature-EN.png" }; 14 15const CAROUSEL_PHOTOS = [ 16 "/assets/images/carrossel-2026/imnt26-01.webp", 17 "/assets/images/carrossel-2026/imnt26-02.webp", 18 "/assets/images/carrossel-2026/imnt26-03.webp", 19 "/assets/images/carrossel-2026/imnt26-04.webp", 20 "/assets/images/carrossel-2026/imnt26-05.webp", 21 "/assets/images/carrossel-2026/imnt26-06.webp", 22 "/assets/images/carrossel-2026/imnt26-07.webp", 23 "/assets/images/carrossel-2026/imnt26-08.webp", 24 "/assets/images/carrossel-2026/imnt26-09.webp", 25 "/assets/images/carrossel-2026/imnt26-10.webp", 26 "/assets/images/carrossel-2026/imnt26-11.webp", 27 "/assets/images/carrossel-2026/imnt26-12.webp", 28 "/assets/images/carrossel-2026/imnt26-13.webp", 29 "/assets/images/carrossel-2026/imnt26-14.webp", 30 "/assets/images/carrossel-2026/imnt26-15.webp", 31 "/assets/images/carrossel-2026/imnt26-16.webp", 32 "/assets/images/carrossel-2026/imnt26-17.webp", 33 "/assets/images/carrossel-2026/imnt26-18.webp", 34 "/assets/images/carrossel-2026/imnt26-19.webp", 35 "/assets/images/carrossel-2026/imnt26-20.webp", 36 "/assets/images/carrossel-2026/imnt26-21.webp", 37 "/assets/images/carrossel-2026/imnt26-22.webp", 38 "/assets/images/carrossel-2026/imnt26-23.webp", 39 "/assets/images/carrossel-2026/imnt26-24.webp", 40 "/assets/images/carrossel-2026/imnt26-25.webp", 41 "/assets/images/carrossel-2026/imnt26-26.webp", 42 "/assets/images/carrossel-2026/imnt26-27.webp", 43 "/assets/images/carrossel-2026/imnt26-28.webp", 44 "/assets/images/carrossel-2026/imnt26-29.webp", 45 "/assets/images/carrossel-2026/imnt26-30.webp", 46 "/assets/images/carrossel-2026/imnt26-31.webp", 47 "/assets/images/carrossel-2026/imnt26-32.webp", 48 "/assets/images/carrossel-2026/imnt26-33.webp" 49]; 50 51const SPONSORS = { 52 pt: { 53 mobile: "/assets/images/sponsors-v6/IMNT2026-APOIOS_4Linhas-Preto.svg", 54 tablet: "/assets/images/sponsors-v6/IMNT2026-APOIOS_3Linhas-Preto.svg", 55 desktop: "/assets/images/sponsors-v6/IMNT2026-APOIOS_2Linhas-Preto.svg" 56 }, 57 en: { 58 mobile: "/assets/images/sponsors-v6/IMNT2026-SPONSORS_4Lines-Black.svg", 59 tablet: "/assets/images/sponsors-v6/IMNT2026-SPONSORS_3Lines-Black.svg", 60 desktop: "/assets/images/sponsors-v6/IMNT2026-SPONSORS_2Lines-Black.svg" 61 } 62}; 63 64const MAP = { 65 display: "/assets/images/mapa-2026/IMNT2026-MapaRecinto-2400.webp", 66 full: "/assets/images/mapa-2026/IMNT2026-MapaRecinto-full.webp" 67}; 68 69const T = { 70 pt: { 71 lang: "pt", 72 keyinfo: "17â20 SETEMBRO 2026 · MARVILA, LISBOA", 73 74 hdr_loc: "ESCOLA INDUSTRIAL AFONSO DOMINGUES · MARVILA, LISBOA", 75 hdr_dates: "17 · 18 · 19 · 20 SETEMBRO 2026", 76 ticker: "FESTIVAL IMINENTE 2026 · 17â20 SETEMBRO · ESCOLA INDUSTRIAL AFONSO DOMINGUES, MARVILA, LISBOA · ", 77 masthead_alt: "Festival Iminente â 17 a 20 de setembro de 2026, Escola Afonso Domingues, Marvila", 78 79 nav: { art: "ARTISTAS", ed: "EDIÃÃES", press: "PRESS", ft: "FICHA TÃCNICA" }, 80 81 ed_title: "10 ANOS, EDIÃÃO A EDIÃÃO", 82 ed_lede: "De um jardim municipal em Oeiras a mais de 70.000 pessoas na praça mais central do paÃs. Cada edição num lugar que a cidade tinha desistido de usar.", 83 ed_teaser: "10 ANOS, EDIÃÃO A EDIÃÃO", 84 ed_teaser_cta: "VER TODAS AS EDIÃÃES", 85 ed_global: "ALÃM DE PORTUGAL", 86 ed_global_lede: "Londres, Xangai, Rio de Janeiro, Marselha.", 87 ed_photo: "FOTOGRAFIA", 88 ed_film: "VER O FILME DA EDIÃÃO", 89 ed_lineup: "CARTAZ", 90 ed_provisional: "Textos e fotografias vindos da apresentação dos 10 anos â provisórios, por aprovar. Ficheiros finais a pedido.", 91 dnames: { "17": "QUINTA-FEIRA", "18": "SEXTA-FEIRA", "19": "SÃBADO", "20": "DOMINGO" }, 92 off: "OFF STAGE", off_perm: "EM PERMANÃNCIA", all: "TODOS", 93 prog_note: "Programa sujeito a alterações.", 94 95 nota_nenny: "Por motivos de saúde, a Nenny não irá atuar no Festival Iminente 2026.", 96 97 pf_title: "Quem veio, sabe. Quem não veio, já percebeu.",
98 pf_car_label: "FOTOGRAFIAS DO FESTIVAL IMINENTE 2026", 99 pf_prev: "FOTOS ANTERIORES", pf_next: "FOTOS SEGUINTES", 100 pf_alt: "Festival Iminente 2026, fotografia", 101 102 claim: "SÃO 10 ANOS DE IMINENTE.", 103 sig_alt: "Quem é que te convidou?", 104 105 map_title: "Fomos felizes aqui. E sabÃamos.", 106 map_open: "ABRIR MAPA EM TAMANHO REAL", 107 map_alt: "Mapa do recinto do Festival Iminente 2026 â palcos, arte, comida, lojas e serviços na Escola Industrial Afonso Domingues, Marvila", 108 109 playlist_title: "Lembra-te Como Foi", 110 fsbtn: "VER EM FULLSCREEN COM SOM", 111 112 cat_lbl: "CATEGORIA", day_lbl: "DIA", 113 art_title: "ARTISTAS", 114 gridnote: "Categorias provisórias â por confirmar.", 115 cats: { "MÃSICA": "MÃSICA", "PERFORMANCE": "PERFORMANCE", "ARTES VISUAIS": "ARTES VISUAIS", "EXPOSIÃÃES": "EXPOSIÃÃES", "CINEMA": "CINEMA", "CONVERSAS IMINENTE": "CONVERSAS IMINENTE", "TALKS": "TALKS" }, 116 see: "QUERES VER", 117 bio: "Apresentação do artista e do projeto que traz ao Iminente 2026. Percurso, contexto e o que esperar em Marvila.", 118 bio_pending: "Texto de apresentação â a fornecer", 119 close: "FECHAR â", 120 121 info_title: "INFORMAÃÃES", 122 ft_title: "FICHA TÃCNICA", 123 124 press_title: "PRESS", epk: "DESCARREGAR EPK", 125 pf: { nome: "NOME", cart: "Nº CARTEIRA DE JORNALISTA", txt: "TEXTO", send: "PEDIR ACREDITAÃÃO", subject: "Pedido de acreditação de imprensa", ok: "Abrimos o teu email com o pedido preenchido â carrega em enviar." }, 126 news_hd: "NOTÃCIAS", news_row: "TÃtulo da notÃcia â conteúdo a fornecer", 127 press_contacts: "CONTACTOS DE IMPRENSA", pc_name: "Nome", pc_email: "Email", pc_phone: "Telefone", pc_tbc: "A fornecer", 128 129 nl_title: "à MELHOR ESTARES NA NEWSLETTER!", 130 nl_ph: "[email protected]", nl_btn: "SUBSCREVER", 131 nl_ok: "Obrigado! Estás na lista.", nl_bad: "Email inválido.", 132 nl_err: "Não foi possÃvel subscrever agora. Tenta outra vez.", 133 nl_consent_a: "Ao forneceres o teu email estás a indicar que leste e aceitaste a nossa ", 134 nl_consent_priv: "PolÃtica de Privacidade", 135 nl_consent_mid: " e ", 136 nl_consent_terms: "Termos e Condições", 137 nl_consent_b: ".", 138 quick: "LINKS RÃPIDOS", redes: "REDES", legal: "LEGAL", 139 terms: "TERMOS E CONDIÃÃES", privacy: "POLÃTICA DE PRIVACIDADE E COOKIES", 140 sponsors_alt: "Parceiros do Festival Iminente 2026", 141 load_fail: "Não foi possÃvel carregar a programação. Tenta recarregar a página." 142 }, 143 en: { 144 lang: "en", 145 keyinfo: "17â20 SEPTEMBER 2026 · MARVILA, LISBON", 146 hdr_loc: "ESCOLA INDUSTRIAL AFONSO DOMINGUES · MARVILA, LISBON", 147 hdr_dates: "17 · 18 · 19 · 20 SEPTEMBER 2026", 148 ticker: "FESTIVAL IMINENTE 2026 · 17â20 SEPTEMBER · ESCOLA INDUSTRIAL AFONSO DOMINGUES, MARVILA, LISBON · ", 149 masthead_alt: "Festival Iminente â 17 to 20 September 2026, Escola Afonso Domingues, Marvila", 150 nav: { art: "ARTISTS", ed: "EDITIONS", press: "PRESS", ft: "CREDITS" }, 151 ed_title: "TEN YEARS, EDITION BY EDITION", 152 ed_lede: "From a municipal garden in Oeiras to more than 70,000 people in the most central square in the country. Every edition built in a place the city had given up on.", 153 ed_teaser: "TEN YEARS, EDITION BY EDITION", 154 ed_teaser_cta: "SEE EVERY EDITION", 155 ed_global: "BEYOND PORTUGAL", 156 ed_global_lede: "London, Shanghai, Rio de Janeiro, Marseille.", 157 ed_photo: "PHOTOGRAPHY", 158 ed_film: "WATCH THE EDITION FILM", 159 ed_lineup: "LINE-UP", 160 ed_provisional: "Copy and photographs taken from the 10th-anniversary presentation â provisional, pending approval. Final files on request.", 161 dnames: { "17": "THURSDAY", "18": "FRIDAY", "19": "SATURDAY", "20": "SUNDAY" }, 162 off: "OFF STAGE", off_perm: "ALL FESTIVAL LONG", all: "ALL", 163 prog_note: "Programme subject to change.", 164 nota_nenny: "For health reasons, Nenny will not be performing at Festival Iminente 2026.", 165 166 pf_title: "If you were there, you know. If you weren't, you get it now.", 167 pf_car_label: "PHOTOS FROM FESTIVAL IMINENTE 2026", 168 pf_prev: "PREVIOUS PHOTOS", pf_next: "NEXT PHOTOS", 169 pf_alt: "Festival Iminente 2026, photo", 170 claim: "TEN YEARS OF IMINENTE.", 171 sig_alt: "Who invited you?", 172 map_title: "We were happy here. And we knew it.", 173 map_open: "OPEN FULL-SIZE MAP", 174 map_alt: "Festival Iminente 2026 venue map â stages, art, food, stores and services at Escola Industrial Afonso Domingues, Marvila", 175 playlist_title: "Remember How It Was", 176 fsbtn: "WATCH FULLSCREEN WITH SOUND", 177 cat_lbl: "CATEGORY", day_lbl: "DAY", 178 art_title: "ARTISTS", 179 gridnote: "Categories provisional â to be confirmed.", 180 cats: { "MÃSICA": "MUSIC", "PERFORMANCE": "PERFORMANCE", "ARTES VISUAIS": "VISUAL ARTS", "EXPOSIÃÃES": "EXHIBITIONS", "CINEMA": "CINEMA", "CONVERSAS IMINENTE": "CONVERSAS IMINENTE", "TALKS": "TALKS" }, 181 see: "WANT TO SEE", 182 bio: "Introduction to the artist and the project they bring to Iminente 2026. Backgroun
182d, context and what to expect in Marvila.", 183 bio_pending: "Artist introduction â to be provided", 184 close: "CLOSE â", 185 info_title: "INFORMATION", 186 ft_title: "CREDITS", 187 press_title: "PRESS", epk: "DOWNLOAD EPK", 188 pf: { nome: "NAME", cart: "PRESS CARD Nº", txt: "MESSAGE", send: "REQUEST ACCREDITATION", subject: "Press accreditation request", ok: "We opened your mail app with the request filled in â press send." }, 189 news_hd: "NEWS", news_row: "News headline â content to be provided", 190 press_contacts: "PRESS CONTACTS", pc_name: "Name", pc_email: "Email", pc_phone: "Phone", pc_tbc: "To be provided", 191 nl_title: "YOU'D BETTER BE ON THE NEWSLETTER!", 192 nl_ph: "[email protected]", nl_btn: "SUBSCRIBE", 193 nl_ok: "Thanks! You're on the list.", nl_bad: "Invalid email.", 194 nl_err: "Could not subscribe right now. Please try again.", 195 nl_consent_a: "By giving us your email you confirm that you have read and accepted our ", 196 nl_consent_priv: "Privacy Policy", 197 nl_consent_mid: " and ", 198 nl_consent_terms: "Terms and Conditions", 199 nl_consent_b: ".", 200 quick: "QUICK LINKS", redes: "SOCIAL", legal: "LEGAL", 201 terms: "TERMS AND CONDITIONS", privacy: "PRIVACY AND COOKIE POLICY", 202 sponsors_alt: "Festival Iminente 2026 partners", 203 load_fail: "Could not load the programme. Try reloading the page." 204 } 205}; 206 207const CATS = ["MÃSICA", "PERFORMANCE", "ARTES VISUAIS", "EXPOSIÃÃES", "CINEMA", "CONVERSAS IMINENTE"]; 208const DAYS = ["17", "18", "19", "20"]; 209 210function frameFor(a) { 211 const d = (a.days || []).find(x => x !== "off-stage"); 212 return d ? "f-d" + d : "f-doff"; 213} 214const CATSLUG = { "musica": "MÃSICA", "performance": "PERFORMANCE", "artes-visuais": "ARTES VISUAIS", "exposicoes": "EXPOSIÃÃES", "cinema": "CINEMA", "conversas": "CONVERSAS IMINENTE" }; 215 216const BASE = ""; 217 218const $ = s => document.querySelector(s); 219const PAGE = document.body.dataset.page; 220 221const HDR_POOL = ["--yellow", "--cyan", "--green", "--pink", "--blue"]; 222const PICK_POOL = ["--yellow", "--cyan", "--green", "--pink", "--blue"]; 223const rnd = a => a[Math.floor(Math.random() * a.length)]; 224(function paintOnce() { 225 const r = document.documentElement.style; 226 r.setProperty("--hdr-bg", `var(${rnd(HDR_POOL)})`); 227 r.setProperty("--pick", `var(${rnd(PICK_POOL)})`); 228})(); 229 230const CAROUSEL_ORDER = (() => { 231 const a = CAROUSEL_PHOTOS.slice(); 232 for (let i = a.length - 1; i > 0; i--) { 233 const j = Math.floor(Math.random() * (i + 1)); 234 const t = a[i]; a[i] = a[j]; a[j] = t; 235 } 236 return a; 237})(); 238 239function pickLang() { 240 const q = new URLSearchParams(location.search).get("lang"); 241 if (q === "en" || q === "pt") return q; 242 try { const s = localStorage.getItem("imnt-lang"); if (s === "en" || s === "pt") return s; } catch (e) { } 243 return "pt"; 244} 245let L = pickLang(); 246let S = T[L]; 247function setLang(next) { 248 if (next === L) return; 249 L = next; S = T[L]; 250 try { localStorage.setItem("imnt-lang", L); } catch (e) { } 251 const p = new URLSearchParams(location.search); 252 L === "pt" ? p.delete("lang") : p.set("lang", "en"); 253 const q = p.toString(); 254 history.replaceState(null, "", location.pathname + (q ? "?" + q : "") + location.hash); 255 render(); 256} 257function esc(x) { const d = document.createElement("div"); d.textContent = x; return d.innerHTML; } 258 259const ICONS = { 260 instagram: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.2c3.2 0 3.6 0 4.9.07 1.2.05 1.8.25 2.2.42.6.23 1 .5 1.4.95.45.4.72.8.95 1.4.17.4.37 1 .42 2.2.07 1.3.07 1.7.07 4.9s0 3.6-.07 4.9c-.05 1.2-.25 1.8-.42 2.2-.23.6-.5 1-.95 1.4-.4.45-.8.72-1.4.95-.4.17-1 .37-2.2.42-1.3.07-1.7.07-4.9.07s-3.6 0-4.9-.07c-1.2-.05-1.8-.25-2.2-.42-.6-.23-1-.5-1.4-.95-.45-.4-.72-.8-.95-1.4-.17-.4-.37-1-.42-2.2C2.2 15.6 2.2 15.2 2.2 12s0-3.6.07-4.9c.05-1.2.25-1.8.42-2.2.23-.6.5-1 .95-1.4.4-.45.8-.72 1.4-.95.4-.17 1-.37 2.2-.42C8.4 2.2 8.8 2.2 12 2.2zm
2600 3.2A6.6 6.6 0 1 0 18.6 12A6.6 6.6 0 0 0 12 5.4zm0 2.2a4.4 4.4 0 1 1 0 8.8a4.4 4.4 0 0 1 0-8.8zm5.6-2.9a1.4 1.4 0 1 1 0 2.8a1.4 1.4 0 0 1 0-2.8z"/></svg>', 261 youtube: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21.6 7.2c-.2-1.1-1-1.9-2-2.1C17.7 4.7 12 4.7 12 4.7s-5.7 0-7.6.4c-1 .2-1.8 1-2 2.1C2 9.1 2 12 2 12s0 2.9.4 4.8c.2 1.1 1 1.9 2 2.1 1.9.4 7.6.4 7.6.4s5.7 0 7.6-.4c1-.2 1.8-1 2-2.1.4-1.9.4-4.8.4-4.8s0-2.9-.4-4.8zM9.8 15.5v-7l6 3.5-6 3.5z"/></svg>', 262 spotify: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2a10 10 0 1 0 0 20a10 10 0 0 0 0-20zm4.6 14.4a.9.9 0 0 1-1.2.3c-3.1-1.9-7-2.3-11.1-1.4a.9.9 0 1 1-.4-1.7c4.5-1 8.9-.5 12.4 1.6a.9.9 0 0 1 .3 1.2zm1.3-3a1.1 1.1 0 0 1-1.5.4c-3.5-2.2-8.9-2.8-13-1.5a1.1 1.1 0 1 1-.7-2.1c4.8-1.5 10.7-.8 14.8 1.7a1.1 1.1 0 0 1 .4 1.5zm.1-3.1C14.1 7.8 7.6 7.6 3.9 8.8a1.3 1.3 0 1 1-.8-2.5C7.4 5 14.6 5.2 19 7.9a1.3 1.3 0 0 1-1 2.4z"/></svg>' 263}; 264const SOCIALS = [ 265 ["instagram", "https://www.instagram.com/festivaliminente/", "Instagram"], 266 ["youtube", "https://www.youtube.com/@festival_iminente", "YouTube"], 267 ["spotify", "https://open.spotify.com/user/neh3z7sjssodwfo47yoxzqynq", "Spotify"] 268]; 269 270const PRESS_CONTACTS = [{ name: "Helena César", email: "[email protected]" }]; 271 272const EPK_URL = "https://drive.google.com/drive/folders/1FJqWTQk3c0y7P2RfQc2nyVckHengordV?usp=sharing"; 273const PRESS_INBOX = "[email protected]"; 274 275function langswHTML() { 276 return `<div class="langsw" role="group" aria-label="PT / EN"> 277 <button type="button" data-lang="pt" aria-pressed="${L === "pt"}">PT</button> 278 <button type="button" data-lang="en" aria-pressed="${L === "en"}">EN</button> 279 </div>`; 280} 281 282function headerHTML() { 283 const cur = k => (PAGE === k ? ' aria-current="page"' : ""); 284 return ` 285<div class="topbar"> 286 ${langswHTML()} 287</div> 288<header class="site"> 289 <a class="masthead" href="${BASE}/${L === "en" ? "?lang=en" : ""}"> 290 <picture> 291 <img src="${LOGO.desktop}" alt="${esc(S.masthead_alt)}"> 292 </picture> 293 </a> 294 <p class="hdrinfo"> 295 <span class="loc">${esc(S.hdr_loc)}</span> 296 <span class="dat">${esc(S.hdr_dates)}</span> 297 </p> 298 <div class="navwrap"> 299 <nav class="main" aria-label="${L === "en" ? "Main navigation" : "Navegação principal"}"> 300 <a href="${BASE}/artistas"${cur("artistas")}>${S.nav.art}</a> 301 <a href="${BASE}/edicoes"${cur("edicoes")}>${S.nav.ed}</a> 302 <a href="${BASE}/press"${cur("press")}>${S.nav.press}</a> 303 <a href="${BASE}/ficha-tecnica"${cur("ficha")}>${S.nav.ft}</a> 304 </nav> 305 </div> 306</header> 307`; 308} 309 310function tenBandHTML() { 311 return ` 312<section class="tenband" aria-label="${esc(S.claim)}"> 313 <p class="claim">${S.claim}</p> 314 <div class="sig"><img src="${SIG[L]}" alt="${esc(S.sig_alt)}"></div> 315</section>`; 316} 317 318function mapBlockHTML(cls) { 319 return ` 320<section class="mapblock${cls ? " " + cls : ""}" aria-labelledby="maphd"> 321 <h2 id="maphd">${S.map_title}</h2> 322 <a class="mapshot" href="${MAP.full}" target="_blank" rel="noopener"> 323 <img src="${MAP.display}" width="2400" height="2400" alt="${esc(S.map_alt)}" loading="lazy" decoding="async"> 324 </a> 325 <a class="mapopen" href="${MAP.full}" target="_blank" rel="noopener">${S.map_open}</a> 326</section>`; 327} 328 329const PF_EAGER = 1; 330function pfBlockHTML() { 331 const i = S.pf_title.indexOf(". "); 332 const halves = i < 0 ? [S.pf_title] : [S.pf_title.slice(0, i + 1), S.pf_title.slice(i + 2)]; 333 const n = CAROUSEL_ORDER.length; 334 const slides = CAROUSEL_ORDER.map((src, k) => ` 335 <li><img ${k < PF_EAGER ? `src="${src}"` : `data-src="${src}"`} width="480" height="600" 336 alt="${esc(S.pf_alt)} ${k + 1}/${n}" 337 ${k === 0 ? 'loading="eager" fetchpriority="high"' : 'loading="lazy"'} decoding="async"></li>`).join(""); 338 return ` 339<section class="pfblock" aria-labelledby="pfhd"> 340 <h2 id="pfhd" class="pfline">${halves.map(h => `<span>${esc(h)}</span>`).join(" ")}</h2> 341 <div class="pfcar" role="group" aria-label="${esc(S.pf_car_label)}"> 342 <button type="button" class="pfnav pfprev" aria-label="${esc(S.pf_prev)}"><span aria-hidden="true"><</span></button> 343 <div class="pfview"><ul class="pftrack">${slides}</ul></div> 344 <button type="button" class="pfnav pfnext" aria-label="${esc(S.pf_next)}"><span aria-hidden="true">></span></button> 345 </div> 346</section>`; 347} 348 349function bindCarousel() { 350 const car = document.querySelector(".pfcar"); 351 if (!car) return; 352 const view = car.querySelector(".pfview"); 353 const track = car.querySelector(".pftrack"); 354 const items = [...track.children]; 355 const prev = car.querySelector(".pfprev"), next = car.querySelector(".pfnext"); 356 let at = 0; 357 358 const step = () => { 359 if (items.length < 2) return items.length ? items[0].getBoundingClientRect().width : 1; 360 361 return items[1].getBoundingClientRect().left - items[0].getBoundingClientRect().left; 362 }; 363 const perView = () => { 364 const s = step(); 365 return s > 0 ? Math.max(1, Math.round(view.clientWidth / s)) : 1; 366 }; 367 function apply() { 368 const last = Math.max(0, items.length - perView()); 369 at = Math.min(Math.max(at, 0), last); 370 track.style.transform = `translateX(${-at * step()}px)`; 371 372 prev.disabled = at === 0; 373 next.disabled = at >= last; 374 } 375 const go = d =>
375 { at += d; apply(); }; 376 prev.onclick = () => go(-1); 377 next.onclick = () => go(1); 378 379 car.addEventListener("keydown", e => { 380 if (e.key === "ArrowLeft") { go(-1); e.preventDefault(); } 381 else if (e.key === "ArrowRight") { go(1); e.preventDefault(); } 382 }); 383 384 if (typeof IntersectionObserver === "function") { 385 const io = new IntersectionObserver((entries, obs) => { 386 entries.forEach(en => { 387 if (!en.isIntersecting) return; 388 const img = en.target.querySelector("img"); 389 if (img && img.dataset.src) { img.src = img.dataset.src; delete img.dataset.src; } 390 obs.unobserve(en.target); 391 }); 392 }, { root: view, rootMargin: "0px 25% 0px 25%" }); 393 items.forEach(li => { if (li.querySelector("img[data-src]")) io.observe(li); }); 394 } else { 395 items.forEach(li => { const i = li.querySelector("img[data-src]"); if (i) { i.src = i.dataset.src; delete i.dataset.src; } }); 396 } 397 carouselApply = apply; 398 apply(); 399} 400let carouselApply = null; 401addEventListener("resize", () => { if (carouselApply) carouselApply(); }); 402 403function footerHTML() { 404 return ` 405<footer class="site"> 406 <div class="news-block"> 407 <h2>${S.nl_title}</h2> 408 <form id="nlform" novalidate> 409 <input id="nlemail" type="email" placeholder="${esc(S.nl_ph)}" aria-label="Email"> 410 <button type="submit">${S.nl_btn}</button> 411 </form> 412 <p class="msg" id="nlmsg" role="status"></p> 413 <p class="consent">${S.nl_consent_a}<a href="${BASE}/privacidade${qLang()}">${S.nl_consent_priv}</a>${S.nl_consent_mid}<a href="${BASE}/termos${qLang()}">${S.nl_consent_terms}</a>${S.nl_consent_b}</p> 414 </div> 415 <div class="sponsors"> 416 <picture> 417 <source media="(max-width:640px)" srcset="${SPONSORS[L].mobile}"> 418 <source media="(max-width:1200px)" srcset="${SPONSORS[L].tablet}"> 419 <img src="${SPONSORS[L].desktop}" alt="${esc(S.sponsors_alt)}" loading="lazy"> 420 </picture> 421 </div> 422 <div class="footgrid"> 423 <div> 424 <ul> 425 <li><a href="${BASE}/artistas">${S.nav.art}</a></li> 426 <li><a href="${BASE}/edicoes">${S.nav.ed}</a></li> 427 <li><a href="${BASE}/press">${S.nav.press}</a></li> 428 <li><a href="${BASE}/ficha-tecnica">${S.nav.ft}</a></li> 429 </ul> 430 </div> 431 <div> 432 <ul> 433 <li><a href="${BASE}/termos${qLang()}">${S.terms}</a></li> 434 <li><a href="${BASE}/privacidade${qLang()}">${S.privacy}</a></li> 435 <li><a class="mail" href="mailto:[email protected]">[email protected]</a></li> 436 </ul> 437 </div> 438 <div> 439 <ul class="socials"> 440 ${SOCIALS.map(([k, href, label]) => `<li><a href="${href}" target="_blank" rel="noopener">${label}</a></li>`).join("")} 441 </ul> 442 </div> 443 </div> 444</footer>`; 445} 446 447function bindShrink() { 448 if (bindShrink.done) return; bindShrink.done = true; 449 let compact = false, queued = false; 450 const SHRINK_AT = 4, RESTORE_AT = 0; 451 const apply = () => { 452 queued = false; 453 const y = window.scrollY; 454 if (!compact && y > SHRINK_AT) compact = true; 455 else if (compact && y <= RESTORE_AT) compact = false; 456 else return; 457 document.body.classList.toggle("scrolled", compact); 458 }; 459 addEventListener("scroll", () => { if (!queued) { queued = true; requestAnimationFrame(apply); } }, { passive: true }); 460 apply(); 461} 462 463function syncHeaderHeight() { 464 const h = document.querySelector("header.site"); 465 if (!h) return; 466 const set = () => document.documentElement.style.setProperty("--hdr-h", Math.round(h.getBoundingClientRect().height) + "px"); 467 set(); 468 if (window.ResizeObserver) new ResizeObserver(set).observe(h); 469 else addEventListener("resize", set); 470} 471 472function slug(n) { 473 return n.toLowerCase().normalize("NFD").replace(/[Ì-ͯ]/g, "") 474 .replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); 475} 476function initials(n) { return n.replace(/[^\p{L}\d ]/gu, "").split(" ").filter(Boolean).slice(0, 2).map(w => w[0]).join("").toUpperCase() || "?"; } 477 478function dayTag(a) { 479 480 const pd = a.presence_days || []; 481 const pres = pd.length ? pd.join(" · ") + (L === "en" ? " SEP" : " SET") : ""; 482 if (a.days.includes("off-stage")) return pres ? S.off + " · " + pres : S.off; 483 return a.days.map(d => d + (L === "en" ? " SEP" : " SET")).join(" · "); 484} 485function catLabel(c) { return (S.cats && S.cats[c]) || c; } 486 487function qLang() { return L === "en" ? "?lang=en" : ""; } 488 489let DATA = null; 490function loadData() { 491 if (DATA) return Promise.resolve(DATA); 492 return fetch("/data/artists.json") 493 .then(r => { if (!r.ok) throw new Error("artists.json " + r.status); return r.json(); }).then(j => { 494 495 j.artists = j.artists.filter(a => !a.off_programme); 496 const seen = {};
497 j.artists.forEach((a, i) => { 498 a.slug = slug(a.name) || ("artista-" + i); 499 if (seen[a.slug]) a.slug += "-" + i; 500 seen[a.slug] = true; 501 }); 502 DATA = j; 503 return j; 504 }); 505} 506 507function festivalMinutes(t) { 508 const [h, m] = t.split(":").map(Number); 509 return (h < 6 ? h + 24 : h) * 60 + m; 510} 511function firstStart(a, day) { 512 const s = slotsFor(a, day); 513 return s.length ? Math.min(...s.map(x => festivalMinutes(x.time))) : Infinity; 514} 515function sortedForDay(list, day) { 516 return list.map((a, i) => ({ a, i, t: firstStart(a, day) })) 517 .sort((x, y) => x.t - y.t || x.i - y.i) 518 .map(x => x.a); 519} 520function slotsFor(a, day) { return (a.slots || []).filter(s => s.day === day); } 521 522function railFor(slots) { 523 if (!slots.length) return null; 524 slots = [...slots].sort((x, y) => festivalMinutes(x.time) - festivalMinutes(y.time)); 525 const times = slots.map(s => s.time).join(" · "); 526 const stages = [...new Set(slots.map(s => s.stage).filter(Boolean))]; 527 if (!stages.length) return { time: times, stage: null }; 528 if (stages.length === 1) return { time: times, stage: stages[0] }; 529 return { time: times, stage: slots.map(s => s.time + " " + s.stage).join(" · ") }; 530} 531 532function progItem(a, opts) { 533 const li = document.createElement("li"); 534 let h = ""; 535 if (opts && opts.time != null) { li.className = "timed"; h += `<span class="tm"><b>${esc(opts.time)}</b></span>`; } 536 if (a.block) h += `<span class="blk">${esc(a.block)}</span>`; 537 h += `<span class="nm">${esc(a.name.toUpperCase())}</span>`; 538 if (opts && opts.stage) h += `<span class="stg">${esc(opts.stage)}</span>`; 539 if (a.detail) h += `<span class="dt">${esc(a.detail)}</span>`; 540 li.innerHTML = h; 541 return li; 542} 543 544function renderProgram() { 545 const ag = $("#agenda"); if (!ag || !DATA) return; 546 ag.innerHTML = ""; 547 DAYS.forEach(d => { 548 const list = DATA.artists.filter(a => a.days.includes(d)); 549 const col = document.createElement("div"); 550 col.className = "daycol d" + d; col.id = "dia" + d; 551 552 col.innerHTML = `<div class="daycol__head"><span class="dnum">${d} ${L === "en" ? "SEP" : "SET"}</span><br><span class="dname">${S.dnames[d]}</span></div>`; 553 const ul = document.createElement("ul"); 554 sortedForDay(list, d).forEach(a => ul.appendChild(progItem(a, railFor(slotsFor(a, d))))); 555 col.appendChild(ul); ag.appendChild(col); 556 }); 557 const pn = $("#prognote"); 558 if (pn) pn.textContent = S.prog_note; 559 const off = DATA.artists.filter(a => a.days.includes("off-stage")); 560 const ohead = $("#offhead"); 561 if (ohead) ohead.innerHTML = `<span class="dnum">${S.off}</span> <span class="dname">${S.off_perm}</span>`; 562 const ol = $("#offlist"); 563 ol.innerHTML = ""; 564 565 off.forEach(a => { 566 const st = [...new Set((a.slots || []).map(s => s.stage).filter(Boolean))]; 567 ol.appendChild(progItem(a, st.length ? { stage: st.join(" · ") } : null)); 568 }); 569} 570 571function renderHome() { 572 573 $("#playtitle").textContent = S.playlist_title; 574 renderProgram(); 575} 576 577function initHome() { 578 const v = $("#promo"), fb = $("#fsbtn"); 579 fb.onclick = async () => { try { await v.requestFullscreen(); v.muted = false; } catch (e) { v.muted = false; } }; 580 document.addEventListener("fullscreenchange", () => { if (!document.fullscreenElement) v.muted = true; }); 581 if (matchMedia("(prefers-reduced-motion: reduce)").matches) { v.autoplay = false; v.pause(); } 582 $("#spotifyframe").src = "https://open.spotify.com/embed/playlist/" + SPOTIFY_PLAYLIST; 583 loadData().then(renderHome).catch(() => { $("#agenda").innerHTML = `<p style="padding:20px" class="small">${S.load_fail}</p>`; }); 584} 585 586const state = { cat: "ALL", day: "ALL" }; 587 588function readURL() { 589 const p = new URLSearchParams(location.search); 590 const c = p.get("cat"), d = p.get("dia"); 591 state.cat = (c && CATSLUG[c]) || "ALL"; 592 state.day = (d && (DAYS.includes(d) || d === "off")) ? (d === "off" ? "off-stage" : d) : "ALL"; 593} 594function writeURL() { 595 const p = new URLSearchParams(); 596 if (state.cat !== "ALL") p.set("cat", Object.keys(CATSLUG).find(k => CATSLUG[k] === state.cat)); 597 if (state.day !== "ALL") p.set("dia", state.day === "off-stage" ? "off" : state.day); 598 if (L === "en") p.set("lang", "en"); 599 const q = p.toString(); 600 history.replaceState(null, "", location.pathname + (q ? "?" + q : "") + location.hash); 601} 602 603const DAYCOL = { "17": "c17", "18": "c18", "19": "c19", "20": "c20", "off-stage": "coff" }; 604function chip(label, val, kind) { 605 const b = document.createElement("button"); 606 b.className = "chip" + (kind === "day" ? " day " + (DAYCOL[val] || "") : ""); b.textContent = label; 607 b.setAttribute("aria-pressed", String((kind === "cat" ? state.cat : state.day) === val)); 608 b.onclick = () => { if (kind === "cat") state.cat = val; else state.day = val; writeURL(); renderFilters(); renderGrid(); }; 609 return b; 610} 611function renderFilters() { 612 $("#daylbl").textContent = S.day_lbl; 613 const c = $("#catchips"); c.innerHTML = "";
614 c.appendChild(chip(S.all, "ALL", "cat")); 615 CATS.forEach(x => c.appendChild(chip(catLabel(x), x, "cat"))); 616 const d = $("#daychips"); d.innerHTML = ""; 617 d.appendChild(chip(S.all, "ALL", "day")); 618 DAYS.forEach(x => d.appendChild(chip(x, x, "day"))); 619 d.appendChild(chip(S.off, "off-stage", "day")); 620} 621function visible() { 622 let l = DATA.artists.filter(a => !a.programme_only); 623 if (state.cat !== "ALL") l = l.filter(a => a.category === state.cat); 624 if (state.day !== "ALL") { 625 if (state.day === "off-stage") l = l.filter(a => a.days.includes("off-stage")); 626 else l = l.filter(a => a.days.includes(state.day) || a.days.includes("off-stage")); 627 } 628 return l.sort((a, b) => a.name.localeCompare(b.name, "pt", { sensitivity: "base" })); 629} 630function renderGrid() { 631 const g = $("#agrid"); g.innerHTML = ""; 632 visible().forEach((a, i) => { 633 const b = document.createElement("button"); 634 b.className = "acell " + frameFor(a); 635 b.setAttribute("aria-label", a.name); 636 b.dataset.slug = a.slug; 637 638 b.innerHTML = `<span class="thumb">${a.photo ? `<img src="${a.photo}" alt="" loading="lazy" decoding="async">` : `<span class="init" aria-hidden="true">${initials(a.name)}</span>`}<span class="dtag">${dayTag(a)}</span><span class="plus" aria-hidden="true">+</span></span> 639 <span class="nm">${esc(a.name.toUpperCase())}</span>`; 640 b.onclick = () => { openDrawer(a, i); history.replaceState(null, "", location.pathname + location.search + "#" + a.slug); }; 641 g.appendChild(b); 642 }); 643} 644 645let lastFocus = null; 646let openArtist = null; 647function openDrawer(a, i) { 648 lastFocus = document.activeElement; 649 openArtist = { a, i }; 650 const t = $("#dr-thumb"); 651 t.className = "thumb " + frameFor(a); 652 t.innerHTML = a.photo ? `<img src="${a.photo}" alt="">` : `<span class="init">${initials(a.name)}</span>`; 653 654 $("#dr-more").innerHTML = (a.photos && a.photos.length > 1) 655 ? a.photos.slice(1).map(p => `<span class="thumb"><img src="${p}" alt="" loading="lazy"></span>`).join("") 656 : ""; 657 $("#dr-name").textContent = a.name.toUpperCase(); 658 659 const slotChips = (a.slots || []).map(sl => 660 `<span>${sl.day} ${L === "en" ? "SEP" : "SET"} · ${esc(sl.time)}` 661 + (sl.stage ? ` · ${esc(sl.stage.toUpperCase())}` : "") 662 + (sl.who ? ` · ${esc(sl.who.toUpperCase())}` : "") + `</span>`).join(""); 663 664 const hasSlotStage = (a.slots || []).some(sl => sl.stage); 665 $("#dr-meta").innerHTML = `<span>${dayTag(a)}</span><span>${esc(catLabel(a.category))}</span>` 666 + (!hasSlotStage && a.stage && a.stage.toUpperCase() !== "TBD" 667 ? `<span>${esc(a.stage.toUpperCase())}</span>` : "") 668
669 + (a.block && a.block !== a.category ? `<span>${esc(a.block)}</span>` : "") 670 + slotChips; 671 672 const bios = a.bios || []; 673 const multi = bios.length > 1; 674 $("#dr-bio").innerHTML = bios.length 675 ? bios.map(b => (multi ? `<h3 class="bioname">${esc(b.name)}</h3>` : "") 676 + (b[L] || b.pt || []).map(p => `<p>${esc(p)}</p>`).join("")).join("") 677 : `<p>${esc(S.bio)}</p>`; 678 $("#dr-pending").textContent = bios.length ? "" : S.bio_pending; 679 $("#dr-pending").hidden = bios.length > 0; 680 681 $("#drclose").textContent = S.close; 682 $("#drawer").hidden = false; $("#scrim").classList.add("on"); 683 requestAnimationFrame(() => $("#drawer").classList.add("open")); 684 $("#drclose").focus(); 685} 686function closeDrawer() { 687 openArtist = null; 688 $("#drawer").classList.remove("open"); $("#scrim").classList.remove("on"); 689 setTimeout(() => { $("#drawer").hidden = true; }, 200); 690 history.replaceState(null, "", location.pathname + location.search); 691 if (lastFocus) lastFocus.focus(); 692} 693 694function renderArtistas() { 695 $("#arttitle").textContent = S.art_title; 696 697 $("#gridnote").hidden = true; 698 renderFilters(); renderGrid(); 699 700 if (openArtist && !$("#drawer").hidden) openDrawer(openArtist.a, openArtist.i); 701} 702 703function initArtistas() { 704 readURL(); 705 loadData().then(() => { 706 renderArtistas(); 707 const h = location.hash.replace(/^#/, ""); 708 if (h) { const a = DATA.artists.find(x => x.slug === h && !x.programme_only); if (a) openDrawer(a, DATA.artists.indexOf(a)); } 709 }).catch(() => { $("#agrid").innerHTML = `<p style="padding:20px" class="small">${S.load_fail}</p>`; }); 710 $("#drclose").onclick = closeDrawer; 711 $("#scrim").onclick = closeDrawer; 712 addEventListener("keydown", e => { if (e.key === "Escape" && !$("#drawer").hidden) closeDrawer(); }); 713 addEventListener("hashchange", () => { 714 const h = location.hash.replace(/^#/, ""); 715 if (!h) { if (!$("#drawer").hidden) closeDrawer(); return; } 716 const a = DATA && DATA.artists.find(x => x.slug === h && !x.programme_only); 717 if (a) openDrawer(a, DATA.artists.indexOf(a)); 718 }); 719} 720 721let INFO = null; 722function loadInfos() { 723 if (INFO) return Promise.resolve(INFO); 724 return fetch("/data/informacoes.json") 725 .then(r => { if (!r.ok) throw new Error("informacoes.json " + r.status); return r.json(); }) 726 .then(j => { INFO = j; return j; }); 727} 728 729function infoIntroHTML(secs) { 730 const first = secs && secs[0] && secs[0].items && secs[0].items[0]; 731 if (!first) return ""; 732 const d = document.createElement("div"); 733 d.innerHTML = first.a || ""; 734 const t = (d.textContent || "").trim(); 735 const m = t.match(/^[^.!?]{40,400}[.!?]/); 736 return m ? `<p>${esc(m[0].trim())}</p>` : ""; 737} 738 739function renderInfos() { 740 $("#infotitle").textContent = S.info_title; 741 const secs = INFO && (INFO[L] || INFO.pt); 742 if (!secs) return; 743 $("#infointro").innerHTML = infoIntroHTML(secs); 744 $("#infolist").innerHTML = secs.map(sec => 745 `<section class="infosec"><h2>${esc(sec.title)}</h2>` + sec.items.map(it => 746 it.q && !it.open 747 ? `<details><summary>${esc(it.q)}</summary><div class="body">${it.a}</div></details>` 748 749 : it.q 750 ? `<div class="lead"><p><strong>${esc(it.q)}</strong></p>${it.a}</div>` 751 : `<div class="lead">${it.a}</div>`).join("") + `</section>`).join(""); 752} 753 754let FT = null; 755function loadFicha() { 756 if (FT) return Promise.resolve(FT); 757 return fetch("/data/ficha-tecnica.json") 758 .then(r => { if (!r.ok) throw new Error("ficha-tecnica.json " + r.status); return r.json(); }) 759 .then(j => { FT = j; return j; }); 760} 761function renderFicha() { 762 $("#fttitle").textContent = S.ft_title; 763 if (!FT) return; 764 const k = L === "en" ? "en" : "pt"; 765 $("#ftlist").innerHTML = FT.groups.map(g => 766 (g.title ? `<h2 class="ftgroup">${esc(g.title)}</h2>` : "") + g.sections.map(sec => 767 `<section class="ftsec${sec.names.length > 8 ? " ftmany" : ""}"><h3>${esc(sec[k])}</h3><ul>` + 768 sec.names.map(n => `<li>${esc(n.name)}${n["role_" + k] ? ` <span class="ftrole">â ${esc(n["role_" + k])}</span>` : ""}</li>`).join("") + 769 `</ul></section>`).join("")).join(""); 770} 771 772function renderPress() { 773 $("#presstitle").textContent = S.press_title; 774 const epk = $("#epk"); 775 epk.textContent = S.epk; 776 epk.href = EPK_URL; 777 epk.target = "_blank"; 778 epk.rel = "noopener"; 779 $("#lb-nome").textContent = S.pf.nome; 780 $("#lb-cart").textContent = S.pf.cart; 781 $("#lb-txt").textContent = S.pf.txt; 782 $("#pf-send").textContent = S.pf.send; 783 $("#pressok").textContent = S.pf.ok; 784 785 $("#newshd").hidden = true; 786 document.querySelector(".newslist").hidden = true; 787 788 const pcRow = (label, val, href) => 789 `<div class="row"><span class="k">${esc(label)}</span>` + 790 (val 791 ? (href 792 ? `<a class="v" href="${href}${esc(href === "tel:" ? val.replace(/[^+\d]/g, "") : val)}">${esc(val)}</a>` 793 : `<span class="v">${esc(val)}</span>`) 794 : `<span class="tbc">${esc(S.pc_tbc)}</span>`) + 795 `</div>`; 796 $("#pcontacts").innerHTML = 797 `<h3 class="pchd">${esc(S.press_contacts)}</h3><div class="pclist">` + 798 PRESS_CONTACTS.map(c => 799 `<div class="pcbox">${pcRow(S.pc_name, c.name)}${pcRow(S.pc_email, c.email, "mailto:")}` 800 + (c.phone ? pcRow(S.pc_phone, c.phone, "tel:") : "") + `</div>`
801 ).join("") + `</div>`; 802} 803function initPress() { 804 $("#pressform").onsubmit = e => { 805 e.preventDefault(); 806 const nome = $("#pf-nome").value.trim(); 807 const cart = $("#pf-cart").value.trim(); 808 const txt = $("#pf-txt").value.trim(); 809 const body = `${S.pf.nome}: ${nome}\n${S.pf.cart}: ${cart}\n\n${txt}`; 810 location.href = `mailto:${PRESS_INBOX}?subject=` 811 + encodeURIComponent(`${S.pf.subject} â ${nome}`) + `&body=` + encodeURIComponent(body); 812 $("#pressok").style.display = "block"; 813 }; 814} 815 816const EDCOLS = ["e-yellow", "e-pink", "e-blue", "e-green", "e-cyan"]; 817let ED = null; 818function loadEditions() { 819 if (ED) return Promise.resolve(ED); 820 return fetch("/data/past-editions.json") 821 .then(r => { if (!r.ok) throw new Error("past-editions.json " + r.status); return r.json(); }).then(j => { ED = j; return j; }); 822} 823function edTitle(e) { return (L === "pt" && e.title_pt) ? e.title_pt : e.title; } 824function edTag(e) { return (L === "en" && e.tag_en) ? e.tag_en : e.tag; } 825function edScale(e) { return (L === "en" && e.scale_en) ? e.scale_en : e.scale; } 826function edNote(g) { return (L === "en" && g.note_en) ? g.note_en : g.note; } 827 828function edField(o, k) { return (L === "en" && o[k + "_en"]) ? o[k + "_en"] : o[k]; } 829 830function filmHTML(v, poster) { 831 if (!v || !(v.id || v.file)) return ""; 832 833 if (!poster && v.poster) poster = { src: v.poster }; 834 const p = poster ? `<img class="poster" src="${poster.src}" alt="" loading="lazy" decoding="async">` : ""; 835 const cred = poster && poster.credit ? `<span class="pcred">© ${esc(poster.credit)}</span>` : ""; 836 837 return `<div class="edfilm${poster ? " hasposter" : ""}${v.portrait ? " portrait" : ""}" data-yt="${v.id || ""}" data-file="${v.file || ""}" data-poster="${v.poster || ""}" data-start="${v.start || 0}"> 838 <button type="button" class="playbtn">${p} 839 <span class="tri" aria-hidden="true"></span> 840 <span class="lbl">${S.ed_film}${v.title ? `<i>${esc(v.title)}</i>` : ""}</span> 841 </button>${cred} 842 </div>`; 843} 844 845function leadPhotoHTML(im) { 846 if (!im) return ""; 847 return `<figure class="edlead"><img src="${im.src}" alt="" loading="lazy" decoding="async">${im.credit ? `<figcaption>© ${esc(im.credit)}</figcaption>` : ""}</figure>`; 848} 849function bindFilms(root) { 850 root.querySelectorAll(".edfilm .playbtn").forEach(btn => { 851 btn.onclick = () => { 852 const wrap = btn.closest(".edfilm"); 853 const start = parseInt(wrap.dataset.start, 10) || 0;
854 wrap.innerHTML = wrap.dataset.file 855 ? `<video src="${wrap.dataset.file}" poster="${wrap.dataset.poster}" controls autoplay playsinline preload="metadata"></video>` 856 : `<iframe src="https://www.youtube.com/embed/${wrap.dataset.yt}?autoplay=1&rel=0${start ? "&start=" + start : ""}" title="Festival Iminente" allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture" allowfullscreen loading="lazy"></iframe>`; 857 }; 858 }); 859} 860 861function gridClass(n) { return n >= 4 ? "g4" : n === 3 ? "g3" : n === 2 ? "g2" : "g1"; } 862function photoHTML(images, cls) { 863 if (!images || !images.length) return ""; 864 return `<div class="edphotos${cls ? " " + cls : ""}">${images.map(im => 865 `<figure><img src="${im.src}" alt="" loading="lazy" decoding="async">${im.credit ? `<figcaption>© ${esc(im.credit)}</figcaption>` : ""}</figure>`).join("")}</div>`; 866} 867 868function renderEdicoes() { 869 if (!ED) return; 870 $("#edtitle").textContent = S.ed_title; 871 $("#edlede").textContent = S.ed_lede; 872 873 $("#edprov").hidden = true; 874 875 $("#edstats").innerHTML = ED.stats.map(s => `<div class="stat"><span class="v">${esc(s.v)}</span><span class="k">${esc((L === "en" && s.k_en) ? s.k_en : s.k)}</span></div>`).join(""); 876 877 $("#edlist").innerHTML = ED.editions.map((e, i) => ` 878 <article class="ed ${EDCOLS[i % EDCOLS.length]}" id="e${e.year}-${slug(e.title)}"> 879 <div class="edhead"> 880 <span class="edyear">${e.year}</span> 881 <div class="edwho"> 882 <h2>${esc(edTitle(e))}</h2> 883 <p class="edvenue">${esc(edField(e, "sub"))} · ${esc(edField(e, "dates"))}</p> 884 <p class="edtag">${esc(edTag(e))}</p> 885 </div> 886 </div> 887 <div class="edmedia"> 888 ${e.video && (e.video.id || e.video.file) ? filmHTML(e.video, (e.images || [])[0]) : leadPhotoHTML((e.images || [])[0])} 889 ${photoHTML((e.images || []).slice(1), gridClass(((e.images || []).length - 1)))} 890 </div> 891 <p class="edscale">${esc(edScale(e))}</p> 892 ${lineupHTML(e)} 893 </article>`).join(""); 894 895 function lineupHTML(e) { 896 if (!e.headliners || !e.headliners.length) return ""; 897 return `<div class="edline"><h3>${S.ed_lineup} <span class="edn">${e.headliners.length}</span></h3><p>${e.headliners.map(esc).join(" · ")}</p></div>`; 898 } 899 $("#edglobaltitle").textContent = S.ed_global; 900 $("#edgloballede").textContent = S.ed_global_lede; 901 $("#edglobal").innerHTML = ED.global.map((g, i) => ` 902 <article class="gcard ${EDCOLS[(i + 2) % EDCOLS.length]}"> 903 <span class="gyear">${g.year}</span> 904 <h3>${esc((L === "pt" && g.title_pt) ? g.title_pt : g.title)}</h3> 905 <p class="gvenue">${esc(edField(g, "venue"))} · ${esc(edField(g, "dates"))}</p> 906 ${g.video && (g.video.id || g.video.file) ? filmHTML(g.video, (g.images || [])[0]) : leadPhotoHTML((g.images || [])[0])} 907 <p class="gnote">${esc(edNote(g))}</p> 908 ${photoHTML((g.images || []).slice(1))} 909 ${lineupHTML(g)} 910 </article>`).join(""); 911 912 $("#edphototitle").textContent = S.ed_photo; 913 $("#edphotogs").innerHTML = ED.photographers.map(p => 914 `<li><b>${esc(p.name)}</b><span>${esc(edField(p, "role"))}${p.editions ? " · " + esc(edField(p, "editions")) : ""}</span></li>`).join(""); 915 916 bindFilms(document); 917 const h = location.hash.replace(/^#/, ""); 918 if (h) { const t = document.getElementById(h); if (t) t.scrollIntoView(); } 919} 920 921const TEASER_YEARS = [2016, 2018, 2019, 2022, 2024]; 922function renderTeaser() { 923 const m = $("#edteaser"); if (!m || !ED) return; 924 const pick = TEASER_YEARS.map(y => ED.editions.find(e => e.year === y && e.country === "Portugal")).filter(Boolean);
925 m.innerHTML = ` 926<section class="teaser"> 927 <div class="teaserhead"> 928 <h2>${S.ed_teaser}</h2> 929 <a class="tcta" href="${BASE}/edicoes${L === "en" ? "?lang=en" : ""}">${S.ed_teaser_cta}</a> 930 </div> 931 <div class="tstrip"> 932 ${pick.map((e, i) => ` 933 <a class="tile ${EDCOLS[i % EDCOLS.length]}" href="${BASE}/edicoes${L === "en" ? "?lang=en" : ""}#e${e.year}-${slug(e.title)}"> 934 ${e.images && e.images[0] ? `<img src="${e.images[0].src}" alt="" loading="lazy" decoding="async">` : ""} 935 <span class="ty">${e.year}</span> 936 <span class="tt">${esc(edTitle(e))}</span> 937 </a>`).join("")} 938 </div> 939</section>`; 940} 941 942function renderLegal() { 943 const doc = LEGAL[document.body.dataset.doc]; 944 const d = doc[L] || doc.pt; 945 $("#legaltitle").textContent = d.title; 946 const mail = `<a class="mail" href="mailto:${esc(LEGAL_MAIL)}">${esc(LEGAL_MAIL)}</a>`; 947 $("#legalbody").innerHTML = d.blocks.map(([kind, v]) => { 948 if (kind === "h") return `<h2>${esc(v)}</h2>`; 949 if (kind === "h2") return `<h3>${esc(v)}</h3>`; 950 if (kind === "p") return `<p>${esc(v)}</p>`; 951 if (kind === "ul") return `<ul>${v.map(li => `<li>${esc(li)}</li>`).join("")}</ul>`; 952 if (kind === "addr") return `<p class="addr">${v.map(esc).join("<br>")}</p>`; 953 if (kind === "mail") return `<p><a class="mail" href="mailto:${esc(v)}">${esc(v)}</a></p>`; 954 955 if (kind === "pmail") return `<p>${esc(v[0])}${mail}${esc(v[1])}</p>`; 956 if (kind === "links") return `<ul>${v.map(([t, href]) => 957 `<li><a href="${href}" target="_blank" rel="noopener">${esc(t)}</a></li>`).join("")}</ul>`; 958 return ""; 959 }).join(""); 960} 961 962function bindNewsletter() { 963 const f = $("#nlform"); if (!f) return; 964 f.onsubmit = e => { 965 e.preventDefault(); 966 const msg = $("#nlmsg"); 967 const btn = f.querySelector("button[type=submit]"); 968 const v = $("#nlemail").value.trim(); 969 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) { msg.textContent = S.nl_bad; return; } 970 btn.disabled = true; 971 972 fetch(NEWSLETTER_ENDPOINT, { 973 method: "POST", 974 headers: { "Content-Type": "application/json" }, 975 body: JSON.stringify({ email: v }) 976 }).then(r => { 977 if (!r.ok) throw new Error("signup " + r.status); 978 msg.textContent = S.nl_ok; 979 f.reset(); 980 }).catch(() => { 981 msg.textContent = S.nl_err; 982 }).finally(() => { btn.disabled = false; }); 983 }; 984} 985 986function render() { 987 document.documentElement.lang = L; 988 989 if (!$(".noise")) { 990 const n = document.createElement("div"); 991 n.className = "noise"; 992 n.setAttribute("aria-hidden", "true"); 993 document.body.appendChild(n); 994 } 995 $("#hdr").innerHTML = headerHTML(); 996 $("#ftr").innerHTML = footerHTML(); 997 const nota = $("#notanenny p"); if (nota) nota.textContent = S.nota_nenny; 998 999 const pf = $("#posfestival"); 1000 if (pf) { pf.innerHTML = pfBlockHTML(); bindCarousel(); } 1001 1002 const mapa = $("#mapa"); if (mapa) mapa.innerHTML = mapBlockHTML(); 1003 const infomapa = $("#infomap"); if (infomapa) infomapa.innerHTML = mapBlockHTML("onblue"); 1004 const band = $("#tenband"); if (band) band.innerHTML = tenBandHTML(); 1005 document.querySelectorAll(".langsw button").forEach(b => b.onclick = () => setLang(b.dataset.lang)); 1006 const fsb = $("#fsbtn"); 1007 if (fsb) fsb.textContent = S.fsbtn; 1008 bindNewsletter(); 1009 syncHeaderHeight(); 1010 bindShrink(); 1011 if (PAGE === "home" && DATA) renderHome(); 1012 if (PAGE === "home" && ED) renderTeaser(); 1013 if (PAGE === "artistas" && DATA) renderArtistas(); 1014 if (PAGE === "informacoes") renderInfos(); 1015 if (PAGE === "ficha") renderFicha(); 1016 if (PAGE === "press") renderPress(); 1017 if (PAGE === "edicoes" && ED) renderEdicoes(); 1018 if (PAGE === "legal") renderLegal(); 1019} 1020 1021render(); 1022if (PAGE === "home") { initHome(); loadEditions().then(renderTeaser).catch(() => { }); } 1023else if (PAGE === "artistas") initArtistas(); 1024else if (PAGE === "press") initPress(); 1025else if (PAGE === "edicoes") loadEditions().then(renderEdicoes).catch(() => { 1026 $("#edlist").innerHTML = `<p style="padding:20px" class="small">${S.load_fail}</p>`; 1027}); 1028else if (PAGE === "ficha") loadFicha().then(renderFicha).catch(() => { 1029 $("#ftlist").innerHTML = `<p style="padding:20px" class="small">${S.load_fail}</p>`; 1030}); 1031else if (PAGE === "informacoes") loadInfos().then(renderInfos).catch(() => { 1032 $("#infolist").innerHTML = `<p style="padding:20px" class="small">${S.load_fail}</p>`; 1033});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.