{"id":69491,"date":"2026-07-23T10:41:00","date_gmt":"2026-07-23T08:41:00","guid":{"rendered":"https:\/\/www.cruillabarcelona.cat\/el-meu-cartell-2027\/"},"modified":"2026-07-24T01:21:48","modified_gmt":"2026-07-23T23:21:48","slug":"el-meu-cartell-2027","status":"publish","type":"page","link":"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/","title":{"rendered":"EL MEU CARTELL 2027"},"content":{"rendered":"\n<template id=\"cruilla-lineup-template\">\n <style>\n :host{\n display:block;\n width:100%;\n max-width:var(--widget-max-width,1040px);\n margin:0 auto;\n font-family:inherit;\n }\n *{box-sizing:border-box}\n button,input{font:inherit}\n .widget{\n --yellow:#FEED01;\n --black:#000000;\n --white:#FFFFFF;\n --page-bg:transparent;\n --frame-width:4px;\n --poster-width:310px;\n --display-face:\"Cruilla\",Arial,Helvetica,sans-serif;\n width:100%;\n background:var(--page-bg);\n color:var(--black);\n font-family:inherit;\n font-size:16px;\n line-height:1.45;\n -webkit-font-smoothing:antialiased;\n }\n .frame{\n width:100%;\n border:var(--frame-width) solid var(--black);\n background:transparent;\n }\n .shell{width:100%;margin:0 auto;padding:30px clamp(18px,3vw,36px) 44px}\n .display{font-family:var(--display-face);font-weight:700;text-transform:uppercase;line-height:.88;letter-spacing:-.005em}\n .label{font-family:var(--display-face);font-weight:700;text-transform:uppercase;letter-spacing:.1em}\n .xmark{display:inline-block;vertical-align:middle;fill:currentColor}\n\n .masthead{text-align:center;margin-bottom:34px}\n .masthead .marks{display:flex;justify-content:center;gap:8px;margin-bottom:20px}\n .masthead .marks .xmark{width:64px;height:64px}\n .pill{\n display:inline-block;background:var(--white);color:var(--black);\n padding:10px 26px 8px;border-radius:999px;border:2px solid var(--black);\n font-family:var(--display-face);font-weight:700;text-transform:uppercase;\n font-size:clamp(1rem,2.6vw,1.35rem);letter-spacing:.01em;\n }\n .masthead h1{font-size:clamp(2.65rem,7.5vw,5rem);margin:20px 0 0}\n .lede{max-width:50ch;margin:14px auto 0;font-size:1rem;font-weight:500}\n\n .layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(250px,var(--poster-width));gap:clamp(28px,4.5vw,50px);align-items:start}\n .poster-rail{width:100%;max-width:var(--poster-width);justify-self:end;position:sticky;top:24px}\n .layout.complete{display:block;max-width:var(--poster-width);margin:0 auto}\n .layout.complete .poster-rail{position:static;max-width:var(--poster-width);margin:0 auto}\n\n .q{border-top:3px solid var(--black);padding:22px 0 6px}\n .q:first-of-type{border-top:none;padding-top:0}\n .q-tier{display:flex;justify-content:flex-start;gap:12px;align-items:center;font-size:11.5px;margin-bottom:8px}\n .q-tier .n{display:flex;align-items:center;gap:8px}\n .q-tier .xmark{width:11px;height:11px}\n .q-tier [data-role=\"requirement\"]{display:none !important}\n .q-optional{opacity:.55}\n .q-title{font-size:clamp(1.45rem,3.5vw,2rem);margin:0 0 5px}\n .q-hint{margin:0 0 14px;font-size:.9rem;font-weight:400;opacity:.75}\n\n .combo{position:relative}\n .combo input{\n width:100%;font-size:1rem;color:var(--black);padding:13px 44px 13px 14px;\n background:var(--white);border:3px solid var(--black);border-radius:0;appearance:none;\n }\n .combo input::placeholder{color:rgba(0,0,0,.42)}\n .combo input:focus{outline:4px solid var(--black);outline-offset:3px}\n .combo .glyph{position:absolute;right:14px;top:50%;transform:translateY(-50%);width:14px;height:14px;pointer-events:none}\n .combo.busy .glyph{animation:blink .7s steps(2,end) infinite}\n @keyframes blink{50%{opacity:.12}}\n\n .results{\n list-style:none;margin:0;padding:0;position:absolute;z-index:20;left:0;right:0;top:calc(100% - 3px);\n background:var(--white);border:3px solid var(--black);max-height:290px;overflow-y:auto;\n box-shadow:5px 5px 0 rgba(0,0,0,.18);\n }\n .results:empty{display:none}\n .results li{display:flex;gap:11px;align-items:center;padding:8px 11px;cursor:pointer;border-bottom:1px solid rgba(0,0,0,.18)}\n .results li:last-child{border-bottom:none}\n .results li[aria-selected=\"true\"]{background:var(--yellow)}\n .results img,.results .noart{width:40px;height:40px;object-fit:cover;flex:none;background:rgba(0,0,0,.12)}\n .results .nm{font-family:var(--display-face);font-weight:700;text-transform:uppercase;font-size:.98rem;line-height:1.1}\n .results .gn{font-size:10px;text-transform:uppercase;letter-spacing:.08em;opacity:.6}\n .results .msg{display:block;padding:12px;font-size:.88rem;opacity:.72;cursor:default}\n\n .chosen{display:flex;gap:12px;align-items:center;padding:11px;background:var(--black);color:var(--yellow);border:3px solid var(--black)}\n .chosen[hidden],.combo[hidden]{display:none}\n .chosen img,.chosen .noart{width:48px;height:48px;object-fit:cover;flex:none;background:rgba(255,255,255,.15)}\n .chosen .nm{font-family:var(--display-face);font-weight:700;text-transform:uppercase;font-size:1.3rem;line-height:.95;display:block}\n .chosen .sub{font-size:10px;letter-spacing:.12em;text-transform:uppercase;opacity:.68}\n .chosen button{\n margin-left:auto;background:none;border:none;cursor:pointer;padding:7px;color:var(--yellow);\n font-family:var(--display-face);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;font-weight:700;\n text-decoration:underline;text-underline-offset:3px;\n }\n .chosen button:focus-visible{outline:3px solid var(--yellow);outline-offset:2px}\n .spotify-credit{font-size:.74rem;opacity:.58;margin:8px 0 0}\n\n .poster{\n position:relative;\n width:100%;background:var(--yellow);border:3px solid var(--black);padding:0;overflow:hidden;\n container-type:inline-size;aspect-ratio:1 \/ 1;display:flex;flex-direction:column;\n }\n.poster-ribbon{\n position:absolute;\n top:16cqw;\n right:-16cqw;\n width:70cqw;\n z-index:10;\n pointer-events:none;\n\n background:var(--black);\n color:var(--white);\n\n font-family:var(--display-face);\n font-weight:700;\n text-transform:uppercase;\n text-align:center;\n\n font-size:var(--ribbon-size,2.05cqw);\n line-height:1;\n letter-spacing:.018em;\n white-space:nowrap;\n\n padding:2.05cqw 3cqw 1.7cqw;\n\n transform:rotate(45deg);\n transform-origin:center;\n\n overflow:hidden;\n}\n .poster-top{padding:4.6cqw 5.8cqw 0;text-align:center;flex:none}\n .toprow{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;min-height:15.2cqw;margin-bottom:2.6cqw}\n .toprow>*{min-width:0}\n .tagline-top{\n justify-self:start;align-self:center;text-align:center;font-family:var(--display-face);font-weight:700;\n text-transform:uppercase;font-size:3.45cqw;line-height:1;letter-spacing:-.015em;\n }\n .toprow .yr-top{justify-self:end;align-self:center;font-family:var(--display-face);font-weight:700;font-size:5.8cqw;line-height:.85;color:var(--black)}\n .poster-top .marks{display:flex;justify-content:center;gap:.75cqw;margin:0}\n .poster-top .marks .xmark{width:13.8cqw;height:13.8cqw}\n .poster .pill{font-size:3.35cqw;line-height:1;padding:1.5cqw 3.8cqw 1.18cqw;border:.24cqw solid var(--black)}\n .poster-subtitle{\n margin:1.25cqw auto 0;\n max-width:82cqw;\n color:var(--black);\n font-family:var(--display-face);\n font-weight:700;\n text-transform:uppercase;\n text-align:center;\n font-size:2.05cqw;\n line-height:1;\n letter-spacing:.018em;\n }\n .bill{\n flex:1;min-height:0;padding:2.5cqw 3cqw 1cqw;display:flex;flex-direction:column;\n justify-content:center;gap:.55cqw;text-align:center;\n }\n .line{\n --line-size:8.5cqw;display:flex;align-items:center;justify-content:center;\n gap:.78cqw;flex-wrap:nowrap;margin:0;min-height:calc(var(--line-size) * .82);\n }\n .line .xmark{width:2.15cqw;height:2.15cqw;flex:none}\n .slot{text-align:center;min-width:0}\n .slot .lbl{display:none}\n .slot .val{\n font-family:var(--display-face);font-weight:700;text-transform:uppercase;line-height:.78;\n letter-spacing:-.024em;white-space:nowrap;display:block;font-size:var(--line-size);\n color:#000000 !important;-webkit-text-fill-color:#000000 !important;\n -webkit-text-stroke:.18px #000000;paint-order:stroke fill;\n text-shadow:none !important;opacity:1 !important;\n }\n .slot.empty{display:none}\n .slot.filled .val{animation:stamp .24s cubic-bezier(.2,1.5,.4,1)}\n @keyframes stamp{from{transform:scale(.9);opacity:0}to{transform:scale(1);opacity:1}}\n .poster-foot{position:relative;width:100%;height:28.5cqw;margin-top:auto;flex:none}\n .band{position:absolute;inset:0;width:100%;height:100%}\n .poster-foot .wm{position:absolute;left:24.5%;bottom:19%;width:22%;height:17.8%;transform:translateX(-50%)}\n .poster-foot .foot-claim{\n position:absolute;left:50%;bottom:18%;transform:translateX(-50%);width:22%;\n color:var(--black);font-family:var(--display-face);font-weight:700;text-transform:uppercase;\n text-align:center;font-size:2.75cqw;line-height:.82;letter-spacing:-.02em;\n }\n .poster-foot .foot-claim.personalized{\n width:26%;\n bottom:15.5%;\n font-size:2.5cqw;\n line-height:.78;\n }\n .poster-foot .foot-claim.personalized span{display:block}\n .poster-foot .foot-claim .claim-brand{\n white-space:nowrap;\n font-size:2.35cqw;\n }\n .poster-foot .foot-claim .claim-x{\n font-size:2.8cqw;\n line-height:.72;\n margin:.22cqw 0;\n }\n .poster-foot .foot-claim .claim-name{\n width:100%;\n white-space:nowrap;\n overflow:hidden;\n text-overflow:ellipsis;\n font-size:var(--claim-name-size,2.65cqw);\n line-height:.82;\n }\n .poster-foot .yr{position:absolute;right:18.3%;bottom:19%;line-height:.76;color:#FFFFFF !important;-webkit-text-fill-color:#FFFFFF !important;-webkit-text-stroke:.18px #FFFFFF;paint-order:stroke fill;text-shadow:none !important;opacity:1 !important;font-family:var(--display-face);font-weight:700;font-size:6.2cqw}\n\n .poster-actions{display:flex;justify-content:center;margin-top:12px}\n .download{\n width:100%;justify-content:center;font-family:var(--display-face);font-weight:700;text-transform:uppercase;line-height:1;\n color:var(--black);background:transparent;border:3px solid var(--black);cursor:pointer;\n display:inline-flex;align-items:center;gap:10px;padding:11px 14px;font-size:.95rem;\n }\n .download:hover:not(:disabled){background:var(--black);color:var(--yellow)}\n .download:disabled{opacity:.38;cursor:not-allowed}\n .download-status{min-height:1.2em;margin-top:7px;text-align:center;font-size:.75rem;opacity:.7}\n\n .finish{margin-top:32px;border-top:4px solid var(--black);padding-top:22px}\n .who{margin-bottom:17px}\n .who label{display:block;font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;margin-bottom:7px;font-family:var(--display-face);font-weight:700}\n .who input{width:100%;max-width:390px;padding:12px 14px;border:3px solid var(--black);background:var(--white);font-size:1rem}\n .who input:focus{outline:4px solid var(--black);outline-offset:3px}\n .privacy-box{max-width:720px;margin:17px 0 19px;font-size:.86rem}\n .privacy-check{display:flex;align-items:flex-start;gap:10px;font-weight:600}\n .privacy-check input{width:19px;height:19px;flex:none;margin:2px 0 0;accent-color:var(--black)}\n .privacy-copy{margin:8px 0 0 29px;opacity:.75}\n .privacy-copy a{color:inherit;text-underline-offset:3px}\n .send,.again{\n font-family:var(--display-face);font-weight:700;text-transform:uppercase;line-height:1;\n color:var(--yellow);background:var(--black);border:3px solid var(--black);cursor:pointer;\n display:inline-flex;align-items:center;gap:13px;transition:transform .1s ease;\n }\n .send{font-size:1.55rem;padding:15px 30px}\n .again{font-size:1.05rem;padding:13px 21px;margin-top:20px}\n .send .xmark{width:16px;height:16px}\n .send:hover:not(:disabled),.again:hover{transform:translate(2px,2px)}\n .send:focus-visible,.again:focus-visible{outline:4px solid var(--black);outline-offset:4px}\n .send:disabled{background:rgba(0,0,0,.22);border-color:transparent;color:rgba(0,0,0,.45);cursor:not-allowed}\n .send:disabled .xmark{fill:rgba(0,0,0,.45)}\n .status{margin-top:14px;font-size:.92rem;min-height:1.4em;font-weight:500}\n .status.err{font-weight:700}\n .done{text-align:center;padding:20px 0 28px}\n .done h2{font-size:clamp(2.4rem,8vw,4.5rem);margin:0 0 14px}\n .done p{max-width:46ch;margin:0 auto;font-weight:500}\n\n @media (max-width:820px){\n :host{max-width:min(100%,680px)}\n .layout{display:flex;flex-direction:column;gap:28px}\n .poster-rail{order:-1;position:static;align-self:center;max-width:min(100%,285px)}\n .layout.complete{max-width:285px}\n .masthead .marks .xmark{width:52px;height:52px}\n }\n @media (max-width:520px){\n .frame{border-width:3px}\n .shell{padding:23px 15px 34px}\n .masthead{margin-bottom:28px}\n .send{width:100%;justify-content:center;font-size:1.3rem;padding:14px 16px}\n .privacy-copy{margin-left:0}\n .poster-rail{max-width:min(100%,260px)}\n .layout.complete{max-width:260px}\n }\n @media (prefers-reduced-motion:reduce){\n *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important;scroll-behavior:auto !important}\n }\n <\/style>\n\n <div class=\"widget\">\n <div class=\"frame\">\n <div class=\"shell\">\n <header class=\"masthead\">\n <div class=\"marks\">\n <svg class=\"xmark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\"><path d=\"M50,76.55 26.55,100 0,73.45 23.45,50 0,26.55 26.55,0 50,23.45 73.45,0 100,26.55 76.55,50 100,73.45 73.45,100Z\" fill=\"#000000\"><\/path><\/svg>\n <svg class=\"xmark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\"><path d=\"M50,76.55 26.55,100 0,73.45 23.45,50 0,26.55 26.55,0 50,23.45 73.45,0 100,26.55 76.55,50 100,73.45 73.45,100Z\" fill=\"#000000\"><\/path><\/svg>\n <\/div>\n <span class=\"pill\" id=\"cl-mast-pill\"><\/span>\n <h1 class=\"display\" id=\"cl-title\"><\/h1>\n <p class=\"lede\" id=\"cl-lede\"><\/p>\n <\/header>\n\n <div class=\"layout\">\n <main id=\"cl-form\"><\/main>\n <aside class=\"poster-rail\">\n <div class=\"poster\" id=\"cl-poster\">\n <div class=\"poster-ribbon\" id=\"cl-poster-ribbon\"><\/div>\n <div class=\"poster-top\">\n <div class=\"toprow\">\n <div class=\"tagline-top\" id=\"cl-poster-tagline\"><\/div>\n <div class=\"marks\">\n <svg class=\"xmark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\"><path d=\"M50,76.55 26.55,100 0,73.45 23.45,50 0,26.55 26.55,0 50,23.45 73.45,0 100,26.55 76.55,50 100,73.45 73.45,100Z\" fill=\"#000000\"><\/path><\/svg>\n <svg class=\"xmark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\"><path d=\"M50,76.55 26.55,100 0,73.45 23.45,50 0,26.55 26.55,0 50,23.45 73.45,0 100,26.55 76.55,50 100,73.45 73.45,100Z\" fill=\"#000000\"><\/path><\/svg>\n <\/div>\n <span class=\"yr-top\" data-year=\"\"><\/span>\n <\/div>\n <span class=\"pill\" id=\"cl-poster-pill\"><\/span>\n <div class=\"poster-subtitle\" id=\"cl-poster-subtitle\"><\/div>\n <\/div>\n <div class=\"bill\" id=\"cl-bill\"><\/div>\n <div class=\"poster-foot\">\n <svg class=\"band\" viewBox=\"0 0 1200 368\" preserveAspectRatio=\"none\" aria-hidden=\"true\"><path fill=\"#000000\" d=\"M525,367.5L62,367.5L136.5,292L1.5,155L157,-0.5L293,134.5L430,-0.5L586.5,156L450.5,292L525,367.5ZM1137,367.5L674,367.5L748.5,292L612.5,156L769,-0.5L905,134.5L1042,-0.5L1197.5,155L1062.5,292L1137,367.5Z\"><\/path><\/svg>\n <svg class=\"wm\" viewBox=\"0 0 689.99 187.59\" role=\"img\" aria-label=\"Cru\u00eflla\"><path fill=\"#FFFFFF\" d=\"M628.73,56.18h-16.49l-48.22,101.5h-37.51V56.18h-30.27v128.69h87.36l9.23-19.94h54.92l9.25,19.94h32.99l-61.27-128.69ZM604.45,140.1l7.42-15.77c4.89-10.52,8.15-18.13,8.15-18.13h.55s3.26,7.62,8.15,18.13l7.44,15.77h-31.72ZM443.69,157.68V56.18h-30.27v128.69h72.13v-27.19h-41.86ZM426.11,12.69l-12.51-12.69-10.88,11.06-11.06-11.06-12.69,12.69,11.06,10.86-11.06,10.88,12.69,12.69,11.06-11.06,10.7,11.06,12.69-12.69-10.7-10.88,10.7-10.86ZM361.4,56.18v128.69h30.27V56.18h-30.27ZM374.09,12.69l-12.51-12.69-10.88,11.06-11.06-11.06-12.69,12.69,11.06,10.86-11.06,10.88,12.69,12.69,11.06-11.06,10.7,11.06,12.69-12.69-10.7-10.88,10.7-10.86ZM309.38,56.18v80.28c0,19.94-15.4,21.75-21.56,21.75s-21.57-1.81-21.57-21.75V56.18h-30.27v81.02c0,34.44,26.65,50.39,51.84,50.39s51.83-15.95,51.83-50.39V56.18h-30.27ZM201.35,137.02c9.43-6.52,18.85-17.4,18.85-34.98,0-26.47-16.13-45.86-56.91-45.86h-38.06v128.69h29.55v-102.95h8.89c19.57,0,26.45,9.07,26.45,20.3,0,13.05-8.87,20.3-24.28,22.11v.72l26.09,59.82h32.27v-.73l-22.85-47.11ZM68.51,82.65c27.01,0,37.88,20.11,37.88,20.11h.9v-36.43s-12.69-12.69-38.96-12.69C30.81,53.64,0,80.46,0,120.52s30.81,67.07,68.33,67.07c26.27,0,38.96-12.69,38.96-12.69v-36.43h-.9s-10.88,20.11-37.88,20.11c-22.47,0-37.16-15.58-37.16-38.06s14.69-37.87,37.16-37.87Z\"><\/path><\/svg>\n <span class=\"foot-claim\" id=\"cl-foot-claim\"><\/span>\n <span class=\"yr\" data-year=\"\"><\/span>\n <\/div>\n <\/div>\n <div class=\"poster-actions\">\n <button class=\"download\" id=\"cl-download\" type=\"button\" disabled=\"\"><\/button>\n <\/div>\n <div class=\"download-status\" id=\"cl-download-status\" role=\"status\" aria-live=\"polite\"><\/div>\n <\/aside>\n <\/div>\n\n <div class=\"finish\" id=\"cl-finish\">\n <div class=\"who\">\n <label for=\"cl-name\" id=\"cl-name-label\"><\/label>\n <input type=\"text\" id=\"cl-name\" autocomplete=\"name\" maxlength=\"40\">\n <\/div>\n\n <div class=\"who\">\n <label for=\"cl-email\" id=\"cl-email-label\"><\/label>\n <input type=\"email\" id=\"cl-email\" autocomplete=\"email\">\n <\/div>\n\n <div class=\"privacy-box\">\n <label class=\"privacy-check\">\n <input type=\"checkbox\" id=\"cl-consent\">\n <span id=\"cl-consent-text\"><\/span>\n <\/label>\n <p class=\"privacy-copy\"><span id=\"cl-privacy-copy\"><\/span> <a id=\"cl-privacy-link\" target=\"_blank\" rel=\"noopener\"><\/a>.<\/p>\n <\/div>\n\n <button class=\"send\" id=\"cl-send\" disabled=\"\">\n <svg class=\"xmark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\"><path d=\"M50,76.55 26.55,100 0,73.45 23.45,50 0,26.55 26.55,0 50,23.45 73.45,0 100,26.55 76.55,50 100,73.45 73.45,100Z\" fill=\"#FEED01\"><\/path><\/svg>\n <span id=\"cl-send-label\"><\/span>\n <\/button>\n <div class=\"status\" id=\"cl-status\" role=\"status\" aria-live=\"polite\"><\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n<\/template>\n\n<div id=\"cruilla-lineup-mount\"><\/div>\n\n<script>\n(function () {\n 'use strict';\n\n \/* ======================================================================\n CONFIGURACI\u00d3 F\u00c0CIL \u2014 EDITA PRINCIPALMENT AQUEST BLOC\n ====================================================================== *\/\n const CONFIG = {\n apiBase: 'https:\/\/cruilla-lineup-api.pau-roca.workers.dev',\n year: '2027',\n\n design: {\n widgetMaxWidth: '1040px',\n posterWidth: '310px',\n frameWidth: '4px',\n pageBackground: 'transparent',\n posterYellow: '#FEED01',\n black: '#000000',\n white: '#FFFFFF'\n },\n\n search: {\n minChars: 2,\n debounceMs: 160,\n maxResults: 8,\n cacheMs: 10 * 60 * 1000\n },\n\n language: {\n fallback: 'en',\n supported: ['ca', 'es', 'en']\n },\n\n privacyUrls: {\n ca: 'https:\/\/www.cruillabarcelona.cat\/politica-de-privacitat\/',\n es: 'https:\/\/www.cruillabarcelona.cat\/politica-de-privacitat\/',\n en: 'https:\/\/www.cruillabarcelona.cat\/politica-de-privacitat\/'\n },\n\n texts: {\n ca: {\n mastPill: 'El meu Cru\u00eflla 2027',\n titleHtml: 'Tu fas<br>el cartell',\n lede: \"Tria els artistes que m\u00e9s t'agradaria veure al Cru\u00eflla i construeix el teu cartell.\",\n posterTaglineHtml: 'CRU\u00cfLLA<br>\u00c9S CASA',\n posterPill: 'El meu Cru\u00eflla 2027',\n posterBottomHtml: 'EL MEU<br>CARTELL',\n posterRibbonSuffix: 'Programaci\u00f3 no oficial',\n posterRibbonFallbackName: 'El meu cartell',\n required: 'Obligatori',\n optional: 'Opcional',\n searchPlaceholder: 'Busca un artista\u2026',\n searchCredit: \"Cerca d'artistes mitjan\u00e7ant el cat\u00e0leg de Spotify.\",\n change: 'Canvia',\n noResults: 'No hem trobat cap artista amb aquest nom.',\n searchBusy: 'Cercant\u2026',\n searchRateLimit: 'Hi ha massa cerques alhora. Torna-ho a provar en uns segons.',\n searchUnavailable: 'La cerca no respon ara mateix. Torna-ho a provar.',\n searchConnection: 'Revisa la connexi\u00f3 i torna-ho a provar.',\n nameLabel: 'El teu nom',\n namePlaceholder: 'Escriu el teu nom',\n emailLabel: 'El teu correu electr\u00f2nic',\n emailPlaceholder: 'nom@exemple.cat',\n consent: \"Accepto que Barcelona Events Musicals, SLU tracti el meu correu i els artistes seleccionats per analitzar les prefer\u00e8ncies musicals del p\u00fablic i per avisar-me si algun d'aquests artistes s'incorpora a la programaci\u00f3 del Cru\u00eflla.\",\n privacyCopy: \"Aquest formulari no comporta l'alta a la newsletter. Pots retirar el consentiment escrivint a festival@cruillabarcelona.cat.\",\n privacyLink: 'Consulta la pol\u00edtica de privacitat',\n send: 'Envia el meu cartell',\n sending: 'Enviant\u2026',\n sendingSlow: 'Encara estem guardant el cartell\u2026',\n submitTimeout: \"El servidor est\u00e0 trigant massa. Torna-ho a provar.\",\n consentRequired: 'Cal acceptar el tractament de dades abans d\u2019enviar el cartell.',\n submitError: \"No hem pogut guardar el cartell. Torna-ho a provar d'aqu\u00ed a un moment.\",\n doneTitle: 'Moltes gr\u00e0cies',\n doneText: \"Ens hem guardat el teu cartell. No prometem res, per\u00f2 si algun dels artistes que has triat s'incorpora a la programaci\u00f3 del Cru\u00eflla, podrem avisar-te per correu.\",\n again: 'Fes un altre cartell',\n download: 'Descarrega el cartell',\n downloading: 'Preparant la imatge\u2026',\n downloadReady: 'Imatge descarregada.',\n downloadError: \"No s'ha pogut crear la imatge.\",\n downloadFilename: 'el-meu-cruilla-2027.png'\n },\n es: {\n mastPill: 'Mi Cru\u00eflla 2027',\n titleHtml: 'T\u00fa haces<br>el cartel',\n lede: 'Elige los artistas que m\u00e1s te gustar\u00eda ver en Cru\u00eflla y construye tu propio cartel.',\n posterTaglineHtml: 'M\u00daSICA<br>ARTES Y COMEDIA',\n posterPill: 'Mi Cru\u00eflla 2027',\n posterBottomHtml: 'MI<br>CARTEL',\n posterRibbonSuffix: 'Programaci\u00f3n no oficial',\n posterRibbonFallbackName: 'Mi cartel',\n required: 'Obligatorio',\n optional: 'Opcional',\n searchPlaceholder: 'Busca un artista\u2026',\n searchCredit: 'B\u00fasqueda de artistas mediante el cat\u00e1logo de Spotify.',\n change: 'Cambiar',\n noResults: 'No hemos encontrado ning\u00fan artista con este nombre.',\n searchBusy: 'Buscando\u2026',\n searchRateLimit: 'Hay demasiadas b\u00fasquedas a la vez. Vuelve a intentarlo en unos segundos.',\n searchUnavailable: 'La b\u00fasqueda no responde ahora mismo. Vuelve a intentarlo.',\n searchConnection: 'Revisa la conexi\u00f3n y vuelve a intentarlo.',\n nameLabel: 'Tu nombre',\n namePlaceholder: 'Escribe tu nombre',\n emailLabel: 'Tu correo electr\u00f3nico',\n emailPlaceholder: 'nombre@ejemplo.com',\n consent: 'Acepto que Barcelona Events Musicals, SLU trate mi correo y los artistas seleccionados para analizar las preferencias musicales del p\u00fablico y avisarme si alguno de estos artistas se incorpora a la programaci\u00f3n de Cru\u00eflla.',\n privacyCopy: 'Este formulario no comporta el alta en la newsletter. Puedes retirar el consentimiento escribiendo a festival@cruillabarcelona.cat.',\n privacyLink: 'Consulta la pol\u00edtica de privacidad',\n send: 'Env\u00eda mi cartel',\n sending: 'Enviando\u2026',\n sendingSlow: 'Todav\u00eda estamos guardando el cartel\u2026',\n submitTimeout: 'El servidor est\u00e1 tardando demasiado. Vuelve a intentarlo.',\n consentRequired: 'Debes aceptar el tratamiento de datos antes de enviar el cartel.',\n submitError: 'No hemos podido guardar el cartel. Vuelve a intentarlo dentro de un momento.',\n doneTitle: 'Muchas gracias!',\n doneText: 'No prometemos nada, pero hemos guardado tu cartel. Si alguno de los artistas que has elegido se incorpora a la programaci\u00f3n de Cru\u00eflla, podremos avisarte por correo.',\n again: 'Haz otro cartel',\n download: 'Descarga el cartel',\n downloading: 'Preparando la imagen\u2026',\n downloadReady: 'Imagen descargada.',\n downloadError: 'No se ha podido crear la imagen.',\n downloadFilename: 'mi-cruilla-2027.png'\n },\n en: {\n mastPill: 'My Cru\u00eflla 2027',\n titleHtml: 'You make<br>the line-up',\n lede: 'Choose the artists you would most like to see at Cru\u00eflla and build your own line-up.',\n posterTaglineHtml: 'CRU\u00cfLLA<br>IS HOME',\n posterPill: 'My Cru\u00eflla 2027',\n posterBottomHtml: 'MY<br>LINE-UP',\n posterRibbonSuffix: 'Unofficial line-up',\n posterRibbonFallbackName: 'My line-up',\n required: 'Required',\n optional: 'Optional',\n searchPlaceholder: 'Search for an artist\u2026',\n searchCredit: 'Artist search powered by the Spotify catalogue.',\n change: 'Change',\n noResults: 'We could not find an artist with that name.',\n searchBusy: 'Searching\u2026',\n searchRateLimit: 'There are too many searches at once. Try again in a few seconds.',\n searchUnavailable: 'Search is currently unavailable. Try again.',\n searchConnection: 'Check your connection and try again.',\n nameLabel: 'Your name',\n namePlaceholder: 'Enter your name',\n emailLabel: 'Your email address',\n emailPlaceholder: 'name@example.com',\n consent: 'I agree that Barcelona Events Musicals, SLU may process my email address and selected artists to analyse audience music preferences and notify me if any of these artists joins the Cru\u00eflla line-up.',\n privacyCopy: 'This form does not subscribe you to the newsletter. You may withdraw consent by emailing festival@cruillabarcelona.cat.',\n privacyLink: 'Read the privacy policy',\n send: 'Send my line-up',\n sending: 'Sending\u2026',\n sendingSlow: 'We are still saving your line-up\u2026',\n submitTimeout: 'The server is taking too long. Please try again.',\n consentRequired: 'You must accept the data processing terms before submitting your line-up.',\n submitError: 'We could not save your line-up. Try again in a moment.',\n doneTitle: 'Thank you!',\n doneText: 'We cannot promise anything, but we have saved your line-up. If one of your selected artists joins the Cru\u00eflla programme, we may notify you by email.',\n again: 'Make another line-up',\n download: 'Download the line-up',\n downloading: 'Preparing the image\u2026',\n downloadReady: 'Image downloaded.',\n downloadError: 'The image could not be created.',\n downloadFilename: 'my-cruilla-2027.png'\n }\n },\n\n categories: [\n {\n key: 'headliner', required: false,\n labels: {\n ca: { tier: 'Cap de cartell', title: 'El cap de cartell que et faria perdre el cap', hint: 'Somia en gran. Aqu\u00ed tamb\u00e9 compten artistes impossibles.' },\n es: { tier: 'Cabeza de cartel', title: 'El cabeza de cartel que te har\u00eda perder la cabeza', hint: 'Sue\u00f1a a lo grande. Aqu\u00ed tambi\u00e9n valen artistas imposibles.' },\n en: { tier: 'Headliner', title: 'The headliner you would lose your mind for', hint: 'Dream big. Impossible bookings count here too.' }\n }\n },\n {\n key: 'favourite', required: false,\n labels: {\n ca: { tier: 'Imprescindible', title: 'El teu artista de sempre', hint: \"Aquell que no et perdries encara que ja l'hagis vist moltes vegades.\" },\n es: { tier: 'Imprescindible', title: 'Tu artista de siempre', hint: 'El que no te perder\u00edas aunque ya lo hayas visto muchas veces.' },\n en: { tier: 'Essential', title: 'Your all-time favourite artist', hint: 'The one you would never miss, even if you have seen them many times.' }\n }\n },\n {\n key: 'rising', required: false,\n labels: {\n ca: { tier: 'Nova escena', title: 'Un artista que mereix un escenari m\u00e9s gran', hint: 'Alg\u00fa que ara toca en sales petites i que creus que est\u00e0 a punt de fer el salt.' },\n es: { tier: 'Nueva escena', title: 'Un artista que merece un escenario m\u00e1s grande', hint: 'Alguien que ahora toca en salas peque\u00f1as y crees que est\u00e1 a punto de dar el salto.' },\n en: { tier: 'New wave', title: 'An artist who deserves a bigger stage', hint: 'Someone playing small venues now who you think is ready to break through.' }\n }\n },\n {\n key: 'firsttime', required: false,\n labels: {\n ca: { tier: 'Pendent', title: 'Alg\u00fa que encara no has vist en directe', hint: \"Un artista que sempre se t'escapa i que voldries poder veure al Cru\u00eflla.\" },\n es: { tier: 'Pendiente', title: 'Alguien a quien a\u00fan no has visto en directo', hint: 'Un artista que siempre se te escapa y que querr\u00edas poder ver en Cru\u00eflla.' },\n en: { tier: 'Still pending', title: 'Someone you have never seen live', hint: 'An artist you always miss and would love to see at Cru\u00eflla.' }\n }\n },\n {\n key: 'local', required: false,\n labels: {\n ca: { tier: \"Talent d'aqu\u00ed\", title: 'Un artista catal\u00e0 per al cartell', hint: \"Ens interessa especialment con\u00e8ixer l'escena local que segueixes.\" },\n es: { tier: 'Talento local', title: 'Un artista catal\u00e1n para el cartel', hint: 'Nos interesa especialmente conocer la escena local que sigues.' },\n en: { tier: 'Local talent', title: 'A Catalan artist for the line-up', hint: 'We are especially interested in the local scene you follow.' }\n }\n },\n {\n key: 'estatal', required: false,\n labels: {\n ca: { tier: 'Estatal', title: 'L\u2019artista estatal que recomanaries a tothom', hint: 'Aquell nom que hauria de ser fam\u00f3s arreu del m\u00f3n.' },\n es: { tier: 'Estatal', title: 'El artista estatal que recomendar\u00edas a todo el mundo', hint: 'Ese nombre que deber\u00eda ser famoso en todo el mundo.' },\n en: { tier: 'Spanish', title: 'The spanish artist you would recommend to everyone', hint: 'A name that should be known around the world.' }\n }\n },\n {\n key: 'party', required: false,\n labels: {\n ca: { tier: 'Festa', title: 'Qui faria ballar tot el F\u00f2rum?', hint: 'L\u2019artista o projecte que convertiria el concert en una gran festa.' },\n es: { tier: 'Fiesta', title: '\u00bfQui\u00e9n har\u00eda bailar a todo el F\u00f2rum?', hint: 'El artista o proyecto que convertir\u00eda el concierto en una gran fiesta.' },\n en: { tier: 'Party', title: 'Who would make the whole F\u00f2rum dance?', hint: 'The artist or project that would turn the show into one huge party.' }\n }\n }\n ],\n\n posterRows: [\n ['headliner'],\n ['favourite', 'rising'],\n ['firsttime'],\n ['local', 'estatal'],\n ['party']\n ]\n };\n\n const template = document.getElementById('cruilla-lineup-template');\n const mount = document.getElementById('cruilla-lineup-mount');\n if (!template || !mount || mount.shadowRoot) return;\n\n mount.style.setProperty('--widget-max-width', CONFIG.design.widgetMaxWidth);\n const root = mount.attachShadow({ mode: 'open' });\n root.appendChild(template.content.cloneNode(true));\n\n const widgetEl = root.querySelector('.widget');\n widgetEl.style.setProperty('--yellow', CONFIG.design.posterYellow);\n widgetEl.style.setProperty('--black', CONFIG.design.black);\n widgetEl.style.setProperty('--white', CONFIG.design.white);\n widgetEl.style.setProperty('--page-bg', CONFIG.design.pageBackground);\n widgetEl.style.setProperty('--frame-width', CONFIG.design.frameWidth);\n widgetEl.style.setProperty('--poster-width', CONFIG.design.posterWidth);\n\n root.querySelectorAll('[data-year]').forEach((el) => { el.textContent = CONFIG.year; });\n\n const X_SEP = '<svg class=\"xmark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\"><path d=\"M50,76.55 26.55,100 0,73.45 23.45,50 0,26.55 26.55,0 50,23.45 73.45,0 100,26.55 76.55,50 100,73.45 73.45,100Z\" fill=\"#FFFFFF\"\/><\/svg>';\n const SMALL_X = '<svg class=\"xmark\" viewBox=\"0 0 100 100\" aria-hidden=\"true\"><path d=\"M50,76.55 26.55,100 0,73.45 23.45,50 0,26.55 26.55,0 50,23.45 73.45,0 100,26.55 76.55,50 100,73.45 73.45,100Z\" fill=\"#000000\"\/><\/svg>';\n\n const picks = {};\n const searchCache = new Map();\n const categoryByKey = Object.fromEntries(CONFIG.categories.map((category) => [category.key, category]));\n const rankOf = Object.fromEntries(CONFIG.categories.map((category, index) => [category.key, index]));\n const comboControllers = new Map();\n\n let currentLanguage = detectLanguage();\n let submitted = false;\n\n const formEl = root.querySelector('#cl-form');\n const billEl = root.querySelector('#cl-bill');\n const finishEl = root.querySelector('#cl-finish');\n const nameEl = root.querySelector('#cl-name');\n const emailEl = root.querySelector('#cl-email');\n const consentEl = root.querySelector('#cl-consent');\n const sendEl = root.querySelector('#cl-send');\n const statusEl = root.querySelector('#cl-status');\n const layoutEl = root.querySelector('.layout');\n const downloadEl = root.querySelector('#cl-download');\n const downloadStatusEl = root.querySelector('#cl-download-status');\n\n buildForm();\n buildBill();\n applyLanguage(currentLanguage);\n fitBill();\n\n if (document.fonts && document.fonts.ready) {\n document.fonts.ready.then(fitBill).catch(() => {});\n }\n\n refreshControls();\n watchLanguageChanges();\n\n nameEl.addEventListener('input', () => {\n updatePosterClaim();\n updatePosterRibbon();\n refreshControls();\n });\n\n emailEl.addEventListener('input', refreshControls);\n consentEl.addEventListener('change', refreshControls);\n downloadEl.addEventListener('click', downloadPoster);\n\n function text(key) {\n return (CONFIG.texts[currentLanguage] && CONFIG.texts[currentLanguage][key])\n || CONFIG.texts[CONFIG.language.fallback][key]\n || key;\n }\n\n function labelsFor(category) {\n return category.labels[currentLanguage]\n || category.labels[CONFIG.language.fallback]\n || Object.values(category.labels)[0];\n }\n\n function buildForm() {\n CONFIG.categories.forEach((category) => {\n const section = document.createElement('section');\n section.className = 'q';\n section.dataset.categoryKey = category.key;\n section.innerHTML = `\n <div class=\"q-tier label\">\n <span class=\"n\">${SMALL_X}<span data-role=\"tier\"><\/span><\/span>\n <span data-role=\"requirement\" class=\"${category.required ? '' : 'q-optional'}\"><\/span>\n <\/div>\n <h2 class=\"q-title display\" data-role=\"title\"><\/h2>\n <p class=\"q-hint\" data-role=\"hint\"><\/p>\n <div class=\"combo\">\n <input type=\"text\" autocomplete=\"off\" spellcheck=\"false\"\n role=\"combobox\" aria-expanded=\"false\" aria-autocomplete=\"list\"\n aria-controls=\"cl-res-${escapeAttribute(category.key)}\">\n ${SMALL_X.replace('class=\"xmark\"', 'class=\"glyph\"')}\n <ul class=\"results\" id=\"cl-res-${escapeAttribute(category.key)}\" role=\"listbox\"><\/ul>\n <\/div>\n <div class=\"chosen\" hidden><\/div>`;\n formEl.appendChild(section);\n wireCombo(section, category);\n });\n\n const credit = document.createElement('p');\n credit.className = 'spotify-credit';\n credit.dataset.role = 'search-credit';\n formEl.appendChild(credit);\n }\n\n function buildBill() {\n CONFIG.posterRows.forEach((row) => {\n const validKeys = row.filter((key) => categoryByKey[key]);\n if (!validKeys.length) return;\n\n const line = document.createElement('div');\n line.className = 'line';\n\n validKeys.forEach((key, index) => {\n if (index > 0) line.insertAdjacentHTML('beforeend', X_SEP);\n const slot = document.createElement('div');\n slot.className = 'slot empty';\n slot.dataset.rank = String(rankOf[key] || 0);\n slot.id = `cl-slot-${key}`;\n slot.innerHTML = '<span class=\"lbl\"><\/span><span class=\"val\"><\/span>';\n line.appendChild(slot);\n });\n\n billEl.appendChild(line);\n });\n }\n\n function applyLanguage(language) {\n currentLanguage = normalizeLanguage(language) || CONFIG.language.fallback;\n root.host.setAttribute('lang', currentLanguage);\n\n root.querySelector('#cl-mast-pill').textContent = text('mastPill');\n root.querySelector('#cl-title').innerHTML = text('titleHtml');\n root.querySelector('#cl-lede').textContent = text('lede');\n root.querySelector('#cl-poster-tagline').innerHTML = text('posterTaglineHtml');\n root.querySelector('#cl-poster-pill').textContent = text('posterPill');\n root.querySelector('#cl-email-label').textContent = text('emailLabel');\n root.querySelector('#cl-name-label').textContent = text('nameLabel');\n nameEl.placeholder = text('namePlaceholder');\n emailEl.placeholder = text('emailPlaceholder');\n root.querySelector('#cl-consent-text').textContent = text('consent');\n root.querySelector('#cl-privacy-copy').textContent = text('privacyCopy');\n\n const privacyLink = root.querySelector('#cl-privacy-link');\n privacyLink.textContent = text('privacyLink');\n privacyLink.href = CONFIG.privacyUrls[currentLanguage] || CONFIG.privacyUrls[CONFIG.language.fallback];\n\n root.querySelector('#cl-send-label').textContent = text('send');\n downloadEl.textContent = text('download');\n\n const credit = root.querySelector('[data-role=\"search-credit\"]');\n if (credit) credit.textContent = text('searchCredit');\n\n CONFIG.categories.forEach((category) => {\n const section = formEl.querySelector(`[data-category-key=\"${cssEscape(category.key)}\"]`);\n if (!section) return;\n\n const labels = labelsFor(category);\n section.querySelector('[data-role=\"tier\"]').textContent = labels.tier;\n section.querySelector('[data-role=\"requirement\"]').textContent = category.required ? text('required') : text('optional');\n section.querySelector('[data-role=\"title\"]').textContent = labels.title;\n section.querySelector('[data-role=\"hint\"]').textContent = labels.hint;\n\n const input = section.querySelector('input');\n input.placeholder = text('searchPlaceholder');\n input.setAttribute('aria-label', labels.title);\n section.querySelector('.results').setAttribute('aria-label', text('searchPlaceholder'));\n\n const chosenEl = section.querySelector('.chosen');\n if (picks[category.key] && !chosenEl.hidden) {\n const sub = chosenEl.querySelector('.sub');\n const button = chosenEl.querySelector('button');\n if (sub) sub.textContent = labels.tier;\n if (button) button.textContent = text('change');\n }\n\n const slot = root.querySelector(`#cl-slot-${cssEscape(category.key)}`);\n if (slot) slot.querySelector('.lbl').textContent = labels.tier;\n });\n\n updatePosterClaim();\n updatePosterRibbon();\n\n if (!submitted) refreshControls();\n }\n\n function wireCombo(section, category) {\n const combo = section.querySelector('.combo');\n const input = section.querySelector('input');\n const listEl = section.querySelector('.results');\n const chosenEl = section.querySelector('.chosen');\n\n let timer = null;\n let cursor = -1;\n let items = [];\n let latestQuery = '';\n\n input.addEventListener('input', () => {\n clearTimeout(timer);\n const query = input.value.trim();\n latestQuery = query;\n\n if (query.length < CONFIG.search.minChars) {\n close();\n return;\n }\n\n timer = setTimeout(() => run(query), CONFIG.search.debounceMs);\n });\n\n input.addEventListener('keydown', (event) => {\n if (!items.length) return;\n\n if (event.key === 'ArrowDown') {\n event.preventDefault();\n move(1);\n } else if (event.key === 'ArrowUp') {\n event.preventDefault();\n move(-1);\n } else if (event.key === 'Enter' && cursor > -1) {\n event.preventDefault();\n choose(items[cursor]);\n } else if (event.key === 'Escape') {\n close();\n }\n });\n\n input.addEventListener('blur', () => setTimeout(close, 150));\n\n async function run(query) {\n const normalizedQuery = query.toLocaleLowerCase().replace(\/\\s+\/g, ' ').trim();\n const cached = searchCache.get(normalizedQuery);\n\n if (cached && Date.now() - cached.savedAt < CONFIG.search.cacheMs) {\n render(cached.artists);\n return;\n }\n\n const previousController = comboControllers.get(category.key);\n if (previousController) previousController.abort();\n\n const controller = new AbortController();\n comboControllers.set(category.key, controller);\n\n combo.classList.add('busy');\n message(text('searchBusy'));\n\n try {\n const response = await fetch(\n `${CONFIG.apiBase}\/api\/search?q=${encodeURIComponent(query)}&#038;limit=${CONFIG.search.maxResults}`,\n { signal: controller.signal, headers: { 'Accept': 'application\/json' } }\n );\n\n const data = await response.json();\n\n if (latestQuery.trim() !== query) return;\n\n if (response.status === 429) {\n message(text('searchRateLimit'));\n return;\n }\n\n if (!response.ok) {\n message(text('searchUnavailable'));\n return;\n }\n\n const artists = (data.artists || []).slice(0, CONFIG.search.maxResults);\n searchCache.set(normalizedQuery, { artists, savedAt: Date.now() });\n render(artists);\n } catch (error) {\n if (error.name !== 'AbortError') message(text('searchConnection'));\n } finally {\n if (comboControllers.get(category.key) === controller) comboControllers.delete(category.key);\n combo.classList.remove('busy');\n }\n }\n\n function render(list) {\n items = list;\n cursor = -1;\n listEl.innerHTML = '';\n\n if (!list.length) {\n message(text('noResults'));\n return;\n }\n\n list.forEach((artist, index) => {\n const item = document.createElement('li');\n item.id = `${category.key}-opt-${index}`;\n item.setAttribute('role', 'option');\n item.setAttribute('aria-selected', 'false');\n item.innerHTML = `\n ${artist.image ? `<img decoding=\"async\" src=\"${escapeAttribute(artist.image)}\" alt=\"\" loading=\"lazy\">` : '<span class=\"noart\"><\/span>'}\n <span><span class=\"nm\">${escapeHtml(artist.name)}<\/span>\n ${artist.genres && artist.genres.length ? `<br><span class=\"gn\">${escapeHtml(artist.genres.join(' \u00b7 '))}<\/span>` : ''}<\/span>`;\n\n item.addEventListener('mousedown', (event) => {\n event.preventDefault();\n choose(artist);\n });\n\n listEl.appendChild(item);\n });\n\n input.setAttribute('aria-expanded', 'true');\n }\n\n function message(messageText) {\n items = [];\n cursor = -1;\n listEl.innerHTML = `<li class=\"msg\">${escapeHtml(messageText)}<\/li>`;\n input.setAttribute('aria-expanded', 'true');\n }\n\n function move(step) {\n const options = Array.from(listEl.querySelectorAll('[role=\"option\"]'));\n if (!options.length) return;\n\n if (cursor > -1) options[cursor].setAttribute('aria-selected', 'false');\n cursor = (cursor + step + options.length) % options.length;\n options[cursor].setAttribute('aria-selected', 'true');\n options[cursor].scrollIntoView({ block: 'nearest' });\n input.setAttribute('aria-activedescendant', options[cursor].id);\n }\n\n function close() {\n listEl.innerHTML = '';\n items = [];\n cursor = -1;\n input.setAttribute('aria-expanded', 'false');\n input.removeAttribute('aria-activedescendant');\n }\n\n function choose(artist) {\n picks[category.key] = artist;\n close();\n combo.hidden = true;\n chosenEl.hidden = false;\n chosenEl.innerHTML = `\n ${artist.image ? `<img decoding=\"async\" src=\"${escapeAttribute(artist.image)}\" alt=\"\">` : '<span class=\"noart\"><\/span>'}\n <span><span class=\"nm\">${escapeHtml(artist.name)}<\/span>\n <span class=\"sub\">${escapeHtml(labelsFor(category).tier)}<\/span><\/span>\n <button type=\"button\">${escapeHtml(text('change'))}<\/button>`;\n\n chosenEl.querySelector('button').addEventListener('click', () => {\n delete picks[category.key];\n chosenEl.hidden = true;\n combo.hidden = false;\n input.value = '';\n input.focus();\n paintSlot(category, null);\n refreshControls();\n });\n\n paintSlot(category, artist);\n refreshControls();\n }\n }\n\n function paintSlot(category, artist) {\n const slot = root.querySelector(`#cl-slot-${cssEscape(category.key)}`);\n if (!slot) return;\n\n const value = slot.querySelector('.val');\n value.textContent = artist ? artist.name : '';\n slot.classList.toggle('empty', !artist);\n slot.classList.remove('filled');\n\n if (artist) {\n void slot.offsetWidth;\n slot.classList.add('filled');\n }\n\n fitBill();\n }\n\n function fitBill() {\n const visibleLines = Array.from(billEl.querySelectorAll('.line')).filter((line) => {\n return line.querySelectorAll('.slot:not(.empty) .val').length > 0;\n });\n\n const baseSize = visibleLines.length <= 3\n ? 10.2\n : visibleLines.length === 4\n ? 9.25\n : 7.8;\n\n const minimumSize = 4.65;\n\n Array.from(billEl.querySelectorAll('.line')).forEach((line) => {\n const values = Array.from(line.querySelectorAll('.slot:not(.empty) .val'));\n const names = values\n .map((element) => element.textContent.trim())\n .filter(Boolean);\n\n if (!names.length) {\n line.hidden = true;\n return;\n }\n\n line.hidden = false;\n\n const separators = Array.from(line.querySelectorAll(':scope > .xmark'));\n separators.forEach((separator) => {\n separator.style.display = names.length > 1 ? '' : 'none';\n });\n\n const availableWidth = line.clientWidth;\n let size = baseSize;\n line.style.setProperty('--line-size', `${size}cqw`);\n\n while (size > minimumSize) {\n const visibleSeparators = separators.filter((separator) => separator.style.display !== 'none');\n const textWidth = values.reduce((sum, element) => sum + element.scrollWidth, 0);\n const separatorWidth = visibleSeparators.reduce((sum, element) => sum + element.getBoundingClientRect().width, 0);\n const gap = parseFloat(getComputedStyle(line).columnGap || getComputedStyle(line).gap || '0');\n const itemCount = values.length + visibleSeparators.length;\n const requiredWidth = textWidth + separatorWidth + Math.max(0, itemCount - 1) * gap;\n\n if (requiredWidth <= availableWidth) break;\n\n size = Math.max(minimumSize, size - 0.1);\n line.style.setProperty('--line-size', `${size.toFixed(2)}cqw`);\n }\n });\n }\n\n function updatePosterClaim() {\n const claimEl = root.querySelector('#cl-foot-claim');\n if (!claimEl) return;\n\n const personName = nameEl.value.trim();\n\n if (!personName) {\n claimEl.innerHTML = text('posterBottomHtml');\n claimEl.classList.remove('personalized');\n claimEl.style.removeProperty('--claim-name-size');\n return;\n }\n\n const length = personName.length;\n const nameSize = length <= 10\n ? '2.65cqw'\n : length <= 16\n ? '2.35cqw'\n : length <= 24\n ? '2.05cqw'\n : '1.75cqw';\n\n claimEl.classList.add('personalized');\n claimEl.style.setProperty('--claim-name-size', nameSize);\n claimEl.innerHTML = `\n <span class=\"claim-brand\">CRU\u00cfLLA<\/span>\n <span class=\"claim-x\">\u00d7<\/span>\n <span class=\"claim-name\">${escapeHtml(personName)}<\/span>`;\n }\n\n function updatePosterRibbon() {\n const ribbonEl = root.querySelector('#cl-poster-ribbon');\n const subtitleEl = root.querySelector('#cl-poster-subtitle');\n if (!ribbonEl) return;\n\n const personName = nameEl.value.trim();\n const displayName = personName || text('posterRibbonFallbackName');\n const ribbonText = `LINE UP CREATED BY    ${displayName} \u00b7 ${text('posterRibbonSuffix')}`;\n\n const length = ribbonText.length;\n const ribbonSize = length <= 52\n ? '2.05cqw'\n : length <= 52\n ? '1.92cqw'\n : length <= 64\n ? '1.95cqw'\n : '1cqw';\n\n ribbonEl.style.setProperty('--ribbon-size', ribbonSize);\n ribbonEl.textContent = ribbonText;\n if (subtitleEl) subtitleEl.textContent = ribbonText;\n }\n\n function refreshControls() {\n const hasAtLeastOnePick = Object.keys(picks).length > 0;\n const validName = nameEl.value.trim().length > 0;\n const email = emailEl.value.trim().toLowerCase();\n const validEmail = \/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\/.test(email);\n\n sendEl.disabled = !(\n hasAtLeastOnePick &&\n validName &&\n validEmail &&\n consentEl.checked\n );\n\n downloadEl.disabled = !submitted;\n }\n\n let isSubmitting = false;\n\n sendEl.addEventListener('click', async () => {\n if (isSubmitting) return;\n\n isSubmitting = true;\n sendEl.disabled = true;\n statusEl.className = 'status';\n statusEl.textContent = text('sending');\n root.querySelector('#cl-send-label').textContent = text('sending');\n\n \/*\n IMPORTANT:\n El nom NO s'inclou en aquest payload.\n Nom\u00e9s existeix al navegador per personalitzar el cartell i el PNG.\n *\/\n const payload = {\n email: emailEl.value.trim().toLowerCase(),\n consent: consentEl.checked,\n language: currentLanguage,\n picks: CONFIG.categories\n .filter((category) => picks[category.key])\n .map((category) => ({\n category: category.labels.ca.tier,\n category_key: category.key,\n category_label: labelsFor(category).tier,\n artist_name: picks[category.key].name,\n artist_id: picks[category.key].id\n }))\n };\n\n const controller = new AbortController();\n const timeoutId = window.setTimeout(() => controller.abort(), 20000);\n const slowMessageId = window.setTimeout(() => {\n if (isSubmitting && !submitted) statusEl.textContent = text('sendingSlow');\n }, 2500);\n\n try {\n const response = await fetch(`${CONFIG.apiBase}\/api\/submit`, {\n method: 'POST',\n headers: { 'Content-Type': 'application\/json', 'Accept': 'application\/json' },\n body: JSON.stringify(payload),\n signal: controller.signal\n });\n\n let data = {};\n try { data = await response.json(); } catch (_) {}\n\n if (response.ok && data.ok === true) {\n showDone();\n return;\n }\n\n console.error('Cru\u00eflla submit failed:', {\n status: response.status,\n error: data.error || 'unknown_error',\n detail: data.detail || null,\n response: data\n });\n\n if (data.error === 'consent_required') {\n fail(text('consentRequired'));\n return;\n }\n\n fail(text('submitError'));\n } catch (error) {\n if (error && error.name === 'AbortError') {\n fail(text('submitTimeout'));\n } else {\n fail(text('searchConnection'));\n }\n } finally {\n window.clearTimeout(timeoutId);\n window.clearTimeout(slowMessageId);\n isSubmitting = false;\n\n if (!submitted) {\n root.querySelector('#cl-send-label').textContent = text('send');\n refreshControls();\n }\n }\n });\n\n function fail(messageText) {\n statusEl.className = 'status err';\n statusEl.textContent = messageText;\n refreshControls();\n }\n\n function showDone() {\n submitted = true;\n downloadEl.disabled = false;\n downloadStatusEl.textContent = '';\n formEl.remove();\n finishEl.remove();\n\n const masthead = root.querySelector('.masthead');\n if (masthead) masthead.remove();\n\n layoutEl.classList.add('complete');\n\n const done = document.createElement('div');\n done.className = 'done';\n done.innerHTML = `\n <h2 class=\"display\">${escapeHtml(text('doneTitle'))}<\/h2>\n <p>${escapeHtml(text('doneText'))}<\/p>\n <button class=\"again\" type=\"button\">${escapeHtml(text('again'))}<\/button>`;\n\n layoutEl.before(done);\n done.querySelector('.again').addEventListener('click', () => window.location.reload());\n mount.scrollIntoView({ behavior: 'smooth', block: 'start' });\n }\n\n let htmlToImageLoader = null;\n\n function loadHtmlToImage() {\n if (window.htmlToImage) return Promise.resolve(window.htmlToImage);\n if (htmlToImageLoader) return htmlToImageLoader;\n\n htmlToImageLoader = new Promise((resolve, reject) => {\n const script = document.createElement('script');\n script.src = 'https:\/\/cdn.jsdelivr.net\/npm\/html-to-image@1.11.13\/dist\/html-to-image.min.js';\n script.async = true;\n script.onload = () => window.htmlToImage\n ? resolve(window.htmlToImage)\n : reject(new Error('html-to-image unavailable'));\n script.onerror = () => reject(new Error('Could not load html-to-image'));\n document.head.appendChild(script);\n });\n\n return htmlToImageLoader;\n }\n\n function nextPaint() {\n return new Promise((resolve) => {\n requestAnimationFrame(() => requestAnimationFrame(resolve));\n });\n }\n\n async function downloadPoster() {\n if (!Object.keys(picks).length) return;\n\n downloadEl.disabled = true;\n downloadStatusEl.textContent = text('downloading');\n\n const EXPORT_SIZE = 2000;\n const poster = root.querySelector('#cl-poster');\n let exportStage = null;\n\n try {\n if (document.fonts && document.fonts.ready) await document.fonts.ready;\n fitBill();\n await nextPaint();\n\n const htmlToImage = await loadHtmlToImage();\n\n exportStage = document.createElement('div');\n exportStage.setAttribute('aria-hidden', 'true');\n exportStage.style.position = 'fixed';\n exportStage.style.left = '-100000px';\n exportStage.style.top = '0';\n exportStage.style.width = `${EXPORT_SIZE}px`;\n exportStage.style.height = `${EXPORT_SIZE}px`;\n exportStage.style.margin = '0';\n exportStage.style.padding = '0';\n exportStage.style.pointerEvents = 'none';\n exportStage.style.overflow = 'hidden';\n\n const exportPoster = poster.cloneNode(true);\n exportPoster.removeAttribute('id');\n exportPoster.style.width = `${EXPORT_SIZE}px`;\n exportPoster.style.height = `${EXPORT_SIZE}px`;\n exportPoster.style.maxWidth = 'none';\n exportPoster.style.aspectRatio = '1 \/ 1';\n exportPoster.style.margin = '0';\n exportPoster.style.transform = 'none';\n exportPoster.style.setProperty('--yellow', CONFIG.design.posterYellow);\n exportPoster.style.setProperty('--black', CONFIG.design.black);\n exportPoster.style.setProperty('--white', CONFIG.design.white);\n exportPoster.style.color = CONFIG.design.black;\n\n exportPoster.querySelectorAll('.slot .val').forEach((el) => {\n el.style.setProperty('color', '#000000', 'important');\n el.style.setProperty('-webkit-text-fill-color', '#000000', 'important');\n el.style.setProperty('-webkit-text-stroke', '.18px #000000');\n el.style.setProperty('paint-order', 'stroke fill');\n el.style.setProperty('text-shadow', 'none', 'important');\n el.style.setProperty('opacity', '1', 'important');\n });\n\n exportPoster.querySelectorAll('.poster-foot .yr').forEach((el) => {\n el.style.setProperty('color', '#FFFFFF', 'important');\n el.style.setProperty('-webkit-text-fill-color', '#FFFFFF', 'important');\n el.style.setProperty('-webkit-text-stroke', '.18px #FFFFFF');\n el.style.setProperty('paint-order', 'stroke fill');\n el.style.setProperty('text-shadow', 'none', 'important');\n el.style.setProperty('opacity', '1', 'important');\n });\n\n exportStage.appendChild(exportPoster);\n widgetEl.appendChild(exportStage);\n\n await nextPaint();\n\n const blob = await htmlToImage.toBlob(exportPoster, {\n backgroundColor: CONFIG.design.posterYellow,\n cacheBust: true,\n width: EXPORT_SIZE,\n height: EXPORT_SIZE,\n canvasWidth: EXPORT_SIZE,\n canvasHeight: EXPORT_SIZE,\n pixelRatio: 1,\n style: {\n margin: '0',\n transform: 'none'\n }\n });\n\n if (!blob) throw new Error('PNG generation failed');\n\n const pngUrl = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = pngUrl;\n link.download = text('downloadFilename');\n document.body.appendChild(link);\n link.click();\n link.remove();\n\n setTimeout(() => URL.revokeObjectURL(pngUrl), 1500);\n downloadStatusEl.textContent = text('downloadReady');\n } catch (error) {\n console.error('Cru\u00eflla poster download error:', error);\n downloadStatusEl.textContent = text('downloadError');\n } finally {\n if (exportStage) exportStage.remove();\n downloadEl.disabled = false;\n }\n }\n\n function detectLanguage() {\n const candidates = [\n document.documentElement.getAttribute('lang'),\n document.body && document.body.getAttribute('lang'),\n new URLSearchParams(window.location.search).get('lang')\n ].filter(Boolean);\n\n const bodyClass = document.body ? document.body.className : '';\n const classMatch = String(bodyClass).match(\/(?:^|\\s)(?:lang-|language-)(ca|es|en)(?:\\s|$)\/i);\n if (classMatch) candidates.push(classMatch[1]);\n\n const pathMatch = window.location.pathname.match(\/^\\\/(ca|es|en)(?:\\\/|$)\/i);\n if (pathMatch) candidates.push(pathMatch[1]);\n\n for (const candidate of candidates) {\n const normalized = normalizeLanguage(candidate);\n if (CONFIG.language.supported.includes(normalized)) return normalized;\n }\n\n return CONFIG.language.fallback;\n }\n\n function normalizeLanguage(value) {\n const normalized = String(value || '').trim().toLowerCase().split(\/[-_]\/)[0];\n return CONFIG.language.supported.includes(normalized) ? normalized : null;\n }\n\n function watchLanguageChanges() {\n const update = () => {\n const next = detectLanguage();\n if (next !== currentLanguage) applyLanguage(next);\n };\n\n const observer = new MutationObserver(update);\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang', 'class'] });\n\n if (document.body) {\n observer.observe(document.body, { attributes: true, attributeFilter: ['lang', 'class'] });\n }\n\n window.addEventListener('popstate', update);\n window.addEventListener('hashchange', update);\n window.addEventListener('cruilla:languagechange', (event) => {\n const requested = event.detail && event.detail.lang;\n applyLanguage(requested || detectLanguage());\n });\n }\n\n function escapeHtml(value) {\n return String(value).replace(\/[&<>\"']\/g, (character) => ({\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;'\n })[character]);\n }\n\n function escapeAttribute(value) {\n return escapeHtml(value).replace(\/`\/g, '&#96;');\n }\n\n function cssEscape(value) {\n if (window.CSS && typeof window.CSS.escape === 'function') {\n return window.CSS.escape(value);\n }\n\n return String(value).replace(\/[^a-zA-Z0-9_-]\/g, '\\\\$&');\n }\n})();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>.<\/p>\n","protected":false},"author":55,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-69491","page","type-page","status-publish","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>EL MEU CARTELL 2027 - Festival Cru\u00eflla<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"EL MEU CARTELL 2027 - Festival Cru\u00eflla\" \/>\n<meta property=\"og:description\" content=\".\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/\" \/>\n<meta property=\"og:site_name\" content=\"Festival Cru\u00eflla\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-23T23:21:48+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/el-meu-cartell-2027\\\/\",\"url\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/el-meu-cartell-2027\\\/\",\"name\":\"EL MEU CARTELL 2027 - Festival Cru\u00eflla\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/#website\"},\"datePublished\":\"2026-07-23T08:41:00+00:00\",\"dateModified\":\"2026-07-23T23:21:48+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/el-meu-cartell-2027\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/el-meu-cartell-2027\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/el-meu-cartell-2027\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"EL MEU CARTELL 2027\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/#website\",\"url\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/\",\"name\":\"Festival Cru\u00eflla\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/#organization\",\"name\":\"Festival Cru\u00eflla\",\"url\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/wp-content\\\/uploads\\\/2025\\\/12\\\/cruill-logo-negativo.png\",\"contentUrl\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/wp-content\\\/uploads\\\/2025\\\/12\\\/cruill-logo-negativo.png\",\"width\":400,\"height\":101,\"caption\":\"Festival Cru\u00eflla\"},\"image\":{\"@id\":\"https:\\\/\\\/www.cruillabarcelona.cat\\\/en\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"EL MEU CARTELL 2027 - Festival Cru\u00eflla","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/","og_locale":"en_US","og_type":"article","og_title":"EL MEU CARTELL 2027 - Festival Cru\u00eflla","og_description":".","og_url":"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/","og_site_name":"Festival Cru\u00eflla","article_modified_time":"2026-07-23T23:21:48+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/","url":"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/","name":"EL MEU CARTELL 2027 - Festival Cru\u00eflla","isPartOf":{"@id":"https:\/\/www.cruillabarcelona.cat\/en\/#website"},"datePublished":"2026-07-23T08:41:00+00:00","dateModified":"2026-07-23T23:21:48+00:00","breadcrumb":{"@id":"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.cruillabarcelona.cat\/en\/el-meu-cartell-2027\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.cruillabarcelona.cat\/en\/"},{"@type":"ListItem","position":2,"name":"EL MEU CARTELL 2027"}]},{"@type":"WebSite","@id":"https:\/\/www.cruillabarcelona.cat\/en\/#website","url":"https:\/\/www.cruillabarcelona.cat\/en\/","name":"Festival Cru\u00eflla","description":"","publisher":{"@id":"https:\/\/www.cruillabarcelona.cat\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.cruillabarcelona.cat\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.cruillabarcelona.cat\/en\/#organization","name":"Festival Cru\u00eflla","url":"https:\/\/www.cruillabarcelona.cat\/en\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.cruillabarcelona.cat\/en\/#\/schema\/logo\/image\/","url":"https:\/\/www.cruillabarcelona.cat\/wp-content\/uploads\/2025\/12\/cruill-logo-negativo.png","contentUrl":"https:\/\/www.cruillabarcelona.cat\/wp-content\/uploads\/2025\/12\/cruill-logo-negativo.png","width":400,"height":101,"caption":"Festival Cru\u00eflla"},"image":{"@id":"https:\/\/www.cruillabarcelona.cat\/en\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/pages\/69491","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/users\/55"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/comments?post=69491"}],"version-history":[{"count":5,"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/pages\/69491\/revisions"}],"predecessor-version":[{"id":69566,"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/pages\/69491\/revisions\/69566"}],"wp:attachment":[{"href":"https:\/\/www.cruillabarcelona.cat\/en\/wp-json\/wp\/v2\/media?parent=69491"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}