{"id":87,"date":"2026-10-14T02:33:47","date_gmt":"2026-10-14T02:33:47","guid":{"rendered":"https:\/\/demo.creanexo.com\/?page_id=87"},"modified":"2026-10-14T03:16:59","modified_gmt":"2026-10-14T03:16:59","slug":"demo-de-radio-circular-reproductor-html","status":"publish","type":"page","link":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/","title":{"rendered":"Demo de Radio Circular | Reproductor HTML"},"content":{"rendered":"<style>body:has(.rcx-demo-page){background:#090a10;overflow-x:hidden}body:has(.rcx-demo-page) .wp-site-blocks>header,body:has(.rcx-demo-page) .wp-site-blocks>footer,body:has(.rcx-demo-page) .wp-block-post-title{display:none!important}body:has(.rcx-demo-page) main.wp-block-group,body:has(.rcx-demo-page) .wp-block-post-content{max-width:none!important;padding:0!important;margin:0!important}body:has(.rcx-demo-page) .wp-block-post-content>*{max-width:none!important;margin-block-start:0!important}.rcx-demo-page{width:100%!important;max-width:none!important;margin:0!important;background:#090a10;color:#f7f4fa;font:16px\/1.65 system-ui,sans-serif}.rcx-demo-page *{box-sizing:border-box}.rcx-demo-page .wrap{width:min(1120px,92%);margin:auto}.rcx-demo-page .nav{display:flex;align-items:center;justify-content:space-between;padding:20px 0}.rcx-demo-page .brand{font-weight:800;font-size:22px;color:white;text-decoration:none}.rcx-demo-page .brand span{color:#f04a98}.rcx-demo-page .hero{padding:55px 0;background:radial-gradient(ellipse at 80% 0,#583492 0,transparent 42%),linear-gradient(135deg,#15111f,#101017)}.rcx-demo-page .tag{font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:#ff8cc2}.rcx-demo-page h1{font-size:clamp(38px,6vw,66px);line-height:1.06;margin:14px 0}.rcx-demo-page h2{font-size:clamp(27px,4vw,38px);line-height:1.15}.rcx-demo-page p{color:#c4bfd0}.rcx-demo-page .player{margin:35px auto 10px;padding:18px;background:#101116;border:1px solid #ffffff25;border-radius:20px;overflow:hidden}.rcx-demo-page .note{padding:18px 20px;border-left:3px solid #f04a98;background:#171620;color:#c4bfd0;border-radius:5px}.rcx-demo-page .btn{display:inline-block;border:1px solid #ffffff50;border-radius:999px;padding:10px 16px;color:white;text-decoration:none}.rcx-demo-page .buy{background:linear-gradient(100deg,#f04493,#9957fc);border-color:transparent;font-weight:700}.rcx-demo-page .foot{padding:25px 0;border-top:1px solid #ffffff22;color:#bbb}.rcx-demo-page .radio-circular-html{width:100%!important;max-width:100%!important;margin:0 auto!important}@media(max-width:600px){.rcx-demo-page .nav{flex-direction:column;align-items:flex-start;gap:12px}.rcx-demo-page .hero{padding:42px 0}.rcx-demo-page .player{padding:8px;margin:24px auto}}<\/style><div class=\"rcx-demo-page\"><header class=\"wrap nav\"><a class=\"brand\" href=\"\/\">Crea<span>Nexo<\/span> \/ Demo<\/a><div><a class=\"btn\" href=\"\/radio-circular-html-reproductor-para-wordpress\/\">\u2190 Detalles del producto<\/a> <a class=\"btn buy\" href=\"https:\/\/plugins.creanexo.com\/product\/radio-circular-html-1-2-0-reproductor-de-radio-para-wordpress\/\">Comprar por US$30 \u2192<\/a><\/div><\/header><section class=\"hero\"><div class=\"wrap\"><div class=\"tag\">Demostraci\u00f3n interactiva \u00b7 Radio Circular HTML 1.2.0<\/div><h1>Prueba el reproductor Radio Circular<\/h1><p>Reproduce las pistas de muestra, cambia de tema, ajusta el volumen y abre el ecualizador para explorar los controles.<\/p><div class=\"note\"><strong>Esta es una demo p\u00fablica.<\/strong> Incluye dos grabaciones de Chopin de Musopen con licencia CC0; no hay una emisora en directo configurada. El reproductor no env\u00eda tus archivos personales al sitio.<\/div><p><a class=\"btn buy\" href=\"https:\/\/plugins.creanexo.com\/product\/radio-circular-html-1-2-0-reproductor-de-radio-para-wordpress\/\">Comprar por US$30 \u2192<\/a><\/p><\/div><\/section><section class=\"wrap player\"><!-- =================== RADIO CIRCULAR HTML 1.2.0-html: INICIO DEL BLOQUE ===================\n     Copia y pega TODO, desde esta l\u00ednea hasta \u00abFIN DEL BLOQUE\u00bb, en un bloque\n     de HTML personalizado que permita style y script. Licencia GPLv2 o posterior. -->\n<div class=\"radio-circular-html alignfull\" data-rcx-root>\n<div data-rcx-codigo style=\"display:none\">\n<!--\n  =====================================================================\n              EDITA AQU\u00cd TU RADIO Y TU LISTA\n  =====================================================================\n  Cambia solo el texto que est\u00e1 entre comillas \"...\".\n  No hace falta tocar nada m\u00e1s abajo de \u00abFIN DE LA ZONA EDITABLE\u00bb.\n\n  Reglas sencillas:\n  * Las direcciones (URL) empiezan por https:\/\/ (o http:\/\/).\n  * Si un texto lleva comillas dobles, escr\u00edbelas como &quot;\n    y el signo & puedes escribirlo como &amp; (se ver\u00e1 como &).\n  * Para dejar un dato vac\u00edo, deja las comillas vac\u00edas: \"\"\n  =====================================================================\n-->\n<template data-rcx-datos>\n\n  <!-- 1. TU RADIO\n       data-bloque  : nombre interno de este bloque (letras sin acentos,\n                      n\u00fameros y guiones). Si pegas dos reproductores en la\n                      misma web, pon uno distinto en cada uno.\n       data-nombre  : nombre de la radio (se ve en el disco).\n       data-eslogan : frase opcional debajo del reproductor.\n       data-logo    : direcci\u00f3n de una imagen (opcional).\n       data-url     : direcci\u00f3n DIRECTA del audio de tu emisi\u00f3n en directo\n                      (SHOUTcast\/Icecast que entregue MP3), por ejemplo:\n                      data-url=\"https:\/\/servidor.ejemplo:8000\/stream\"\n                      No uses la p\u00e1gina web del servidor ni su panel. -->\n  <div data-radio\n       data-bloque=\"radio-circular-demo\"\n       data-nombre=\"CreaNexo Radio Demo\"\n       data-eslogan=\"\"\n       data-logo=\"\"\n       data-url=\"\"><\/div>\n\n  <!-- 2. LISTA INICIAL (la ven todos los visitantes, despu\u00e9s de tu radio)\n       Una fila <li ...><\/li> por elemento. Para a\u00f1adir uno, copia una fila\n       entera y cambia sus datos; para quitarlo, borra su fila.\n       data-id      : identificador fijo y distinto en cada fila (letras sin\n                      acentos, n\u00fameros y guiones). No lo cambies despu\u00e9s: el\n                      orden que guarda cada visitante lo usa.\n       data-tipo    : \"directo\" (emisi\u00f3n sin final) o \"archivo\" (audio con\n                      duraci\u00f3n, por ejemplo un MP3).\n       data-titulo  : t\u00edtulo que se ve en la lista.\n       data-artista : opcional.\n       data-url     : direcci\u00f3n directa del audio.\n       Los dos MP3 de ejemplo son grabaciones de Chopin de Musopen en\n       archive.org, con licencia CC0 (dominio p\u00fablico), y permiten\n       analizar el audio (CORS): c\u00e1mbialos por los tuyos. -->\n  <ul data-lista>\n    <li data-id=\"ejemplo-1\" data-tipo=\"archivo\" data-titulo=\"Grande Valse Brillante, Op. 18\" data-artista=\"Chopin \u00b7 Musopen (CC0)\" data-url=\"https:\/\/archive.org\/download\/musopen-chopin\/Grande%20Valse%20Brilliante%20Op.18%20In%20E%20flat%20major.mp3\"><\/li>\n    <li data-id=\"ejemplo-2\" data-tipo=\"archivo\" data-titulo=\"Fantasie-Impromptu, Op. 66\" data-artista=\"Chopin \u00b7 Musopen (CC0)\" data-url=\"https:\/\/archive.org\/download\/musopen-chopin\/Fantasie%20Impromptu%20Op.%2066.mp3\"><\/li>\n  <\/ul>\n  <!-- Ejemplo de fila para otra emisora en directo (c\u00f3piala dentro de la lista):\n    <li data-id=\"otra-emisora\" data-tipo=\"directo\" data-titulo=\"Otra emisora\" data-artista=\"\" data-url=\"https:\/\/servidor.ejemplo:8000\/stream\"><\/li>\n  -->\n\n  <!-- 3. COLORES Y FONDOS\n       Colores en formato #rrggbb. Vac\u00edo (\"\") = color autom\u00e1tico.\n       data-acento        : aro, enlaces y tiempo (y botones si no eliges otro).\n       data-fondo-color   : fondo del reproductor completo (la \u00abp\u00e1gina\u00bb).\n       data-fondo-imagen  : imagen de fondo del reproductor completo.\n       data-letras-color  : t\u00edtulos, textos y men\u00fa.\n       data-botones-color : bot\u00f3n de reproducci\u00f3n y \u00abReintentar\u00bb.\n       data-iconos-color  : iconos de reproducir\/detener, volumen y men\u00fa.\n       data-disco-color   : fondo del interior del disco.\n       data-disco-imagen  : imagen del interior del disco.\n       data-visualizador  : \"si\" para intentar el visualizador real en el aro\n                            (solo funciona si el servidor del audio permite\n                            CORS); \"no\" para el aro est\u00e1tico.\n       data-pagina-completa: \"si\" para que la p\u00e1gina donde pegas el bloque\n                            muestre solo el reproductor, a todo el ancho y alto\n                            (oculta cabecera, pie, t\u00edtulo y comentarios del\n                            tema SOLO en esa p\u00e1gina); \"no\" si pegas el bloque\n                            dentro de un art\u00edculo con m\u00e1s contenido. -->\n  <div data-apariencia\n       data-acento=\"#ff2d8a\"\n       data-fondo-color=\"\"\n       data-fondo-imagen=\"\"\n       data-letras-color=\"\"\n       data-botones-color=\"\"\n       data-iconos-color=\"\"\n       data-disco-color=\"\"\n       data-disco-imagen=\"\"\n       data-visualizador=\"si\"\n       data-pagina-completa=\"no\"><\/div>\n\n  <!-- 4. BLOG Y MEN\u00da\n       El bot\u00f3n \u00abMen\u00fa\u00bb del reproductor muestra estos enlaces.\n       data-url del blog: direcci\u00f3n de tu blog (se a\u00f1ade como primer enlace\n       del men\u00fa). D\u00e9jala vac\u00eda si no tienes blog.\n       data-nueva-ventana=\"si\" abre el enlace en otra pesta\u00f1a.\n       Recuerda: al abrir otra p\u00e1gina, la radio se detiene. -->\n  <div data-blog data-texto=\"Blog\" data-url=\"\/blog\/\" data-nueva-ventana=\"no\"><\/div>\n  <ul data-menu>\n    <li data-texto=\"Inicio\" data-url=\"\/\" data-nueva-ventana=\"no\"><\/li>\n  <\/ul>\n\n<\/template>\n<!-- ===================  FIN DE LA ZONA EDITABLE  =================== -->\n<!-- Interfaz y estilos del reproductor (no hace falta editarlos). -->\n<template data-rcx-ui><style>\n\/* Radio Circular HTML 1.2.0-html \u00b7 GPLv2 o posterior.\n   Estilos del tema Radio Circular 1.2.0 adaptados a un componente aislado:\n   viven dentro de un Shadow DOM, as\u00ed que no alteran la web anfitriona\n   y los estilos de la web no alteran el reproductor.\n   :root y body del tema -> .rcx-page (el contenedor del componente).\n   Las medidas en rem pasan a px (base 16 px) para no depender del tama\u00f1o\n   de letra ra\u00edz de la web anfitriona. *\/\n\n:host {\n\tdisplay: block;\n}\n\n\/* 1. Variables (valores oscuros por defecto; el motor calcula los definitivos\n   con los colores del propietario, igual que hac\u00eda functions.php). *\/\n.rcx-page {\n\t--rc-bg: #101114;\n\t--rc-surface: #15151d;\n\t--rc-surface-2: #1d1d28;\n\t--rc-border: #2c2c3a;\n\t--rc-text: #f2f2f7;\n\t--rc-muted: #a8a8ba;\n\t--rc-accent: #ff2d8a;\n\t--rc-accent-text: #ff6aa9;\n\t--rc-on-accent: #0b0b10;\n\t--rc-button: #ff2d8a;\n\t--rc-on-button: #0b0b10;\n\t--rc-danger: #ff8a8a;\n\t--rc-radius: 14px;\n\t--rc-gutter: 16px;\n\t--rc-header-h: 64px;\n\t--rc-font: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n\t--rc-control: #ff2d8a;\n\t--rc-on-control: #0b0b10;\n\t--rc-panel: #14141c;\n\t--rc-panel-surface: #1c1c25;\n\t--rc-panel-border: #34343f;\n\t--rc-panel-text: #f2f2f7;\n\t--rc-panel-muted: #a8a8ba;\n\t--rc-panel-accent: #ff6aa9;\n\t--rc-panel-ui: #ff2d8a;\n\t--rc-on-panel-ui: #0b0b10;\n\t--rc-panel-danger: #ff8a8a;\n\tcolor-scheme: dark;\n}\n\n\/* 2. Base (equivale a body del tema, aplicada solo al componente). *\/\n.rcx-page {\n\tall: initial;\n\tdisplay: flex;\n\tflex-direction: column;\n\tposition: relative;\n\tbox-sizing: border-box;\n\twidth: 100%;\n\tmin-height: var(--rcx-alto-minimo, 0px);\n\tbackground: var(--rc-bg);\n\tcolor: var(--rc-text);\n\tfont-family: var(--rc-font);\n\tfont-size: 17px;\n\tline-height: 1.65;\n\ttext-align: left;\n\t-webkit-text-size-adjust: 100%;\n\toverflow-wrap: normal;\n}\n\n\/* P\u00e1gina del reproductor a pantalla completa (opci\u00f3n data-pagina-completa activada): como m\u00ednimo el alto de\n   la ventana, con el fondo oscuro cubri\u00e9ndola entera. Dentro de un art\u00edculo (\u00abno\u00bb) o en listados de\n   entradas, el bloque solo ocupa lo que necesita. *\/\n:host([data-rcx-pagina]) .rcx-page {\n\tmin-height: var(--rcx-alto-minimo, 100vh);\n\tmin-height: var(--rcx-alto-minimo, 100svh);\n}\n\n\/* Navegadores sin overflow: clip (Safari 15 y anteriores): los picos del aro no deben provocar\n   desplazamiento horizontal. *\/\n@supports not (overflow: clip) {\n\t.rcx-page {\n\t\toverflow-x: hidden;\n\t}\n}\n\n\/* Imagen de fondo de la p\u00e1gina\/componente: capa del color de fondo encima. *\/\n.rcx-page[data-bg-image] {\n\tbackground:\n\t\tlinear-gradient(color-mix(in srgb, var(--rc-bg) 55%, transparent), color-mix(in srgb, var(--rc-bg) 55%, transparent)),\n\t\tvar(--rcx-page-image) center \/ cover no-repeat,\n\t\tvar(--rc-bg);\n}\n\n@media (pointer: fine) {\n\t.rcx-page[data-bg-image] {\n\t\tbackground-attachment: fixed;\n\t}\n}\n\n*,\n*::before,\n*::after {\n\tbox-sizing: border-box;\n}\n\nimg {\n\tmax-width: 100%;\n\theight: auto;\n}\n\na {\n\tcolor: var(--rc-accent-text);\n\ttext-underline-offset: 0.18em;\n}\n\na:hover {\n\ttext-decoration-thickness: 2px;\n}\n\n:focus-visible {\n\toutline: 3px solid var(--rc-accent-text);\n\toutline-offset: 3px;\n}\n\nh2,\nh3,\nh4 {\n\tline-height: 1.25;\n\tmargin: 1.6em 0 0.6em;\n}\n\nbutton,\ninput,\nselect,\ntextarea {\n\tfont: inherit;\n\tcolor: inherit;\n}\n\nbutton,\na {\n\t-webkit-tap-highlight-color: transparent;\n}\n\ninput[type=\"text\"],\ninput[type=\"search\"],\ninput[type=\"url\"] {\n\tbackground: var(--rc-surface);\n\tborder: 1px solid var(--rc-border);\n\tborder-radius: 8px;\n\tpadding: 0.55em 0.75em;\n\tmax-width: 100%;\n}\n\n.rc-button,\nbutton.rc-button {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 0.4em;\n\tmin-height: 44px;\n\tpadding: 0.5em 1.1em;\n\tborder: 0;\n\tborder-radius: 999px;\n\tbackground: var(--rc-button);\n\tcolor: var(--rc-on-button);\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n\n.rc-button--ghost {\n\tbackground: transparent;\n\tcolor: var(--rc-text);\n\tborder: 1px solid var(--rc-border);\n}\n\n\/* Texto solo para lectores de pantalla. *\/\n.screen-reader-text {\n\tborder: 0;\n\tclip: rect(1px, 1px, 1px, 1px);\n\tclip-path: inset(50%);\n\theight: 1px;\n\tmargin: -1px;\n\toverflow: hidden;\n\tpadding: 0;\n\tposition: absolute !important;\n\twidth: 1px;\n\tword-wrap: normal !important;\n}\n\n[hidden] {\n\tdisplay: none !important;\n}\n\n\/* Contenedor que mide el ancho real del bloque (no la ventana): las reglas\n   que en el tema depend\u00edan del ancho de pantalla usan este contenedor. *\/\n.rcx-sizer {\n\tcontainer-name: rcx;\n\tcontainer-type: inline-size;\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 1 0 auto;\n\twidth: 100%;\n}\n\n\/* 3. Cabecera y men\u00fa desplegable *\/\n.site-header {\n\tposition: relative;\n\tz-index: 50;\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px 12px;\n\tmin-height: var(--rc-header-h);\n\tpadding: 14px var(--rc-gutter) 10px;\n\twidth: 100%;\n}\n\n\/* Sin t\u00edtulo en la cabecera: solo el logotipo opcional, a la izquierda del grupo de paneles. *\/\n.site-branding {\n\tdisplay: contents;\n}\n\n.site-branding .custom-logo-link {\n\tdisplay: block;\n\tflex: 0 1 auto;\n\tmin-width: 0;\n}\n\n.site-branding .custom-logo {\n\tdisplay: block;\n\tmax-height: 44px;\n\tmax-width: 100%;\n\twidth: auto;\n}\n\n.site-header__actions {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex: 0 0 auto;\n}\n\n.menu-toggle {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-height: 44px;\n\tpadding: 0 16px;\n\tbackground: var(--rc-surface);\n\tborder: 1px solid var(--rc-border);\n\tborder-radius: 999px;\n\tcursor: pointer;\n\tfont-weight: 600;\n}\n\n.menu-toggle[aria-expanded=\"true\"] {\n\tborder-color: var(--rc-accent);\n}\n\n.menu-toggle__icon {\n\twidth: 18px;\n\theight: 18px;\n\tflex: 0 0 auto;\n\tcolor: var(--rc-icon, currentColor);\n}\n\n.menu-toggle__icon .bar {\n\ttransition: transform 0.2s ease, opacity 0.2s ease;\n\ttransform-origin: center;\n}\n\n.menu-toggle[aria-expanded=\"true\"] .bar-1 {\n\ttransform: translateY(5px) rotate(45deg);\n}\n\n.menu-toggle[aria-expanded=\"true\"] .bar-2 {\n\topacity: 0;\n}\n\n.menu-toggle[aria-expanded=\"true\"] .bar-3 {\n\ttransform: translateY(-5px) rotate(-45deg);\n}\n\n.site-menu {\n\tbackground: var(--rc-surface);\n\tborder: 1px solid var(--rc-border);\n\tborder-radius: var(--rc-radius);\n\tpadding: 12px;\n\tposition: absolute;\n\ttop: calc(100% - 4px);\n\tright: var(--rc-gutter);\n\twidth: min(340px, calc(100cqi - 2 * var(--rc-gutter)));\n\tmax-height: calc(100vh - var(--rc-header-h) - 24px);\n\toverflow-y: auto;\n\tbox-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);\n}\n\n.site-menu ul {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.site-menu a {\n\tdisplay: flex;\n\talign-items: center;\n\tmin-height: 44px;\n\tpadding: 6px 12px;\n\tborder-radius: 10px;\n\tcolor: var(--rc-text);\n\ttext-decoration: none;\n}\n\n.site-menu a:hover,\n.site-menu a:focus-visible {\n\tbackground: var(--rc-surface-2);\n}\n\n.site-menu .current-menu-item > a {\n\tcolor: var(--rc-accent-text);\n\tfont-weight: 600;\n}\n\n.site-menu__hint {\n\tmargin: 0;\n\tcolor: var(--rc-muted);\n\tfont-size: 13.6px;\n\tpadding: 6px 12px;\n}\n\n\/* 5. Reproductor circular *\/\n.site-main {\n\tflex: 1 0 auto;\n\tdisplay: flex;\n\tflex-direction: column;\n\twidth: 100%;\n}\n\n.rc-player {\n\tposition: relative;\n\tflex: 1 0 auto;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 18px;\n\tpadding: 8px var(--rc-gutter) 40px;\n\tjustify-content: center;\n\toverflow-x: clip;\n}\n\n.rc-disc {\n\tposition: relative;\n\tcontainer-type: inline-size;\n\tcontainer-name: rcx-disc;\n\twidth: clamp(240px, min(92cqi, calc(100vh - 250px)), 600px);\n\twidth: clamp(240px, min(92cqi, calc(100svh - 250px)), 600px);\n\tmax-width: 100%;\n\taspect-ratio: 1 \/ 1;\n\tflex: 0 0 auto;\n}\n\n\/* El lienzo del aro mide el 130 % del disco: los trazos salen hacia fuera del borde. *\/\n.rc-ring {\n\tposition: absolute;\n\tinset: -15%;\n\twidth: 130%;\n\theight: 130%;\n\tdisplay: block;\n\tpointer-events: none;\n}\n\n.rc-face {\n\tposition: absolute;\n\tinset: 10.5%;\n\tborder-radius: 50%;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: clamp(6px, 2.6cqi, 16px);\n\tpadding: 7% 12%;\n\ttext-align: center;\n\tbackground:\n\t\tradial-gradient(circle at 50% 35%, #1d1d28 0%, #15151d 55%, #0f0f15 100%);\n\tcolor: var(--rc-text);\n\tborder: 1px solid color-mix(in srgb, var(--rc-text) 14%, transparent);\n\tbox-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 30px 80px rgba(0, 0, 0, 0.55);\n\ttransition: box-shadow 0.4s ease;\n}\n\n\/* Ning\u00fan elemento del c\u00edrculo debe encogerse: evita recortar el nombre. *\/\n.rc-face > * {\n\tflex-shrink: 0;\n}\n\n.rc-player[data-state=\"live\"] .rc-face,\n.rc-player[data-state=\"playing\"] .rc-face {\n\tbox-shadow: 0 0 0 1px color-mix(in srgb, var(--rc-accent) 55%, transparent), 0 0 70px color-mix(in srgb, var(--rc-accent) 22%, transparent);\n}\n\n.rc-name {\n\tmargin: 0;\n\tfont-size: clamp(15px, 5.6cqi, 34px);\n\tfont-weight: 800;\n\tline-height: 1.15;\n\tpadding: 0.08em 0 0.12em; \/* deja ver los trazos descendentes (g, p, y) *\/\n\tletter-spacing: -0.01em;\n\tmax-width: 100%;\n\toverflow-wrap: anywhere;\n\tdisplay: -webkit-box;\n\t-webkit-line-clamp: 2;\n\t-webkit-box-orient: vertical;\n\toverflow: hidden;\n}\n\n.rc-status {\n\tmargin: 0;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 7px;\n\tmin-height: 1.6em;\n\tpadding: 2px 12px;\n\tborder-radius: 999px;\n\tfont-size: clamp(12px, 3.1cqi, 16px);\n\tfont-weight: 600;\n\tcolor: var(--rc-muted);\n\tbackground: rgba(255, 255, 255, 0.04);\n\tmax-width: 100%;\n}\n\n.rc-status__dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: currentColor;\n\tflex: 0 0 auto;\n\tdisplay: none;\n}\n\n.rc-player[data-state=\"live\"] .rc-status {\n\tcolor: var(--rc-accent-text);\n}\n\n.rc-player[data-state=\"live\"] .rc-status__dot {\n\tdisplay: inline-block;\n\tbackground: var(--rc-accent);\n\tanimation: rc-pulse 1.6s ease-in-out infinite;\n}\n\n.rc-player[data-state=\"error\"] .rc-status,\n.rc-player[data-state=\"blocked\"] .rc-status {\n\tcolor: var(--rc-danger);\n}\n\n@keyframes rc-pulse {\n\t0%,\n\t100% {\n\t\topacity: 1;\n\t}\n\t50% {\n\t\topacity: 0.35;\n\t}\n}\n\n.rc-play {\n\tposition: relative;\n\twidth: clamp(56px, 21cqi, 116px);\n\theight: clamp(56px, 21cqi, 116px);\n\tborder-radius: 50%;\n\tborder: 0;\n\tbackground: var(--rc-button);\n\tcolor: var(--rc-icon, var(--rc-on-button));\n\tdisplay: grid;\n\tplace-items: center;\n\tcursor: pointer;\n\tflex: 0 0 auto;\n\tbox-shadow: 0 10px 30px color-mix(in srgb, var(--rc-button) 35%, transparent);\n\ttransition: transform 0.15s ease;\n}\n\n.rc-play:hover:not(:disabled) {\n\ttransform: scale(1.04);\n}\n\n.rc-play:active:not(:disabled) {\n\ttransform: scale(0.97);\n}\n\n.rc-play:focus-visible {\n\toutline-offset: 5px;\n}\n\n.rc-play:disabled {\n\tbackground: var(--rc-surface-2);\n\tcolor: var(--rc-muted);\n\tbox-shadow: none;\n\tcursor: not-allowed;\n}\n\n.rc-play svg {\n\twidth: 42%;\n\theight: 42%;\n\tdisplay: none;\n}\n\n.rc-play[data-icon=\"play\"] .rc-icon-play,\n.rc-play[data-icon=\"stop\"] .rc-icon-stop,\n.rc-play[data-icon=\"pause\"] .rc-icon-pause {\n\tdisplay: block;\n}\n\n.rc-play[data-icon=\"play\"] .rc-icon-play {\n\ttransform: translateX(6%);\n}\n\n.rc-time {\n\tfont-size: clamp(12px, 3cqi, 15px);\n\tcolor: var(--rc-accent-text);\n\tfont-variant-numeric: tabular-nums;\n\twidth: 70%;\n}\n\n.rc-progress {\n\tdisplay: block;\n\theight: 4px;\n\tmargin-top: 4px;\n\tborder-radius: 2px;\n\tbackground: var(--rc-border);\n\toverflow: hidden;\n}\n\n.rc-progress span {\n\tdisplay: block;\n\theight: 100%;\n\twidth: 0;\n\tbackground: var(--rc-accent);\n}\n\n.rc-volume {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\twidth: min(100%, 210px);\n}\n\n.rc-mute {\n\tflex: 0 0 auto;\n\twidth: 44px;\n\theight: 44px;\n\tdisplay: grid;\n\tplace-items: center;\n\tborder-radius: 50%;\n\tborder: 0;\n\tbackground: transparent;\n\tcolor: var(--rc-icon, var(--rc-text));\n\tcursor: pointer;\n\tpadding: 0;\n}\n\n.rc-mute:hover:not(:disabled) {\n\tbackground: rgba(255, 255, 255, 0.07);\n}\n\n.rc-mute svg {\n\twidth: 22px;\n\theight: 22px;\n}\n\n.rc-mute .rc-icon-muted,\n.rc-mute[aria-pressed=\"true\"] .rc-icon-volume {\n\tdisplay: none;\n}\n\n.rc-mute[aria-pressed=\"true\"] .rc-icon-muted {\n\tdisplay: block;\n}\n\n.rc-mute:disabled,\n.rc-volume input:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n\n.rc-volume input[type=\"range\"] {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\theight: 44px;\n\tmargin: 0;\n\tbackground: transparent;\n\taccent-color: var(--rc-accent);\n\tcursor: pointer;\n}\n\n.rc-message {\n\tmax-width: 520px;\n\twidth: 100%;\n\ttext-align: center;\n\tbackground: var(--rc-surface);\n\tborder: 1px solid var(--rc-border);\n\tborder-radius: var(--rc-radius);\n\tpadding: 14px 16px;\n}\n\n.rc-message p {\n\tmargin: 0 0 10px;\n}\n\n.rc-message p:last-child {\n\tmargin-bottom: 0;\n}\n\n.rc-message[data-kind=\"error\"] {\n\tborder-color: color-mix(in srgb, var(--rc-danger) 50%, var(--rc-border));\n}\n\n.rc-message[data-kind=\"info\"] .rc-retry {\n\tdisplay: none;\n}\n\n.rc-tagline {\n\tmargin: 0;\n\tcolor: var(--rc-muted);\n\ttext-align: center;\n\tmax-width: 560px;\n}\n\n.rc-admin-note {\n\tmargin: 0;\n\tfont-size: 14.4px;\n\tcolor: var(--rc-muted);\n\ttext-align: center;\n\tmax-width: 560px;\n}\n\n\/* Pantallas bajas en horizontal (m\u00f3vil girado). *\/\n@media (max-height: 520px) and (orientation: landscape) {\n\t.rc-disc {\n\t\twidth: clamp(240px, min(92cqi, calc(100vh - 90px)), 600px);\n\t}\n}\n\n\/* Movimiento reducido *\/\n@media (prefers-reduced-motion: reduce) {\n\t*,\n\t*::before,\n\t*::after {\n\t\tanimation-duration: 0.001ms !important;\n\t\tanimation-iteration-count: 1 !important;\n\t\ttransition-duration: 0.001ms !important;\n\t\tscroll-behavior: auto !important;\n\t}\n\n\t.rc-play:hover:not(:disabled),\n\t.rc-play:active:not(:disabled) {\n\t\ttransform: none;\n\t}\n}\n\n\/* Modo de alto contraste de Windows *\/\n@media (forced-colors: active) {\n\t.rc-play,\n\t.rc-button {\n\t\tborder: 2px solid ButtonText;\n\t}\n}\n\n\/* 10. Controles y paneles del reproductor *\/\n\n\/* Grupos de botones (esquinas) *\/\n.rc-group {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 5px;\n\tborder: 1px solid color-mix(in srgb, var(--rc-control) 45%, transparent);\n\tborder-radius: 12px;\n}\n\n.rc-icon-btn {\n\tdisplay: inline-grid;\n\tplace-items: center;\n\twidth: 40px;\n\theight: 40px;\n\tpadding: 0;\n\tborder: 1px solid color-mix(in srgb, var(--rc-control) 40%, transparent);\n\tborder-radius: 10px;\n\tbackground: transparent;\n\tcolor: var(--rc-control);\n\tcursor: pointer;\n}\n\n.rc-icon-btn svg {\n\twidth: 20px;\n\theight: 20px;\n}\n\n.rc-icon-btn:hover {\n\tbackground: color-mix(in srgb, var(--rc-control) 12%, transparent);\n}\n\n.rc-icon-btn[aria-pressed=\"true\"],\n.rc-icon-btn[aria-expanded=\"true\"],\n.rc-icon-btn[aria-selected=\"true\"] {\n\tbackground: color-mix(in srgb, var(--rc-control) 24%, transparent);\n\tborder-color: var(--rc-control);\n}\n\n.site-header .rc-mode {\n\tflex: 0 0 auto;\n}\n\n\/* Barra de paneles del reproductor: esquina superior izquierda; modo de color y men\u00fa, a la derecha. *\/\n.site-header .rc-tools {\n\tflex: 0 0 auto;\n\tmargin-right: auto;\n}\n\n\/* Disco: pista, anterior\/siguiente *\/\n.rc-track {\n\tmargin: 0;\n\tmax-width: 100%;\n\tfont-size: clamp(12px, 3.3cqi, 18px);\n\tcolor: var(--rc-accent-text);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.rc-transport {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: clamp(6px, 3cqi, 22px);\n}\n\n.rc-skip {\n\tdisplay: grid;\n\tplace-items: center;\n\twidth: clamp(40px, 11cqi, 60px);\n\theight: clamp(40px, 11cqi, 60px);\n\tpadding: 0;\n\tborder: 0;\n\tborder-radius: 50%;\n\tbackground: transparent;\n\tcolor: var(--rc-disc-ui, var(--rc-accent));\n\tcursor: pointer;\n}\n\n.rc-skip svg {\n\twidth: 78%;\n\theight: auto;\n}\n\n.rc-skip:hover:not([aria-disabled=\"true\"]) {\n\tbackground: color-mix(in srgb, var(--rc-disc-ui, var(--rc-accent)) 14%, transparent);\n}\n\n.rc-skip[aria-disabled=\"true\"] {\n\topacity: 0.35;\n\tcursor: not-allowed;\n}\n\n\/* Fila inferior *\/\n.rc-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tjustify-content: center;\n\talign-items: center;\n\tgap: 14px;\n}\n\n.rc-round {\n\tposition: relative;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 4px;\n\tmin-width: 46px;\n\theight: 46px;\n\tpadding: 0 10px;\n\tborder: 2px solid var(--rc-control);\n\tborder-radius: 999px;\n\tbackground: transparent;\n\tcolor: var(--rc-control);\n\tcursor: pointer;\n\tfont-weight: 700;\n}\n\n.rc-round > svg {\n\twidth: 22px;\n\theight: 22px;\n\tflex: 0 0 auto;\n}\n\n.rc-round[aria-pressed=\"true\"],\n.rc-round[aria-expanded=\"true\"] {\n\tbackground: var(--rc-control);\n\tcolor: var(--rc-on-control);\n}\n\n.rc-round__badge {\n\tdisplay: none;\n\tposition: absolute;\n\ttop: -6px;\n\tright: -6px;\n\tmin-width: 18px;\n\theight: 18px;\n\tborder-radius: 9px;\n\tbackground: var(--rc-control);\n\tcolor: var(--rc-on-control);\n\tfont-size: 11px;\n\tline-height: 18px;\n\ttext-align: center;\n}\n\n.rc-round[data-repeat=\"one\"] .rc-round__badge {\n\tdisplay: block;\n\toutline: 2px solid var(--rc-bg);\n}\n\n.rc-speed select {\n\tappearance: none;\n\t-webkit-appearance: none;\n\tborder: 0;\n\tbackground: transparent;\n\tcolor: inherit;\n\tfont-weight: 700;\n\tpadding: 0 2px;\n\tcursor: pointer;\n}\n\n.rc-speed select option {\n\tcolor: #17171f;\n\tbackground: #ffffff;\n}\n\n.rc-speed:focus-within {\n\toutline: 3px solid var(--rc-accent-text);\n\toutline-offset: 3px;\n}\n\n.rc-speed select:focus-visible {\n\toutline: none;\n}\n\n.rc-speed[data-disabled=\"1\"] {\n\topacity: 0.45;\n}\n\n.rc-row-note {\n\tmin-height: 1.4em;\n\tmargin: -6px 0 0;\n\tfont-size: 14px;\n\tcolor: var(--rc-muted);\n\ttext-align: center;\n}\n\n\/* Bot\u00f3n de cierre de paneles *\/\n.rc-close {\n\tdisplay: grid;\n\tplace-items: center;\n\twidth: 40px;\n\theight: 40px;\n\tpadding: 0;\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 10px;\n\tbackground: transparent;\n\tcolor: var(--rc-panel-accent);\n\tcursor: pointer;\n}\n\n.rc-close svg {\n\twidth: 20px;\n\theight: 20px;\n}\n\n\/* Panel lateral (fijo en la ventana, como en el tema) *\/\n.rc-drawer {\n\tposition: fixed;\n\tz-index: 2147483000;\n\ttop: 12px;\n\tbottom: 12px;\n\tleft: 12px;\n\twidth: min(400px, calc(100vw - 24px));\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground: var(--rc-panel);\n\tcolor: var(--rc-panel-text);\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 16px;\n\tbox-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);\n\tfont-size: 17px;\n\tline-height: 1.65;\n}\n\n.rc-drawer__head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n\tpadding: 10px;\n\tborder-bottom: 1px solid var(--rc-panel-border);\n}\n\n.rc-drawer .rc-group {\n\tborder-color: var(--rc-panel-border);\n}\n\n.rc-drawer .rc-icon-btn {\n\tcolor: var(--rc-panel-accent);\n\tborder-color: var(--rc-panel-border);\n}\n\n.rc-drawer .rc-icon-btn[aria-selected=\"true\"] {\n\tbackground: color-mix(in srgb, var(--rc-panel-ui) 22%, transparent);\n\tborder-color: var(--rc-panel-ui);\n}\n\n.rc-drawer__body {\n\tflex: 1 1 auto;\n\toverflow-y: auto;\n\toverscroll-behavior: contain;\n\tpadding: 12px;\n}\n\n.rc-drawer a,\n.rc-eq a {\n\tcolor: var(--rc-panel-accent);\n}\n\n.rc-drawer :focus-visible,\n.rc-eq :focus-visible {\n\toutline-color: var(--rc-panel-accent);\n}\n\n.rc-pane-note {\n\tmargin: 10px 0 0;\n\tfont-size: 14px;\n\tcolor: var(--rc-panel-muted);\n}\n\n.rc-pane-heading {\n\tmargin: 18px 0 8px;\n\tfont-size: 15.2px;\n\tcolor: var(--rc-panel-accent);\n}\n\n.rc-callout {\n\tmargin: 0;\n\tpadding: 10px 12px;\n\tborder: 1px solid color-mix(in srgb, var(--rc-panel-ui) 45%, transparent);\n\tborder-radius: 10px;\n\tbackground: color-mix(in srgb, var(--rc-panel-ui) 10%, transparent);\n\tfont-size: 14.4px;\n}\n\n\/* Buscador y lista *\/\n.rc-search {\n\tposition: relative;\n\tmargin-bottom: 10px;\n}\n\n.rc-search svg {\n\tposition: absolute;\n\tleft: 12px;\n\ttop: 50%;\n\twidth: 18px;\n\theight: 18px;\n\ttransform: translateY(-50%);\n\tcolor: var(--rc-panel-muted);\n\tpointer-events: none;\n}\n\n.rc-drawer input[type=\"search\"],\n.rc-drawer input[type=\"text\"],\n.rc-drawer input[type=\"url\"],\n.rc-drawer select {\n\twidth: 100%;\n\tmin-height: 44px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 10px;\n\tbackground: var(--rc-panel-surface);\n\tcolor: var(--rc-panel-text);\n}\n\n.rc-search input {\n\tpadding-left: 40px !important;\n}\n\n.rc-list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: grid;\n\tgap: 8px;\n}\n\n.rc-item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 4px;\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 12px;\n\tbackground: var(--rc-panel-surface);\n}\n\n.rc-item[aria-current=\"true\"] {\n\tborder-color: var(--rc-panel-ui);\n\tbackground: color-mix(in srgb, var(--rc-panel-ui) 14%, var(--rc-panel-surface));\n}\n\n.rc-item__main {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmin-height: 52px;\n\tpadding: 4px 6px;\n\tborder: 0;\n\tborder-radius: 8px;\n\tbackground: transparent;\n\tcolor: inherit;\n\ttext-align: left;\n\tcursor: pointer;\n}\n\n.rc-item__icon {\n\tflex: 0 0 auto;\n\tdisplay: grid;\n\tplace-items: center;\n\twidth: 40px;\n\theight: 40px;\n\tborder-radius: 8px;\n\tbackground: color-mix(in srgb, var(--rc-panel-ui) 16%, transparent);\n\tcolor: var(--rc-panel-accent);\n}\n\n.rc-item__icon svg {\n\twidth: 20px;\n\theight: 20px;\n}\n\n.rc-item__text {\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.rc-item__title {\n\tfont-weight: 600;\n\tline-height: 1.3;\n\toverflow-wrap: anywhere;\n\tdisplay: -webkit-box;\n\t-webkit-line-clamp: 2;\n\t-webkit-box-orient: vertical;\n\toverflow: hidden;\n}\n\n.rc-item__meta {\n\tfont-size: 12.8px;\n\tcolor: var(--rc-panel-muted);\n\toverflow-wrap: anywhere;\n}\n\n.rc-item__acts {\n\tflex: 0 0 auto;\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tjustify-content: flex-end;\n\tgap: 4px;\n\tmax-width: 116px;\n}\n\n.rc-act {\n\tdisplay: grid;\n\tplace-items: center;\n\twidth: 34px;\n\theight: 34px;\n\tpadding: 0;\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 8px;\n\tbackground: transparent;\n\tcolor: var(--rc-panel-accent);\n\tcursor: pointer;\n}\n\n.rc-act svg {\n\twidth: 17px;\n\theight: 17px;\n}\n\n.rc-act:disabled,\n.rc-act[aria-disabled=\"true\"] {\n\topacity: 0.35;\n\tcursor: not-allowed;\n}\n\n.rc-act--danger {\n\tcolor: var(--rc-panel-danger);\n}\n\n.rc-list__empty {\n\tpadding: 12px;\n\tcolor: var(--rc-panel-muted);\n}\n\n.rc-share-manual input {\n\tmargin-top: 6px;\n}\n\n\/* A\u00f1adir *\/\n.rc-subtabs {\n\tdisplay: flex;\n\tmargin-bottom: 12px;\n\tborder-bottom: 1px solid var(--rc-panel-border);\n}\n\n.rc-subtabs button {\n\tflex: 1 1 0;\n\tmin-height: 44px;\n\tborder: 0;\n\tborder-bottom: 2px solid transparent;\n\tbackground: transparent;\n\tcolor: var(--rc-panel-muted);\n\tfont-weight: 600;\n\tcursor: pointer;\n}\n\n.rc-subtabs button[aria-selected=\"true\"] {\n\tcolor: var(--rc-panel-accent);\n\tborder-bottom-color: var(--rc-panel-ui);\n}\n\n.rc-drop {\n\tposition: relative;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 26px 16px;\n\tborder: 2px dashed color-mix(in srgb, var(--rc-panel-ui) 60%, transparent);\n\tborder-radius: 12px;\n\ttext-align: center;\n\tcolor: var(--rc-panel-accent);\n\tcursor: pointer;\n}\n\n.rc-drop svg {\n\twidth: 44px;\n\theight: 44px;\n}\n\n.rc-drop span {\n\tcolor: var(--rc-panel-muted);\n\tfont-size: 14px;\n}\n\n.rc-drop[data-over=\"1\"],\n.rc-drop:hover {\n\tbackground: color-mix(in srgb, var(--rc-panel-ui) 10%, transparent);\n}\n\n.rc-drop:focus-within {\n\toutline: 3px solid var(--rc-panel-accent);\n\toutline-offset: 2px;\n}\n\n.rc-drop__input {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\topacity: 0;\n\toverflow: hidden;\n}\n\n.rc-field {\n\tmargin: 0 0 12px;\n}\n\n.rc-field label,\n.rc-field legend {\n\tdisplay: block;\n\tmargin-bottom: 4px;\n\tfont-weight: 600;\n\tfont-size: 14.4px;\n}\n\n.rc-kind {\n\tborder: 0;\n\tpadding: 0;\n}\n\n.rc-kind label {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-height: 36px;\n\tfont-weight: 400;\n}\n\n.rc-kind input {\n\taccent-color: var(--rc-panel-ui);\n\twidth: 18px;\n\theight: 18px;\n}\n\n.rc-drawer [aria-invalid=\"true\"] {\n\tborder-color: var(--rc-panel-danger) !important;\n}\n\n.rc-button--block {\n\twidth: 100%;\n}\n\n.rc-drawer .rc-button,\n.rc-eq .rc-button {\n\tbackground: var(--rc-panel-ui);\n\tcolor: var(--rc-on-panel-ui);\n}\n\n.rc-drawer .rc-button--ghost {\n\tbackground: transparent;\n\tcolor: var(--rc-panel-danger);\n\tborder: 1px solid var(--rc-panel-border);\n}\n\n.rc-drawer .rc-button:disabled {\n\topacity: 0.45;\n\tcursor: not-allowed;\n}\n\n.rc-feedback p {\n\tmargin: 6px 0 0;\n}\n\n.rc-feedback[data-kind=\"error\"] p:first-child {\n\tcolor: var(--rc-panel-danger);\n}\n\n\/* Ayuda *\/\n.rc-keys {\n\tmargin: 0;\n}\n\n.rc-keys div {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 5px 0;\n}\n\n.rc-keys dt {\n\tflex: 0 0 96px;\n\tmargin: 0;\n}\n\n.rc-keys dd {\n\tmargin: 0;\n\tfont-size: 14.4px;\n}\n\n.rc-drawer kbd {\n\tdisplay: inline-block;\n\tmin-width: 32px;\n\tpadding: 3px 8px;\n\tborder: 1px solid color-mix(in srgb, var(--rc-panel-ui) 60%, transparent);\n\tborder-radius: 6px;\n\tbackground: var(--rc-panel-surface);\n\tcolor: var(--rc-panel-accent);\n\tfont: 600 12.8px\/1.4 ui-monospace, SFMono-Regular, Consolas, monospace;\n\ttext-align: center;\n}\n\n.rc-help-list {\n\tmargin: 0;\n\tpadding-left: 18px;\n\tfont-size: 14.4px;\n}\n\n.rc-help-list li {\n\tmargin-bottom: 6px;\n}\n\n\/* Ajustes *\/\n.rc-setting {\n\tmargin-top: 16px;\n\tpadding: 12px;\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 12px;\n}\n\n.rc-setting__label {\n\tdisplay: block;\n\tmargin-bottom: 8px;\n\tfont-size: 12.8px;\n\tfont-weight: 700;\n\tletter-spacing: 0.04em;\n\ttext-transform: uppercase;\n\tcolor: var(--rc-panel-accent);\n}\n\n.rc-setting__row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.rc-setting__row input[type=\"range\"] {\n\tflex: 1 1 auto;\n\taccent-color: var(--rc-panel-ui);\n}\n\n.rc-setting__row output {\n\tmin-width: 56px;\n\tpadding: 4px 8px;\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 8px;\n\ttext-align: center;\n\tfont-variant-numeric: tabular-nums;\n}\n\n\/* Ecualizador (fijo en la ventana, como en el tema) *\/\n.rc-eq {\n\tposition: fixed;\n\tz-index: 2147483000;\n\tleft: 50%;\n\tbottom: 24px;\n\ttransform: translateX(-50%);\n\twidth: min(680px, calc(100vw - 24px));\n\tmax-height: calc(100vh - 48px);\n\toverflow-y: auto;\n\tpadding: 14px 18px 18px;\n\tbackground: var(--rc-panel);\n\tcolor: var(--rc-panel-text);\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 16px;\n\tbox-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);\n\tfont-size: 17px;\n\tline-height: 1.65;\n}\n\n.rc-eq__head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n}\n\n.rc-panel-title {\n\tmargin: 0;\n\tfont-size: 16.8px;\n\tcolor: var(--rc-panel-accent);\n}\n\n.rc-eq__status {\n\tmargin: 6px 0 10px;\n\tfont-size: 14px;\n\tcolor: var(--rc-panel-muted);\n}\n\n.rc-eq[data-status=\"cors\"] .rc-eq__status,\n.rc-eq[data-status=\"reconnect\"] .rc-eq__status,\n.rc-eq[data-status=\"unsupported\"] .rc-eq__status {\n\tcolor: var(--rc-panel-danger);\n}\n\n.rc-eq__bands {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(10, 1fr);\n\tgap: 4px;\n\tpadding-bottom: 12px;\n\tborder-bottom: 1px solid var(--rc-panel-border);\n}\n\n.rc-eq__band {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 12px;\n\tcolor: var(--rc-panel-accent);\n}\n\n.rc-eq__band input[type=\"range\"] {\n\twriting-mode: vertical-lr;\n\tdirection: rtl;\n\twidth: 28px;\n\theight: 140px;\n\tmargin: 0;\n\taccent-color: var(--rc-panel-ui);\n\tcursor: pointer;\n}\n\n.rc-eq__band output {\n\tfont-variant-numeric: tabular-nums;\n\tcolor: var(--rc-panel-muted);\n\twhite-space: nowrap;\n}\n\n.rc-eq__presets {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tjustify-content: center;\n\tgap: 8px;\n\tmargin-top: 12px;\n}\n\n.rc-chip {\n\tmin-height: 40px;\n\tpadding: 0 14px;\n\tborder: 1px solid var(--rc-panel-border);\n\tborder-radius: 8px;\n\tbackground: transparent;\n\tcolor: var(--rc-panel-accent);\n\tcursor: pointer;\n}\n\n.rc-chip:hover {\n\tbackground: color-mix(in srgb, var(--rc-panel-ui) 12%, transparent);\n}\n\n\/* Disco m\u00e1s compacto cuando es peque\u00f1o (m\u00f3vil) *\/\n@container rcx-disc (max-width: 380px) {\n\t.rc-face {\n\t\tgap: 4px;\n\t}\n\n\t.rc-name {\n\t\tfont-size: 14px;\n\t}\n\n\t.rc-play {\n\t\twidth: 50px;\n\t\theight: 50px;\n\t}\n\n\t.rc-volume {\n\t\theight: 36px;\n\t}\n\n\t.rc-mute {\n\t\twidth: 36px;\n\t\theight: 36px;\n\t}\n\n\t.rc-volume input[type=\"range\"] {\n\t\theight: 36px;\n\t}\n}\n\n\/* Bloque estrecho (antes: pantalla estrecha). *\/\n@container rcx (max-width: 480px) {\n\t.site-header,\n\t.rc-player {\n\t\t--rc-header-h: 60px;\n\t}\n\n\t.rc-player {\n\t\tgap: 14px;\n\t}\n\n\t.rc-face {\n\t\tpadding: 6% 10%;\n\t}\n\n\t.menu-toggle__label {\n\t\tposition: absolute;\n\t\twidth: 1px;\n\t\theight: 1px;\n\t\toverflow: hidden;\n\t\tclip: rect(1px, 1px, 1px, 1px);\n\t\tclip-path: inset(50%);\n\t\twhite-space: nowrap;\n\t}\n\n\t.menu-toggle {\n\t\tpadding: 0 12px;\n\t}\n\n\t.site-header .rc-icon-btn {\n\t\twidth: 36px;\n\t\theight: 36px;\n\t}\n\n\t.site-header .rc-group {\n\t\tgap: 3px;\n\t\tpadding: 3px;\n\t}\n\n\t.site-header {\n\t\tgap: 8px;\n\t}\n\n\t.rc-row {\n\t\tgap: 10px;\n\t}\n}\n\n\/* Cabecera en una sola fila hasta 355 px de ancho; por debajo, dos filas ordenadas. *\/\n@container rcx (max-width: 400px) {\n\t.site-header {\n\t\tpadding-left: 12px;\n\t\tpadding-right: 12px;\n\t}\n}\n\n@container rcx (max-width: 366px) {\n\t.site-header {\n\t\tgap: 6px;\n\t\tpadding-left: 8px;\n\t\tpadding-right: 8px;\n\t}\n\n\t.site-header__actions {\n\t\tgap: 6px;\n\t}\n}\n\n@container rcx (max-width: 354px) {\n\t.site-header {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: minmax(0, 1fr) auto;\n\t\tgrid-template-areas:\n\t\t\t\"marca acciones\"\n\t\t\t\"paneles paneles\";\n\t}\n\n\t.site-branding .custom-logo-link {\n\t\tgrid-area: marca;\n\t\tjustify-self: start;\n\t}\n\n\t.site-header__actions {\n\t\tgrid-area: acciones;\n\t}\n\n\t.site-header .rc-tools {\n\t\tgrid-area: paneles;\n\t\tjustify-self: start;\n\t\tmargin-right: 0;\n\t}\n}\n\n@media (max-width: 600px) {\n\t.rc-eq {\n\t\tbottom: 12px;\n\t\tpadding: 10px 14px 14px;\n\t}\n\n\t.rc-eq__bands {\n\t\tgrid-template-columns: 1fr;\n\t\tgap: 2px;\n\t}\n\n\t.rc-eq__band {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 58px 1fr 56px;\n\t\talign-items: center;\n\t}\n\n\t.rc-eq__band input[type=\"range\"] {\n\t\twriting-mode: horizontal-tb;\n\t\tdirection: ltr;\n\t\twidth: 100%;\n\t\theight: 28px;\n\t}\n\n\t.rc-eq__status {\n\t\tmargin: 4px 0 6px;\n\t}\n\n\t.rc-eq__presets {\n\t\tgap: 6px;\n\t\tmargin-top: 8px;\n\t}\n\n\t.rc-chip {\n\t\tmin-height: 36px;\n\t\tpadding: 0 10px;\n\t}\n}\n<\/style>\n<div class=\"rcx-page\" data-rc-mode=\"dark\">\n<div class=\"rcx-sizer\">\n\t<div class=\"site-header\">\n\t\t<div class=\"site-branding\">\n\t\t\t<span class=\"custom-logo-link\" data-rcx-logo-wrap hidden><img class=\"custom-logo\" data-rcx-logo alt=\"\"><\/span>\n\t\t<\/div>\n\n\t\t<div class=\"rc-group rc-tools\" role=\"group\" aria-label=\"Paneles del reproductor\">\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" data-rc-open=\"list\" aria-controls=\"rc-drawer\" aria-expanded=\"false\" title=\"Lista\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M9 17.5V5.5l11-2v12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"\/><circle cx=\"6.5\" cy=\"17.5\" r=\"2.5\" fill=\"currentColor\"\/><circle cx=\"17.5\" cy=\"15.5\" r=\"2.5\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Lista de emisoras y pistas<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" data-rc-open=\"add\" aria-controls=\"rc-drawer\" aria-expanded=\"false\" title=\"A\u00f1adir\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M7 18h-.5a4.5 4.5 0 0 1-.6-8.96A6 6 0 0 1 17.6 8.1 4.5 4.5 0 0 1 17.5 18H17\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><path d=\"M12 20v-8m-3 3 3-3 3 3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">A\u00f1adir audio<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" data-rc-open=\"help\" aria-controls=\"rc-drawer\" aria-expanded=\"false\" title=\"Ayuda\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M9.2 9a3 3 0 1 1 4.3 2.7c-.9.4-1.5 1.1-1.5 2.1v.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><circle cx=\"12\" cy=\"18\" r=\"1.3\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Ayuda y atajos de teclado<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" data-rc-open=\"settings\" aria-controls=\"rc-drawer\" aria-expanded=\"false\" title=\"Ajustes\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\/><path d=\"M12 2.8v2.4M12 18.8v2.4M2.8 12h2.4M18.8 12h2.4M5.5 5.5l1.7 1.7M16.8 16.8l1.7 1.7M5.5 18.5l1.7-1.7M16.8 7.2l1.7-1.7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><circle cx=\"12\" cy=\"12\" r=\"6.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Ajustes del reproductor<\/span>\n\t\t\t<\/button>\n\t\t<\/div>\n\n\t\t<div class=\"site-header__actions\">\n\t\t\t<div class=\"rc-group rc-mode\" role=\"group\" aria-label=\"Modo de color\" data-rc-modes>\n\t\t\t\t<button type=\"button\" class=\"rc-icon-btn\" data-rc-mode-btn=\"system\" aria-pressed=\"false\" title=\"Seg\u00fan el sistema\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\/><path d=\"M12 4a8 8 0 0 1 0 16z\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t\t<span class=\"screen-reader-text\">Seg\u00fan el sistema<\/span>\n\t\t\t\t<\/button>\n\t\t\t\t<button type=\"button\" class=\"rc-icon-btn\" data-rc-mode-btn=\"light\" aria-pressed=\"false\" title=\"Modo claro\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"4\" fill=\"currentColor\"\/><path d=\"M12 2.5v2.5M12 19v2.5M2.5 12H5M19 12h2.5M5.3 5.3l1.8 1.8M16.9 16.9l1.8 1.8M5.3 18.7l1.8-1.8M16.9 7.1l1.8-1.8\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><\/svg>\n\t\t\t\t\t<span class=\"screen-reader-text\">Modo claro<\/span>\n\t\t\t\t<\/button>\n\t\t\t\t<button type=\"button\" class=\"rc-icon-btn\" data-rc-mode-btn=\"dark\" aria-pressed=\"false\" title=\"Modo oscuro\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5z\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t\t<span class=\"screen-reader-text\">Modo oscuro<\/span>\n\t\t\t\t<\/button>\n\t\t\t<\/div>\n\n\t\t\t<button type=\"button\" class=\"menu-toggle\" aria-expanded=\"false\" aria-controls=\"site-menu\">\n\t\t\t\t<svg class=\"menu-toggle__icon\" viewBox=\"0 0 18 18\" aria-hidden=\"true\" focusable=\"false\">\n\t\t\t\t\t<rect class=\"bar bar-1\" x=\"2\" y=\"3\" width=\"14\" height=\"2\" rx=\"1\" fill=\"currentColor\"\/>\n\t\t\t\t\t<rect class=\"bar bar-2\" x=\"2\" y=\"8\" width=\"14\" height=\"2\" rx=\"1\" fill=\"currentColor\"\/>\n\t\t\t\t\t<rect class=\"bar bar-3\" x=\"2\" y=\"13\" width=\"14\" height=\"2\" rx=\"1\" fill=\"currentColor\"\/>\n\t\t\t\t<\/svg>\n\t\t\t\t<span class=\"menu-toggle__label\">Men\u00fa<\/span>\n\t\t\t<\/button>\n\t\t<\/div>\n\n\t\t<nav id=\"site-menu\" class=\"site-menu\" aria-label=\"Men\u00fa de la radio\" hidden>\n\t\t\t<ul id=\"primary-menu\" class=\"menu\" data-rcx-menu><\/ul>\n\t\t\t<p class=\"site-menu__hint\" data-rcx-menu-empty hidden>Este men\u00fa no tiene enlaces configurados.<\/p>\n\t\t<\/nav>\n\t<\/div>\n\n\t<div class=\"site-main\">\n\t<section class=\"rc-player\" data-rc-player data-state=\"nourl\" aria-labelledby=\"rc-title\">\n\n\t\t<div class=\"rc-disc\">\n\t\t\t<canvas class=\"rc-ring\" aria-hidden=\"true\"><\/canvas>\n\n\t\t\t<div class=\"rc-face\">\n\t\t\t\t<h2 id=\"rc-title\" class=\"rc-name\" data-rcx-name><\/h2>\n\t\t\t\t<p class=\"rc-track\" data-rc-track hidden><\/p>\n\n\t\t\t\t<p class=\"rc-status\" role=\"status\">\n\t\t\t\t\t<span class=\"rc-status__dot\" aria-hidden=\"true\"><\/span>\n\t\t\t\t\t<span class=\"rc-status__text\" data-rc-status><\/span>\n\t\t\t\t<\/p>\n\n\t\t\t\t<div class=\"rc-transport\">\n\t\t\t\t\t<button type=\"button\" class=\"rc-skip\" data-rc-prev aria-disabled=\"true\" aria-describedby=\"rc-nav-note\">\n\t\t\t\t\t\t<svg viewBox=\"0 0 32 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 4v16L3 12zM29 4v16L17 12z\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t\t\t<span class=\"screen-reader-text\">Anterior<\/span>\n\t\t\t\t\t<\/button>\n\n\t\t\t\t\t<button type=\"button\" class=\"rc-play\" data-rc-play data-icon=\"play\" disabled>\n\t\t\t\t\t\t<svg class=\"rc-icon-play\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M7 4.5v15a1 1 0 0 0 1.52.85l12-7.5a1 1 0 0 0 0-1.7l-12-7.5A1 1 0 0 0 7 4.5z\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t\t\t<svg class=\"rc-icon-stop\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><rect x=\"5\" y=\"5\" width=\"14\" height=\"14\" rx=\"2\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t\t\t<svg class=\"rc-icon-pause\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><rect x=\"5\" y=\"4\" width=\"5\" height=\"16\" rx=\"1.5\" fill=\"currentColor\"\/><rect x=\"14\" y=\"4\" width=\"5\" height=\"16\" rx=\"1.5\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t\t\t<span class=\"screen-reader-text\" data-rc-play-label>Reproducir<\/span>\n\t\t\t\t\t<\/button>\n\n\t\t\t\t\t<button type=\"button\" class=\"rc-skip\" data-rc-next aria-disabled=\"true\" aria-describedby=\"rc-nav-note\">\n\t\t\t\t\t\t<svg viewBox=\"0 0 32 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M3 4v16l12-8zM17 4v16l12-8z\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t\t\t<span class=\"screen-reader-text\">Siguiente<\/span>\n\t\t\t\t\t<\/button>\n\t\t\t\t<\/div>\n\t\t\t\t<span id=\"rc-nav-note\" class=\"screen-reader-text\" data-rc-nav-note><\/span>\n\n\t\t\t\t<div class=\"rc-volume\">\n\t\t\t\t\t<button type=\"button\" class=\"rc-mute\" data-rc-mute aria-pressed=\"false\" disabled>\n\t\t\t\t\t\t<svg class=\"rc-icon-volume\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M4 9v6h4l5 4V5L8 9H4z\" fill=\"currentColor\"\/><path d=\"M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><\/svg>\n\t\t\t\t\t\t<svg class=\"rc-icon-muted\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M4 9v6h4l5 4V5L8 9H4z\" fill=\"currentColor\"\/><path d=\"M16.5 9.5l5 5m0-5l-5 5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><\/svg>\n\t\t\t\t\t\t<span class=\"screen-reader-text\">Silenciar<\/span>\n\t\t\t\t\t<\/button>\n\t\t\t\t\t<label class=\"screen-reader-text\" for=\"rc-volume\">Volumen<\/label>\n\t\t\t\t\t<input type=\"range\" id=\"rc-volume\" data-rc-volume min=\"0\" max=\"100\" step=\"1\" value=\"80\" disabled>\n\t\t\t\t<\/div>\n\n\t\t\t\t<div class=\"rc-time\" data-rc-time>\n\t\t\t\t\t<span class=\"screen-reader-text\">Tiempo:<\/span>\n\t\t\t\t\t<span data-rc-time-text>00:00<\/span>\n\t\t\t\t\t<span class=\"rc-progress\" aria-hidden=\"true\" hidden><span data-rc-progress><\/span><\/span>\n\t\t\t\t<\/div>\n\t\t\t<\/div>\n\t\t<\/div>\n\n\t\t<div class=\"rc-row\" role=\"group\" aria-label=\"Opciones de reproducci\u00f3n\">\n\t\t\t<button type=\"button\" class=\"rc-round\" data-rc-shuffle aria-pressed=\"false\" title=\"Aleatorio\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M3 7h3.5c2 0 3 1 4.2 2.6l2.6 3.8C14.5 15 15.5 17 17.5 17H21m0 0-2.5-2.5M21 17l-2.5 2.5M3 17h3.5c1.3 0 2.2-.5 3-1.3M21 7h-3.5c-1.3 0-2.2.5-3 1.3M21 7l-2.5-2.5M21 7l-2.5 2.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Aleatorio<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-round\" data-rc-repeat data-repeat=\"off\" aria-pressed=\"false\" title=\"Repetir\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M17 3l3 3-3 3M20 6H8a4 4 0 0 0-4 4v1M7 21l-3-3 3-3M4 18h12a4 4 0 0 0 4-4v-1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n\t\t\t\t<span class=\"rc-round__badge\" aria-hidden=\"true\">1<\/span>\n\t\t\t\t<span class=\"screen-reader-text\" data-rc-repeat-label>Repetir: desactivado<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-round\" data-rc-eq-open aria-expanded=\"false\" aria-controls=\"rc-eq\" title=\"Ecualizador\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M4 7h10M18 7h2M4 17h4M12 17h8M4 12h4M12 12h8\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"\/><circle cx=\"16\" cy=\"7\" r=\"2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"\/><circle cx=\"10\" cy=\"17\" r=\"2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"\/><circle cx=\"10\" cy=\"12\" r=\"2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Ecualizador<\/span>\n\t\t\t<\/button>\n\t\t\t<label class=\"rc-round rc-speed\" title=\"Velocidad\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M4.5 17a8.5 8.5 0 1 1 15 0\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"\/><path d=\"M12 14l4-5\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"\/><circle cx=\"12\" cy=\"14\" r=\"1.6\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Velocidad<\/span>\n\t\t\t\t<span id=\"rc-speed-why\" class=\"screen-reader-text\" data-rc-speed-why><\/span>\n\t\t\t\t<select data-rc-speed aria-describedby=\"rc-speed-why\">\n\t\t\t\t\t<option value=\"0.5\">0.5\u00d7<\/option>\n\t\t\t\t\t<option value=\"0.75\">0.75\u00d7<\/option>\n\t\t\t\t\t<option value=\"1\" selected>1\u00d7<\/option>\n\t\t\t\t\t<option value=\"1.25\">1.25\u00d7<\/option>\n\t\t\t\t\t<option value=\"1.5\">1.5\u00d7<\/option>\n\t\t\t\t\t<option value=\"2\">2\u00d7<\/option>\n\t\t\t\t<\/select>\n\t\t\t<\/label>\n\t\t<\/div>\n\t\t<p class=\"rc-row-note\" data-rc-row-note aria-live=\"polite\"><\/p>\n\n\t\t<div class=\"rc-message\" data-rc-message data-kind=\"error\" hidden>\n\t\t\t<p data-rc-message-text><\/p>\n\t\t\t<button type=\"button\" class=\"rc-button rc-retry\" data-rc-retry>Reintentar<\/button>\n\t\t<\/div>\n\n\t\t<p class=\"rc-tagline\" data-rcx-tagline hidden><\/p>\n\t\t<p class=\"rc-admin-note\" data-rcx-owner-note hidden><\/p>\n\t<\/section>\n\t<\/div>\n<\/div>\n\n<!-- Ecualizador: panel no modal. -->\n<div class=\"rc-eq\" id=\"rc-eq\" role=\"dialog\" aria-modal=\"false\" aria-labelledby=\"rc-eq-title\" hidden data-rc-eq>\n\t<div class=\"rc-eq__head\">\n\t\t<h3 id=\"rc-eq-title\" class=\"rc-panel-title\">Ecualizador<\/h3>\n\t\t<button type=\"button\" class=\"rc-close\" data-rc-eq-close>\n\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6 6l12 12M18 6 6 18\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><\/svg>\n\t\t\t<span class=\"screen-reader-text\">Cerrar el ecualizador<\/span>\n\t\t<\/button>\n\t<\/div>\n\t<p class=\"rc-eq__status\" data-rc-eq-status role=\"status\"><\/p>\n\t<div class=\"rc-eq__bands\">\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-0\">60 Hz<\/label><input type=\"range\" id=\"rc-eq-0\" data-rc-eq-band=\"0\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-0\" data-rc-eq-value=\"0\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-1\">170 Hz<\/label><input type=\"range\" id=\"rc-eq-1\" data-rc-eq-band=\"1\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-1\" data-rc-eq-value=\"1\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-2\">310 Hz<\/label><input type=\"range\" id=\"rc-eq-2\" data-rc-eq-band=\"2\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-2\" data-rc-eq-value=\"2\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-3\">600 Hz<\/label><input type=\"range\" id=\"rc-eq-3\" data-rc-eq-band=\"3\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-3\" data-rc-eq-value=\"3\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-4\">1 kHz<\/label><input type=\"range\" id=\"rc-eq-4\" data-rc-eq-band=\"4\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-4\" data-rc-eq-value=\"4\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-5\">3 kHz<\/label><input type=\"range\" id=\"rc-eq-5\" data-rc-eq-band=\"5\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-5\" data-rc-eq-value=\"5\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-6\">6 kHz<\/label><input type=\"range\" id=\"rc-eq-6\" data-rc-eq-band=\"6\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-6\" data-rc-eq-value=\"6\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-7\">12 kHz<\/label><input type=\"range\" id=\"rc-eq-7\" data-rc-eq-band=\"7\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-7\" data-rc-eq-value=\"7\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-8\">14 kHz<\/label><input type=\"range\" id=\"rc-eq-8\" data-rc-eq-band=\"8\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-8\" data-rc-eq-value=\"8\">0<\/output><\/div>\n\t\t<div class=\"rc-eq__band\"><label for=\"rc-eq-9\">16 kHz<\/label><input type=\"range\" id=\"rc-eq-9\" data-rc-eq-band=\"9\" min=\"-12\" max=\"12\" step=\"1\" value=\"0\"><output for=\"rc-eq-9\" data-rc-eq-value=\"9\">0<\/output><\/div>\n\t<\/div>\n\t<div class=\"rc-eq__presets\" role=\"group\" aria-label=\"Ajustes predefinidos\">\n\t\t<button type=\"button\" class=\"rc-chip\" data-rc-eq-preset=\"flat\">Plano<\/button>\n\t\t<button type=\"button\" class=\"rc-chip\" data-rc-eq-preset=\"bass\">Graves<\/button>\n\t\t<button type=\"button\" class=\"rc-chip\" data-rc-eq-preset=\"treble\">Agudos<\/button>\n\t\t<button type=\"button\" class=\"rc-chip\" data-rc-eq-preset=\"vocal\">Voz<\/button>\n\t<\/div>\n<\/div>\n\n<!-- Panel lateral: lista, a\u00f1adir, ayuda y ajustes. Panel no modal. -->\n<aside class=\"rc-drawer\" id=\"rc-drawer\" role=\"dialog\" aria-modal=\"false\" aria-labelledby=\"rc-drawer-title\" hidden data-rc-drawer>\n\t<div class=\"rc-drawer__head\">\n\t\t<div class=\"rc-group\" role=\"tablist\" aria-label=\"Secciones del panel\">\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" role=\"tab\" id=\"rc-tab-list\" aria-controls=\"rc-pane-list\" aria-selected=\"false\" data-rc-tab=\"list\" tabindex=\"-1\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M9 17.5V5.5l11-2v12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"\/><circle cx=\"6.5\" cy=\"17.5\" r=\"2.5\" fill=\"currentColor\"\/><circle cx=\"17.5\" cy=\"15.5\" r=\"2.5\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Lista<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" role=\"tab\" id=\"rc-tab-add\" aria-controls=\"rc-pane-add\" aria-selected=\"false\" data-rc-tab=\"add\" tabindex=\"-1\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M7 18h-.5a4.5 4.5 0 0 1-.6-8.96A6 6 0 0 1 17.6 8.1 4.5 4.5 0 0 1 17.5 18H17\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><path d=\"M12 20v-8m-3 3 3-3 3 3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">A\u00f1adir<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" role=\"tab\" id=\"rc-tab-help\" aria-controls=\"rc-pane-help\" aria-selected=\"false\" data-rc-tab=\"help\" tabindex=\"-1\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M9.2 9a3 3 0 1 1 4.3 2.7c-.9.4-1.5 1.1-1.5 2.1v.4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><circle cx=\"12\" cy=\"18\" r=\"1.3\" fill=\"currentColor\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Ayuda<\/span>\n\t\t\t<\/button>\n\t\t\t<button type=\"button\" class=\"rc-icon-btn\" role=\"tab\" id=\"rc-tab-settings\" aria-controls=\"rc-pane-settings\" aria-selected=\"false\" data-rc-tab=\"settings\" tabindex=\"-1\">\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\/><path d=\"M12 2.8v2.4M12 18.8v2.4M2.8 12h2.4M18.8 12h2.4M5.5 5.5l1.7 1.7M16.8 16.8l1.7 1.7M5.5 18.5l1.7-1.7M16.8 7.2l1.7-1.7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><circle cx=\"12\" cy=\"12\" r=\"6.6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\/><\/svg>\n\t\t\t\t<span class=\"screen-reader-text\">Ajustes<\/span>\n\t\t\t<\/button>\n\t\t<\/div>\n\t\t<h3 id=\"rc-drawer-title\" class=\"screen-reader-text\" data-rc-drawer-title>Lista<\/h3>\n\t\t<button type=\"button\" class=\"rc-close\" data-rc-drawer-close>\n\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6 6l12 12M18 6 6 18\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><\/svg>\n\t\t\t<span class=\"screen-reader-text\">Cerrar el panel<\/span>\n\t\t<\/button>\n\t<\/div>\n\n\t<div class=\"rc-drawer__body\">\n\t\t<!-- Lista -->\n\t\t<div class=\"rc-pane\" id=\"rc-pane-list\" role=\"tabpanel\" aria-labelledby=\"rc-tab-list\" data-rc-pane=\"list\" data-title=\"Lista\" hidden>\n\t\t\t<div class=\"rc-search\">\n\t\t\t\t<label class=\"screen-reader-text\" for=\"rc-search\">Buscar en la lista<\/label>\n\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"11\" cy=\"11\" r=\"6.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\/><path d=\"m16 16 4 4\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\/><\/svg>\n\t\t\t\t<input type=\"search\" id=\"rc-search\" data-rc-search placeholder=\"Buscar\u2026\" autocomplete=\"off\">\n\t\t\t<\/div>\n\t\t\t<ul class=\"rc-list\" data-rc-list aria-label=\"Emisoras y pistas\"><\/ul>\n\t\t\t<p class=\"rc-pane-note\" data-rc-list-status role=\"status\"><\/p>\n\t\t<\/div>\n\n\t\t<!-- A\u00f1adir -->\n\t\t<div class=\"rc-pane\" id=\"rc-pane-add\" role=\"tabpanel\" aria-labelledby=\"rc-tab-add\" data-rc-pane=\"add\" data-title=\"A\u00f1adir\" hidden>\n\t\t\t<div class=\"rc-subtabs\" role=\"tablist\" aria-label=\"Forma de a\u00f1adir\">\n\t\t\t\t<button type=\"button\" role=\"tab\" id=\"rc-subtab-files\" aria-controls=\"rc-add-files\" aria-selected=\"true\" data-rc-subtab=\"files\">Subir archivos<\/button>\n\t\t\t\t<button type=\"button\" role=\"tab\" id=\"rc-subtab-url\" aria-controls=\"rc-add-url\" aria-selected=\"false\" data-rc-subtab=\"url\" tabindex=\"-1\">A\u00f1adir por URL<\/button>\n\t\t\t<\/div>\n\n\t\t\t<div id=\"rc-add-files\" role=\"tabpanel\" aria-labelledby=\"rc-subtab-files\" data-rc-subpane=\"files\">\n\t\t\t\t<label class=\"rc-drop\" data-rc-drop>\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M7 18h-.5a4.5 4.5 0 0 1-.6-8.96A6 6 0 0 1 17.6 8.1 4.5 4.5 0 0 1 17.5 18H17\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"\/><path d=\"M12 20v-8m-3 3 3-3 3 3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n\t\t\t\t\t<strong>Arrastra aqu\u00ed archivos de audio o listas .m3u<\/strong>\n\t\t\t\t\t<span>o pulsa para elegirlos<\/span>\n\t\t\t\t\t<input type=\"file\" class=\"rc-drop__input\" data-rc-file multiple accept=\"audio\/*,.mp3,.m4a,.aac,.ogg,.oga,.opus,.wav,.flac,.webm,.m3u,audio\/x-mpegurl,audio\/mpegurl\">\n\t\t\t\t<\/label>\n\t\t\t\t<p class=\"rc-pane-note\">Los archivos se quedan en este navegador: no se suben a esta web ni a ning\u00fan servidor, y solo los ves t\u00fa. Se guardan en el navegador si hay espacio; puedes borrarlos en Ajustes. Los formatos que se pueden reproducir dependen de tu navegador.<\/p>\n\t\t\t<\/div>\n\n\t\t\t<form id=\"rc-add-url\" role=\"tabpanel\" aria-labelledby=\"rc-subtab-url\" data-rc-subpane=\"url\" data-rc-url-form hidden novalidate>\n\t\t\t\t<p class=\"rc-field\">\n\t\t\t\t\t<label for=\"rc-url-title\">T\u00edtulo (opcional)<\/label>\n\t\t\t\t\t<input type=\"text\" id=\"rc-url-title\" data-rc-url-title maxlength=\"120\" autocomplete=\"off\">\n\t\t\t\t<\/p>\n\t\t\t\t<p class=\"rc-field\">\n\t\t\t\t\t<label for=\"rc-url\">Direcci\u00f3n del audio<\/label>\n\t\t\t\t\t<input type=\"url\" id=\"rc-url\" data-rc-url placeholder=\"https:\/\/\" required autocomplete=\"off\" inputmode=\"url\">\n\t\t\t\t<\/p>\n\t\t\t\t<fieldset class=\"rc-field rc-kind\">\n\t\t\t\t\t<legend>Tipo<\/legend>\n\t\t\t\t\t<label><input type=\"radio\" name=\"rc-kind\" value=\"live\" checked> Emisi\u00f3n en directo<\/label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"rc-kind\" value=\"file\"> Archivo de audio<\/label>\n\t\t\t\t\t<label><input type=\"radio\" name=\"rc-kind\" value=\"m3u\"> Lista M3U<\/label>\n\t\t\t\t<\/fieldset>\n\t\t\t\t<button type=\"submit\" class=\"rc-button rc-button--block\">A\u00f1adir a mi lista<\/button>\n\t\t\t\t<p class=\"rc-pane-note\">Usa el enlace directo al audio (no una p\u00e1gina web). Lo que a\u00f1ades solo se guarda en este navegador. Una lista M3U remota solo se puede leer si su servidor lo permite (CORS).<\/p>\n\t\t\t<\/form>\n\t\t\t<div class=\"rc-pane-note rc-feedback\" data-rc-add-status role=\"status\"><\/div>\n\t\t<\/div>\n\n\t\t<!-- Ayuda -->\n\t\t<div class=\"rc-pane\" id=\"rc-pane-help\" role=\"tabpanel\" aria-labelledby=\"rc-tab-help\" data-rc-pane=\"help\" data-title=\"Ayuda\" hidden>\n\t\t\t<p class=\"rc-callout\">Atajos de teclado para ordenador. Funcionan cuando est\u00e1s usando este reproductor (despu\u00e9s de pulsar en \u00e9l) y no mientras escribes en un campo de texto.<\/p>\n\t\t\t<h4 class=\"rc-pane-heading\">Reproducci\u00f3n<\/h4>\n\t\t\t<dl class=\"rc-keys\">\n\t\t\t\t<div><dt><kbd>Espacio<\/kbd><\/dt><dd>Reproducir \/ pausar (o detener en directo)<\/dd><\/div>\n\t\t\t\t<div><dt><kbd>M<\/kbd><\/dt><dd>Silenciar \/ quitar silencio<\/dd><\/div>\n\t\t\t\t<div><dt><kbd>N<\/kbd> \/ <kbd>P<\/kbd><\/dt><dd>Siguiente \/ anterior de la lista<\/dd><\/div>\n\t\t\t\t<div><dt><kbd>S<\/kbd><\/dt><dd>Aleatorio s\u00ed \/ no<\/dd><\/div>\n\t\t\t\t<div><dt><kbd>R<\/kbd><\/dt><dd>Repetir: desactivado \u2192 lista \u2192 pista<\/dd><\/div>\n\t\t\t\t<div><dt><kbd>\u2190<\/kbd> \/ <kbd>\u2192<\/kbd><\/dt><dd>Retroceder \/ avanzar 5 s (May\u00fas: 10 s). Solo archivos.<\/dd><\/div>\n\t\t\t<\/dl>\n\t\t\t<h4 class=\"rc-pane-heading\">Paneles<\/h4>\n\t\t\t<dl class=\"rc-keys\">\n\t\t\t\t<div><dt><kbd>?<\/kbd><\/dt><dd>Abrir esta ayuda<\/dd><\/div>\n\t\t\t\t<div><dt><kbd>Esc<\/kbd><\/dt><dd>Cerrar el panel abierto<\/dd><\/div>\n\t\t\t<\/dl>\n\t\t\t<h4 class=\"rc-pane-heading\">C\u00f3mo funciona<\/h4>\n\t\t\t<ul class=\"rc-help-list\">\n\t\t\t\t<li>En directo no hay duraci\u00f3n, velocidad ni b\u00fasqueda: el bot\u00f3n detiene la conexi\u00f3n y al volver a pulsar se conecta al momento actual.<\/li>\n\t\t\t\t<li>Aleatorio y repetir se aplican al terminar un archivo. Una emisi\u00f3n en directo sigue sonando hasta que cambies de elemento.<\/li>\n\t\t\t\t<li>El ecualizador funciona con tus archivos locales y con medios cuyo servidor permite procesar el audio (CORS). Si no lo permite, el medio suena igual, sin ecualizar.<\/li>\n\t\t\t\t<li>Al abrir otra p\u00e1gina de la web el audio se detiene.<\/li>\n\t\t\t<\/ul>\n\t\t<\/div>\n\n\t\t<!-- Ajustes del visitante -->\n\t\t<div class=\"rc-pane\" id=\"rc-pane-settings\" role=\"tabpanel\" aria-labelledby=\"rc-tab-settings\" data-rc-pane=\"settings\" data-title=\"Ajustes\" hidden>\n\t\t\t<p class=\"rc-pane-note\">Estos ajustes solo se guardan en este navegador y no cambian la web para nadie m\u00e1s.<\/p>\n\t\t\t<div class=\"rc-setting\">\n\t\t\t\t<label for=\"rc-set-volume\" class=\"rc-setting__label\">Volumen<\/label>\n\t\t\t\t<div class=\"rc-setting__row\">\n\t\t\t\t\t<input type=\"range\" id=\"rc-set-volume\" data-rc-set-volume min=\"0\" max=\"100\" step=\"1\" value=\"80\">\n\t\t\t\t\t<output for=\"rc-set-volume\" data-rc-set-volume-out>80 %<\/output>\n\t\t\t\t<\/div>\n\t\t\t<\/div>\n\t\t\t<div class=\"rc-setting\">\n\t\t\t\t<label for=\"rc-set-mode\" class=\"rc-setting__label\">Modo de color<\/label>\n\t\t\t\t<select id=\"rc-set-mode\" data-rc-set-mode>\n\t\t\t\t\t<option value=\"system\">Seg\u00fan el sistema<\/option>\n\t\t\t\t\t<option value=\"light\">Claro<\/option>\n\t\t\t\t\t<option value=\"dark\">Oscuro<\/option>\n\t\t\t\t<\/select>\n\t\t\t<\/div>\n\t\t\t<div class=\"rc-setting\">\n\t\t\t\t<label for=\"rc-set-speed\" class=\"rc-setting__label\">Velocidad de los archivos<\/label>\n\t\t\t\t<select id=\"rc-set-speed\" data-rc-set-speed>\n\t\t\t\t\t<option value=\"0.5\">0.5\u00d7<\/option>\n\t\t\t\t\t<option value=\"0.75\">0.75\u00d7<\/option>\n\t\t\t\t\t<option value=\"1\">1\u00d7<\/option>\n\t\t\t\t\t<option value=\"1.25\">1.25\u00d7<\/option>\n\t\t\t\t\t<option value=\"1.5\">1.5\u00d7<\/option>\n\t\t\t\t\t<option value=\"2\">2\u00d7<\/option>\n\t\t\t\t<\/select>\n\t\t\t<\/div>\n\t\t\t<div class=\"rc-setting\">\n\t\t\t\t<span class=\"rc-setting__label\">Tu lista local<\/span>\n\t\t\t\t<p class=\"rc-pane-note\" data-rc-local-count><\/p>\n\t\t\t\t<button type=\"button\" class=\"rc-button rc-button--ghost rc-button--block\" data-rc-clear-local>Borrar mi lista y mis archivos de este navegador<\/button>\n\t\t\t\t<p class=\"rc-pane-note\" data-rc-clear-status role=\"status\"><\/p>\n\t\t\t<\/div>\n\t\t<\/div>\n\t<\/div>\n<\/aside>\n<\/div>\n<\/template>\n<!-- Motor del reproductor (no hace falta editarlo). -->\n<script>\n\/*\n * Radio Circular HTML 1.2.0-html \u00b7 motor del reproductor.\n * Licencia GPLv2 o posterior (derivado del tema Radio Circular 1.2.0).\n * No edites este c\u00f3digo: los datos de tu radio est\u00e1n en la secci\u00f3n\n * \u00abEDITA AQU\u00cd TU RADIO Y TU LISTA\u00bb, al principio del bloque.\n *\n * Principios (heredados del tema):\n * - Un \u00fanico elemento audio activo por bloque y una sola reproducci\u00f3n en la p\u00e1gina\n *   entre bloques de Radio Circular: al iniciar uno, los dem\u00e1s se pausan\/detienen.\n * - Cada elemento declara su tipo: directo (live) o archivo (file).\n * - La interfaz refleja eventos reales: \u00abEn directo\u00bb solo aparece con \u00abplaying\u00bb.\n * - Web Audio (ecualizador\/visualizador) solo con medios procesables: archivos locales\n *   (blob:), mismo origen o servidores que permiten CORS. Si no, suena sin procesar.\n * - T\u00edtulos, URL y M3U se tratan siempre como texto, nunca como HTML.\n * - Cada bloque vive en su propio Shadow DOM y guarda sus datos con su identificador.\n *\/\n( function () {\n\t'use strict';\n\n\tvar VERSION = '1.2.0-html';\n\n\t\/* =============================================================\n\t * Textos\n\t * =========================================================== *\/\n\n\tvar T = {\n\t\tidle: 'Pulsa para escuchar',\n\t\tnoUrl: 'Emisi\u00f3n no configurada',\n\t\tconnecting: 'Conectando\u2026',\n\t\tlive: 'En directo',\n\t\tbuffering: 'Cargando\u2026',\n\t\tstopped: 'Detenida',\n\t\tplayingFile: 'Reproduciendo archivo',\n\t\tpaused: 'En pausa',\n\t\tended: 'Archivo terminado',\n\t\tblocked: 'Reproducci\u00f3n bloqueada',\n\t\terror: 'Sin conexi\u00f3n',\n\t\tlabelPlay: 'Reproducir',\n\t\tlabelStop: 'Detener la emisi\u00f3n',\n\t\tlabelPause: 'Pausar',\n\t\tlabelResume: 'Reanudar',\n\t\tlabelCancel: 'Cancelar la conexi\u00f3n',\n\t\tmsgBlocked: 'El navegador no permiti\u00f3 iniciar el audio. Pulsa de nuevo el bot\u00f3n de reproducci\u00f3n.',\n\t\tmsgNetwork: 'Se perdi\u00f3 la conexi\u00f3n. Comprueba tu conexi\u00f3n a internet y vuelve a intentarlo.',\n\t\tmsgUnsupported: 'No se pudo abrir este medio. Puede que el servidor no est\u00e9 emitiendo ahora, que la direcci\u00f3n no apunte directamente al audio o que el formato no sea compatible con este navegador.',\n\t\tmsgTimeout: 'El medio no responde. Puede que el servidor est\u00e9 desconectado.',\n\t\tmsgEndedLive: 'La emisi\u00f3n se ha interrumpido.',\n\t\tmsgMixed: 'Esta p\u00e1gina usa HTTPS y el medio usa HTTP: muchos navegadores bloquean esa combinaci\u00f3n. Usa una URL HTTPS.',\n\t\tmsgLocalMissing: 'No se encontr\u00f3 el archivo local (quiz\u00e1 se borr\u00f3 el almacenamiento del navegador). Qu\u00edtalo de la lista y vuelve a a\u00f1adirlo.',\n\t\tnavSingle: 'Solo hay un elemento en la lista. A\u00f1ade m\u00e1s desde el bot\u00f3n \u00abA\u00f1adir\u00bb.',\n\t\tbadgeLive: 'En directo',\n\t\tbadgeFile: 'Archivo',\n\t\tbadgeLocal: 'Archivo local',\n\t\tbadgeSession: 'solo esta sesi\u00f3n',\n\t\tbadgeMine: 'a\u00f1adido por ti',\n\t\tnowPlaying: 'Seleccionado',\n\t\tlistEmpty: 'No hay elementos que coincidan con la b\u00fasqueda.',\n\t\tactPlay: 'Reproducir \u00ab%s\u00bb',\n\t\tactUp: 'Subir \u00ab%s\u00bb en la lista',\n\t\tactDown: 'Bajar \u00ab%s\u00bb en la lista',\n\t\tactDownload: 'Descargar o abrir \u00ab%s\u00bb',\n\t\tactShare: 'Compartir el enlace de \u00ab%s\u00bb',\n\t\tactRemove: 'Quitar \u00ab%s\u00bb de mi lista',\n\t\tshareCopied: 'Enlace copiado al portapapeles.',\n\t\tshareManual: 'Copia el enlace:',\n\t\tshareLocal: 'Un archivo local no tiene enlace para compartir.',\n\t\tremoved: '\u00ab%s\u00bb se quit\u00f3 de tu lista.',\n\t\taddedN: 'A\u00f1adidos a tu lista: %d.',\n\t\tfileUnsupported: '\u00ab%s\u00bb: este navegador no puede reproducir ese formato.',\n\t\tfileNotAudio: '\u00ab%s\u00bb no es un archivo de audio ni una lista M3U.',\n\t\tstoredSession: 'No se pudo guardar en este navegador (%s): el archivo solo estar\u00e1 disponible hasta que cierres o recargues la p\u00e1gina.',\n\t\tquota: 'espacio insuficiente',\n\t\tstorageUnavailable: 'almacenamiento no disponible',\n\t\tm3uLocalPaths: 'Rutas de tu ordenador que una lista no puede abrir: %d. A\u00f1ade esos archivos directamente en \u00abSubir archivos\u00bb.',\n\t\tm3uNested: 'Listas anidadas ignoradas (no se abren): %d.',\n\t\tm3uInvalid: 'L\u00edneas no v\u00e1lidas ignoradas: %d.',\n\t\tm3uTruncated: 'Solo se han le\u00eddo las primeras entradas (m\u00e1ximo %d).',\n\t\tm3uEmpty: 'La lista no contiene enlaces de audio utilizables.',\n\t\tm3uTooBig: 'La lista es demasiado grande (m\u00e1ximo 512 KB).',\n\t\tm3uHls: 'Es una lista HLS (.m3u8) de v\u00eddeo o audio segmentado: no es compatible con este reproductor.',\n\t\tm3uFetch: 'No se pudo leer la lista: el servidor no responde o no permite leerla desde esta p\u00e1gina (CORS).',\n\t\turlEmpty: 'Escribe la direcci\u00f3n del audio.',\n\t\turlInvalid: 'La direcci\u00f3n no es v\u00e1lida.',\n\t\turlProtocol: 'Solo se admiten direcciones que empiecen por http:\/\/ o https:\/\/.',\n\t\turlCredentials: 'No incluyas usuario ni contrase\u00f1a en la direcci\u00f3n.',\n\t\turlHls: 'Los enlaces .m3u8 (HLS) no son compatibles.',\n\t\turlPlaylist: 'Parece una lista de reproducci\u00f3n: elige el tipo \u00abLista M3U\u00bb.',\n\t\turlMixed: 'Aviso: esta p\u00e1gina usa HTTPS y el enlace HTTP; es probable que el navegador lo bloquee.',\n\t\turlDuplicate: 'Ese enlace ya est\u00e1 en la lista.',\n\t\turlAdded: '\u00ab%s\u00bb se a\u00f1adi\u00f3 a tu lista. Si no suena, comprueba que el enlace apunte directamente al audio.',\n\t\teqApplied: 'Ecualizador activo en este medio.',\n\t\teqFlat: 'Plano: el sonido no se modifica.',\n\t\teqIdle: 'Se aplicar\u00e1 cuando reproduzcas un medio compatible.',\n\t\teqCors: 'Desactivado para este medio: su servidor no permite procesar el audio (falta la cabecera CORS). Se reproduce sin ecualizador.',\n\t\teqReconnect: 'Este medio externo se conect\u00f3 sin permiso de procesado. Para ecualizarlo, det\u00e9n y vuelve a reproducir (solo funciona si su servidor permite CORS).',\n\t\teqUnsupported: 'Este navegador no admite Web Audio: el ecualizador no est\u00e1 disponible.',\n\t\tspeedLive: 'La velocidad solo se aplica a archivos; en una emisi\u00f3n en directo no se puede cambiar.',\n\t\tspeedSet: 'Velocidad: %s',\n\t\tshuffleOn: 'Aleatorio activado',\n\t\tshuffleOff: 'Aleatorio desactivado',\n\t\trepeatOff: 'Repetir: desactivado',\n\t\trepeatAll: 'Repetir: toda la lista',\n\t\trepeatOne: 'Repetir: esta pista',\n\t\tseekLive: 'En directo no se puede avanzar ni retroceder.',\n\t\tlocalCount: 'Elementos a\u00f1adidos por ti en este navegador: %d.',\n\t\tconfirmClear: 'Pulsa otra vez para confirmar: se borrar\u00e1n tu lista local y tus archivos guardados.',\n\t\tcleared: 'Tu lista local y tus archivos se han borrado de este navegador.',\n\t\tblogLabel: 'Blog',\n\t\townerNoUrl: 'Emisi\u00f3n principal sin configurar: el propietario debe escribir su URL directa (data-url) en la secci\u00f3n \u00abEDITA AQU\u00cd TU RADIO Y TU LISTA\u00bb del bloque.',\n\t\townerErrors: 'Configuraci\u00f3n del bloque: se han ignorado %d datos no v\u00e1lidos (detalles en la consola del navegador).'\n\t};\n\n\t\/* =============================================================\n\t * Utilidades compartidas\n\t * =========================================================== *\/\n\n\tvar M3U_MAX_BYTES = 512 * 1024;\n\tvar M3U_MAX_ENTRIES = 200;\n\tvar TITLE_MAX = 120;\n\tvar OWNER_LIST_MAX = 50;\n\tvar EQ_FREQS = [ 60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000 ];\n\tvar EQ_PRESETS = {\n\t\tflat: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],\n\t\tbass: [ 7, 6, 4, 2, 0, 0, 0, 0, 0, 0 ],\n\t\ttreble: [ 0, 0, 0, 0, 0, 1, 3, 5, 6, 6 ],\n\t\tvocal: [ -3, -2, 0, 2, 4, 4, 2, 0, -1, -2 ]\n\t};\n\tvar SPEEDS = [ 0.5, 0.75, 1, 1.25, 1.5, 2 ];\n\tvar CONNECT_TIMEOUT = 20000;\n\n\tfunction log( kind, msg ) {\n\t\tif ( window.console && window.console[ kind ] ) {\n\t\t\twindow.console[ kind ]( 'Radio Circular: ' + msg );\n\t\t}\n\t}\n\n\tfunction fmtStr( str ) {\n\t\tvar args = Array.prototype.slice.call( arguments, 1 );\n\t\treturn String( str || '' ).replace( \/%[sd]\/g, function () {\n\t\t\treturn args.length ? String( args.shift() ) : '';\n\t\t} );\n\t}\n\n\tfunction cleanText( s, max ) {\n\t\treturn String( s == null ? '' : s ).replace( \/[\\u0000-\\u001f\\u007f]\/g, ' ' ).replace( \/\\s+\/g, ' ' ).trim().slice( 0, max || TITLE_MAX );\n\t}\n\n\tfunction clampNumber( v, min, max, fallback ) {\n\t\tv = Number( v );\n\t\treturn isFinite( v ) && v >= min && max >= v ? v : fallback;\n\t}\n\n\tfunction validateUrl( str ) {\n\t\tstr = String( str == null ? '' : str ).trim();\n\t\tif ( ! str ) {\n\t\t\treturn { ok: false, error: 'empty' };\n\t\t}\n\t\tvar u;\n\t\ttry {\n\t\t\tu = new URL( str );\n\t\t} catch ( e ) {\n\t\t\treturn { ok: false, error: 'invalid' };\n\t\t}\n\t\tif ( u.protocol !== 'http:' && u.protocol !== 'https:' ) {\n\t\t\treturn { ok: false, error: 'protocol' };\n\t\t}\n\t\tif ( u.username || u.password ) {\n\t\t\treturn { ok: false, error: 'credentials' };\n\t\t}\n\t\tif ( ! u.hostname ) {\n\t\t\treturn { ok: false, error: 'invalid' };\n\t\t}\n\t\treturn { ok: true, url: u.href };\n\t}\n\n\tfunction isHlsUrl( url ) {\n\t\treturn \/\\.m3u8(\\?|#|$)\/i.test( url );\n\t}\n\n\tfunction isPlaylistUrl( url ) {\n\t\treturn \/\\.(m3u|m3u8|pls|xspf)(\\?|#|$)\/i.test( url );\n\t}\n\n\t\/**\n\t * Lector de listas M3U (no HLS).\n\t * @param {string} text        Contenido de la lista.\n\t * @param {string|null} base   URL de la lista remota (para rutas relativas) o null si es local.\n\t * @param {string} defaultKind 'live' | 'file' cuando #EXTINF no indica duraci\u00f3n.\n\t *\/\n\tfunction parseM3U( text, base, defaultKind ) {\n\t\tvar res = { items: [], skippedLocal: 0, skippedNested: 0, skippedInvalid: 0, truncated: false };\n\t\tif ( typeof text !== 'string' ) {\n\t\t\tres.error = 'invalid';\n\t\t\treturn res;\n\t\t}\n\t\tif ( text.length > M3U_MAX_BYTES ) {\n\t\t\tres.error = 'too-big';\n\t\t\treturn res;\n\t\t}\n\t\tif ( \/#EXT-X-\/i.test( text ) ) {\n\t\t\tres.error = 'hls';\n\t\t\treturn res;\n\t\t}\n\t\tvar lines = text.replace( \/^\\uFEFF\/, '' ).split( \/\\r\\n|\\r|\\n\/ );\n\t\tvar pending = null;\n\t\tvar sawEntry = false;\n\t\tfor ( var i = 0; lines.length > i; i++ ) {\n\t\t\tvar line = lines[ i ].trim();\n\t\t\tif ( ! line ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ( line.charAt( 0 ) === '#' ) {\n\t\t\t\tvar m = \/^#EXTINF:\\s*(-?\\d+(?:\\.\\d+)?)[^,]*,(.*)$\/i.exec( line );\n\t\t\t\tif ( m ) {\n\t\t\t\t\tpending = { dur: parseFloat( m[ 1 ] ), title: cleanText( m[ 2 ] ) };\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsawEntry = true;\n\t\t\tif ( res.items.length >= M3U_MAX_ENTRIES ) {\n\t\t\t\tres.truncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tvar href = null;\n\t\t\tif ( \/^https?:\\\/\\\/\/i.test( line ) ) {\n\t\t\t\thref = line;\n\t\t\t} else if ( base && ! \/^[a-z]:[\\\\\/]\/i.test( line ) && ! \/^file:\/i.test( line ) && line.indexOf( '\\\\' ) === -1 ) {\n\t\t\t\ttry {\n\t\t\t\t\thref = new URL( line, base ).href;\n\t\t\t\t} catch ( e ) {\n\t\t\t\t\thref = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( ! href ) {\n\t\t\t\tif ( base ) {\n\t\t\t\t\tres.skippedInvalid++;\n\t\t\t\t} else {\n\t\t\t\t\tres.skippedLocal++; \/\/ Rutas del ordenador: no se pueden abrir autom\u00e1ticamente.\n\t\t\t\t}\n\t\t\t\tpending = null;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar v = validateUrl( href );\n\t\t\tif ( ! v.ok ) {\n\t\t\t\tres.skippedInvalid++;\n\t\t\t\tpending = null;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ( isPlaylistUrl( v.url ) ) {\n\t\t\t\tres.skippedNested++; \/\/ No se siguen listas anidadas.\n\t\t\t\tpending = null;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar kind = defaultKind === 'file' ? 'file' : 'live';\n\t\t\tvar title = '';\n\t\t\tvar artist = '';\n\t\t\tif ( pending ) {\n\t\t\t\tif ( 0 > pending.dur ) {\n\t\t\t\t\tkind = 'live';\n\t\t\t\t} else if ( pending.dur > 0 ) {\n\t\t\t\t\tkind = 'file';\n\t\t\t\t}\n\t\t\t\ttitle = pending.title;\n\t\t\t\tvar sep = title.indexOf( ' - ' );\n\t\t\t\tif ( sep > 0 ) {\n\t\t\t\t\tartist = title.slice( 0, sep ).trim();\n\t\t\t\t\ttitle = title.slice( sep + 3 ).trim();\n\t\t\t\t}\n\t\t\t}\n\t\t\tres.items.push( { url: v.url, title: title, artist: artist, kind: kind } );\n\t\t\tpending = null;\n\t\t}\n\t\tif ( ! sawEntry ) {\n\t\t\tres.error = 'empty';\n\t\t}\n\t\treturn res;\n\t}\n\n\t\/* =============================================================\n\t * Almacenamiento local del visitante, separado por bloque\n\t * (antes rc-store.js). Nada se env\u00eda a ning\u00fan servidor.\n\t * =========================================================== *\/\n\n\tfunction createStore( blockId ) {\n\t\tvar PREFIX = 'radioCircularHTML.' + blockId + '.';\n\t\tvar DB_NAME = 'radio-circular-html.' + blockId;\n\t\tvar DB_STORE = 'files';\n\n\t\tfunction readJSON( key, fallback ) {\n\t\t\ttry {\n\t\t\t\tvar raw = window.localStorage.getItem( PREFIX + key );\n\t\t\t\treturn raw ? JSON.parse( raw ) : fallback;\n\t\t\t} catch ( e ) {\n\t\t\t\treturn fallback; \/\/ Almacenamiento bloqueado o datos da\u00f1ados.\n\t\t\t}\n\t\t}\n\n\t\tfunction writeJSON( key, value ) {\n\t\t\ttry {\n\t\t\t\twindow.localStorage.setItem( PREFIX + key, JSON.stringify( value ) );\n\t\t\t\treturn true;\n\t\t\t} catch ( e ) {\n\t\t\t\treturn false; \/\/ Cuota llena o almacenamiento no disponible: se sigue en memoria.\n\t\t\t}\n\t\t}\n\n\t\tfunction removeKey( key ) {\n\t\t\ttry {\n\t\t\t\twindow.localStorage.removeItem( PREFIX + key );\n\t\t\t} catch ( e ) {}\n\t\t}\n\n\t\tvar DEFAULT_PREFS = {\n\t\t\tvolume: 0.8,\n\t\t\tmuted: false,\n\t\t\tshuffle: false,\n\t\t\trepeat: 'off', \/\/ 'off' | 'all' | 'one'\n\t\t\tspeed: 1,\n\t\t\teq: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],\n\t\t\tlastId: ''\n\t\t};\n\n\t\tfunction getPrefs() {\n\t\t\tvar p = readJSON( 'prefs', null );\n\t\t\tvar out = JSON.parse( JSON.stringify( DEFAULT_PREFS ) );\n\t\t\tif ( ! p || typeof p !== 'object' ) {\n\t\t\t\treturn out;\n\t\t\t}\n\t\t\tout.volume = clampNumber( p.volume, 0, 1, out.volume );\n\t\t\tout.muted = !! p.muted;\n\t\t\tout.shuffle = !! p.shuffle;\n\t\t\tout.repeat = [ 'off', 'all', 'one' ].indexOf( p.repeat ) >= 0 ? p.repeat : 'off';\n\t\t\tout.speed = SPEEDS.indexOf( Number( p.speed ) ) >= 0 ? Number( p.speed ) : 1;\n\t\t\tif ( Array.isArray( p.eq ) && p.eq.length === 10 ) {\n\t\t\t\tout.eq = p.eq.map( function ( g ) {\n\t\t\t\t\treturn Math.round( clampNumber( g, -12, 12, 0 ) );\n\t\t\t\t} );\n\t\t\t}\n\t\t\tout.lastId = typeof p.lastId === 'string' ? p.lastId.slice( 0, 80 ) : '';\n\t\t\treturn out;\n\t\t}\n\n\t\tfunction getLocalItems() {\n\t\t\tvar list = readJSON( 'items', [] );\n\t\t\tif ( ! Array.isArray( list ) ) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tvar out = [];\n\t\t\tlist.forEach( function ( it ) {\n\t\t\t\tif ( ! it || typeof it.id !== 'string' || ! \/^v-[a-z0-9-]{4,40}$\/.test( it.id ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar kind = it.kind === 'file' ? 'file' : 'live';\n\t\t\t\tif ( it.source === 'url' ) {\n\t\t\t\t\tvar v = validateUrl( it.url );\n\t\t\t\t\tif ( v.ok ) {\n\t\t\t\t\t\tout.push( { id: it.id, source: 'url', title: cleanText( it.title ), artist: cleanText( it.artist ), url: v.url, kind: kind, persisted: true } );\n\t\t\t\t\t}\n\t\t\t\t} else if ( it.source === 'file' ) {\n\t\t\t\t\tout.push( { id: it.id, source: 'file', title: cleanText( it.title ), artist: cleanText( it.artist ), kind: 'file', persisted: true, size: clampNumber( it.size, 0, 1e12, 0 ) } );\n\t\t\t\t}\n\t\t\t} );\n\t\t\treturn out;\n\t\t}\n\n\t\tfunction saveLocalItems( items ) {\n\t\t\tvar data = items.filter( function ( it ) {\n\t\t\t\treturn it.persisted && ( it.source === 'url' || it.source === 'file' );\n\t\t\t} ).map( function ( it ) {\n\t\t\t\treturn { id: it.id, source: it.source, title: it.title, artist: it.artist, url: it.source === 'url' ? it.url : undefined, kind: it.kind, size: it.size };\n\t\t\t} );\n\t\t\treturn writeJSON( 'items', data );\n\t\t}\n\n\t\tfunction getOrder() {\n\t\t\tvar o = readJSON( 'order', [] );\n\t\t\treturn Array.isArray( o ) ? o.filter( function ( id ) {\n\t\t\t\treturn typeof id === 'string';\n\t\t\t} ).slice( 0, 1000 ) : [];\n\t\t}\n\n\t\t\/* IndexedDB para archivos locales (una base de datos por bloque). *\/\n\t\tvar dbPromise = null;\n\n\t\tfunction openDb() {\n\t\t\tif ( dbPromise ) {\n\t\t\t\treturn dbPromise;\n\t\t\t}\n\t\t\tdbPromise = new Promise( function ( resolve, reject ) {\n\t\t\t\tvar req;\n\t\t\t\ttry {\n\t\t\t\t\tif ( ! window.indexedDB ) {\n\t\t\t\t\t\tthrow new Error( 'unavailable' );\n\t\t\t\t\t}\n\t\t\t\t\treq = window.indexedDB.open( DB_NAME, 1 );\n\t\t\t\t} catch ( e ) {\n\t\t\t\t\treject( e );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treq.onupgradeneeded = function () {\n\t\t\t\t\treq.result.createObjectStore( DB_STORE );\n\t\t\t\t};\n\t\t\t\treq.onsuccess = function () {\n\t\t\t\t\tresolve( req.result );\n\t\t\t\t};\n\t\t\t\treq.onerror = function () {\n\t\t\t\t\treject( req.error || new Error( 'unavailable' ) );\n\t\t\t\t};\n\t\t\t\treq.onblocked = function () {\n\t\t\t\t\treject( new Error( 'blocked' ) );\n\t\t\t\t};\n\t\t\t} );\n\t\t\tdbPromise.catch( function () {\n\t\t\t\tdbPromise = null;\n\t\t\t} );\n\t\t\treturn dbPromise;\n\t\t}\n\n\t\tfunction tx( mode, fn ) {\n\t\t\treturn openDb().then( function ( db ) {\n\t\t\t\treturn new Promise( function ( resolve, reject ) {\n\t\t\t\t\tvar t;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tt = db.transaction( DB_STORE, mode );\n\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\treject( e );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tvar store = t.objectStore( DB_STORE );\n\t\t\t\t\tvar result;\n\t\t\t\t\tvar req = fn( store );\n\t\t\t\t\tif ( req ) {\n\t\t\t\t\t\treq.onsuccess = function () {\n\t\t\t\t\t\t\tresult = req.result;\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\tt.oncomplete = function () {\n\t\t\t\t\t\tresolve( result );\n\t\t\t\t\t};\n\t\t\t\t\tt.onerror = function () {\n\t\t\t\t\t\treject( t.error || ( req && req.error ) );\n\t\t\t\t\t};\n\t\t\t\t\tt.onabort = function () {\n\t\t\t\t\t\treject( t.error || new Error( 'abort' ) );\n\t\t\t\t\t};\n\t\t\t\t} );\n\t\t\t} );\n\t\t}\n\n\t\treturn {\n\t\t\tgetPrefs: getPrefs,\n\t\t\tsavePrefs: function ( prefs ) {\n\t\t\t\treturn writeJSON( 'prefs', prefs );\n\t\t\t},\n\t\t\tgetLocalItems: getLocalItems,\n\t\t\tsaveLocalItems: saveLocalItems,\n\t\t\tgetOrder: getOrder,\n\t\t\tsaveOrder: function ( ids ) {\n\t\t\t\treturn writeJSON( 'order', ids );\n\t\t\t},\n\t\t\tgetMode: function () {\n\t\t\t\tvar m = readJSON( 'mode', null );\n\t\t\t\treturn m === 'system' || m === 'light' || m === 'dark' ? m : 'dark';\n\t\t\t},\n\t\t\tsaveMode: function ( m ) {\n\t\t\t\treturn writeJSON( 'mode', m );\n\t\t\t},\n\t\t\tclearLocalData: function () {\n\t\t\t\tremoveKey( 'items' );\n\t\t\t\tremoveKey( 'order' );\n\t\t\t},\n\t\t\tnewId: function () {\n\t\t\t\treturn 'v-' + Date.now().toString( 36 ) + '-' + Math.random().toString( 36 ).slice( 2, 8 );\n\t\t\t},\n\t\t\tputFile: function ( id, file ) {\n\t\t\t\treturn tx( 'readwrite', function ( s ) {\n\t\t\t\t\treturn s.put( { blob: file, name: file.name, type: file.type }, id );\n\t\t\t\t} );\n\t\t\t},\n\t\t\tgetFile: function ( id ) {\n\t\t\t\treturn tx( 'readonly', function ( s ) {\n\t\t\t\t\treturn s.get( id );\n\t\t\t\t} ).then( function ( rec ) {\n\t\t\t\t\tif ( ! rec || ! rec.blob ) {\n\t\t\t\t\t\tthrow new Error( 'missing' );\n\t\t\t\t\t}\n\t\t\t\t\treturn rec.blob;\n\t\t\t\t} );\n\t\t\t},\n\t\t\tdeleteFile: function ( id ) {\n\t\t\t\treturn tx( 'readwrite', function ( s ) {\n\t\t\t\t\treturn s.delete( id );\n\t\t\t\t} );\n\t\t\t},\n\t\t\tclearFiles: function () {\n\t\t\t\treturn tx( 'readwrite', function ( s ) {\n\t\t\t\t\treturn s.clear();\n\t\t\t\t} );\n\t\t\t},\n\t\t\tlimits: { m3uBytes: M3U_MAX_BYTES, m3uEntries: M3U_MAX_ENTRIES }\n\t\t};\n\t}\n\n\t\/* =============================================================\n\t * Datos del propietario (secci\u00f3n \u00abEDITA AQU\u00cd TU RADIO Y TU LISTA\u00bb)\n\t * =========================================================== *\/\n\n\tfunction attr( el, name ) {\n\t\tvar v = el ? el.getAttribute( name ) : null;\n\t\treturn v == null ? '' : String( v ).trim();\n\t}\n\n\tfunction hexColor( v ) {\n\t\tv = String( v || '' ).trim();\n\t\treturn \/^#([0-9a-f]{3}|[0-9a-f]{6})$\/i.test( v ) ? v.toLowerCase() : '';\n\t}\n\n\t\/\/ Imagen para url(\"...\") en CSS: http\/https, ruta relativa de la web o data:image.\n\tfunction imageCssUrl( v ) {\n\t\tv = String( v || '' ).trim();\n\t\tif ( ! v ) {\n\t\t\treturn '';\n\t\t}\n\t\tvar abs;\n\t\tif ( \/^data:image\\\/(png|jpe?g|gif|webp|avif|svg\\+xml)[;,]\/i.test( v ) ) {\n\t\t\tabs = v;\n\t\t} else {\n\t\t\ttry {\n\t\t\t\tvar u = new URL( v, document.baseURI );\n\t\t\t\tif ( u.protocol !== 'http:' && u.protocol !== 'https:' && u.protocol !== location.protocol ) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\tif ( u.username || u.password ) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\tabs = u.href;\n\t\t\t} catch ( e ) {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t}\n\t\t\/\/ Codifica los caracteres que podr\u00edan cerrar url(\"...\") en CSS.\n\t\treturn abs.replace( \/[\"'() \\\\]\/g, function ( c ) {\n\t\t\treturn { '\"': '%22', \"'\": '%27', '(': '%28', ')': '%29', ' ': '%20', '\\\\': '%5C' }[ c ];\n\t\t} );\n\t}\n\n\t\/\/ Enlace del men\u00fa: http\/https, mailto\/tel o ruta relativa de la web. Nunca javascript:.\n\tfunction linkUrl( v ) {\n\t\tv = String( v || '' ).trim();\n\t\tif ( ! v ) {\n\t\t\treturn '';\n\t\t}\n\t\ttry {\n\t\t\tvar u = new URL( v, document.baseURI );\n\t\t\tvar ok = [ 'http:', 'https:', 'mailto:', 'tel:' ].indexOf( u.protocol ) >= 0 || u.protocol === location.protocol;\n\t\t\tif ( ! ok || u.username || u.password ) {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t\treturn u.href;\n\t\t} catch ( e ) {\n\t\t\treturn '';\n\t\t}\n\t}\n\n\tfunction readConfig( tpl ) {\n\t\tvar frag = tpl && tpl.content ? tpl.content : document.createDocumentFragment();\n\t\tvar problems = [];\n\t\tvar radio = frag.querySelector( '[data-radio]' );\n\t\tvar look = frag.querySelector( '[data-apariencia]' );\n\t\tvar blog = frag.querySelector( '[data-blog]' );\n\n\t\tvar cfg = {\n\t\t\tblockId: attr( radio, 'data-bloque' ),\n\t\t\tname: cleanText( attr( radio, 'data-nombre' ) ),\n\t\t\ttagline: cleanText( attr( radio, 'data-eslogan' ), 300 ),\n\t\t\tlogo: '',\n\t\t\titems: [],\n\t\t\tcolors: {\n\t\t\t\taccent: hexColor( attr( look, 'data-acento' ) ) || '#ff2d8a',\n\t\t\t\tpageBg: hexColor( attr( look, 'data-fondo-color' ) ),\n\t\t\t\tpageImage: imageCssUrl( attr( look, 'data-fondo-imagen' ) ),\n\t\t\t\ttext: hexColor( attr( look, 'data-letras-color' ) ),\n\t\t\t\tbutton: hexColor( attr( look, 'data-botones-color' ) ),\n\t\t\t\ticon: hexColor( attr( look, 'data-iconos-color' ) ),\n\t\t\t\tdiscBg: hexColor( attr( look, 'data-disco-color' ) ),\n\t\t\t\tdiscImage: imageCssUrl( attr( look, 'data-disco-imagen' ) )\n\t\t\t},\n\t\t\tvisualizer: \/^(si|s\u00ed|yes|1|true)$\/i.test( attr( look, 'data-visualizador' ) ),\n\t\t\t\/\/ Activado salvo que se ponga \"no\": la p\u00e1gina del reproductor sin cabecera ni pie del tema.\n\t\t\tfullPage: ! \/^(no|0|false)$\/i.test( attr( look, 'data-pagina-completa' ) ),\n\t\t\tmenu: [],\n\t\t\tblogUrl: '',\n\t\t\tblogText: cleanText( attr( blog, 'data-texto' ), 60 ) || T.blogLabel,\n\t\t\tblogNewTab: \/^(si|s\u00ed|yes|1|true)$\/i.test( attr( blog, 'data-nueva-ventana' ) ),\n\t\t\tmainUrlMissing: false,\n\t\t\tproblems: problems\n\t\t};\n\n\t\tif ( ! radio ) {\n\t\t\tproblems.push( 'falta el elemento data-radio de la secci\u00f3n editable.' );\n\t\t}\n\n\t\t[ [ 'data-acento', 'acento' ], [ 'data-fondo-color', 'fondo' ], [ 'data-letras-color', 'letras' ], [ 'data-botones-color', 'botones' ], [ 'data-iconos-color', 'iconos' ], [ 'data-disco-color', 'disco' ] ].forEach( function ( p ) {\n\t\t\tif ( attr( look, p[ 0 ] ) && ! hexColor( attr( look, p[ 0 ] ) ) ) {\n\t\t\t\tproblems.push( p[ 0 ] + ' no es un color #rrggbb v\u00e1lido; se usa el valor autom\u00e1tico.' );\n\t\t\t}\n\t\t} );\n\t\t[ 'data-fondo-imagen', 'data-disco-imagen' ].forEach( function ( a ) {\n\t\t\tif ( attr( look, a ) && ! imageCssUrl( attr( look, a ) ) ) {\n\t\t\t\tproblems.push( a + ' no es una direcci\u00f3n de imagen v\u00e1lida.' );\n\t\t\t}\n\t\t} );\n\n\t\tvar logo = attr( radio, 'data-logo' );\n\t\tif ( logo ) {\n\t\t\tvar lu = imageCssUrl( logo ) ? linkUrl( logo ) || ( \/^data:image\\\/\/i.test( logo ) ? logo : '' ) : '';\n\t\t\tif ( lu ) {\n\t\t\t\tcfg.logo = lu;\n\t\t\t} else {\n\t\t\t\tproblems.push( 'data-logo no es una direcci\u00f3n de imagen v\u00e1lida.' );\n\t\t\t}\n\t\t}\n\n\t\t\/\/ Emisi\u00f3n principal: siempre \u00abdirecto\u00bb.\n\t\tvar mainUrl = attr( radio, 'data-url' );\n\t\tif ( mainUrl ) {\n\t\t\tvar mv = validateUrl( mainUrl );\n\t\t\tif ( mv.ok && ! isHlsUrl( mv.url ) ) {\n\t\t\t\tcfg.items.push( { id: 'main', source: 'main', kind: 'live', title: cfg.name, artist: '', url: mv.url } );\n\t\t\t} else {\n\t\t\t\tproblems.push( 'data-url de la radio principal no es v\u00e1lida (solo http\/https directo, sin usuario ni contrase\u00f1a, sin .m3u8).' );\n\t\t\t\tcfg.mainUrlMissing = true;\n\t\t\t}\n\t\t} else {\n\t\t\tcfg.mainUrlMissing = true;\n\t\t}\n\n\t\t\/\/ Lista inicial: una fila \u00abli\u00bb por elemento.\n\t\tvar seen = { main: true };\n\t\tvar kinds = { directo: 'live', live: 'live', archivo: 'file', file: 'file' };\n\t\tvar rows = frag.querySelectorAll( '[data-lista] > li' );\n\t\tvar ownerCount = 0;\n\t\tArray.prototype.forEach.call( rows, function ( li, n ) {\n\t\t\tvar where = 'lista, fila ' + ( n + 1 ) + ': ';\n\t\t\tif ( ownerCount >= OWNER_LIST_MAX ) {\n\t\t\t\tproblems.push( where + 'm\u00e1ximo ' + OWNER_LIST_MAX + ' filas; se ignoran las siguientes.' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar kind = kinds[ attr( li, 'data-tipo' ).toLowerCase() ];\n\t\t\tvar title = cleanText( attr( li, 'data-titulo' ) );\n\t\t\tvar v = validateUrl( attr( li, 'data-url' ) );\n\t\t\tvar id = attr( li, 'data-id' );\n\t\t\tif ( ! kind ) {\n\t\t\t\tproblems.push( where + 'data-tipo debe ser \u00abdirecto\u00bb o \u00abarchivo\u00bb.' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! v.ok || isHlsUrl( v.url ) ) {\n\t\t\t\tproblems.push( where + 'data-url no es v\u00e1lida (solo http\/https, sin usuario ni contrase\u00f1a, sin .m3u8).' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! title ) {\n\t\t\t\tproblems.push( where + 'falta data-titulo.' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! \/^[A-Za-z0-9_-]{1,40}$\/.test( id ) || \/^v-\/.test( id ) ) {\n\t\t\t\tproblems.push( where + 'data-id debe tener de 1 a 40 letras sin acentos, n\u00fameros, - o _ (y no empezar por \u00abv-\u00bb).' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( seen[ id ] ) {\n\t\t\t\tproblems.push( where + 'data-id \u00ab' + id + '\u00bb est\u00e1 repetido.' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tseen[ id ] = true;\n\t\t\townerCount++;\n\t\t\tcfg.items.push( { id: id, source: 'admin', kind: kind, title: title, artist: cleanText( attr( li, 'data-artista' ) ), url: v.url } );\n\t\t} );\n\n\t\t\/\/ Men\u00fa y blog.\n\t\tvar blogUrl = attr( blog, 'data-url' );\n\t\tif ( blogUrl ) {\n\t\t\tcfg.blogUrl = linkUrl( blogUrl );\n\t\t\tif ( ! cfg.blogUrl ) {\n\t\t\t\tproblems.push( 'data-url del blog no es v\u00e1lida.' );\n\t\t\t}\n\t\t}\n\t\tArray.prototype.forEach.call( frag.querySelectorAll( '[data-menu] > li' ), function ( li, n ) {\n\t\t\tvar text = cleanText( attr( li, 'data-texto' ), 60 );\n\t\t\tvar href = linkUrl( attr( li, 'data-url' ) );\n\t\t\tif ( ! text || ! href ) {\n\t\t\t\tproblems.push( 'men\u00fa, fila ' + ( n + 1 ) + ': falta data-texto o data-url no es v\u00e1lida.' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcfg.menu.push( { text: text, url: href, newTab: \/^(si|s\u00ed|yes|1|true)$\/i.test( attr( li, 'data-nueva-ventana' ) ) } );\n\t\t} );\n\n\t\treturn cfg;\n\t}\n\n\t\/* =============================================================\n\t * Colores con contraste garantizado (antes en template-tags.php)\n\t * =========================================================== *\/\n\n\tvar DEFAULT_ACCENT = '#ff2d8a';\n\tvar DEFAULT_BG = '#101114';\n\tvar DEFAULT_TEXT = '#f2f2f7';\n\n\tfunction hexToRgb( hex ) {\n\t\thex = String( hex ).replace( \/^#\/, '' );\n\t\tif ( hex.length === 3 ) {\n\t\t\thex = hex.charAt( 0 ) + hex.charAt( 0 ) + hex.charAt( 1 ) + hex.charAt( 1 ) + hex.charAt( 2 ) + hex.charAt( 2 );\n\t\t}\n\t\treturn [ parseInt( hex.slice( 0, 2 ), 16 ), parseInt( hex.slice( 2, 4 ), 16 ), parseInt( hex.slice( 4, 6 ), 16 ) ];\n\t}\n\n\tfunction rgbToHex( rgb ) {\n\t\treturn '#' + rgb.map( function ( v ) {\n\t\t\tvar s = Math.max( 0, Math.min( 255, v ) ).toString( 16 );\n\t\t\treturn 2 > s.length ? '0' + s : s;\n\t\t} ).join( '' );\n\t}\n\n\tfunction luminance( rgb ) {\n\t\tvar c = rgb.map( function ( v ) {\n\t\t\tv = v \/ 255;\n\t\t\treturn 0.03928 >= v ? v \/ 12.92 : Math.pow( ( v + 0.055 ) \/ 1.055, 2.4 );\n\t\t} );\n\t\treturn 0.2126 * c[ 0 ] + 0.7152 * c[ 1 ] + 0.0722 * c[ 2 ];\n\t}\n\n\tfunction contrast( a, b ) {\n\t\tvar la = luminance( a );\n\t\tvar lb = luminance( b );\n\t\treturn ( Math.max( la, lb ) + 0.05 ) \/ ( Math.min( la, lb ) + 0.05 );\n\t}\n\n\tfunction phpRound( x ) {\n\t\t\/\/ round() de PHP: redondea las mitades alej\u00e1ndose de cero.\n\t\treturn 0 > x ? -Math.round( -x ) : Math.round( x );\n\t}\n\n\tfunction mix( a, b, t ) {\n\t\treturn [ phpRound( a[ 0 ] + ( b[ 0 ] - a[ 0 ] ) * t ), phpRound( a[ 1 ] + ( b[ 1 ] - a[ 1 ] ) * t ), phpRound( a[ 2 ] + ( b[ 2 ] - a[ 2 ] ) * t ) ];\n\t}\n\n\tfunction adjustToContrast( rgb, bg, target ) {\n\t\tvar toward = 0.4 > luminance( bg ) ? [ 255, 255, 255 ] : [ 0, 0, 0 ];\n\t\tfor ( var t = 0; 1.0001 >= t; t += 0.05 ) {\n\t\t\tvar mixed = mix( rgb, toward, t );\n\t\t\tif ( contrast( mixed, bg ) >= target ) {\n\t\t\t\treturn mixed;\n\t\t\t}\n\t\t}\n\t\treturn toward;\n\t}\n\n\tfunction bestOn( bg ) {\n\t\treturn contrast( bg, [ 11, 11, 16 ] ) >= contrast( bg, [ 255, 255, 255 ] ) ? '#0b0b10' : '#ffffff';\n\t}\n\n\tfunction rgba( rgb, alpha ) {\n\t\treturn 'rgba(' + rgb[ 0 ] + ',' + rgb[ 1 ] + ',' + rgb[ 2 ] + ',' + alpha + ')';\n\t}\n\n\tfunction explicitColor( v, def ) {\n\t\treturn ! v || ( def && v.toLowerCase() === def.toLowerCase() ) ? '' : v;\n\t}\n\n\t\/**\n\t * Variables de color para un modo ('dark' | 'light'). Misma prioridad que el tema:\n\t * los colores e im\u00e1genes del propietario se mantienen en su zona (p\u00e1gina\/componente\n\t * o disco) en ambos modos; el modo solo cambia lo predeterminado y los paneles.\n\t *\/\n\tfunction modeVars( c, mode ) {\n\t\tvar light = mode === 'light';\n\t\tvar def = light ?\n\t\t\t{ bg: '#f4f3f8', text: '#17171f', panel: '#ffffff', panelText: '#17171f', disc: '#f1f0f6' } :\n\t\t\t{ bg: DEFAULT_BG, text: DEFAULT_TEXT, panel: '#14141c', panelText: '#f2f2f7', disc: '#15151d' };\n\n\t\tvar bgHex = explicitColor( c.pageBg, DEFAULT_BG );\n\t\tvar textHex = explicitColor( c.text, DEFAULT_TEXT );\n\t\tvar bg = hexToRgb( bgHex || def.bg );\n\t\tvar text;\n\t\tif ( textHex ) {\n\t\t\ttext = hexToRgb( textHex );\n\t\t} else if ( bgHex ) {\n\t\t\ttext = hexToRgb( bestOn( bg ) );\n\t\t} else {\n\t\t\ttext = hexToRgb( def.text );\n\t\t}\n\t\ttext = adjustToContrast( text, bg, 4.5 );\n\t\tvar surf = mix( bg, text, 0.06 );\n\t\tvar surf2 = mix( bg, text, 0.11 );\n\t\tvar border = mix( bg, text, 0.2 );\n\t\tvar muted = adjustToContrast( mix( text, bg, 0.35 ), surf2, 4.5 );\n\t\tvar danger = adjustToContrast( [ 255, 138, 138 ], surf2, 4.5 );\n\n\t\tvar accent = hexToRgb( c.accent || DEFAULT_ACCENT );\n\t\tvar accentUi = adjustToContrast( accent, surf2, 3 );\n\t\tvar accentText = adjustToContrast( accent, surf2, 4.5 );\n\t\tvar button = c.button ? adjustToContrast( hexToRgb( c.button ), bg, 3 ) : accentUi;\n\n\t\tvar panel = hexToRgb( def.panel );\n\t\tvar panelText = hexToRgb( def.panelText );\n\t\tvar panelSurf = mix( panel, panelText, 0.06 );\n\t\tvar panelBorder = mix( panel, panelText, 0.18 );\n\t\tvar panelMuted = adjustToContrast( mix( panelText, panel, 0.35 ), panelSurf, 4.5 );\n\t\tvar panelAccent = adjustToContrast( accent, panelSurf, 4.5 );\n\t\tvar panelUi = adjustToContrast( accent, panel, 3 );\n\t\tvar panelDanger = adjustToContrast( [ 255, 120, 120 ], panelSurf, 4.5 );\n\t\tvar control = adjustToContrast( accent, bg, 3 );\n\n\t\tvar root = {\n\t\t\t'color-scheme': 0.4 > luminance( bg ) ? 'dark' : 'light',\n\t\t\t'--rc-bg': rgbToHex( bg ),\n\t\t\t'--rc-surface': rgbToHex( surf ),\n\t\t\t'--rc-surface-2': rgbToHex( surf2 ),\n\t\t\t'--rc-border': rgbToHex( border ),\n\t\t\t'--rc-text': rgbToHex( text ),\n\t\t\t'--rc-muted': rgbToHex( muted ),\n\t\t\t'--rc-danger': rgbToHex( danger ),\n\t\t\t'--rc-accent': rgbToHex( accentUi ),\n\t\t\t'--rc-accent-text': rgbToHex( accentText ),\n\t\t\t'--rc-on-accent': bestOn( accentUi ),\n\t\t\t'--rc-button': rgbToHex( button ),\n\t\t\t'--rc-on-button': bestOn( button ),\n\t\t\t'--rc-control': rgbToHex( control ),\n\t\t\t'--rc-on-control': bestOn( control ),\n\t\t\t'--rc-panel': rgbToHex( panel ),\n\t\t\t'--rc-panel-surface': rgbToHex( panelSurf ),\n\t\t\t'--rc-panel-border': rgbToHex( panelBorder ),\n\t\t\t'--rc-panel-text': rgbToHex( panelText ),\n\t\t\t'--rc-panel-muted': rgbToHex( panelMuted ),\n\t\t\t'--rc-panel-accent': rgbToHex( panelAccent ),\n\t\t\t'--rc-panel-ui': rgbToHex( panelUi ),\n\t\t\t'--rc-on-panel-ui': bestOn( panelUi ),\n\t\t\t'--rc-panel-danger': rgbToHex( panelDanger ),\n\t\t\t'--rc-icon': c.icon || ''\n\t\t};\n\n\t\t\/\/ Disco: un fondo elegido (color o imagen) se mantiene en ambos modos.\n\t\tvar explicit = !! ( c.discBg || c.discImage );\n\t\tvar disc = hexToRgb( c.discBg || ( explicit ? '#15151d' : def.disc ) );\n\t\tvar dText = contrast( text, disc ) >= 4.5 ? text : hexToRgb( bestOn( disc ) );\n\t\tvar face = {\n\t\t\t'--rc-text': rgbToHex( dText ),\n\t\t\t'--rc-muted': rgbToHex( adjustToContrast( mix( dText, disc, 0.35 ), disc, 4.5 ) ),\n\t\t\t'--rc-accent-text': rgbToHex( adjustToContrast( accent, disc, 4.5 ) ),\n\t\t\t'--rc-disc-ui': rgbToHex( adjustToContrast( accent, disc, 3 ) ),\n\t\t\tbackground: ''\n\t\t};\n\t\tif ( c.discImage ) {\n\t\t\tvar veil = rgba( disc, 0.5 );\n\t\t\tface.background = 'linear-gradient(' + veil + ',' + veil + '),url(\"' + c.discImage + '\") center\/cover no-repeat,' + rgbToHex( disc );\n\t\t} else if ( c.discBg ) {\n\t\t\tface.background = rgbToHex( disc );\n\t\t} else if ( light ) {\n\t\t\tface.background = 'radial-gradient(circle at 50% 35%,#ffffff 0%,#f1f0f6 55%,#e4e3ec 100%)';\n\t\t}\n\t\treturn { root: root, face: face };\n\t}\n\n\t\/* =============================================================\n\t * Coordinaci\u00f3n global: un solo juego de escuchas para todos los\n\t * bloques de la p\u00e1gina, que reparte cada evento a su bloque.\n\t * =========================================================== *\/\n\n\tfunction createGlobal() {\n\t\tvar G = {\n\t\t\tversion: VERSION,\n\t\t\tinstances: [],\n\t\t\tlastActive: null,\n\t\t\treduceMq: window.matchMedia ? window.matchMedia( '(prefers-reduced-motion: reduce)' ) : null,\n\t\t\tlightMq: window.matchMedia ? window.matchMedia( '(prefers-color-scheme: light)' ) : null\n\t\t};\n\n\t\tfunction each( fn ) {\n\t\t\tG.instances.slice().forEach( function ( inst ) {\n\t\t\t\ttry {\n\t\t\t\t\tfn( inst );\n\t\t\t\t} catch ( e ) {\n\t\t\t\t\tlog( 'error', e && e.message ? e.message : String( e ) );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\tfunction pathOf( e ) {\n\t\t\treturn e.composedPath ? e.composedPath() : [ e.target ];\n\t\t}\n\n\t\tfunction instanceIn( path ) {\n\t\t\tfor ( var i = 0; G.instances.length > i; i++ ) {\n\t\t\t\tif ( path.indexOf( G.instances[ i ].host ) >= 0 ) {\n\t\t\t\t\treturn G.instances[ i ];\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\tfunction isPageLevel( el ) {\n\t\t\treturn ! el || el === document || el === document.body || el === document.documentElement || el === window;\n\t\t}\n\n\t\t\/\/ Los atajos solo act\u00faan en el bloque donde est\u00e1 el foco o, si el foco est\u00e1 en\n\t\t\/\/ la p\u00e1gina (body), en el \u00faltimo bloque que se us\u00f3. Nunca en campos de la web.\n\t\tG.mountAll = function () {\n\t\t\tvar roots = document.querySelectorAll( '[data-rcx-root]' );\n\t\t\tArray.prototype.forEach.call( roots, function ( host ) {\n\t\t\t\tif ( host.getAttribute( 'data-rcx-ready' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thost.setAttribute( 'data-rcx-ready', 'mounting' );\n\t\t\t\ttry {\n\t\t\t\t\tmount( host, G );\n\t\t\t\t\thost.setAttribute( 'data-rcx-ready', '1' );\n\t\t\t\t} catch ( e ) {\n\t\t\t\t\thost.setAttribute( 'data-rcx-ready', 'error' );\n\t\t\t\t\tlog( 'error', 'no se pudo iniciar el bloque: ' + ( e && e.message ? e.message : e ) );\n\t\t\t\t}\n\t\t\t} );\n\t\t};\n\n\t\t\/\/ Al empezar a reproducir un bloque se pausan\/detienen los dem\u00e1s Radio Circular.\n\t\tG.claim = function ( inst ) {\n\t\t\tG.lastActive = inst;\n\t\t\teach( function ( other ) {\n\t\t\t\tif ( other !== inst ) {\n\t\t\t\t\tother.yieldPlayback();\n\t\t\t\t}\n\t\t\t} );\n\t\t};\n\n\t\t\/\/ Los paneles son fijos en la ventana: al abrir uno se cierran los de otros bloques.\n\t\tG.panelOpened = function ( inst ) {\n\t\t\teach( function ( other ) {\n\t\t\t\tif ( other !== inst ) {\n\t\t\t\t\tother.closePanels();\n\t\t\t\t}\n\t\t\t} );\n\t\t};\n\n\t\tdocument.addEventListener( 'keydown', function ( e ) {\n\t\t\tvar path = pathOf( e );\n\t\t\tvar inst = instanceIn( path );\n\t\t\tif ( ! inst && isPageLevel( path[ 0 ] ) ) {\n\t\t\t\tif ( ( e.key === 'Escape' || e.key === 'Esc' ) && ! G.lastActive ) {\n\t\t\t\t\t\/\/ Esc con el foco en la p\u00e1gina: cierra los paneles abiertos de los bloques.\n\t\t\t\t\teach( function ( i ) {\n\t\t\t\t\t\ti.onKey( e, path[ 0 ] );\n\t\t\t\t\t} );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tinst = G.lastActive;\n\t\t\t}\n\t\t\tif ( inst ) {\n\t\t\t\tinst.onKey( e, path[ 0 ] );\n\t\t\t}\n\t\t} );\n\n\t\tdocument.addEventListener( 'click', function ( e ) {\n\t\t\tvar path = pathOf( e );\n\t\t\teach( function ( inst ) {\n\t\t\t\tinst.onDocClick( path );\n\t\t\t} );\n\t\t} );\n\n\t\tfunction track( e ) {\n\t\t\tG.lastActive = instanceIn( pathOf( e ) );\n\t\t}\n\t\tdocument.addEventListener( 'pointerdown', track, true );\n\t\tdocument.addEventListener( 'focusin', track, true );\n\n\t\tdocument.addEventListener( 'visibilitychange', function () {\n\t\t\teach( function ( inst ) {\n\t\t\t\tinst.onVisibility();\n\t\t\t} );\n\t\t} );\n\n\t\tfunction onMq( mq, fn ) {\n\t\t\tif ( ! mq ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( mq.addEventListener ) {\n\t\t\t\tmq.addEventListener( 'change', fn );\n\t\t\t} else if ( mq.addListener ) {\n\t\t\t\tmq.addListener( fn );\n\t\t\t}\n\t\t}\n\t\tonMq( G.reduceMq, function () {\n\t\t\teach( function ( inst ) {\n\t\t\t\tinst.onMotion();\n\t\t\t} );\n\t\t} );\n\t\tonMq( G.lightMq, function () {\n\t\t\teach( function ( inst ) {\n\t\t\t\tinst.onScheme();\n\t\t\t} );\n\t\t} );\n\n\t\tif ( ! window.ResizeObserver ) {\n\t\t\twindow.addEventListener( 'resize', function () {\n\t\t\t\teach( function ( inst ) {\n\t\t\t\t\tinst.onResize();\n\t\t\t\t} );\n\t\t\t} );\n\t\t}\n\n\t\treturn G;\n\t}\n\n\t\/* =============================================================\n\t * Montaje de un bloque\n\t * =========================================================== *\/\n\n\t\/* =============================================================\n\t * P\u00e1gina del reproductor a pantalla completa (solo donde est\u00e1 el bloque)\n\t * =========================================================== *\/\n\n\t\/\/ Cabecera, pie, t\u00edtulo de p\u00e1gina, comentarios y barras de widgets habituales de los temas de WordPress\n\t\/\/ (de bloques y cl\u00e1sicos). Solo se ocultan en la p\u00e1gina que contiene este bloque.\n\tvar THEME_CHROME = [\n\t\t'header', 'footer', '[role=\"banner\"]', '[role=\"contentinfo\"]',\n\t\t'#masthead', '#colophon', '.site-header', '.site-footer', '#site-header', '#site-footer',\n\t\t'.elementor-location-header', '.elementor-location-footer',\n\t\t'.wp-block-post-title', '.entry-header', '.page-header', '.entry-title', '.page-title',\n\t\t'.wp-block-comments', '#comments', '.comments-area', '.wp-block-post-comments-form',\n\t\t'.widget-area', '#secondary', '.sidebar'\n\t].join( ',' );\n\n\tvar FULL_PAGE_CSS =\n\t\t'html body [data-rcx-oculto][data-rcx-oculto]{display:none!important}' +\n\t\t\/\/ Contenedores del bloque: sin m\u00e1rgenes, rellenos ni ancho m\u00e1ximo del tema.\n\t\t'html [data-rcx-cadena][data-rcx-cadena]{margin:0!important;padding:0!important;max-width:none!important;width:auto!important;border:0!important;min-height:0!important}' +\n\t\t'html body[data-rcx-cadena][data-rcx-cadena]{min-height:100vh!important}' +\n\t\t'html body [data-rcx-root][data-rcx-pagina]{margin:0!important;max-width:none!important;width:auto!important}';\n\n\t\/\/ Solo en la vista individual de la p\u00e1gina o entrada que contiene el bloque. En la portada del\n\t\/\/ blog, archivos, b\u00fasquedas o bucles de entradas el tema puede mostrar el contenido completo de\n\t\/\/ varias entradas: ah\u00ed no se toca la web (clases que WordPress pone en body y bloque \u00abConsulta\u00bb).\n\tfunction isListing( host ) {\n\t\tvar c = document.body.classList;\n\t\treturn c.contains( 'blog' ) || c.contains( 'archive' ) || c.contains( 'search' ) ||\n\t\t\t!! host.closest( '.wp-block-query, .wp-block-post-template' );\n\t}\n\n\tfunction fullPage( host ) {\n\t\tif ( ! document.body || ! host.isConnected || isListing( host ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! document.getElementById( 'rcx-pagina-completa' ) ) {\n\t\t\tvar style = document.createElement( 'style' );\n\t\t\tstyle.id = 'rcx-pagina-completa';\n\t\t\tstyle.textContent = FULL_PAGE_CSS;\n\t\t\t( document.head || document.body ).appendChild( style );\n\t\t}\n\t\thost.setAttribute( 'data-rcx-pagina', '' );\n\t\tvar chain = [];\n\t\tfor ( var el = host.parentElement; el; el = el.parentElement ) {\n\t\t\tif ( el === document.documentElement ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tel.setAttribute( 'data-rcx-cadena', '' );\n\t\t\tchain.push( el );\n\t\t}\n\t\t\/\/ Lo que acompa\u00f1a al bloque en su mismo contenedor es contenido del propietario y se mantiene.\n\t\t\/\/ Lo que rodea a ese contenedor (t\u00edtulo, fecha, autor, navegaci\u00f3n, comentarios, entradas\n\t\t\/\/ relacionadas\u2026) es del tema y se oculta. Los hijos directos de body solo se ocultan si son\n\t\t\/\/ cabecera o pie (lista de abajo): ah\u00ed suelen estar avisos de cookies o la barra de administraci\u00f3n.\n\t\tfor ( var i = 1; chain.length > i && chain[ i ] !== document.body; i++ ) {\n\t\t\tArray.prototype.forEach.call( chain[ i ].children, hideThemeElement );\n\t\t}\n\t\tArray.prototype.forEach.call( document.querySelectorAll( THEME_CHROME ), hideThemeElement );\n\t}\n\n\tfunction hideThemeElement( el ) {\n\t\t\/\/ Nunca se oculta un reproductor (ni lo que lo contiene), la barra de administraci\u00f3n de\n\t\t\/\/ WordPress, elementos fijos o pegajosos (avisos de cookies, botones de chat) ni c\u00f3digo.\n\t\tif ( el.hasAttribute( 'data-rcx-cadena' ) || el.closest( '[data-rcx-root], #wpadminbar' ) || el.querySelector( '[data-rcx-root]' ) ||\n\t\t\t\/^(SCRIPT|STYLE|LINK|META|TEMPLATE|NOSCRIPT)$\/.test( el.tagName ) ) {\n\t\t\treturn;\n\t\t}\n\t\tvar pos = window.getComputedStyle( el ).position;\n\t\tif ( pos === 'fixed' || pos === 'sticky' ) {\n\t\t\tif ( ! el.matches( 'header, footer, #masthead, #colophon, .site-header, .site-footer' ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tel.setAttribute( 'data-rcx-oculto', '' );\n\t}\n\n\tfunction mount( host, G ) {\n\t\tvar uiTpl = host.querySelector( 'template[data-rcx-ui]' );\n\t\tvar dataTpl = host.querySelector( 'template[data-rcx-datos]' );\n\t\tif ( ! uiTpl || ! uiTpl.content || ! host.attachShadow ) {\n\t\t\tthrow new Error( 'falta la plantilla de la interfaz o el navegador no admite Shadow DOM' );\n\t\t}\n\n\t\tvar cfg = readConfig( dataTpl );\n\t\tif ( cfg.fullPage ) {\n\t\t\tfullPage( host );\n\t\t\t\/\/ El bloque se inicia antes de que el navegador lea el pie del tema: se repite al terminar.\n\t\t\tif ( document.readyState === 'loading' ) {\n\t\t\t\tdocument.addEventListener( 'DOMContentLoaded', function () {\n\t\t\t\t\tfullPage( host );\n\t\t\t\t} );\n\t\t\t}\n\t\t\twindow.addEventListener( 'load', function () {\n\t\t\t\tfullPage( host );\n\t\t\t} );\n\t\t}\n\n\t\t\/\/ Identificador estable del bloque: separa sus datos locales de los de otros bloques.\n\t\tvar id = String( cfg.blockId || '' ).toLowerCase();\n\t\tif ( ! \/^[a-z0-9_-]{1,40}$\/.test( id ) ) {\n\t\t\tif ( cfg.blockId ) {\n\t\t\t\tcfg.problems.push( 'data-bloque solo admite letras sin acentos, n\u00fameros, - y _ (m\u00e1ximo 40).' );\n\t\t\t}\n\t\t\tid = 'radio';\n\t\t}\n\t\tvar used = G.instances.map( function ( i ) {\n\t\t\treturn i.id;\n\t\t} );\n\t\tif ( used.indexOf( id ) >= 0 ) {\n\t\t\tvar n = 2;\n\t\t\twhile ( used.indexOf( id + '-' + n ) >= 0 ) {\n\t\t\t\tn++;\n\t\t\t}\n\t\t\tlog( 'warn', 'hay dos bloques con data-bloque=\"' + id + '\". Pon un identificador distinto en cada bloque; este usar\u00e1 \u00ab' + id + '-' + n + '\u00bb mientras tanto.' );\n\t\t\tid = id + '-' + n;\n\t\t}\n\n\t\tvar shadow = host.attachShadow( { mode: 'open' } );\n\t\tshadow.appendChild( uiTpl.content.cloneNode( true ) );\n\n\t\tvar inst = { id: id, host: host, shadow: shadow };\n\t\tvar ctx = {\n\t\t\tG: G,\n\t\t\tinst: inst,\n\t\t\thost: host,\n\t\t\tshadow: shadow,\n\t\t\tpage: shadow.querySelector( '.rcx-page' ),\n\t\t\tcfg: cfg,\n\t\t\tstore: createStore( id )\n\t\t};\n\n\t\tcfg.problems.forEach( function ( p ) {\n\t\t\tlog( 'warn', '[' + id + '] ' + p );\n\t\t} );\n\n\t\trenderOwnerData( ctx );\n\t\tctx.mode = createMode( ctx );\n\t\tctx.P = createPlayer( ctx );\n\t\tctx.panels = createPanels( ctx );\n\t\tctx.nav = createNav( ctx );\n\n\t\tinst.yieldPlayback = function () {\n\t\t\tvar P = ctx.P;\n\t\t\tif ( P.isActive() || P.state() === 'connecting' ) {\n\t\t\t\tP.stop();\n\t\t\t}\n\t\t};\n\t\tinst.onKey = function ( e, target ) {\n\t\t\tctx.nav.onKey( e );\n\t\t\tctx.panels.onKey( e, target );\n\t\t};\n\t\tinst.closePanels = ctx.panels.closeAll;\n\t\tinst.onDocClick = ctx.nav.onDocClick;\n\t\tinst.onVisibility = ctx.P.onVisibility;\n\t\tinst.onMotion = ctx.P.onMotion;\n\t\tinst.onResize = ctx.P.onResize;\n\t\tinst.onScheme = ctx.mode.onScheme;\n\t\tinst.api = ctx.P;\n\n\t\tG.instances.push( inst );\n\t\thost.dispatchEvent( new CustomEvent( 'radiocircular:ready', { bubbles: true, detail: { id: id } } ) );\n\t}\n\n\tfunction renderOwnerData( ctx ) {\n\t\tvar cfg = ctx.cfg;\n\t\tvar s = ctx.shadow;\n\t\tvar name = cfg.name || 'Radio';\n\t\ts.querySelector( '[data-rcx-name]' ).textContent = name;\n\t\tif ( cfg.logo ) {\n\t\t\tvar img = s.querySelector( '[data-rcx-logo]' );\n\t\t\timg.src = cfg.logo;\n\t\t\timg.alt = name;\n\t\t\ts.querySelector( '[data-rcx-logo-wrap]' ).hidden = false;\n\t\t}\n\t\tvar tag = s.querySelector( '[data-rcx-tagline]' );\n\t\ttag.textContent = cfg.tagline;\n\t\ttag.hidden = ! cfg.tagline;\n\n\t\tvar notes = [];\n\t\tif ( cfg.mainUrlMissing ) {\n\t\t\tnotes.push( T.ownerNoUrl );\n\t\t\tlog( 'warn', '[' + ctx.inst.id + '] ' + T.ownerNoUrl );\n\t\t}\n\t\tif ( cfg.problems.length ) {\n\t\t\tnotes.push( fmtStr( T.ownerErrors, cfg.problems.length ) );\n\t\t}\n\t\tvar note = s.querySelector( '[data-rcx-owner-note]' );\n\t\tnote.textContent = notes.join( ' ' );\n\t\tnote.hidden = ! notes.length;\n\n\t\tif ( cfg.colors.pageImage ) {\n\t\t\tctx.page.style.setProperty( '--rcx-page-image', 'url(\"' + cfg.colors.pageImage + '\")' );\n\t\t\tctx.page.setAttribute( 'data-bg-image', '' );\n\t\t}\n\t}\n\n\t\/* =============================================================\n\t * Modo de color del visitante (antes theme-mode.js + CSS de PHP)\n\t * =========================================================== *\/\n\n\tfunction createMode( ctx ) {\n\t\tvar page = ctx.page;\n\t\tvar face = ctx.shadow.querySelector( '.rc-face' );\n\t\tvar buttons = ctx.shadow.querySelectorAll( '[data-rc-mode-btn]' );\n\t\tvar vars = { dark: modeVars( ctx.cfg.colors, 'dark' ), light: modeVars( ctx.cfg.colors, 'light' ) };\n\t\tvar mode = ctx.store.getMode();\n\t\tvar listeners = [];\n\n\t\tfunction resolved() {\n\t\t\tif ( mode === 'system' ) {\n\t\t\t\treturn ctx.G.lightMq && ctx.G.lightMq.matches ? 'light' : 'dark';\n\t\t\t}\n\t\t\treturn mode;\n\t\t}\n\n\t\tfunction apply() {\n\t\t\tvar v = vars[ resolved() ];\n\t\t\tpage.setAttribute( 'data-rc-mode', mode );\n\t\t\tObject.keys( v.root ).forEach( function ( k ) {\n\t\t\t\tif ( v.root[ k ] ) {\n\t\t\t\t\tpage.style.setProperty( k, v.root[ k ] );\n\t\t\t\t} else {\n\t\t\t\t\tpage.style.removeProperty( k );\n\t\t\t\t}\n\t\t\t} );\n\t\t\tObject.keys( v.face ).forEach( function ( k ) {\n\t\t\t\tif ( k === 'background' ) {\n\t\t\t\t\tface.style.background = v.face.background;\n\t\t\t\t} else {\n\t\t\t\t\tface.style.setProperty( k, v.face[ k ] );\n\t\t\t\t}\n\t\t\t} );\n\t\t\tArray.prototype.forEach.call( buttons, function ( b ) {\n\t\t\t\tb.setAttribute( 'aria-pressed', b.getAttribute( 'data-rc-mode-btn' ) === mode ? 'true' : 'false' );\n\t\t\t} );\n\t\t\tlisteners.forEach( function ( fn ) {\n\t\t\t\tfn( mode );\n\t\t\t} );\n\t\t}\n\n\t\tfunction set( m ) {\n\t\t\tif ( [ 'system', 'light', 'dark' ].indexOf( m ) === -1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tmode = m;\n\t\t\tctx.store.saveMode( m );\n\t\t\tapply();\n\t\t}\n\n\t\tArray.prototype.forEach.call( buttons, function ( b ) {\n\t\t\tb.addEventListener( 'click', function () {\n\t\t\t\tset( b.getAttribute( 'data-rc-mode-btn' ) );\n\t\t\t} );\n\t\t} );\n\n\t\tapply();\n\n\t\treturn {\n\t\t\tget: function () {\n\t\t\t\treturn mode;\n\t\t\t},\n\t\t\tset: set,\n\t\t\tonChange: function ( fn ) {\n\t\t\t\tlisteners.push( fn );\n\t\t\t},\n\t\t\tonScheme: function () {\n\t\t\t\tif ( mode === 'system' ) {\n\t\t\t\t\tapply();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t\/* =============================================================\n\t * Men\u00fa desplegable accesible (antes navigation.js)\n\t * Abrir o cerrar el men\u00fa no afecta al audio.\n\t * =========================================================== *\/\n\n\tfunction createNav( ctx ) {\n\t\tvar s = ctx.shadow;\n\t\tvar button = s.querySelector( '.menu-toggle' );\n\t\tvar panel = s.querySelector( '#site-menu' );\n\t\tvar list = s.querySelector( '[data-rcx-menu]' );\n\t\tvar cfg = ctx.cfg;\n\n\t\t\/\/ Enlaces del propietario. El enlace \u00abBlog\u00bb se a\u00f1ade al principio si no est\u00e1 ya.\n\t\tvar links = cfg.menu.slice();\n\t\tif ( cfg.blogUrl && ! links.some( function ( l ) {\n\t\t\treturn l.url.replace( \/\\\/$\/, '' ) === cfg.blogUrl.replace( \/\\\/$\/, '' );\n\t\t} ) ) {\n\t\t\tlinks.unshift( { text: cfg.blogText, url: cfg.blogUrl, newTab: cfg.blogNewTab, blog: true } );\n\t\t}\n\t\tvar here = location.href.split( '#' )[ 0 ];\n\t\tlinks.forEach( function ( l ) {\n\t\t\tvar li = document.createElement( 'li' );\n\t\t\tli.className = 'menu-item' + ( l.blog ? ' menu-item-blog' : '' );\n\t\t\tvar a = document.createElement( 'a' );\n\t\t\ta.href = l.url;\n\t\t\ta.textContent = l.text;\n\t\t\tif ( l.newTab ) {\n\t\t\t\ta.target = '_blank';\n\t\t\t\ta.rel = 'noopener';\n\t\t\t}\n\t\t\tif ( l.url.split( '#' )[ 0 ] === here && ! \/#.\/.test( l.url ) ) {\n\t\t\t\tli.className += ' current-menu-item';\n\t\t\t\ta.setAttribute( 'aria-current', 'page' );\n\t\t\t}\n\t\t\tli.appendChild( a );\n\t\t\tlist.appendChild( li );\n\t\t} );\n\t\ts.querySelector( '[data-rcx-menu-empty]' ).hidden = links.length > 0;\n\n\t\tpanel.hidden = true;\n\n\t\tfunction isOpen() {\n\t\t\treturn button.getAttribute( 'aria-expanded' ) === 'true';\n\t\t}\n\n\t\tfunction open() {\n\t\t\tpanel.hidden = false;\n\t\t\tbutton.setAttribute( 'aria-expanded', 'true' );\n\t\t}\n\n\t\tfunction close( returnFocus ) {\n\t\t\tif ( ! isOpen() ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpanel.hidden = true;\n\t\t\tbutton.setAttribute( 'aria-expanded', 'false' );\n\t\t\tif ( returnFocus ) {\n\t\t\t\tbutton.focus();\n\t\t\t}\n\t\t}\n\n\t\tbutton.addEventListener( 'click', function () {\n\t\t\tif ( isOpen() ) {\n\t\t\t\tclose( false );\n\t\t\t} else {\n\t\t\t\topen();\n\t\t\t}\n\t\t} );\n\n\t\tpanel.addEventListener( 'focusout', function ( event ) {\n\t\t\tvar next = event.relatedTarget;\n\t\t\tif ( next && ! panel.contains( next ) && next !== button ) {\n\t\t\t\tclose( false );\n\t\t\t}\n\t\t} );\n\n\t\treturn {\n\t\t\tonKey: function ( event ) {\n\t\t\t\tif ( ( event.key === 'Escape' || event.key === 'Esc' ) && isOpen() ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tclose( true );\n\t\t\t\t}\n\t\t\t},\n\t\t\tonDocClick: function ( path ) {\n\t\t\t\tif ( isOpen() && path.indexOf( panel ) === -1 && path.indexOf( button ) === -1 ) {\n\t\t\t\t\tclose( false );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t\/* =============================================================\n\t * Reproductor: motor + controles del disco (antes radio-player.js)\n\t * =========================================================== *\/\n\n\tfunction createPlayer( ctx ) {\n\t\tvar root = ctx.shadow.querySelector( '[data-rc-player]' );\n\t\tvar Store = ctx.store;\n\t\tvar cfg = ctx.cfg;\n\t\tvar t = T;\n\n\t\tvar els = {\n\t\t\tdisc: root.querySelector( '.rc-disc' ),\n\t\t\tcanvas: root.querySelector( '.rc-ring' ),\n\t\t\tstatus: root.querySelector( '[data-rc-status]' ),\n\t\t\ttrack: root.querySelector( '[data-rc-track]' ),\n\t\t\tplay: root.querySelector( '[data-rc-play]' ),\n\t\t\tplayLabel: root.querySelector( '[data-rc-play-label]' ),\n\t\t\tprev: root.querySelector( '[data-rc-prev]' ),\n\t\t\tnext: root.querySelector( '[data-rc-next]' ),\n\t\t\tmute: root.querySelector( '[data-rc-mute]' ),\n\t\t\tvolume: root.querySelector( '[data-rc-volume]' ),\n\t\t\ttime: root.querySelector( '[data-rc-time]' ),\n\t\t\ttimeText: root.querySelector( '[data-rc-time-text]' ),\n\t\t\tprogress: root.querySelector( '[data-rc-progress]' ),\n\t\t\tprogressBar: root.querySelector( '.rc-progress' ),\n\t\t\tmessage: root.querySelector( '[data-rc-message]' ),\n\t\t\tmessageText: root.querySelector( '[data-rc-message-text]' ),\n\t\t\tretry: root.querySelector( '[data-rc-retry]' )\n\t\t};\n\n\t\tvar prefs = Store.getPrefs();\n\n\t\tvar state = '';\n\t\tvar audio = null;\n\t\tvar gen = 0; \/\/ Se incrementa al crear\/destruir audio: invalida eventos de elementos viejos.\n\t\tvar failedGen = -1;\n\t\tvar wantPlay = false;\n\t\tvar playedThisAttempt = false;\n\t\tvar connectTimer = 0;\n\t\tvar startToken = 0;\n\t\tvar currentUrl = '';\n\t\tvar currentBlobUrl = '';\n\t\tvar corsFailed = {}; \/\/ URL -> true si su servidor no permiti\u00f3 CORS en esta sesi\u00f3n.\n\n\t\tvar items = [];\n\t\tvar baseItems = [];\n\t\tvar localItems = Store.getLocalItems();\n\t\t\/\/ Orden de la lista en memoria (se guarda si el navegador lo permite): as\u00ed subir\/bajar\n\t\t\/\/ funciona durante la sesi\u00f3n aunque localStorage no est\u00e9 disponible.\n\t\tvar order = Store.getOrder();\n\t\tvar sessionFiles = {}; \/\/ id -> File (archivos no guardados en IndexedDB).\n\t\tvar currentId = '';\n\t\tvar history = [];\n\n\t\tvar actx = null;\n\t\tvar srcNode = null;\n\t\tvar eqNodes = null;\n\t\tvar analyser = null;\n\t\tvar gainNode = null;\n\t\tvar freq = null;\n\t\tvar rafId = 0;\n\n\t\tvar listeners = {};\n\t\tvar reduceMq = ctx.G.reduceMq;\n\n\t\t\/* Utilidades *\/\n\n\t\tfunction on( ev, fn ) {\n\t\t\t( listeners[ ev ] = listeners[ ev ] || [] ).push( fn );\n\t\t}\n\n\t\tfunction emit( ev, data ) {\n\t\t\t( listeners[ ev ] || [] ).forEach( function ( fn ) {\n\t\t\t\ttry {\n\t\t\t\t\tfn( data );\n\t\t\t\t} catch ( e ) {\n\t\t\t\t\tif ( window.console ) {\n\t\t\t\t\t\twindow.console.error( e );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\tfunction reducedMotion() {\n\t\t\treturn !! ( reduceMq && reduceMq.matches );\n\t\t}\n\n\t\tfunction fmt( seconds ) {\n\t\t\tif ( ! isFinite( seconds ) || 0 > seconds ) {\n\t\t\t\tseconds = 0;\n\t\t\t}\n\t\t\tvar h = Math.floor( seconds \/ 3600 );\n\t\t\tvar m = Math.floor( ( seconds % 3600 ) \/ 60 );\n\t\t\tvar s = Math.floor( seconds % 60 );\n\t\t\tvar mm = ( 10 > m ? '0' : '' ) + m + ':' + ( 10 > s ? '0' : '' ) + s;\n\t\t\treturn h ? h + ':' + mm : mm;\n\t\t}\n\n\t\tfunction savePrefs() {\n\t\t\tStore.savePrefs( prefs );\n\t\t}\n\n\t\tfunction current() {\n\t\t\tfor ( var i = 0; items.length > i; i++ ) {\n\t\t\t\tif ( items[ i ].id === currentId ) {\n\t\t\t\t\treturn items[ i ];\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\tfunction indexOfId( id ) {\n\t\t\tfor ( var i = 0; items.length > i; i++ ) {\n\t\t\t\tif ( items[ i ].id === id ) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn -1;\n\t\t}\n\n\t\tfunction kindNow() {\n\t\t\tvar it = current();\n\t\t\treturn it ? it.kind : 'live';\n\t\t}\n\n\t\tfunction isSameOrigin( u ) {\n\t\t\tif ( \/^blob:\/i.test( u ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tvar o = new URL( u, window.location.href ).origin;\n\t\t\t\treturn o !== 'null' && o === window.location.origin;\n\t\t\t} catch ( e ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tfunction isMixedContent( u ) {\n\t\t\treturn window.location.protocol === 'https:' && \/^http:\/i.test( u || '' );\n\t\t}\n\n\t\t\/* Lista: emisi\u00f3n principal + lista del propietario + elementos del visitante *\/\n\n\t\t( cfg.items || [] ).forEach( function ( it ) {\n\t\t\tbaseItems.push( {\n\t\t\t\tid: String( it.id ),\n\t\t\t\tsource: it.source === 'main' ? 'main' : 'admin',\n\t\t\t\ttitle: cleanText( it.title ),\n\t\t\t\tartist: cleanText( it.artist ),\n\t\t\t\turl: String( it.url ),\n\t\t\t\tkind: it.kind === 'file' ? 'file' : 'live'\n\t\t\t} );\n\t\t} );\n\n\t\tfunction rebuildItems() {\n\t\t\tvar merged = baseItems.concat( localItems );\n\t\t\tvar pos = {};\n\t\t\torder.forEach( function ( id, i ) {\n\t\t\t\tpos[ id ] = i;\n\t\t\t} );\n\t\t\tmerged.forEach( function ( it, i ) {\n\t\t\t\tit._n = i;\n\t\t\t} );\n\t\t\tmerged.sort( function ( a, b ) {\n\t\t\t\tvar pa = a.id in pos ? pos[ a.id ] : 1e6 + a._n;\n\t\t\t\tvar pb = b.id in pos ? pos[ b.id ] : 1e6 + b._n;\n\t\t\t\treturn pa - pb;\n\t\t\t} );\n\t\t\titems = merged;\n\t\t\tif ( ! current() ) {\n\t\t\t\tcurrentId = items.length ? items[ 0 ].id : '';\n\t\t\t}\n\t\t\tupdateNav();\n\t\t\trenderTrack();\n\t\t\temit( 'items', items );\n\t\t}\n\n\t\tfunction persistLocal() {\n\t\t\tStore.saveLocalItems( localItems );\n\t\t}\n\n\t\tfunction addLocalItems( list ) {\n\t\t\tlist.forEach( function ( it ) {\n\t\t\t\tlocalItems.push( it );\n\t\t\t} );\n\t\t\tpersistLocal();\n\t\t\trebuildItems();\n\t\t}\n\n\t\tfunction removeLocalItem( id ) {\n\t\t\tvar it = null;\n\t\t\tlocalItems = localItems.filter( function ( x ) {\n\t\t\t\tif ( x.id === id ) {\n\t\t\t\t\tit = x;\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t} );\n\t\t\tif ( ! it ) {\n\t\t\t\treturn Promise.resolve();\n\t\t\t}\n\t\t\tvar wasCurrent = id === currentId;\n\t\t\tif ( wasCurrent ) {\n\t\t\t\twantPlay = false;\n\t\t\t\tdestroyAudio();\n\t\t\t\tcurrentId = '';\n\t\t\t}\n\t\t\tdelete sessionFiles[ id ];\n\t\t\tpersistLocal();\n\t\t\torder = order.filter( function ( x ) {\n\t\t\t\treturn x !== id;\n\t\t\t} );\n\t\t\tStore.saveOrder( order );\n\t\t\trebuildItems();\n\t\t\tif ( wasCurrent ) {\n\t\t\t\tsetState( baseState() );\n\t\t\t\temit( 'item', current() );\n\t\t\t}\n\t\t\treturn it.source === 'file' && it.persisted ? Store.deleteFile( id ).catch( function () {} ) : Promise.resolve();\n\t\t}\n\n\t\tfunction clearLocal() {\n\t\t\tvar ids = localItems.map( function ( x ) {\n\t\t\t\treturn x.id;\n\t\t\t} );\n\t\t\t\/\/ Solo se detiene y se reinicia el estado si lo que estaba seleccionado era local;\n\t\t\t\/\/ un elemento del propietario que est\u00e1 sonando sigue sonando con su estado real.\n\t\t\tvar wasCurrent = ids.indexOf( currentId ) >= 0;\n\t\t\tif ( wasCurrent ) {\n\t\t\t\twantPlay = false;\n\t\t\t\tdestroyAudio();\n\t\t\t\tcurrentId = '';\n\t\t\t}\n\t\t\tlocalItems = [];\n\t\t\tsessionFiles = {};\n\t\t\torder = [];\n\t\t\tStore.clearLocalData();\n\t\t\trebuildItems();\n\t\t\tif ( wasCurrent ) {\n\t\t\t\tsetState( baseState() );\n\t\t\t\temit( 'item', current() );\n\t\t\t}\n\t\t\treturn Store.clearFiles().catch( function () {} );\n\t\t}\n\n\t\tfunction moveItem( id, delta ) {\n\t\t\tvar i = indexOfId( id );\n\t\t\tvar j = i + delta;\n\t\t\tif ( 0 > i || 0 > j || j >= items.length ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar ids = items.map( function ( x ) {\n\t\t\t\treturn x.id;\n\t\t\t} );\n\t\t\tvar tmp = ids[ i ];\n\t\t\tids[ i ] = ids[ j ];\n\t\t\tids[ j ] = tmp;\n\t\t\torder = ids;\n\t\t\tStore.saveOrder( order );\n\t\t\trebuildItems();\n\t\t\treturn true;\n\t\t}\n\n\t\tfunction resolveUrl( it ) {\n\t\t\tif ( it.source !== 'file' ) {\n\t\t\t\treturn Promise.resolve( it.url );\n\t\t\t}\n\t\t\tvar blobPromise = sessionFiles[ it.id ] ? Promise.resolve( sessionFiles[ it.id ] ) : Store.getFile( it.id );\n\t\t\t\/\/ La URL blob se registra en connect(), despu\u00e9s de crear el elemento nuevo:\n\t\t\t\/\/ as\u00ed destroyAudio() solo revoca la del elemento anterior.\n\t\t\treturn blobPromise.then( function ( blob ) {\n\t\t\t\treturn URL.createObjectURL( blob );\n\t\t\t} );\n\t\t}\n\n\t\tfunction revokeBlob() {\n\t\t\tif ( currentBlobUrl ) {\n\t\t\t\tURL.revokeObjectURL( currentBlobUrl );\n\t\t\t\tcurrentBlobUrl = '';\n\t\t\t}\n\t\t}\n\n\t\t\/* Estado e interfaz del disco *\/\n\n\t\tvar STATUS_TEXT = {\n\t\t\tnourl: 'noUrl',\n\t\t\tidle: 'idle',\n\t\t\tconnecting: 'connecting',\n\t\t\tlive: 'live',\n\t\t\tbuffering: 'buffering',\n\t\t\tstopped: 'stopped',\n\t\t\tplaying: 'playingFile',\n\t\t\tpaused: 'paused',\n\t\t\tended: 'ended',\n\t\t\tblocked: 'blocked',\n\t\t\terror: 'error'\n\t\t};\n\n\t\tfunction baseState() {\n\t\t\treturn items.length ? 'idle' : 'nourl';\n\t\t}\n\n\t\tfunction setState( next ) {\n\t\t\tstate = next;\n\t\t\troot.setAttribute( 'data-state', next );\n\t\t\troot.setAttribute( 'data-kind', kindNow() );\n\t\t\tif ( els.status ) {\n\t\t\t\tels.status.textContent = t[ STATUS_TEXT[ next ] ] || '';\n\t\t\t}\n\n\t\t\tvar file = kindNow() === 'file';\n\t\t\tvar icon = 'play';\n\t\t\tvar label = t.labelPlay;\n\t\t\tswitch ( next ) {\n\t\t\t\tcase 'connecting':\n\t\t\t\t\ticon = 'stop';\n\t\t\t\t\tlabel = t.labelCancel;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'buffering':\n\t\t\t\t\ticon = file ? 'pause' : 'stop';\n\t\t\t\t\tlabel = file ? t.labelPause : t.labelStop;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'live':\n\t\t\t\t\ticon = 'stop';\n\t\t\t\t\tlabel = t.labelStop;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'playing':\n\t\t\t\t\ticon = 'pause';\n\t\t\t\t\tlabel = t.labelPause;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'paused':\n\t\t\t\t\tlabel = t.labelResume;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( els.play ) {\n\t\t\t\tels.play.setAttribute( 'data-icon', icon );\n\t\t\t\tels.play.disabled = next === 'nourl';\n\t\t\t}\n\t\t\tif ( els.playLabel ) {\n\t\t\t\tels.playLabel.textContent = label || '';\n\t\t\t}\n\n\t\t\tif ( isActive() && analyser && wantsVisualizer() ) {\n\t\t\t\tstartLoop();\n\t\t\t} else {\n\t\t\t\tstopLoop();\n\t\t\t}\n\t\t\temit( 'state', next );\n\t\t}\n\n\t\tfunction isActive() {\n\t\t\treturn state === 'live' || state === 'playing' || state === 'buffering';\n\t\t}\n\n\t\tfunction showMessage( text, type ) {\n\t\t\tif ( ! els.message ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tels.messageText.textContent = text;\n\t\t\tels.message.setAttribute( 'data-kind', type || 'error' );\n\t\t\tels.message.hidden = false;\n\t\t}\n\n\t\tfunction hideMessage() {\n\t\t\tif ( els.message ) {\n\t\t\t\tels.message.hidden = true;\n\t\t\t}\n\t\t}\n\n\t\tfunction renderTrack() {\n\t\t\tvar it = current();\n\t\t\tif ( ! els.track ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! it || it.source === 'main' ) {\n\t\t\t\tels.track.textContent = '';\n\t\t\t\tels.track.hidden = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tels.track.textContent = it.artist ? it.title + ' \u00b7 ' + it.artist : it.title || it.url || '';\n\t\t\tels.track.hidden = false;\n\t\t}\n\n\t\tfunction updateNav() {\n\t\t\tvar single = 2 > items.length;\n\t\t\t[ els.prev, els.next ].forEach( function ( b ) {\n\t\t\t\tif ( b ) {\n\t\t\t\t\tb.setAttribute( 'aria-disabled', single ? 'true' : 'false' );\n\t\t\t\t\tb.title = single ? t.navSingle || '' : '';\n\t\t\t\t}\n\t\t\t} );\n\t\t\tvar note = root.querySelector( '[data-rc-nav-note]' );\n\t\t\tif ( note ) {\n\t\t\t\tnote.textContent = single ? t.navSingle || '' : '';\n\t\t\t}\n\t\t\troot.setAttribute( 'data-single', single ? '1' : '0' );\n\n\t\t\t\/\/ Sin ning\u00fan elemento no hay nada que reproducir; al a\u00f1adir el primero se activa.\n\t\t\tvar none = ! items.length;\n\t\t\t[ els.mute, els.volume, els.play ].forEach( function ( el ) {\n\t\t\t\tif ( el ) {\n\t\t\t\t\tel.disabled = none;\n\t\t\t\t}\n\t\t\t} );\n\t\t\tif ( state === 'nourl' && ! none ) {\n\t\t\t\tsetState( 'idle' );\n\t\t\t} else if ( none && state && ! audio ) {\n\t\t\t\tsetState( 'nourl' );\n\t\t\t}\n\t\t}\n\n\t\tfunction updateVolumeUI() {\n\t\t\tif ( els.volume ) {\n\t\t\t\tvar pct = Math.round( prefs.volume * 100 );\n\t\t\t\tels.volume.value = String( pct );\n\t\t\t\tels.volume.setAttribute( 'aria-valuetext', pct + ' %' );\n\t\t\t}\n\t\t\tif ( els.mute ) {\n\t\t\t\tels.mute.setAttribute( 'aria-pressed', prefs.muted || prefs.volume === 0 ? 'true' : 'false' );\n\t\t\t}\n\t\t\temit( 'volume', { volume: prefs.volume, muted: prefs.muted } );\n\t\t}\n\n\t\tfunction resetTime() {\n\t\t\tif ( els.timeText ) {\n\t\t\t\tels.timeText.textContent = fmt( 0 );\n\t\t\t}\n\t\t\tif ( els.progressBar ) {\n\t\t\t\tels.progressBar.hidden = true;\n\t\t\t}\n\t\t\tif ( els.progress ) {\n\t\t\t\tels.progress.style.width = '0';\n\t\t\t}\n\t\t}\n\n\t\t\/\/ Archivo: tiempo actual \/ duraci\u00f3n y barra. Directo: solo el tiempo de escucha\n\t\t\/\/ transcurrido (real), sin duraci\u00f3n total ni barra.\n\t\tfunction updateProgress() {\n\t\t\tif ( ! audio || ! els.time ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar c = audio.currentTime;\n\t\t\tif ( kindNow() !== 'file' ) {\n\t\t\t\tels.timeText.textContent = fmt( c );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar d = audio.duration;\n\t\t\tels.timeText.textContent = fmt( c ) + ' \/ ' + ( isFinite( d ) && d > 0 ? fmt( d ) : '--:--' );\n\t\t\tif ( els.progressBar ) {\n\t\t\t\tels.progressBar.hidden = false;\n\t\t\t}\n\t\t\tels.progress.style.width = ( d > 0 && isFinite( d ) ? Math.min( 100, ( c \/ d ) * 100 ) : 0 ) + '%';\n\t\t}\n\n\t\t\/* Volumen y velocidad *\/\n\n\t\tfunction applyVolume() {\n\t\t\tif ( ! audio ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tif ( srcNode && gainNode ) {\n\t\t\t\t\taudio.volume = 1;\n\t\t\t\t\taudio.muted = false;\n\t\t\t\t\tgainNode.gain.value = prefs.muted ? 0 : prefs.volume;\n\t\t\t\t} else {\n\t\t\t\t\taudio.volume = prefs.volume;\n\t\t\t\t\taudio.muted = prefs.muted;\n\t\t\t\t}\n\t\t\t} catch ( e ) {}\n\t\t}\n\n\t\tfunction setVolume( v ) {\n\t\t\tprefs.volume = Math.max( 0, Math.min( 1, Number( v ) || 0 ) );\n\t\t\tif ( prefs.volume > 0 ) {\n\t\t\t\tprefs.muted = false;\n\t\t\t}\n\t\t\tapplyVolume();\n\t\t\tupdateVolumeUI();\n\t\t\tsavePrefs();\n\t\t}\n\n\t\tfunction toggleMute() {\n\t\t\tif ( prefs.volume === 0 ) {\n\t\t\t\tprefs.volume = 0.5;\n\t\t\t\tprefs.muted = false;\n\t\t\t} else {\n\t\t\t\tprefs.muted = ! prefs.muted;\n\t\t\t}\n\t\t\tapplyVolume();\n\t\t\tupdateVolumeUI();\n\t\t\tsavePrefs();\n\t\t}\n\n\t\tfunction applySpeed() {\n\t\t\tif ( ! audio ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\taudio.playbackRate = kindNow() === 'file' ? prefs.speed : 1;\n\t\t\t} catch ( e ) {}\n\t\t}\n\n\t\tfunction setSpeed( v ) {\n\t\t\tv = Number( v );\n\t\t\tif ( SPEEDS.indexOf( v ) === -1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprefs.speed = v;\n\t\t\tsavePrefs();\n\t\t\tapplySpeed();\n\t\t\temit( 'speed', v );\n\t\t}\n\n\t\t\/* Ciclo de vida del elemento audio *\/\n\n\t\tvar handlers = {\n\t\t\tplaying: function () {\n\t\t\t\tclearConnectTimer();\n\t\t\t\tplayedThisAttempt = true;\n\t\t\t\thideMessage();\n\t\t\t\tapplySpeed();\n\t\t\t\tif ( wantsProcessing() && canRoute() && ! srcNode ) {\n\t\t\t\t\tconnectGraph();\n\t\t\t\t}\n\t\t\t\tsetState( kindNow() === 'file' ? 'playing' : 'live' );\n\t\t\t\temit( 'eq', eqStatus() );\n\t\t\t},\n\t\t\twaiting: function () {\n\t\t\t\tif ( wantPlay && state !== 'connecting' ) {\n\t\t\t\t\tsetState( 'buffering' );\n\t\t\t\t\tstartConnectTimer();\n\t\t\t\t}\n\t\t\t},\n\t\t\tpause: function () {\n\t\t\t\t\/\/ Pausa externa (teclas multimedia, auriculares, sistema).\n\t\t\t\tif ( ! wantPlay || ! audio || audio.ended ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\twantPlay = false;\n\t\t\t\tclearConnectTimer();\n\t\t\t\tif ( kindNow() === 'file' ) {\n\t\t\t\t\tsetState( 'paused' );\n\t\t\t\t} else {\n\t\t\t\t\tdestroyAudio();\n\t\t\t\t\tsetState( 'stopped' );\n\t\t\t\t}\n\t\t\t},\n\t\t\tended: function () {\n\t\t\t\tclearConnectTimer();\n\t\t\t\tif ( kindNow() === 'file' ) {\n\t\t\t\t\tupdateProgress();\n\t\t\t\t\tonFileEnded();\n\t\t\t\t} else {\n\t\t\t\t\twantPlay = false;\n\t\t\t\t\tfail( 'ended' );\n\t\t\t\t}\n\t\t\t},\n\t\t\terror: function () {\n\t\t\t\tif ( audio && audio.error ) {\n\t\t\t\t\tfail( audio.error.code );\n\t\t\t\t}\n\t\t\t},\n\t\t\tloadedmetadata: function () {\n\t\t\t\tapplySpeed();\n\t\t\t\tupdateProgress();\n\t\t\t},\n\t\t\tdurationchange: updateProgress,\n\t\t\ttimeupdate: updateProgress\n\t\t};\n\n\t\tfunction createAudio( useCors ) {\n\t\t\tdestroyAudio();\n\t\t\tgen++;\n\t\t\tvar myGen = gen;\n\t\t\tvar a = document.createElement( 'audio' );\n\t\t\ta.preload = 'none';\n\t\t\ta.hidden = true;\n\t\t\ta.setAttribute( 'data-rc-audio', '' );\n\t\t\ta.setAttribute( 'data-cors', useCors ? '1' : '0' );\n\t\t\tif ( useCors ) {\n\t\t\t\ta.crossOrigin = 'anonymous';\n\t\t\t}\n\t\t\tObject.keys( handlers ).forEach( function ( name ) {\n\t\t\t\ta.addEventListener( name, function ( event ) {\n\t\t\t\t\tif ( myGen === gen ) {\n\t\t\t\t\t\thandlers[ name ]( event );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t} );\n\t\t\troot.appendChild( a );\n\t\t\taudio = a;\n\t\t\tresetTime();\n\t\t\tapplyVolume();\n\t\t\treturn a;\n\t\t}\n\n\t\tfunction destroyAudio() {\n\t\t\tclearConnectTimer();\n\t\t\tif ( ! audio ) {\n\t\t\t\trevokeBlob();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar a = audio;\n\t\t\taudio = null;\n\t\t\tgen++;\n\t\t\tdisconnectSource();\n\t\t\ttry {\n\t\t\t\ta.pause();\n\t\t\t\ta.removeAttribute( 'src' );\n\t\t\t\ta.load(); \/\/ Cierra la conexi\u00f3n HTTP con el servidor.\n\t\t\t} catch ( e ) {}\n\t\t\tif ( a.parentNode ) {\n\t\t\t\ta.parentNode.removeChild( a );\n\t\t\t}\n\t\t\trevokeBlob();\n\t\t\tcurrentUrl = '';\n\t\t\tresetTime();\n\t\t\temit( 'eq', eqStatus() );\n\t\t}\n\n\t\tfunction startConnectTimer() {\n\t\t\tclearConnectTimer();\n\t\t\tconnectTimer = window.setTimeout( function () {\n\t\t\t\tconnectTimer = 0;\n\t\t\t\tif ( audio && wantPlay ) {\n\t\t\t\t\tfail( 'timeout' );\n\t\t\t\t}\n\t\t\t}, CONNECT_TIMEOUT );\n\t\t}\n\n\t\tfunction clearConnectTimer() {\n\t\t\tif ( connectTimer ) {\n\t\t\t\twindow.clearTimeout( connectTimer );\n\t\t\t\tconnectTimer = 0;\n\t\t\t}\n\t\t}\n\n\t\t\/* Acciones de reproducci\u00f3n *\/\n\n\t\tfunction eqIsFlat() {\n\t\t\treturn prefs.eq.every( function ( g ) {\n\t\t\t\treturn g === 0;\n\t\t\t} );\n\t\t}\n\n\t\tfunction wantsVisualizer() {\n\t\t\treturn !! cfg.visualizer && ! reducedMotion();\n\t\t}\n\n\t\tfunction wantsProcessing() {\n\t\t\treturn wantsVisualizer() || ! eqIsFlat();\n\t\t}\n\n\t\tfunction hasWebAudio() {\n\t\t\treturn !! ( window.AudioContext || window.webkitAudioContext );\n\t\t}\n\n\t\tfunction canRoute() {\n\t\t\treturn !! audio && ( audio.getAttribute( 'data-cors' ) === '1' || isSameOrigin( currentUrl ) );\n\t\t}\n\n\t\t\/\/ Inicia (o reanuda) el elemento actual. gesture = true si viene de una acci\u00f3n del visitante.\n\t\tfunction start( gesture ) {\n\t\t\tvar it = current();\n\t\t\tif ( ! it ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tctx.G.claim( ctx.inst ); \/\/ Un solo Radio Circular sonando en la p\u00e1gina.\n\t\t\thideMessage();\n\t\t\tif ( gesture && hasWebAudio() && ( wantsProcessing() || it.source === 'file' ) ) {\n\t\t\t\tensureContext();\n\t\t\t}\n\n\t\t\t\/\/ Archivo en pausa o terminado: se reutiliza el mismo elemento.\n\t\t\tif ( audio && it.kind === 'file' && ( state === 'paused' || state === 'ended' ) ) {\n\t\t\t\tif ( state === 'ended' ) {\n\t\t\t\t\taudio.currentTime = 0;\n\t\t\t\t}\n\t\t\t\twantPlay = true;\n\t\t\t\trequestPlay();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar token = ++startToken;\n\t\t\twantPlay = true;\n\t\t\tsetState( 'connecting' );\n\t\t\tresolveUrl( it ).then( function ( u ) {\n\t\t\t\tif ( token !== startToken || it.id !== currentId ) {\n\t\t\t\t\tif ( \/^blob:\/i.test( u ) ) {\n\t\t\t\t\t\tURL.revokeObjectURL( u ); \/\/ Ya no se usar\u00e1.\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar useCors = ! isSameOrigin( u ) && wantsProcessing() && hasWebAudio() && ! corsFailed[ u ];\n\t\t\t\tconnect( u, useCors );\n\t\t\t}, function () {\n\t\t\t\tif ( token !== startToken ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\twantPlay = false;\n\t\t\t\tsetState( 'error' );\n\t\t\t\tshowMessage( t.msgLocalMissing, 'error' );\n\t\t\t} );\n\t\t}\n\n\t\tfunction connect( u, useCors ) {\n\t\t\tcreateAudio( useCors );\n\t\t\tif ( \/^blob:\/i.test( u ) ) {\n\t\t\t\tcurrentBlobUrl = u; \/\/ Se revocar\u00e1 al destruir este elemento.\n\t\t\t}\n\t\t\tcurrentUrl = u;\n\t\t\tfailedGen = -1;\n\t\t\tplayedThisAttempt = false;\n\t\t\twantPlay = true;\n\t\t\taudio.src = u;\n\t\t\tapplySpeed();\n\t\t\tsetState( 'connecting' );\n\t\t\tstartConnectTimer();\n\t\t\trequestPlay();\n\t\t}\n\n\t\tfunction requestPlay() {\n\t\t\tvar a = audio;\n\t\t\tvar myGen = gen;\n\t\t\tvar p;\n\t\t\ttry {\n\t\t\t\tp = a.play();\n\t\t\t} catch ( err ) {\n\t\t\t\tonPlayRejected( err, myGen );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( p && typeof p.then === 'function' ) {\n\t\t\t\tp.catch( function ( err ) {\n\t\t\t\t\tonPlayRejected( err, myGen );\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\n\t\tfunction onPlayRejected( err, myGen ) {\n\t\t\tif ( myGen !== gen ) {\n\t\t\t\treturn; \/\/ Elemento ya sustituido o detenido.\n\t\t\t}\n\t\t\tvar name = err && err.name;\n\t\t\tif ( name === 'NotAllowedError' ) {\n\t\t\t\twantPlay = false;\n\t\t\t\tclearConnectTimer();\n\t\t\t\tif ( kindNow() !== 'file' ) {\n\t\t\t\t\tdestroyAudio();\n\t\t\t\t}\n\t\t\t\tsetState( 'blocked' );\n\t\t\t\tshowMessage( t.msgBlocked, 'error' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( name === 'AbortError' ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfail( audio && audio.error ? audio.error.code : 4 );\n\t\t}\n\n\t\tfunction stop() {\n\t\t\twantPlay = false;\n\t\t\tstartToken++;\n\t\t\tclearConnectTimer();\n\t\t\tif ( audio && kindNow() === 'file' ) {\n\t\t\t\taudio.pause();\n\t\t\t\tsetState( 'paused' );\n\t\t\t} else {\n\t\t\t\tdestroyAudio();\n\t\t\t\tsetState( 'stopped' );\n\t\t\t}\n\t\t}\n\n\t\tfunction toggle( gesture ) {\n\t\t\tswitch ( state ) {\n\t\t\t\tcase 'connecting':\n\t\t\t\tcase 'live':\n\t\t\t\tcase 'playing':\n\t\t\t\tcase 'buffering':\n\t\t\t\t\tstop();\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tstart( gesture );\n\t\t\t}\n\t\t}\n\n\t\tfunction fail( reason ) {\n\t\t\tif ( failedGen === gen ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tfailedGen = gen;\n\t\t\tclearConnectTimer();\n\n\t\t\tvar wasCors = !! audio && audio.getAttribute( 'data-cors' ) === '1';\n\t\t\tif ( wasCors && ! playedThisAttempt && reason !== 'timeout' ) {\n\t\t\t\t\/\/ Lo m\u00e1s probable: el servidor no env\u00eda cabeceras CORS. Se reconecta por la v\u00eda\n\t\t\t\t\/\/ normal, sin Web Audio, para que suene igualmente.\n\t\t\t\tcorsFailed[ currentUrl ] = true;\n\t\t\t\tlog( 'info', 'este medio no admite CORS; se reproduce sin ecualizador ni visualizador.' );\n\t\t\t\tconnect( currentUrl, false );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar failedUrl = currentUrl;\n\t\t\twantPlay = false;\n\t\t\tdestroyAudio();\n\t\t\tsetState( 'error' );\n\n\t\t\tvar msg;\n\t\t\tif ( reason === 'timeout' ) {\n\t\t\t\tmsg = t.msgTimeout;\n\t\t\t} else if ( reason === 'ended' ) {\n\t\t\t\tmsg = t.msgEndedLive;\n\t\t\t} else if ( reason === 2 ) {\n\t\t\t\tmsg = t.msgNetwork;\n\t\t\t} else {\n\t\t\t\tmsg = t.msgUnsupported;\n\t\t\t}\n\t\t\tif ( isMixedContent( failedUrl ) ) {\n\t\t\t\tmsg = t.msgMixed + ' ' + msg;\n\t\t\t}\n\t\t\tshowMessage( msg, 'error' );\n\t\t}\n\n\t\t\/* Lista: selecci\u00f3n, anterior\/siguiente, aleatorio, repetir *\/\n\n\t\tfunction select( id, autoplay, gesture ) {\n\t\t\tif ( indexOfId( id ) === -1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( id !== currentId ) {\n\t\t\t\twantPlay = false;\n\t\t\t\tstartToken++;\n\t\t\t\tdestroyAudio();\n\t\t\t\thideMessage(); \/\/ El mensaje de error era del elemento anterior.\n\t\t\t\tif ( currentId ) {\n\t\t\t\t\thistory.push( currentId );\n\t\t\t\t\tif ( history.length > 50 ) {\n\t\t\t\t\t\thistory.shift();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurrentId = id;\n\t\t\t\tprefs.lastId = id;\n\t\t\t\tsavePrefs();\n\t\t\t\trenderTrack();\n\t\t\t\tsetState( baseState() );\n\t\t\t\temit( 'item', current() );\n\t\t\t}\n\t\t\tif ( autoplay ) {\n\t\t\t\tstart( gesture );\n\t\t\t}\n\t\t}\n\n\t\tfunction randomOther() {\n\t\t\tif ( 2 > items.length ) {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t\tvar cur = indexOfId( currentId );\n\t\t\tvar j = Math.floor( Math.random() * ( items.length - 1 ) );\n\t\t\tif ( j >= cur ) {\n\t\t\t\tj++;\n\t\t\t}\n\t\t\treturn items[ j ].id;\n\t\t}\n\n\t\t\/\/ manual = true cuando lo pide el visitante (bot\u00f3n o tecla).\n\t\tfunction pickNext( manual ) {\n\t\t\tvar n = items.length;\n\t\t\tif ( 2 > n ) {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t\tif ( prefs.shuffle ) {\n\t\t\t\treturn randomOther();\n\t\t\t}\n\t\t\tvar i = indexOfId( currentId ) + 1;\n\t\t\tif ( i >= n ) {\n\t\t\t\tif ( ! manual && prefs.repeat !== 'all' ) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\ti = 0;\n\t\t\t}\n\t\t\treturn items[ i ].id;\n\t\t}\n\n\t\tfunction pickPrev() {\n\t\t\tvar n = items.length;\n\t\t\tif ( 2 > n ) {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t\tif ( prefs.shuffle ) {\n\t\t\t\twhile ( history.length ) {\n\t\t\t\t\tvar id = history.pop();\n\t\t\t\t\tif ( indexOfId( id ) >= 0 && id !== currentId ) {\n\t\t\t\t\t\treturn id;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn randomOther();\n\t\t\t}\n\t\t\tvar i = indexOfId( currentId ) - 1;\n\t\t\treturn items[ 0 > i ? n - 1 : i ].id;\n\t\t}\n\n\t\tfunction playingOrConnecting() {\n\t\t\treturn isActive() || state === 'connecting';\n\t\t}\n\n\t\tfunction next( gesture ) {\n\t\t\tvar id = pickNext( true );\n\t\t\tif ( id ) {\n\t\t\t\tselect( id, playingOrConnecting(), gesture ); \/\/ select() guarda el anterior en el historial.\n\t\t\t}\n\t\t}\n\n\t\tfunction prev( gesture ) {\n\t\t\tvar id = pickPrev();\n\t\t\tif ( id ) {\n\t\t\t\tselect( id, playingOrConnecting(), gesture );\n\t\t\t\tif ( prefs.shuffle ) {\n\t\t\t\t\thistory.pop(); \/\/ Retroceder no debe volver a apilar el elemento que se deja.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t\/\/ Al terminar un ARCHIVO: repetir pista, siguiente seg\u00fan orden\/aleatorio, o detener.\n\t\tfunction onFileEnded() {\n\t\t\tif ( prefs.repeat === 'one' ) {\n\t\t\t\taudio.currentTime = 0;\n\t\t\t\twantPlay = true;\n\t\t\t\trequestPlay();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar id = items.length === 1 && prefs.repeat === 'all' ? currentId : pickNext( false );\n\t\t\tif ( id === currentId ) {\n\t\t\t\taudio.currentTime = 0;\n\t\t\t\twantPlay = true;\n\t\t\t\trequestPlay();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( id ) {\n\t\t\t\tselect( id, true, false );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\twantPlay = false;\n\t\t\tsetState( 'ended' );\n\t\t}\n\n\t\tfunction setShuffle( on ) {\n\t\t\tprefs.shuffle = !! on;\n\t\t\tsavePrefs();\n\t\t\temit( 'modes', { shuffle: prefs.shuffle, repeat: prefs.repeat } );\n\t\t}\n\n\t\tfunction setRepeat( mode ) {\n\t\t\tif ( [ 'off', 'all', 'one' ].indexOf( mode ) === -1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprefs.repeat = mode;\n\t\t\tsavePrefs();\n\t\t\temit( 'modes', { shuffle: prefs.shuffle, repeat: prefs.repeat } );\n\t\t}\n\n\t\tfunction cycleRepeat() {\n\t\t\tsetRepeat( { off: 'all', all: 'one', one: 'off' }[ prefs.repeat ] );\n\t\t}\n\n\t\tfunction seek( delta ) {\n\t\t\tif ( ! audio || kindNow() !== 'file' || ! isFinite( audio.duration ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\taudio.currentTime = Math.max( 0, Math.min( audio.duration - 0.25, audio.currentTime + delta ) );\n\t\t\tupdateProgress();\n\t\t\treturn true;\n\t\t}\n\n\t\t\/* Web Audio: un \u00fanico grafo (ecualizador -> analizador -> ganancia) *\/\n\n\t\tfunction ensureContext() {\n\t\t\tvar AC = window.AudioContext || window.webkitAudioContext;\n\t\t\tif ( ! AC ) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif ( ! actx ) {\n\t\t\t\ttry {\n\t\t\t\t\tactx = new AC();\n\t\t\t\t\tactx.onstatechange = function () {\n\t\t\t\t\t\tif ( actx.state === 'suspended' && srcNode && isActive() && actx.resume ) {\n\t\t\t\t\t\t\tactx.resume().catch( function () {} );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t} catch ( e ) {\n\t\t\t\t\tactx = null;\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( actx.state === 'suspended' && actx.resume ) {\n\t\t\t\tactx.resume().catch( function () {} );\n\t\t\t}\n\t\t\treturn actx;\n\t\t}\n\n\t\tfunction ensureGraph() {\n\t\t\tif ( eqNodes || ! actx ) {\n\t\t\t\treturn !! eqNodes;\n\t\t\t}\n\t\t\teqNodes = EQ_FREQS.map( function ( f, i ) {\n\t\t\t\tvar n = actx.createBiquadFilter();\n\t\t\t\tn.type = i === 0 ? 'lowshelf' : i === EQ_FREQS.length - 1 ? 'highshelf' : 'peaking';\n\t\t\t\tn.frequency.value = f;\n\t\t\t\tn.Q.value = 1.1;\n\t\t\t\tn.gain.value = prefs.eq[ i ];\n\t\t\t\treturn n;\n\t\t\t} );\n\t\t\tfor ( var i = 0; eqNodes.length - 1 > i; i++ ) {\n\t\t\t\teqNodes[ i ].connect( eqNodes[ i + 1 ] );\n\t\t\t}\n\t\t\tanalyser = actx.createAnalyser();\n\t\t\tanalyser.fftSize = 1024; \/\/ 512 bandas: suficientes para un aro de trazos finos.\n\t\t\tanalyser.smoothingTimeConstant = 0.8;\n\t\t\tgainNode = actx.createGain();\n\t\t\teqNodes[ eqNodes.length - 1 ].connect( analyser );\n\t\t\tanalyser.connect( gainNode );\n\t\t\tgainNode.connect( actx.destination );\n\t\t\tfreq = new Uint8Array( analyser.frequencyBinCount );\n\t\t\treturn true;\n\t\t}\n\n\t\t\/\/ Conecta el elemento actual al grafo. Solo se llama si canRoute() (CORS o mismo origen).\n\t\tfunction connectGraph() {\n\t\t\tif ( ! audio || srcNode || ! actx || actx.state !== 'running' || ! canRoute() ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tensureGraph();\n\t\t\t\tsrcNode = actx.createMediaElementSource( audio );\n\t\t\t} catch ( e ) {\n\t\t\t\tsrcNode = null;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tsrcNode.connect( eqNodes[ 0 ] );\n\t\t\t} catch ( e ) {\n\t\t\t\ttry {\n\t\t\t\t\tsrcNode.connect( actx.destination ); \/\/ Nunca dejarlo mudo.\n\t\t\t\t} catch ( e2 ) {}\n\t\t\t}\n\t\t\troot.setAttribute( 'data-visualizer', wantsVisualizer() ? 'on' : 'off' );\n\t\t\tapplyVolume();\n\t\t\tif ( isActive() && wantsVisualizer() ) {\n\t\t\t\tstartLoop();\n\t\t\t}\n\t\t\temit( 'eq', eqStatus() );\n\t\t\treturn true;\n\t\t}\n\n\t\tfunction disconnectSource() {\n\t\t\tstopLoop();\n\t\t\tif ( srcNode ) {\n\t\t\t\ttry {\n\t\t\t\t\tsrcNode.disconnect();\n\t\t\t\t} catch ( e ) {}\n\t\t\t}\n\t\t\tsrcNode = null;\n\t\t\troot.setAttribute( 'data-visualizer', 'off' );\n\t\t}\n\n\t\tfunction setEqGain( i, db ) {\n\t\t\tif ( 0 > i || i >= 10 ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprefs.eq[ i ] = Math.max( -12, Math.min( 12, Math.round( Number( db ) || 0 ) ) );\n\t\t\tsavePrefs();\n\t\t\tif ( eqNodes ) {\n\t\t\t\teqNodes[ i ].gain.value = prefs.eq[ i ];\n\t\t\t}\n\t\t\tafterEqChange();\n\t\t}\n\n\t\tfunction setEqPreset( name ) {\n\t\t\tvar p = EQ_PRESETS[ name ];\n\t\t\tif ( ! p ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprefs.eq = p.slice();\n\t\t\tsavePrefs();\n\t\t\tif ( eqNodes ) {\n\t\t\t\teqNodes.forEach( function ( n, i ) {\n\t\t\t\t\tn.gain.value = prefs.eq[ i ];\n\t\t\t\t} );\n\t\t\t}\n\t\t\tafterEqChange();\n\t\t}\n\n\t\t\/\/ Si el medio actual se puede procesar sin reconectar (CORS ya concedido o mismo\n\t\t\/\/ origen), se conecta ahora. Nunca se sustituye el elemento por abrir\/cerrar paneles.\n\t\tfunction afterEqChange() {\n\t\t\tif ( wantsProcessing() && audio && ! srcNode && canRoute() ) {\n\t\t\t\tensureContext();\n\t\t\t\tif ( actx && actx.state !== 'running' && actx.resume ) {\n\t\t\t\t\tactx.resume().then( connectGraph, function () {} );\n\t\t\t\t} else {\n\t\t\t\t\tconnectGraph();\n\t\t\t\t}\n\t\t\t}\n\t\t\temit( 'eq', eqStatus() );\n\t\t}\n\n\t\t\/**\n\t\t * Estado del ecualizador para el medio actual:\n\t\t * 'applied' | 'flat' | 'idle' | 'cors' | 'reconnect' | 'unsupported'\n\t\t *\/\n\t\tfunction eqStatus() {\n\t\t\tif ( ! hasWebAudio() ) {\n\t\t\t\treturn 'unsupported';\n\t\t\t}\n\t\t\tif ( srcNode ) {\n\t\t\t\treturn eqIsFlat() ? 'flat' : 'applied';\n\t\t\t}\n\t\t\tif ( audio && corsFailed[ currentUrl ] ) {\n\t\t\t\treturn 'cors';\n\t\t\t}\n\t\t\tif ( eqIsFlat() ) {\n\t\t\t\treturn 'flat';\n\t\t\t}\n\t\t\tif ( ! audio ) {\n\t\t\t\treturn 'idle';\n\t\t\t}\n\t\t\treturn canRoute() ? 'idle' : 'reconnect';\n\t\t}\n\n\t\t\/* Aro: est\u00e1tico (decorativo) o con datos reales del analizador *\/\n\n\t\tvar c2d = els.canvas && els.canvas.getContext ? els.canvas.getContext( '2d' ) : null;\n\t\tvar dpr = 1;\n\n\t\tfunction resizeCanvas() {\n\t\t\tif ( ! c2d || ! els.canvas ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\/\/ El lienzo es mayor que el disco (ver .rc-ring en los estilos) para que los picos salgan hacia fuera.\n\t\t\tvar size = els.canvas.getBoundingClientRect().width;\n\t\t\tdpr = Math.min( window.devicePixelRatio || 1, 2 );\n\t\t\tvar px = Math.max( 1, Math.round( size * dpr ) );\n\t\t\tif ( els.canvas.width !== px ) {\n\t\t\t\tels.canvas.width = px;\n\t\t\t\tels.canvas.height = px;\n\t\t\t}\n\t\t\tdrawRing( null );\n\t\t}\n\n\t\tfunction accentColor() {\n\t\t\tvar v = window.getComputedStyle( root ).getPropertyValue( '--rc-accent' );\n\t\t\treturn ( v && v.trim() ) || '#ff2d8a';\n\t\t}\n\n\t\t\/\/ Geometr\u00eda del aro. El lienzo mide el 130 % del disco y el interior del disco el 79 %,\n\t\t\/\/ as\u00ed que el borde del interior queda a 0.79 \/ 2 \/ 1.3 del ancho del lienzo.\n\t\tfunction ringGeometry( w ) {\n\t\t\tvar face = w * ( 0.79 \/ 2 \/ 1.3 );\n\t\t\tvar r0 = face + 6 * dpr;\n\t\t\tvar maxLen = Math.max( 1, Math.min( w \/ 2 - 3 * dpr - r0, face * 0.55 ) );\n\t\t\tvar base = Math.max( 4 * dpr, w * 0.013 );\n\t\t\tvar n = Math.round( ( Math.PI * 2 * r0 ) \/ dpr \/ 5 );\n\t\t\tn = Math.max( 96, Math.min( 320, n - ( n % 2 ) ) );\n\t\t\treturn { r0: r0, maxLen: maxLen, base: base, n: n };\n\t\t}\n\n\t\t\/\/ levels: valores 0..1 del analizador (uno por trazo) o null para el aro en reposo.\n\t\tfunction drawRing( levels ) {\n\t\t\tif ( ! c2d ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar w = els.canvas.width;\n\t\t\tif ( ! w ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar g = ringGeometry( w );\n\t\t\tvar center = w \/ 2;\n\t\t\tvar n = g.n;\n\t\t\tvar step = ( Math.PI * 2 ) \/ n;\n\t\t\tvar active = isActive() || state === 'connecting';\n\n\t\t\tc2d.clearRect( 0, 0, w, w );\n\t\t\tc2d.strokeStyle = accentColor();\n\t\t\tc2d.lineWidth = 2 * dpr;\n\t\t\tc2d.lineCap = 'round';\n\t\t\tfor ( var i = 0; n > i; i++ ) {\n\t\t\t\tvar v = levels ? levels[ i ] || 0 : 0; \/\/ Sin datos: base fija, no simula se\u00f1al.\n\t\t\t\tvar a = i * step - Math.PI \/ 2;\n\t\t\t\tvar cos = Math.cos( a );\n\t\t\t\tvar sin = Math.sin( a );\n\t\t\t\t\/\/ Abajo, picos m\u00e1s cortos para no invadir la fila de botones bajo el disco.\n\t\t\t\tvar len = g.base + ( g.maxLen - g.base ) * v * ( 1 - 0.4 * Math.max( 0, sin ) );\n\t\t\t\tc2d.globalAlpha = ( active ? 0.55 : 0.35 ) + 0.45 * v;\n\t\t\t\tc2d.beginPath();\n\t\t\t\tc2d.moveTo( center + cos * g.r0, center + sin * g.r0 );\n\t\t\t\tc2d.lineTo( center + cos * ( g.r0 + len ), center + sin * ( g.r0 + len ) );\n\t\t\t\tc2d.stroke();\n\t\t\t}\n\t\t\tc2d.globalAlpha = 1;\n\t\t}\n\n\t\t\/\/ Reparto de bandas de frecuencia entre los trazos (en espejo: graves arriba, agudos abajo).\n\t\tvar bandMap = null;\n\n\t\tfunction bands( n ) {\n\t\t\tif ( bandMap && bandMap.n === n && bandMap.len === freq.length ) {\n\t\t\t\treturn bandMap;\n\t\t\t}\n\t\t\tvar half = n \/ 2;\n\t\t\tvar usable = Math.floor( freq.length * 0.55 ); \/\/ Hasta unos 12 kHz: por encima apenas hay energ\u00eda.\n\t\t\tvar from = [];\n\t\t\tvar to = [];\n\t\t\tfor ( var j = 0; half > j; j++ ) {\n\t\t\t\tvar t0 = Math.pow( j \/ half, 1.7 );\n\t\t\t\tvar t1 = Math.pow( ( j + 1 ) \/ half, 1.7 );\n\t\t\t\tvar b0 = 1 + Math.floor( ( usable - 1 ) * t0 );\n\t\t\t\tfrom.push( b0 );\n\t\t\t\tto.push( Math.max( b0 + 1, 1 + Math.floor( ( usable - 1 ) * t1 ) ) );\n\t\t\t}\n\t\t\tbandMap = { n: n, len: freq.length, half: half, from: from, to: to };\n\t\t\treturn bandMap;\n\t\t}\n\n\t\tfunction frame() {\n\t\t\trafId = window.requestAnimationFrame( frame );\n\t\t\tif ( ! analyser || ! freq || ! srcNode || ! els.canvas.width ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tanalyser.getByteFrequencyData( freq );\n\t\t\tvar n = ringGeometry( els.canvas.width ).n;\n\t\t\tvar m = bands( n );\n\t\t\tvar levels = new Array( n );\n\t\t\tfor ( var i = 0; n > i; i++ ) {\n\t\t\t\tvar j = m.half > i ? i : n - 1 - i;\n\t\t\t\tvar sum = 0;\n\t\t\t\tfor ( var k = m.from[ j ]; m.to[ j ] > k; k++ ) {\n\t\t\t\t\tsum += freq[ k ];\n\t\t\t\t}\n\t\t\t\tvar v = sum \/ ( m.to[ j ] - m.from[ j ] ) \/ 255;\n\t\t\t\t\/\/ Base discreta y picos marcados; los agudos, m\u00e1s d\u00e9biles, se compensan un poco.\n\t\t\t\tv = Math.max( 0, ( v - 0.4 ) \/ 0.6 );\n\t\t\t\tv = Math.min( 1, Math.pow( v, 1.8 ) * ( 1 + 0.3 * ( j \/ m.half ) ) );\n\t\t\t\tlevels[ i ] = v;\n\t\t\t}\n\t\t\tdrawRing( levels );\n\t\t}\n\n\t\tfunction startLoop() {\n\t\t\tif ( rafId || reducedMotion() || document.hidden || ! analyser || ! srcNode || ! wantsVisualizer() ) {\n\t\t\t\tif ( ! rafId ) {\n\t\t\t\t\tdrawRing( null );\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trafId = window.requestAnimationFrame( frame );\n\t\t}\n\n\t\tfunction stopLoop() {\n\t\t\tif ( rafId ) {\n\t\t\t\twindow.cancelAnimationFrame( rafId );\n\t\t\t\trafId = 0;\n\t\t\t}\n\t\t\tdrawRing( null );\n\t\t}\n\n\t\t\/* Eventos de la interfaz del disco *\/\n\n\t\tif ( els.play ) {\n\t\t\tels.play.addEventListener( 'click', function () {\n\t\t\t\ttoggle( true );\n\t\t\t} );\n\t\t}\n\t\tif ( els.prev ) {\n\t\t\tels.prev.addEventListener( 'click', function () {\n\t\t\t\tif ( els.prev.getAttribute( 'aria-disabled' ) !== 'true' ) {\n\t\t\t\t\tprev( true );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t\tif ( els.next ) {\n\t\t\tels.next.addEventListener( 'click', function () {\n\t\t\t\tif ( els.next.getAttribute( 'aria-disabled' ) !== 'true' ) {\n\t\t\t\t\tnext( true );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t\tif ( els.retry ) {\n\t\t\tels.retry.addEventListener( 'click', function () {\n\t\t\t\tstart( true );\n\t\t\t\tif ( els.play ) {\n\t\t\t\t\tels.play.focus();\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t\tif ( els.mute ) {\n\t\t\tels.mute.addEventListener( 'click', toggleMute );\n\t\t}\n\t\tif ( els.volume ) {\n\t\t\tels.volume.addEventListener( 'input', function () {\n\t\t\t\tsetVolume( Number( els.volume.value ) \/ 100 );\n\t\t\t} );\n\t\t}\n\n\t\tif ( window.ResizeObserver && els.canvas ) {\n\t\t\tnew window.ResizeObserver( resizeCanvas ).observe( els.canvas );\n\t\t}\n\n\t\t\/* Inicio (sin reproducir: nunca hay sonido al cargar la p\u00e1gina) *\/\n\n\t\trebuildItems();\n\t\tif ( prefs.lastId && indexOfId( prefs.lastId ) >= 0 ) {\n\t\t\tcurrentId = prefs.lastId;\n\t\t\trenderTrack();\n\t\t}\n\t\tupdateVolumeUI();\n\t\troot.setAttribute( 'data-visualizer', 'off' );\n\t\tsetState( baseState() );\n\t\tupdateNav();\n\t\tresizeCanvas();\n\n\t\t\/* API para los paneles *\/\n\n\t\tvar api = {\n\t\t\ton: on,\n\t\t\titems: function () {\n\t\t\t\treturn items.slice();\n\t\t\t},\n\t\t\tcurrentId: function () {\n\t\t\t\treturn currentId;\n\t\t\t},\n\t\t\tcurrent: current,\n\t\t\tstate: function () {\n\t\t\t\treturn state;\n\t\t\t},\n\t\t\tselect: select,\n\t\t\ttoggle: toggle,\n\t\t\tstart: start,\n\t\t\tstop: stop,\n\t\t\tnext: next,\n\t\t\tprev: prev,\n\t\t\tseek: seek,\n\t\t\tisActive: isActive,\n\t\t\tprefs: function () {\n\t\t\t\treturn JSON.parse( JSON.stringify( prefs ) );\n\t\t\t},\n\t\t\tsetVolume: setVolume,\n\t\t\ttoggleMute: toggleMute,\n\t\t\tsetShuffle: setShuffle,\n\t\t\tsetRepeat: setRepeat,\n\t\t\tcycleRepeat: cycleRepeat,\n\t\t\tsetSpeed: setSpeed,\n\t\t\tspeeds: SPEEDS.slice(),\n\t\t\teqFreqs: EQ_FREQS.slice(),\n\t\t\tsetEqGain: setEqGain,\n\t\t\tsetEqPreset: setEqPreset,\n\t\t\teqStatus: eqStatus,\n\t\t\taddLocalItems: addLocalItems,\n\t\t\taddSessionFile: function ( id, file ) {\n\t\t\t\tsessionFiles[ id ] = file;\n\t\t\t},\n\t\t\tremoveLocalItem: removeLocalItem,\n\t\t\tclearLocal: clearLocal,\n\t\t\tlocalCount: function () {\n\t\t\t\treturn localItems.length;\n\t\t\t},\n\t\t\tmoveItem: moveItem,\n\t\t\tonVisibility: function () {\n\t\t\t\tif ( document.hidden ) {\n\t\t\t\t\tif ( rafId ) {\n\t\t\t\t\t\twindow.cancelAnimationFrame( rafId );\n\t\t\t\t\t\trafId = 0;\n\t\t\t\t\t}\n\t\t\t\t} else if ( isActive() ) {\n\t\t\t\t\tstartLoop();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonMotion: function () {\n\t\t\t\tif ( reducedMotion() ) {\n\t\t\t\t\tstopLoop();\n\t\t\t\t} else if ( isActive() ) {\n\t\t\t\t\tstartLoop();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonResize: resizeCanvas,\n\t\t\tredraw: function () {\n\t\t\t\tdrawRing( null );\n\t\t\t}\n\t\t};\n\n\t\t\/\/ Al cambiar el modo de color se redibuja el aro con el acento del modo nuevo.\n\t\tctx.mode.onChange( function () {\n\t\t\tif ( ! rafId ) {\n\t\t\t\tdrawRing( null );\n\t\t\t}\n\t\t} );\n\n\t\twindow.setTimeout( function () {\n\t\t\temit( 'item', current() );\n\t\t}, 0 );\n\t\treturn api;\n\t}\n\n\t\/* =============================================================\n\t * Paneles y controles (antes player-panels.js)\n\t * - Panel lateral (lista, a\u00f1adir, ayuda, ajustes): panel NO modal.\n\t * - Ecualizador: panel no modal con la misma gesti\u00f3n de foco.\n\t * - Atajos de teclado, ignorados mientras se escribe en campos o selectores.\n\t * Ning\u00fan panel crea audio: todos usan el mismo reproductor del bloque.\n\t * =========================================================== *\/\n\n\tfunction createPanels( ctx ) {\n\t\tvar P = ctx.P;\n\t\tvar Store = ctx.store;\n\t\tvar shadow = ctx.shadow;\n\t\tvar t = T;\n\n\t\tfunction $( sel, c ) {\n\t\t\treturn ( c || shadow ).querySelector( sel );\n\t\t}\n\t\tfunction $$( sel, c ) {\n\t\t\treturn Array.prototype.slice.call( ( c || shadow ).querySelectorAll( sel ) );\n\t\t}\n\t\tvar fmt = fmtStr;\n\t\tfunction focused() {\n\t\t\treturn shadow.activeElement;\n\t\t}\n\t\tfunction svg( paths, vb ) {\n\t\t\tvar ns = 'http:\/\/www.w3.org\/2000\/svg';\n\t\t\tvar s = document.createElementNS( ns, 'svg' );\n\t\t\ts.setAttribute( 'viewBox', vb || '0 0 24 24' );\n\t\t\ts.setAttribute( 'aria-hidden', 'true' );\n\t\t\ts.setAttribute( 'focusable', 'false' );\n\t\t\tpaths.forEach( function ( d ) {\n\t\t\t\tvar p = document.createElementNS( ns, 'path' );\n\t\t\t\tp.setAttribute( 'd', d );\n\t\t\t\tp.setAttribute( 'fill', 'none' );\n\t\t\t\tp.setAttribute( 'stroke', 'currentColor' );\n\t\t\t\tp.setAttribute( 'stroke-width', '2' );\n\t\t\t\tp.setAttribute( 'stroke-linecap', 'round' );\n\t\t\t\tp.setAttribute( 'stroke-linejoin', 'round' );\n\t\t\t\ts.appendChild( p );\n\t\t\t} );\n\t\t\treturn s;\n\t\t}\n\t\tfunction srText( text ) {\n\t\t\tvar s = document.createElement( 'span' );\n\t\t\ts.className = 'screen-reader-text';\n\t\t\ts.textContent = text;\n\t\t\treturn s;\n\t\t}\n\n\t\tvar ICON = {\n\t\t\tup: [ 'M6 15l6-6 6 6' ],\n\t\t\tdown: [ 'M6 9l6 6 6-6' ],\n\t\t\tdownload: [ 'M12 4v11', 'M7 10l5 5 5-5', 'M5 20h14' ],\n\t\t\tshare: [ 'M8.6 13.5l6.8 4', 'M15.4 6.5l-6.8 4', 'M18 5a2 2 0 1 0 .01 0', 'M6 10a2 2 0 1 0 .01 0', 'M18 17a2 2 0 1 0 .01 0' ],\n\t\t\tremove: [ 'M6 6l12 12', 'M18 6 6 18' ],\n\t\t\tnote: [ 'M9 17V5l10-2v12', 'M9 17a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z', 'M19 15a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z' ],\n\t\t\tlive: [ 'M12 12h.01', 'M8.5 8.5a5 5 0 0 0 0 7', 'M15.5 8.5a5 5 0 0 1 0 7', 'M5.6 5.6a9 9 0 0 0 0 12.8', 'M18.4 5.6a9 9 0 0 1 0 12.8' ]\n\t\t};\n\n\t\t\/* Panel lateral *\/\n\n\t\tvar drawer = $( '[data-rc-drawer]' );\n\t\tvar eqPanel = $( '[data-rc-eq]' );\n\t\tvar eqOpenBtn = $( '[data-rc-eq-open]' );\n\t\tvar openers = $$( '[data-rc-open]' );\n\t\tvar tabs = $$( '[data-rc-tab]', drawer );\n\t\tvar drawerTab = 'list';\n\t\tvar drawerOpener = null;\n\n\t\tfunction drawerOpen() {\n\t\t\treturn ! drawer.hidden;\n\t\t}\n\n\t\tfunction syncOpeners() {\n\t\t\topeners.forEach( function ( b ) {\n\t\t\t\tb.setAttribute( 'aria-expanded', drawerOpen() && b.getAttribute( 'data-rc-open' ) === drawerTab ? 'true' : 'false' );\n\t\t\t} );\n\t\t}\n\n\t\tfunction showTab( name, focusTab ) {\n\t\t\tdrawerTab = name;\n\t\t\ttabs.forEach( function ( b ) {\n\t\t\t\tvar on = b.getAttribute( 'data-rc-tab' ) === name;\n\t\t\t\tb.setAttribute( 'aria-selected', on ? 'true' : 'false' );\n\t\t\t\tb.tabIndex = on ? 0 : -1;\n\t\t\t\tif ( on && focusTab ) {\n\t\t\t\t\tb.focus();\n\t\t\t\t}\n\t\t\t} );\n\t\t\t$$( '[data-rc-pane]', drawer ).forEach( function ( p ) {\n\t\t\t\tvar on = p.getAttribute( 'data-rc-pane' ) === name;\n\t\t\t\tp.hidden = ! on;\n\t\t\t\tif ( on ) {\n\t\t\t\t\t$( '[data-rc-drawer-title]', drawer ).textContent = p.getAttribute( 'data-title' ) || '';\n\t\t\t\t}\n\t\t\t} );\n\t\t\tif ( name === 'list' ) {\n\t\t\t\trenderList();\n\t\t\t}\n\t\t\tif ( name === 'settings' ) {\n\t\t\t\trenderSettings();\n\t\t\t}\n\t\t\tsyncOpeners();\n\t\t}\n\n\t\tfunction openDrawer( name, opener ) {\n\t\t\tctx.G.panelOpened( ctx.inst );\n\t\t\tcloseEq( false );\n\t\t\tdrawerOpener = opener || drawerOpener;\n\t\t\tdrawer.hidden = false;\n\t\t\tshowTab( name, true );\n\t\t}\n\n\t\tfunction closeDrawer( restoreFocus ) {\n\t\t\tif ( ! drawerOpen() ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar hadFocus = drawer.contains( focused() );\n\t\t\tdrawer.hidden = true;\n\t\t\tsyncOpeners();\n\t\t\tif ( ( restoreFocus || hadFocus ) && drawerOpener ) {\n\t\t\t\tdrawerOpener.focus();\n\t\t\t}\n\t\t}\n\n\t\topeners.forEach( function ( b ) {\n\t\t\tb.addEventListener( 'click', function () {\n\t\t\t\tvar name = b.getAttribute( 'data-rc-open' );\n\t\t\t\tif ( drawerOpen() && drawerTab === name ) {\n\t\t\t\t\tcloseDrawer( true );\n\t\t\t\t} else {\n\t\t\t\t\topenDrawer( name, b );\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\n\t\ttabs.forEach( function ( b, i ) {\n\t\t\tb.addEventListener( 'click', function () {\n\t\t\t\tshowTab( b.getAttribute( 'data-rc-tab' ), true );\n\t\t\t\tdrawerOpener = $( '[data-rc-open=\"' + drawerTab + '\"]' ) || drawerOpener;\n\t\t\t} );\n\t\t\tb.addEventListener( 'keydown', function ( e ) {\n\t\t\t\tvar dir = { ArrowRight: 1, ArrowLeft: -1, Home: -99, End: 99 }[ e.key ];\n\t\t\t\tif ( ! dir ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\te.preventDefault();\n\t\t\t\tvar j = dir === -99 ? 0 : dir === 99 ? tabs.length - 1 : ( i + dir + tabs.length ) % tabs.length;\n\t\t\t\tshowTab( tabs[ j ].getAttribute( 'data-rc-tab' ), true );\n\t\t\t\tdrawerOpener = $( '[data-rc-open=\"' + drawerTab + '\"]' ) || drawerOpener;\n\t\t\t} );\n\t\t} );\n\n\t\t$( '[data-rc-drawer-close]', drawer ).addEventListener( 'click', function () {\n\t\t\tcloseDrawer( true );\n\t\t} );\n\n\t\t\/* Lista *\/\n\n\t\tvar listEl = $( '[data-rc-list]', drawer );\n\t\tvar searchEl = $( '[data-rc-search]', drawer );\n\t\tvar listStatus = $( '[data-rc-list-status]', drawer );\n\n\t\tfunction itemMeta( it ) {\n\t\t\tvar parts = [];\n\t\t\tif ( it.artist ) {\n\t\t\t\tparts.push( it.artist );\n\t\t\t}\n\t\t\tif ( it.source === 'file' ) {\n\t\t\t\tparts.push( t.badgeLocal + ( it.persisted ? '' : ' \u00b7 ' + t.badgeSession ) );\n\t\t\t} else {\n\t\t\t\tparts.push( it.kind === 'live' ? t.badgeLive : t.badgeFile );\n\t\t\t\tif ( it.source === 'url' ) {\n\t\t\t\t\tparts.push( t.badgeMine );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn parts.join( ' \u00b7 ' );\n\t\t}\n\n\t\tfunction actionButton( icon, label, cls ) {\n\t\t\tvar b = document.createElement( 'button' );\n\t\t\tb.type = 'button';\n\t\t\tb.className = 'rc-act ' + ( cls || '' );\n\t\t\tb.appendChild( svg( ICON[ icon ] ) );\n\t\t\tb.appendChild( srText( label ) );\n\t\t\tb.title = label;\n\t\t\treturn b;\n\t\t}\n\n\t\tfunction cssId( id ) {\n\t\t\treturn window.CSS && window.CSS.escape ? window.CSS.escape( id ) : String( id ).replace( \/[^A-Za-z0-9_-]\/g, '' );\n\t\t}\n\n\t\tfunction renderList( focusSel ) {\n\t\t\tif ( ! listEl ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar q = ( searchEl.value || '' ).trim().toLowerCase();\n\t\t\tvar all = P.items();\n\t\t\tvar curId = P.currentId();\n\t\t\tlistEl.textContent = '';\n\t\t\tvar shown = 0;\n\t\t\tall.forEach( function ( it, idx ) {\n\t\t\t\tvar title = it.title || it.url || '';\n\t\t\t\tif ( q && ( title + ' ' + ( it.artist || '' ) ).toLowerCase().indexOf( q ) === -1 ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tshown++;\n\t\t\t\tvar li = document.createElement( 'li' );\n\t\t\t\tli.className = 'rc-item';\n\t\t\t\tli.setAttribute( 'data-id', it.id );\n\t\t\t\tif ( it.id === curId ) {\n\t\t\t\t\tli.setAttribute( 'aria-current', 'true' );\n\t\t\t\t}\n\n\t\t\t\tvar main = document.createElement( 'button' );\n\t\t\t\tmain.type = 'button';\n\t\t\t\tmain.className = 'rc-item__main';\n\t\t\t\tmain.setAttribute( 'data-act', 'play' );\n\t\t\t\tmain.setAttribute( 'aria-label', fmt( t.actPlay, title ) + ( it.id === curId ? ' (' + t.nowPlaying + ')' : '' ) );\n\t\t\t\tvar thumb = document.createElement( 'span' );\n\t\t\t\tthumb.className = 'rc-item__icon';\n\t\t\t\tthumb.appendChild( svg( it.kind === 'live' ? ICON.live : ICON.note ) );\n\t\t\t\tvar text = document.createElement( 'span' );\n\t\t\t\ttext.className = 'rc-item__text';\n\t\t\t\tvar tt = document.createElement( 'span' );\n\t\t\t\ttt.className = 'rc-item__title';\n\t\t\t\ttt.textContent = title;\n\t\t\t\tvar mm = document.createElement( 'span' );\n\t\t\t\tmm.className = 'rc-item__meta';\n\t\t\t\tmm.textContent = itemMeta( it );\n\t\t\t\ttext.appendChild( tt );\n\t\t\t\ttext.appendChild( mm );\n\t\t\t\tmain.appendChild( thumb );\n\t\t\t\tmain.appendChild( text );\n\t\t\t\tli.appendChild( main );\n\n\t\t\t\tvar acts = document.createElement( 'span' );\n\t\t\t\tacts.className = 'rc-item__acts';\n\t\t\t\tif ( ! q ) {\n\t\t\t\t\tvar up = actionButton( 'up', fmt( t.actUp, title ) );\n\t\t\t\t\tup.setAttribute( 'data-act', 'up' );\n\t\t\t\t\tup.disabled = idx === 0;\n\t\t\t\t\tvar down = actionButton( 'down', fmt( t.actDown, title ) );\n\t\t\t\t\tdown.setAttribute( 'data-act', 'down' );\n\t\t\t\t\tdown.disabled = idx === all.length - 1;\n\t\t\t\t\tacts.appendChild( up );\n\t\t\t\t\tacts.appendChild( down );\n\t\t\t\t}\n\t\t\t\t\/\/ Descargar: solo archivos con URL directa (no directo, no archivos locales).\n\t\t\t\tif ( it.kind === 'file' && it.url && it.source !== 'file' ) {\n\t\t\t\t\tvar a = document.createElement( 'a' );\n\t\t\t\t\ta.className = 'rc-act';\n\t\t\t\t\ta.href = it.url;\n\t\t\t\t\ta.setAttribute( 'download', '' );\n\t\t\t\t\ta.target = '_blank';\n\t\t\t\t\ta.rel = 'noopener';\n\t\t\t\t\ta.title = fmt( t.actDownload, title );\n\t\t\t\t\ta.appendChild( svg( ICON.download ) );\n\t\t\t\t\ta.appendChild( srText( fmt( t.actDownload, title ) ) );\n\t\t\t\t\tacts.appendChild( a );\n\t\t\t\t}\n\t\t\t\t\/\/ Compartir: enlaces reales; un archivo local no tiene URL compartible.\n\t\t\t\tvar share = actionButton( 'share', it.source === 'file' ? t.shareLocal : fmt( t.actShare, title ) );\n\t\t\t\tshare.setAttribute( 'data-act', 'share' );\n\t\t\t\tif ( it.source === 'file' ) {\n\t\t\t\t\tshare.setAttribute( 'aria-disabled', 'true' );\n\t\t\t\t}\n\t\t\t\tacts.appendChild( share );\n\t\t\t\t\/\/ Quitar: solo lo que a\u00f1adi\u00f3 el visitante.\n\t\t\t\tif ( it.source === 'url' || it.source === 'file' ) {\n\t\t\t\t\tvar rm = actionButton( 'remove', fmt( t.actRemove, title ), 'rc-act--danger' );\n\t\t\t\t\trm.setAttribute( 'data-act', 'remove' );\n\t\t\t\t\tacts.appendChild( rm );\n\t\t\t\t}\n\t\t\t\tli.appendChild( acts );\n\t\t\t\tlistEl.appendChild( li );\n\t\t\t} );\n\t\t\tif ( ! shown ) {\n\t\t\t\tvar empty = document.createElement( 'li' );\n\t\t\t\tempty.className = 'rc-list__empty';\n\t\t\t\tempty.textContent = t.listEmpty;\n\t\t\t\tlistEl.appendChild( empty );\n\t\t\t}\n\t\t\tif ( focusSel ) {\n\t\t\t\tvar f = listEl.querySelector( focusSel );\n\t\t\t\tif ( f && ! f.disabled ) {\n\t\t\t\t\tf.focus();\n\t\t\t\t} else if ( searchEl ) {\n\t\t\t\t\tsearchEl.focus();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction shareItem( it ) {\n\t\t\tvar url = it.url;\n\t\t\tvar title = it.title || url;\n\t\t\tlistStatus.textContent = '';\n\t\t\tif ( navigator.share ) {\n\t\t\t\tnavigator.share( { title: title, url: url } ).catch( function ( e ) {\n\t\t\t\t\tif ( e && e.name !== 'AbortError' ) {\n\t\t\t\t\t\tcopyOrShow( url );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcopyOrShow( url );\n\t\t}\n\n\t\tfunction copyOrShow( url ) {\n\t\t\tif ( navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext ) {\n\t\t\t\tnavigator.clipboard.writeText( url ).then( function () {\n\t\t\t\t\tlistStatus.textContent = t.shareCopied;\n\t\t\t\t}, function () {\n\t\t\t\t\tshowUrl( url );\n\t\t\t\t} );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tshowUrl( url );\n\t\t}\n\n\t\tfunction showUrl( url ) {\n\t\t\tlistStatus.textContent = '';\n\t\t\tvar label = document.createElement( 'label' );\n\t\t\tlabel.className = 'rc-share-manual';\n\t\t\tlabel.appendChild( document.createTextNode( t.shareManual + ' ' ) );\n\t\t\tvar input = document.createElement( 'input' );\n\t\t\tinput.type = 'text';\n\t\t\tinput.readOnly = true;\n\t\t\tinput.value = url;\n\t\t\tlabel.appendChild( input );\n\t\t\tlistStatus.appendChild( label );\n\t\t\tinput.focus();\n\t\t\tinput.select();\n\t\t}\n\n\t\tif ( listEl ) {\n\t\t\tlistEl.addEventListener( 'click', function ( e ) {\n\t\t\t\tvar btn = e.target.closest( '[data-act]' );\n\t\t\t\tvar li = e.target.closest( '.rc-item' );\n\t\t\t\tif ( ! btn || ! li ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar id = li.getAttribute( 'data-id' );\n\t\t\t\tvar it = P.items().filter( function ( x ) {\n\t\t\t\t\treturn x.id === id;\n\t\t\t\t} )[ 0 ];\n\t\t\t\tif ( ! it ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar act = btn.getAttribute( 'data-act' );\n\t\t\t\tif ( act === 'play' ) {\n\t\t\t\t\t\/\/ El elemento que ya suena no se reconecta.\n\t\t\t\t\tif ( ! ( id === P.currentId() && ( P.isActive() || P.state() === 'connecting' ) ) ) {\n\t\t\t\t\t\tP.select( id, true, true );\n\t\t\t\t\t}\n\t\t\t\t\trenderList( '[data-id=\"' + cssId( id ) + '\"] [data-act=\"play\"]' );\n\t\t\t\t} else if ( act === 'up' || act === 'down' ) {\n\t\t\t\t\tP.moveItem( id, act === 'up' ? -1 : 1 );\n\t\t\t\t\trenderList( '[data-id=\"' + cssId( id ) + '\"] [data-act=\"' + act + '\"]' );\n\t\t\t\t} else if ( act === 'share' ) {\n\t\t\t\t\tif ( it.source === 'file' ) {\n\t\t\t\t\t\tlistStatus.textContent = t.shareLocal;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tshareItem( it );\n\t\t\t\t\t}\n\t\t\t\t} else if ( act === 'remove' ) {\n\t\t\t\t\tvar nextLi = li.nextElementSibling || li.previousElementSibling;\n\t\t\t\t\tvar nextId = nextLi && nextLi.getAttribute( 'data-id' );\n\t\t\t\t\tP.removeLocalItem( id );\n\t\t\t\t\tlistStatus.textContent = fmt( t.removed, it.title || it.url || '' );\n\t\t\t\t\trenderList( nextId ? '[data-id=\"' + cssId( nextId ) + '\"] [data-act=\"play\"]' : null );\n\t\t\t\t\tif ( ! nextId && searchEl ) {\n\t\t\t\t\t\tsearchEl.focus();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t\tif ( searchEl ) {\n\t\t\tsearchEl.addEventListener( 'input', function () {\n\t\t\t\trenderList();\n\t\t\t} );\n\t\t}\n\n\t\tP.on( 'items', function () {\n\t\t\tif ( drawerOpen() && drawerTab === 'list' ) {\n\t\t\t\trenderList();\n\t\t\t}\n\t\t\trenderSettings();\n\t\t} );\n\t\tP.on( 'item', function () {\n\t\t\tif ( drawerOpen() && drawerTab === 'list' ) {\n\t\t\t\trenderList();\n\t\t\t}\n\t\t\tsyncKind();\n\t\t} );\n\n\t\t\/* A\u00f1adir: archivos locales y URL *\/\n\n\t\tvar addStatus = $( '[data-rc-add-status]', drawer );\n\t\tvar subtabs = $$( '[data-rc-subtab]', drawer );\n\n\t\tfunction showSubtab( name, focus ) {\n\t\t\tsubtabs.forEach( function ( b ) {\n\t\t\t\tvar on = b.getAttribute( 'data-rc-subtab' ) === name;\n\t\t\t\tb.setAttribute( 'aria-selected', on ? 'true' : 'false' );\n\t\t\t\tb.tabIndex = on ? 0 : -1;\n\t\t\t\tif ( on && focus ) {\n\t\t\t\t\tb.focus();\n\t\t\t\t}\n\t\t\t} );\n\t\t\t$$( '[data-rc-subpane]', drawer ).forEach( function ( p ) {\n\t\t\t\tp.hidden = p.getAttribute( 'data-rc-subpane' ) !== name;\n\t\t\t} );\n\t\t}\n\t\tsubtabs.forEach( function ( b, i ) {\n\t\t\tb.addEventListener( 'click', function () {\n\t\t\t\tshowSubtab( b.getAttribute( 'data-rc-subtab' ), true );\n\t\t\t} );\n\t\t\tb.addEventListener( 'keydown', function ( e ) {\n\t\t\t\tif ( e.key === 'ArrowRight' || e.key === 'ArrowLeft' ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tvar j = ( i + ( e.key === 'ArrowRight' ? 1 : -1 ) + subtabs.length ) % subtabs.length;\n\t\t\t\t\tshowSubtab( subtabs[ j ].getAttribute( 'data-rc-subtab' ), true );\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\n\t\tfunction setAddStatus( lines, isError ) {\n\t\t\taddStatus.textContent = '';\n\t\t\taddStatus.setAttribute( 'data-kind', isError ? 'error' : 'ok' );\n\t\t\tlines.filter( Boolean ).forEach( function ( l ) {\n\t\t\t\tvar p = document.createElement( 'p' );\n\t\t\t\tp.textContent = l;\n\t\t\t\taddStatus.appendChild( p );\n\t\t\t} );\n\t\t}\n\n\t\tvar AUDIO_EXT = \/\\.(mp3|m4a|aac|ogg|oga|opus|wav|flac|webm)$\/i;\n\t\tvar MIME_BY_EXT = { mp3: 'audio\/mpeg', m4a: 'audio\/mp4', aac: 'audio\/aac', ogg: 'audio\/ogg', oga: 'audio\/ogg', opus: 'audio\/ogg; codecs=opus', wav: 'audio\/wav', flac: 'audio\/flac', webm: 'audio\/webm' };\n\t\tvar probe = document.createElement( 'audio' );\n\n\t\tfunction existingUrls() {\n\t\t\tvar set = {};\n\t\t\tP.items().forEach( function ( it ) {\n\t\t\t\tif ( it.url ) {\n\t\t\t\t\tset[ it.url ] = true;\n\t\t\t\t}\n\t\t\t} );\n\t\t\treturn set;\n\t\t}\n\n\t\tfunction m3uMessages( res ) {\n\t\t\tvar msgs = [];\n\t\t\tif ( res.skippedLocal ) {\n\t\t\t\tmsgs.push( fmt( t.m3uLocalPaths, res.skippedLocal ) );\n\t\t\t}\n\t\t\tif ( res.skippedNested ) {\n\t\t\t\tmsgs.push( fmt( t.m3uNested, res.skippedNested ) );\n\t\t\t}\n\t\t\tif ( res.skippedInvalid ) {\n\t\t\t\tmsgs.push( fmt( t.m3uInvalid, res.skippedInvalid ) );\n\t\t\t}\n\t\t\tif ( res.truncated ) {\n\t\t\t\tmsgs.push( fmt( t.m3uTruncated, Store.limits.m3uEntries ) );\n\t\t\t}\n\t\t\treturn msgs;\n\t\t}\n\n\t\tfunction m3uError( code ) {\n\t\t\treturn { hls: t.m3uHls, 'too-big': t.m3uTooBig, empty: t.m3uEmpty }[ code ] || t.m3uEmpty;\n\t\t}\n\n\t\tfunction itemsFromM3U( res ) {\n\t\t\tvar known = existingUrls();\n\t\t\tvar out = [];\n\t\t\tres.items.forEach( function ( e ) {\n\t\t\t\tif ( known[ e.url ] ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tknown[ e.url ] = true;\n\t\t\t\tout.push( { id: Store.newId(), source: 'url', title: e.title || e.url, artist: e.artist, url: e.url, kind: e.kind, persisted: true } );\n\t\t\t} );\n\t\t\treturn out;\n\t\t}\n\n\t\tfunction handleFiles( fileList ) {\n\t\t\tvar files = Array.prototype.slice.call( fileList || [] );\n\t\t\tif ( ! files.length ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar added = [];\n\t\t\tvar msgs = [];\n\t\t\tvar hadError = false;\n\t\t\tvar chain = Promise.resolve();\n\t\t\tfiles.forEach( function ( file ) {\n\t\t\t\tchain = chain.then( function () {\n\t\t\t\t\tvar name = file.name || '';\n\t\t\t\t\tif ( \/\\.m3u8$\/i.test( name ) ) {\n\t\t\t\t\t\tmsgs.push( name + ': ' + t.m3uHls );\n\t\t\t\t\t\thadError = true;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tif ( \/\\.m3u$\/i.test( name ) || \/mpegurl\/i.test( file.type ) ) {\n\t\t\t\t\t\tif ( file.size > Store.limits.m3uBytes ) {\n\t\t\t\t\t\t\tmsgs.push( name + ': ' + t.m3uTooBig );\n\t\t\t\t\t\t\thadError = true;\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn file.text().then( function ( text ) {\n\t\t\t\t\t\t\tvar res = parseM3U( text, null, 'live' );\n\t\t\t\t\t\t\tif ( res.error ) {\n\t\t\t\t\t\t\t\tmsgs.push( name + ': ' + m3uError( res.error ) );\n\t\t\t\t\t\t\t\tmsgs = msgs.concat( m3uMessages( res ) );\n\t\t\t\t\t\t\t\thadError = true;\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tadded = added.concat( itemsFromM3U( res ) );\n\t\t\t\t\t\t\tmsgs = msgs.concat( m3uMessages( res ) );\n\t\t\t\t\t\t} );\n\t\t\t\t\t}\n\t\t\t\t\tvar ext = ( \/\\.([a-z0-9]+)$\/i.exec( name ) || [] )[ 1 ];\n\t\t\t\t\tvar type = file.type || ( ext && MIME_BY_EXT[ ext.toLowerCase() ] ) || '';\n\t\t\t\t\tif ( ! \/^audio\\\/\/i.test( type ) && ! AUDIO_EXT.test( name ) ) {\n\t\t\t\t\t\tmsgs.push( fmt( t.fileNotAudio, name ) );\n\t\t\t\t\t\thadError = true;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tif ( type && probe.canPlayType && probe.canPlayType( type ) === '' ) {\n\t\t\t\t\t\tmsgs.push( fmt( t.fileUnsupported, name ) );\n\t\t\t\t\t\thadError = true;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\tvar item = { id: Store.newId(), source: 'file', title: cleanText( name.replace( \/\\.[^.]+$\/, '' ) ) || name, artist: '', kind: 'file', size: file.size, persisted: false };\n\t\t\t\t\treturn Store.putFile( item.id, file ).then( function () {\n\t\t\t\t\t\titem.persisted = true;\n\t\t\t\t\t\tadded.push( item );\n\t\t\t\t\t}, function ( err ) {\n\t\t\t\t\t\tvar reason = err && err.name === 'QuotaExceededError' ? t.quota : t.storageUnavailable;\n\t\t\t\t\t\tP.addSessionFile( item.id, file );\n\t\t\t\t\t\tadded.push( item );\n\t\t\t\t\t\tmsgs.push( name + ': ' + fmt( t.storedSession, reason ) );\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t} );\n\t\t\tchain.then( function () {\n\t\t\t\tif ( added.length ) {\n\t\t\t\t\tP.addLocalItems( added );\n\t\t\t\t}\n\t\t\t\tsetAddStatus( [ added.length ? fmt( t.addedN, added.length ) : '' ].concat( msgs ), hadError && ! added.length );\n\t\t\t} );\n\t\t}\n\n\t\tvar fileInput = $( '[data-rc-file]', drawer );\n\t\tvar drop = $( '[data-rc-drop]', drawer );\n\t\tif ( fileInput ) {\n\t\t\tfileInput.addEventListener( 'change', function () {\n\t\t\t\thandleFiles( fileInput.files );\n\t\t\t\tfileInput.value = '';\n\t\t\t} );\n\t\t}\n\t\tif ( drop ) {\n\t\t\t[ 'dragenter', 'dragover' ].forEach( function ( ev ) {\n\t\t\t\tdrop.addEventListener( ev, function ( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tdrop.setAttribute( 'data-over', '1' );\n\t\t\t\t} );\n\t\t\t} );\n\t\t\t[ 'dragleave', 'drop' ].forEach( function ( ev ) {\n\t\t\t\tdrop.addEventListener( ev, function ( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tdrop.removeAttribute( 'data-over' );\n\t\t\t\t} );\n\t\t\t} );\n\t\t\tdrop.addEventListener( 'drop', function ( e ) {\n\t\t\t\tif ( e.dataTransfer && e.dataTransfer.files ) {\n\t\t\t\t\thandleFiles( e.dataTransfer.files );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\tvar urlForm = $( '[data-rc-url-form]', drawer );\n\t\tif ( urlForm ) {\n\t\t\turlForm.addEventListener( 'submit', function ( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tvar titleIn = $( '[data-rc-url-title]', urlForm );\n\t\t\t\tvar urlIn = $( '[data-rc-url]', urlForm );\n\t\t\t\tvar kindIn = urlForm.querySelector( 'input[name=\"rc-kind\"]:checked' );\n\t\t\t\tvar kind = kindIn ? kindIn.value : 'live';\n\t\t\t\tvar v = validateUrl( urlIn.value );\n\t\t\t\turlIn.removeAttribute( 'aria-invalid' );\n\t\t\t\tif ( ! v.ok ) {\n\t\t\t\t\turlIn.setAttribute( 'aria-invalid', 'true' );\n\t\t\t\t\tsetAddStatus( [ { empty: t.urlEmpty, protocol: t.urlProtocol, credentials: t.urlCredentials }[ v.error ] || t.urlInvalid ], true );\n\t\t\t\t\turlIn.focus();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif ( isHlsUrl( v.url ) ) {\n\t\t\t\t\tsetAddStatus( [ t.urlHls ], true );\n\t\t\t\t\turlIn.focus();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif ( kind !== 'm3u' && isPlaylistUrl( v.url ) ) {\n\t\t\t\t\tsetAddStatus( [ t.urlPlaylist ], true );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar warn = window.location.protocol === 'https:' && \/^http:\/i.test( v.url ) ? t.urlMixed : '';\n\n\t\t\t\tif ( kind === 'm3u' ) {\n\t\t\t\t\taddRemoteM3U( v.url, warn );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif ( existingUrls()[ v.url ] ) {\n\t\t\t\t\tsetAddStatus( [ t.urlDuplicate ], true );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar title = cleanText( titleIn.value ) || v.url;\n\t\t\t\tP.addLocalItems( [ { id: Store.newId(), source: 'url', title: title, artist: '', url: v.url, kind: kind === 'file' ? 'file' : 'live', persisted: true } ] );\n\t\t\t\tsetAddStatus( [ fmt( t.urlAdded, title ), warn ], false );\n\t\t\t\turlForm.reset();\n\t\t\t} );\n\t\t}\n\n\t\tfunction addRemoteM3U( url, warn ) {\n\t\t\tvar ctl = window.AbortController ? new AbortController() : null;\n\t\t\tvar timer = window.setTimeout( function () {\n\t\t\t\tif ( ctl ) {\n\t\t\t\t\tctl.abort();\n\t\t\t\t}\n\t\t\t}, 10000 );\n\t\t\tfetch( url, { mode: 'cors', credentials: 'omit', signal: ctl ? ctl.signal : undefined } ).then( function ( r ) {\n\t\t\t\tif ( ! r.ok ) {\n\t\t\t\t\tthrow new Error( 'http' );\n\t\t\t\t}\n\t\t\t\tvar len = Number( r.headers.get( 'content-length' ) || 0 );\n\t\t\t\tif ( len > Store.limits.m3uBytes ) {\n\t\t\t\t\tthrow new Error( 'too-big' );\n\t\t\t\t}\n\t\t\t\treturn r.text().then( function ( text ) {\n\t\t\t\t\treturn { text: text, base: r.url || url };\n\t\t\t\t} );\n\t\t\t} ).then( function ( d ) {\n\t\t\t\twindow.clearTimeout( timer );\n\t\t\t\tvar res = parseM3U( d.text, d.base, 'live' );\n\t\t\t\tif ( res.error ) {\n\t\t\t\t\tsetAddStatus( [ m3uError( res.error ) ].concat( m3uMessages( res ) ), true );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tvar list = itemsFromM3U( res );\n\t\t\t\tif ( list.length ) {\n\t\t\t\t\tP.addLocalItems( list );\n\t\t\t\t}\n\t\t\t\tsetAddStatus( [ list.length ? fmt( t.addedN, list.length ) : t.urlDuplicate, warn ].concat( m3uMessages( res ) ), ! list.length );\n\t\t\t\tif ( list.length ) {\n\t\t\t\t\turlForm.reset();\n\t\t\t\t}\n\t\t\t} ).catch( function ( err ) {\n\t\t\t\twindow.clearTimeout( timer );\n\t\t\t\tsetAddStatus( [ err && err.message === 'too-big' ? t.m3uTooBig : t.m3uFetch, warn ], true );\n\t\t\t} );\n\t\t}\n\n\t\t\/* Ajustes del visitante *\/\n\n\t\tvar setVol = $( '[data-rc-set-volume]', drawer );\n\t\tvar setVolOut = $( '[data-rc-set-volume-out]', drawer );\n\t\tvar setMode = $( '[data-rc-set-mode]', drawer );\n\t\tvar setSpeed = $( '[data-rc-set-speed]', drawer );\n\t\tvar localCount = $( '[data-rc-local-count]', drawer );\n\t\tvar clearBtn = $( '[data-rc-clear-local]', drawer );\n\t\tvar clearStatus = $( '[data-rc-clear-status]', drawer );\n\t\tvar clearArmed = false;\n\n\t\tfunction renderSettings() {\n\t\t\tvar p = P.prefs();\n\t\t\tif ( setVol ) {\n\t\t\t\tsetVol.value = String( Math.round( p.volume * 100 ) );\n\t\t\t\tsetVolOut.textContent = ( p.muted ? 0 : Math.round( p.volume * 100 ) ) + ' %';\n\t\t\t}\n\t\t\tif ( setMode ) {\n\t\t\t\tsetMode.value = ctx.mode.get();\n\t\t\t}\n\t\t\tif ( setSpeed ) {\n\t\t\t\tsetSpeed.value = String( p.speed );\n\t\t\t}\n\t\t\tif ( localCount ) {\n\t\t\t\tlocalCount.textContent = fmt( t.localCount, P.localCount() );\n\t\t\t}\n\t\t\tif ( clearBtn ) {\n\t\t\t\tclearBtn.disabled = ! P.localCount();\n\t\t\t}\n\t\t}\n\t\tif ( setVol ) {\n\t\t\tsetVol.addEventListener( 'input', function () {\n\t\t\t\tP.setVolume( Number( setVol.value ) \/ 100 );\n\t\t\t} );\n\t\t}\n\t\tif ( setMode ) {\n\t\t\tsetMode.addEventListener( 'change', function () {\n\t\t\t\tctx.mode.set( setMode.value );\n\t\t\t} );\n\t\t\tctx.mode.onChange( renderSettings );\n\t\t}\n\t\tif ( setSpeed ) {\n\t\t\tsetSpeed.addEventListener( 'change', function () {\n\t\t\t\tP.setSpeed( Number( setSpeed.value ) );\n\t\t\t} );\n\t\t}\n\t\tif ( clearBtn ) {\n\t\t\tclearBtn.addEventListener( 'click', function () {\n\t\t\t\tif ( ! clearArmed ) {\n\t\t\t\t\tclearArmed = true;\n\t\t\t\t\tclearStatus.textContent = t.confirmClear;\n\t\t\t\t\twindow.setTimeout( function () {\n\t\t\t\t\t\tclearArmed = false;\n\t\t\t\t\t}, 6000 );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tclearArmed = false;\n\t\t\t\tP.clearLocal().then( function () {\n\t\t\t\t\tclearStatus.textContent = t.cleared;\n\t\t\t\t\trenderSettings();\n\t\t\t\t} );\n\t\t\t} );\n\t\t}\n\t\tP.on( 'volume', renderSettings );\n\n\t\t\/* Fila inferior: aleatorio, repetir, ecualizador, velocidad *\/\n\n\t\tvar shuffleBtn = $( '[data-rc-shuffle]' );\n\t\tvar repeatBtn = $( '[data-rc-repeat]' );\n\t\tvar repeatLabel = $( '[data-rc-repeat-label]' );\n\t\tvar speedSel = $( '[data-rc-speed]' );\n\t\tvar rowNote = $( '[data-rc-row-note]' );\n\t\tvar noteTimer = 0;\n\n\t\tfunction note( text ) {\n\t\t\trowNote.textContent = text;\n\t\t\twindow.clearTimeout( noteTimer );\n\t\t\tnoteTimer = window.setTimeout( function () {\n\t\t\t\trowNote.textContent = '';\n\t\t\t}, 6000 );\n\t\t}\n\n\t\tfunction renderModes() {\n\t\t\tvar p = P.prefs();\n\t\t\tshuffleBtn.setAttribute( 'aria-pressed', p.shuffle ? 'true' : 'false' );\n\t\t\trepeatBtn.setAttribute( 'data-repeat', p.repeat );\n\t\t\trepeatBtn.setAttribute( 'aria-pressed', p.repeat === 'off' ? 'false' : 'true' );\n\t\t\tvar rl = { off: t.repeatOff, all: t.repeatAll, one: t.repeatOne }[ p.repeat ];\n\t\t\trepeatLabel.textContent = rl;\n\t\t\trepeatBtn.title = rl;\n\t\t\tshuffleBtn.title = p.shuffle ? t.shuffleOn : t.shuffleOff;\n\t\t\tspeedSel.value = String( p.speed );\n\t\t}\n\n\t\tfunction syncKind() {\n\t\t\tvar it = P.current();\n\t\t\tvar live = ! it || it.kind === 'live';\n\t\t\tspeedSel.setAttribute( 'aria-disabled', live ? 'true' : 'false' );\n\t\t\tspeedSel.parentNode.setAttribute( 'data-disabled', live ? '1' : '0' );\n\t\t\tspeedSel.parentNode.title = live ? t.speedLive : fmt( t.speedSet, String( P.prefs().speed ) + '\u00d7' );\n\t\t\t\/\/ Explicaci\u00f3n para lectores de pantalla (aria-describedby del selector).\n\t\t\tvar why = $( '[data-rc-speed-why]' );\n\t\t\tif ( why ) {\n\t\t\t\twhy.textContent = live ? t.speedLive : '';\n\t\t\t}\n\t\t\tspeedSel.value = live ? '1' : String( P.prefs().speed );\n\t\t}\n\n\t\tshuffleBtn.addEventListener( 'click', function () {\n\t\t\tP.setShuffle( ! P.prefs().shuffle );\n\t\t\tnote( P.prefs().shuffle ? t.shuffleOn : t.shuffleOff );\n\t\t} );\n\t\trepeatBtn.addEventListener( 'click', function () {\n\t\t\tP.cycleRepeat();\n\t\t\tnote( { off: t.repeatOff, all: t.repeatAll, one: t.repeatOne }[ P.prefs().repeat ] );\n\t\t} );\n\n\t\tfunction speedBlocked( e ) {\n\t\t\tif ( speedSel.getAttribute( 'aria-disabled' ) === 'true' ) {\n\t\t\t\tif ( e.type === 'mousedown' || [ ' ', 'Enter', 'ArrowUp', 'ArrowDown', 'Home', 'End' ].indexOf( e.key ) >= 0 ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tnote( t.speedLive );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tspeedSel.addEventListener( 'mousedown', speedBlocked );\n\t\tspeedSel.addEventListener( 'keydown', speedBlocked );\n\t\tspeedSel.addEventListener( 'change', function () {\n\t\t\tif ( speedSel.getAttribute( 'aria-disabled' ) === 'true' ) {\n\t\t\t\tspeedSel.value = '1';\n\t\t\t\tnote( t.speedLive );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tP.setSpeed( Number( speedSel.value ) );\n\t\t\tnote( fmt( t.speedSet, speedSel.value + '\u00d7' ) );\n\t\t} );\n\n\t\t\/\/ Anterior\/siguiente desactivados: explicaci\u00f3n visible adem\u00e1s de la accesible.\n\t\t$$( '[data-rc-prev], [data-rc-next]' ).forEach( function ( b ) {\n\t\t\tb.addEventListener( 'click', function () {\n\t\t\t\tif ( b.getAttribute( 'aria-disabled' ) === 'true' ) {\n\t\t\t\t\tnote( t.navSingle );\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\n\t\tP.on( 'modes', renderModes );\n\t\tP.on( 'speed', function () {\n\t\t\trenderModes();\n\t\t\tsyncKind();\n\t\t\trenderSettings();\n\t\t} );\n\n\t\t\/* Ecualizador *\/\n\n\t\tvar eqStatusEl = $( '[data-rc-eq-status]', eqPanel );\n\t\tvar bands = $$( '[data-rc-eq-band]', eqPanel );\n\n\t\tfunction renderEq() {\n\t\t\tvar p = P.prefs();\n\t\t\tbands.forEach( function ( b ) {\n\t\t\t\tvar i = Number( b.getAttribute( 'data-rc-eq-band' ) );\n\t\t\t\tb.value = String( p.eq[ i ] );\n\t\t\t\tvar out = $( '[data-rc-eq-value=\"' + i + '\"]', eqPanel );\n\t\t\t\tout.textContent = ( p.eq[ i ] > 0 ? '+' : '' ) + p.eq[ i ] + ' dB';\n\t\t\t\tb.setAttribute( 'aria-valuetext', out.textContent );\n\t\t\t} );\n\t\t\tvar s = P.eqStatus();\n\t\t\teqStatusEl.textContent = { applied: t.eqApplied, flat: t.eqFlat, idle: t.eqIdle, cors: t.eqCors, reconnect: t.eqReconnect, unsupported: t.eqUnsupported }[ s ] || '';\n\t\t\teqPanel.setAttribute( 'data-status', s );\n\t\t}\n\n\t\tfunction openEq() {\n\t\t\tctx.G.panelOpened( ctx.inst );\n\t\t\tcloseDrawer( false );\n\t\t\teqPanel.hidden = false;\n\t\t\teqOpenBtn.setAttribute( 'aria-expanded', 'true' );\n\t\t\trenderEq();\n\t\t\tbands[ 0 ].focus();\n\t\t}\n\n\t\tfunction closeEq( restore ) {\n\t\t\tif ( eqPanel.hidden ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar hadFocus = eqPanel.contains( focused() );\n\t\t\teqPanel.hidden = true;\n\t\t\teqOpenBtn.setAttribute( 'aria-expanded', 'false' );\n\t\t\tif ( restore || hadFocus ) {\n\t\t\t\teqOpenBtn.focus();\n\t\t\t}\n\t\t}\n\n\t\teqOpenBtn.addEventListener( 'click', function () {\n\t\t\tif ( eqPanel.hidden ) {\n\t\t\t\topenEq();\n\t\t\t} else {\n\t\t\t\tcloseEq( true );\n\t\t\t}\n\t\t} );\n\t\t$( '[data-rc-eq-close]', eqPanel ).addEventListener( 'click', function () {\n\t\t\tcloseEq( true );\n\t\t} );\n\t\tbands.forEach( function ( b ) {\n\t\t\tb.addEventListener( 'input', function () {\n\t\t\t\tP.setEqGain( Number( b.getAttribute( 'data-rc-eq-band' ) ), Number( b.value ) );\n\t\t\t\trenderEq();\n\t\t\t} );\n\t\t} );\n\t\t$$( '[data-rc-eq-preset]', eqPanel ).forEach( function ( b ) {\n\t\t\tb.addEventListener( 'click', function () {\n\t\t\t\tP.setEqPreset( b.getAttribute( 'data-rc-eq-preset' ) );\n\t\t\t\trenderEq();\n\t\t\t} );\n\t\t} );\n\t\tP.on( 'eq', function () {\n\t\t\tif ( ! eqPanel.hidden ) {\n\t\t\t\trenderEq();\n\t\t\t}\n\t\t} );\n\n\t\t\/* Atajos de teclado (los reparte el coordinador global a este bloque) *\/\n\n\t\tfunction isTyping( el ) {\n\t\t\tif ( ! el || el === document.body ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar tag = el.tagName;\n\t\t\treturn tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || !! el.isContentEditable;\n\t\t}\n\n\t\tfunction onKey( e, target ) {\n\t\t\tif ( e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( e.key === 'Escape' || e.key === 'Esc' ) {\n\t\t\t\tif ( ! eqPanel.hidden ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tcloseEq( true );\n\t\t\t\t} else if ( drawerOpen() ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tcloseDrawer( true );\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( isTyping( target ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar k = e.key || '';\n\t\t\tif ( k === ' ' || k === 'Spacebar' ) {\n\t\t\t\tif ( target && target.closest && target.closest( 'button, a, summary, [role=\"tab\"], [role=\"button\"]' ) ) {\n\t\t\t\t\treturn; \/\/ Activaci\u00f3n nativa del control con foco.\n\t\t\t\t}\n\t\t\t\te.preventDefault();\n\t\t\t\tP.toggle( true );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( k === '?' ) {\n\t\t\t\te.preventDefault();\n\t\t\t\topenDrawer( 'help', $( '[data-rc-open=\"help\"]' ) );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tswitch ( k.toLowerCase() ) {\n\t\t\t\tcase 'm':\n\t\t\t\t\tP.toggleMute();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'n':\n\t\t\t\t\tP.next( true );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'p':\n\t\t\t\t\tP.prev( true );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 's':\n\t\t\t\t\tshuffleBtn.click();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'r':\n\t\t\t\t\trepeatBtn.click();\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'arrowleft':\n\t\t\t\tcase 'arrowright':\n\t\t\t\t\tif ( ! P.seek( ( k === 'ArrowLeft' ? -1 : 1 ) * ( e.shiftKey ? 10 : 5 ) ) ) {\n\t\t\t\t\t\tvar it = P.current();\n\t\t\t\t\t\tif ( it && it.kind === 'live' ) {\n\t\t\t\t\t\t\tnote( t.seekLive );\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\treturn;\n\t\t\t\tdefault:\n\t\t\t\t\treturn;\n\t\t\t}\n\t\t\te.preventDefault();\n\t\t}\n\n\t\t\/* Inicio *\/\n\t\trenderModes();\n\t\tsyncKind();\n\t\trenderSettings();\n\t\tshowSubtab( 'files', false );\n\n\t\treturn {\n\t\t\tonKey: onKey,\n\t\t\tcloseAll: function () {\n\t\t\t\tcloseEq( false );\n\t\t\t\tcloseDrawer( false );\n\t\t\t}\n\t\t};\n\t}\n\n\t\/* =============================================================\n\t * Arranque: un \u00fanico coordinador para todos los bloques de la p\u00e1gina.\n\t * =========================================================== *\/\n\n\tvar G = window.RadioCircularHTML;\n\tif ( ! G || typeof G.mountAll !== 'function' ) {\n\t\tG = createGlobal();\n\t\ttry {\n\t\t\tObject.defineProperty( window, 'RadioCircularHTML', { value: G, configurable: true } );\n\t\t} catch ( e ) {\n\t\t\twindow.RadioCircularHTML = G;\n\t\t}\n\t}\n\tG.mountAll();\n} )();\n<\/script>\n<\/div>\n<div data-rcx-sin-js style=\"box-sizing:border-box;padding:24px 16px;background:#101114;color:#f2f2f7;font:16px\/1.5 system-ui,sans-serif;text-align:center;border-radius:14px\"><p style=\"margin:0\">El reproductor de radio necesita JavaScript para funcionar.<\/p><p style=\"margin:8px 0 0;font-size:14px;color:#a8a8ba\">Si administras esta web y ves este aviso con JavaScript activado, el editor ha eliminado o modificado el c\u00f3digo del bloque (consulta INSTRUCCIONES-RADIO-HTML.txt).<\/p><\/div>\n<\/div>\n<!-- =================== RADIO CIRCULAR HTML: FIN DEL BLOQUE =================== -->\n<\/section><section class=\"wrap\" style=\"padding:25px 0 55px\"><p>Si el audio no comienza, pulsa el bot\u00f3n de reproducci\u00f3n y confirma que tu navegador permita audio. La lectura de listas M3U remotas depende de que el servidor permita CORS.<\/p><p><a class=\"btn\" href=\"\/radio-circular-wordpress-tema-para-radio\/\">Conocer el tema WordPress \u2192<\/a><\/p><\/section><footer class=\"wrap foot\"><a href=\"\/\" style=\"color:inherit\">CreaNexo Demo<\/a> \u00b7 <a href=\"https:\/\/creanexo.com\/contacto\/\" style=\"color:inherit\">Ayuda o contacto<\/a><\/footer><\/div>","protected":false},"excerpt":{"rendered":"<p>CreaNexo \/ Demo \u2190 Detalles del producto Comprar por US$30 \u2192 Demostraci\u00f3n interactiva \u00b7 Radio Circular HTML 1.2.0 Prueba el reproductor Radio Circular Reproduce las pistas de muestra, cambia de tema, ajusta el volumen y abre el ecualizador para explorar los controles. Esta es una demo p\u00fablica. Incluye dos grabaciones de Chopin de Musopen con [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-87","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Demo de Radio Circular | Reproductor HTML - CreaNexo Demo<\/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:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Demo de Radio Circular | Reproductor HTML - CreaNexo Demo\" \/>\n<meta property=\"og:description\" content=\"CreaNexo \/ Demo \u2190 Detalles del producto Comprar por US$30 \u2192 Demostraci\u00f3n interactiva \u00b7 Radio Circular HTML 1.2.0 Prueba el reproductor Radio Circular Reproduce las pistas de muestra, cambia de tema, ajusta el volumen y abre el ecualizador para explorar los controles. Esta es una demo p\u00fablica. Incluye dos grabaciones de Chopin de Musopen con [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/\" \/>\n<meta property=\"og:site_name\" content=\"CreaNexo Demo\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-14T03:16:59+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"3 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/demo-de-radio-circular-reproductor-html\\\/\",\"url\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/demo-de-radio-circular-reproductor-html\\\/\",\"name\":\"Demo de Radio Circular | Reproductor HTML - CreaNexo Demo\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/#website\"},\"datePublished\":\"2026-10-14T02:33:47+00:00\",\"dateModified\":\"2026-10-14T03:16:59+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/demo-de-radio-circular-reproductor-html\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/demo-de-radio-circular-reproductor-html\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/demo-de-radio-circular-reproductor-html\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Demo de Radio Circular | Reproductor HTML\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/#website\",\"url\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/\",\"name\":\"CreaNexo Demo\",\"description\":\"Soluciones web, WordPress y reproductores de audio\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/creanexostudio.com\\\/demos\\\/creanexo-demo\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Demo de Radio Circular | Reproductor HTML - CreaNexo Demo","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:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/","og_locale":"es_ES","og_type":"article","og_title":"Demo de Radio Circular | Reproductor HTML - CreaNexo Demo","og_description":"CreaNexo \/ Demo \u2190 Detalles del producto Comprar por US$30 \u2192 Demostraci\u00f3n interactiva \u00b7 Radio Circular HTML 1.2.0 Prueba el reproductor Radio Circular Reproduce las pistas de muestra, cambia de tema, ajusta el volumen y abre el ecualizador para explorar los controles. Esta es una demo p\u00fablica. Incluye dos grabaciones de Chopin de Musopen con [&hellip;]","og_url":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/","og_site_name":"CreaNexo Demo","article_modified_time":"2026-10-14T03:16:59+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"3 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/","url":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/","name":"Demo de Radio Circular | Reproductor HTML - CreaNexo Demo","isPartOf":{"@id":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/#website"},"datePublished":"2026-10-14T02:33:47+00:00","dateModified":"2026-10-14T03:16:59+00:00","breadcrumb":{"@id":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/demo-de-radio-circular-reproductor-html\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/"},{"@type":"ListItem","position":2,"name":"Demo de Radio Circular | Reproductor HTML"}]},{"@type":"WebSite","@id":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/#website","url":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/","name":"CreaNexo Demo","description":"Soluciones web, WordPress y reproductores de audio","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"}]}},"_links":{"self":[{"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/pages\/87","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/comments?post=87"}],"version-history":[{"count":2,"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/pages\/87\/revisions"}],"predecessor-version":[{"id":100,"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/pages\/87\/revisions\/100"}],"wp:attachment":[{"href":"https:\/\/creanexostudio.com\/demos\/creanexo-demo\/wp-json\/wp\/v2\/media?parent=87"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}