[Partage] Media Player 100% Button-Card : Inspiration Winamp & Design Glassmorphism 3D

Bonjour à tous :waving_hand:

Je suis en train de refaire tout mon dashboard principal en utilisant exculsivement la custom:button-card et en jouant avec des effets "Glassmorphism" et 3D (ombres portées, reflets de verre bombé, encastrements...). Pour ma vue multimédia, je cherchais un lecteur audio qui s'adapte parfaitement à ce design exigeant.

Le constat de base : On sait tous à quel point c'est une galère d'obtenir un lecteur audio 100% personnalisé sur Home Assistant. En général, on se retrouve à empiler des couches de cartes (mini-media-player + slider-entity-row), à injecter du card_mod dans tous les sens pour corriger des bordures, pour finir avec un rendu qui n'est jamais exactement celui qu'on voulait au pixel près.

Puisque je construis déjà l'intégralité de mon dashboard autour de la custom:button-card, forger mon propre lecteur de A à Z avec cet outil était pour moi la suite logique (et ça évite les usines à gaz ! ou pas ....).

Et puis, mon côté vieux geek nostalgique a pris le dessus... Je me suis dit : "Pourquoi ne pas recréer l'ambiance du mythique lecteur Winamp de la fin des années 90, mais avec un look moderne ?" :radio::sparkles:

Honnêtement, je suis hyper content du résultat final et comme je trouve la carte vachement sympa, j'ai eu très envie de vous la partager.

Voici le résultat en action :

winanp standalone
winanp standalone 02

:hammer_and_wrench: Ce que fait cette carte (et sa logique)

Dans mon dashboard personnel, j'utilise de nombreux templates (button_card_templates) et des variables externes pour centraliser mon code. Mais pour ce partage sur le forum, j'ai tout reconsolidé : ce code est 100% Standalone (autonome). Aucun template externe à gérer, pas de variables complexes. Vous prenez le code, vous changez l'entité à la ligne 2, et ça marche ! (La carte est construite sur les standards de Home Assistant, elle a été testée intensivement sur Spotify).

Voici ses secrets de fabrication :

1. L'écran LCD intelligent (Effet Marquee) :pager:

  • Si le nom de l'artiste ou du titre est trop long, le texte se met automatiquement à défiler.
  • La petite subtilité : le code Javascript calcule mathématiquement la vitesse de défilement pour que les deux lignes (Artiste et Titre) terminent leur boucle exactement en même temps. L'une attend l'autre pour recommencer, gardant l'écran toujours harmonieux ! (Vous pouvez voir cette synchronisation sur le 2ème GIF au-dessus).
  • Gestion de l'arrêt et de la pause :
    • Si vous mettez la musique en Pause, la pochette disparaît (pour faire place au châssis vide) et l'écran affiche un [ PAUSE ] clignotant.
    • Si le lecteur est totalement à l'arrêt (idle ou off), l'écran affiche un [ STOP ] clignotant très rétro.
    • (Bonus : Ces textes sont facilement traduisibles via la [ZONE MODIFIABLE] en haut du code !)

2. Des boutons tactiles "Verre Bombé" :control_knobs:

  • Tout le design (ombres internes inset et externes) est fait en CSS pur pour simuler de vrais boutons physiques qu'on enfonce sous le doigt.
  • Rétro-éclairage dynamique : Le bouton Play s'allume en vert uniquement quand la musique joue. Les boutons Shuffle et Repeat s'illuminent aussi pour refléter l'état réel du lecteur.
  • Bouton Stop adaptatif : Grâce à une lecture du supported_features de l'entité, le bouton Stop envoie une vraie commande media_stop si l'appareil le gère, sinon il agit sagement comme une fonction Pause pour éviter les erreurs HA !

3. Le curseur de progression (Fader) :level_slider:

  • Il s'agit d'un véritable curseur interactif natif (input type="range") maquillé de la tête aux pieds en CSS. Il se colore en vert au fil de la lecture, et permet de naviguer dans la piste de manière ultra-fluide.

:laptop: Le Code

Pour l'utiliser, le seul prérequis est d'avoir la carte custom:button-card installée via HACS.
Ajoutez une carte manuelle dans votre Dashboard, collez le code, et changez l'entité ligne 2.

:light_bulb: Note : J'ai pris le temps de commenter le code au maximum pour vous aider à comprendre sa logique et faciliter vos propres modifications. Cherchez les balises [ZONE MODIFIABLE] pour personnaliser facilement les couleurs et les textes !

# ==============================================================================
# 🎵 LECTEUR WINAMP STANDALONE - HOME ASSISTANT
# ==============================================================================
# Type : custom:button-card (Nécessite l'installation via HACS)
# Description : Carte générique reproduisant l'esthétique et la logique d'un lecteur Winamp classique.
# Auteur : Gaël avec l'aide de l'IA Antigravity
# Prérequis :
#   - Remplacer l'entité ci-dessous par votre propre media_player.
# ==============================================================================
type: custom:button-card
entity: [A_CHANGER]
show_name: false
show_icon: false
show_state: false
show_ripple: false
tap_action:
  action: none
hold_action:
  action: none
double_tap_action:
  action: none

styles:
  card:
    - padding: "12px"
    - color: "#ffffff"
    - overflow: "visible"
    - pointer-events: "auto"
    - box-sizing: "border-box"
    - width: "100%"
  grid:
    - padding: 0
    - width: 100% !important
    - min-width: 100% !important
    - flex: 1
    - justify-self: stretch
    - align-self: stretch
    # --- ARCHITECTURE DE LA CARTE (CSS GRID) --- 
    # Le 'grid-template-areas' définit l'ossature (les fondations).
    # Ici : la pochette ("art") prend toute la colonne de gauche.
    # À droite s'empilent l'écran ("info"), la barre ("progress") et les boutons ("controls").
    - grid-template-areas: |
        "art info"
        "art progress"
        "art controls"
    - grid-template-columns: var(--size-art-cover) calc(100% - var(--size-art-cover) - 24px)
    - grid-template-rows: 1fr auto auto
    - column-gap: 24px
    - row-gap: 8px
  custom_fields:
    art:
      - justify-self: start
      - align-self: start
      - grid-row: 1 / -1
    info:
      - justify-self: stretch
      - align-self: stretch
      - width: 100%
      - height: 100%
    progress:
      - justify-self: stretch
      - align-self: center
      - pointer-events: auto
    controls:
      - justify-self: start
      - align-self: center
      - display: flex
      - gap: 4px
      - width: 100%

custom_fields:
  art: >
    [[[
      // --- 1. LOGIQUE DE LA POCHETTE (ART COVER) ---
      // Extraction des attributs du media_player
      const attr = entity ? entity.attributes : {};
      
      // La pochette n'est visible que si la musique est EN COURS (playing).
      // Cela évite d'afficher une ancienne pochette restée en cache lorsque le lecteur est arrêté.
      const isVisible = entity && entity.state === 'playing';
      const image = isVisible ? (attr.entity_picture || '') : '';
      
      return `
        <!-- 
          [ZONE MODIFIABLE] : DIMENSIONS DE LA POCHETTE (HTML)
          ⚠️ ATTENTION : Si vous modifiez 'width' et 'height' ici (ex: 100px), 
          vous DEVEZ obligatoirement aller tout en bas du fichier et modifier la 
          variable globale '--size-art-cover' avec la même valeur.
        -->
        <div style="width: 190px; height: 190px; border-radius: var(--radius-md); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); background: radial-gradient(ellipse at center, #1a1a1a 0%, #000000 100%); box-shadow: var(--shadow-encastrement); position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center;">
          ${image ? `<img src="${image}" style="width: 100%; height: 100%; object-fit: cover;">` : `<ha-icon icon="mdi:music-circle" style="width: 50%; height: 50%; color: var(--color-chassis-highlight-deep);"></ha-icon>`}
          <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%, rgba(0,0,0,0.3) 100%); pointer-events: none;"></div>
        </div>
      `;
    ]]]
  info: >
    [[[
      // --- 2. LOGIQUE DE L'ÉCRAN LCD ---
      const attr = entity ? entity.attributes : {};
      
      // [ZONE MODIFIABLE] : TEXTES PAR DÉFAUT (Quand aucune musique n'est lancée)
      // Modifiez ces textes ("Stopped", "Paused") par ce que vous préférez voir à l'arrêt
      const title = attr.media_title || (entity ? 'Stopped' : 'En attente...');
      const artist = attr.media_artist || (entity ? 'Paused' : 'Veuillez configurer un lecteur');
      const album = attr.media_album_name || '';
      
      // Conversion de la durée totale (secondes) en format lisible (MM:SS)
      let timeString = '';
      if (attr.media_duration) {
        const mins = Math.floor(attr.media_duration / 60);
        const secs = Math.floor(attr.media_duration % 60);
        timeString = `${mins}:${secs.toString().padStart(2, '0')}`;
      } else if (!entity) {
        timeString = '0:00';
      }

      // Construction du sous-titre complet (Titre - Album (Temps))
      let subtitle = title;
      if (album) subtitle += ` - ${album}`;
      if (timeString) subtitle += ` (${timeString})`;
      
      // --- ANIMATION DE DÉFILEMENT (MARQUEE) ---
      // On vérifie si les textes dépassent la largeur de l'écran LCD
      let artistNeedsScroll = artist.length > 18;
      let subtitleNeedsScroll = subtitle.length > 28;
      
      // [ZONE MODIFIABLE] : VITESSE DE DÉFILEMENT DU TEXTE (MARQUEE)
      // Modificateurs de vitesse : Plus le chiffre est grand, plus le texte défile LENTEMENT.
      const speedArtist = 0.55;   // Vitesse de la ligne 1 (Artiste)
      const speedSubtitle = 0.40; // Vitesse de la ligne 2 (Titre et Album)
      
      // La vitesse de défilement s'adapte automatiquement au nombre de caractères
      let artistDuration = artist.length * speedArtist;
      let subtitleDuration = subtitle.length * speedSubtitle;
      
      // Synchronisation des deux lignes pour qu'elles tournent en boucle ensemble
      let maxDuration = 0;
      if (artistNeedsScroll && subtitleNeedsScroll) {
        maxDuration = Math.max(artistDuration, subtitleDuration);
      } else if (artistNeedsScroll) {
        maxDuration = artistDuration;
      } else if (subtitleNeedsScroll) {
        maxDuration = subtitleDuration;
      }
      
      let artistPercentage = maxDuration > 0 ? (artistDuration / maxDuration) * 100 : 100;
      let subtitlePercentage = maxDuration > 0 ? (subtitleDuration / maxDuration) * 100 : 100;
      
      artistPercentage = Math.min(100, Math.max(0, artistPercentage));
      subtitlePercentage = Math.min(100, Math.max(0, subtitlePercentage));
      
      // --- ÉTATS DU LECTEUR ---
      const isPaused = entity ? (entity.state === 'paused' || entity.state === 'idle') : true;
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const shuffle = attr.shuffle || false;
      const repeat = attr.repeat || 'off';
      
      // [ZONE MODIFIABLE] : TEXTES D'ARRÊT (LCD)
      // Remplacez les mots ci-dessous si vous préférez du français (ex: "EN PAUSE", "ARRÊTÉ")
      const textStop = "STOP";
      const textPause = "PAUSE";
      const textStatus = entity ? (entity.state === 'idle' ? textStop : textPause) : 'NO ENTITY';
      
      // Source du lecteur (Fallbacks : attribut source > spécificité Spotify > Nom de l'enceinte > Inconnu)
      const source = attr.source || attr.sp_device_name || attr.friendly_name || 'Inconnu';
      
      // --- GÉNÉRATEUR DYNAMIQUE D'ÉGALISEUR ---
      const numEqBars = 16; // [ZONE MODIFIABLE] : Nombre de barres de l'égaliseur (ex: 5, 16, 25)
      let eqHtml = '';
      // Séquence d'animations déterministe pour éviter les sauts graphiques
      const eqPattern = [1, 5, 9, 3, 7, 2, 6, 10, 4, 8, 3, 9, 2, 8, 5, 1, 7, 10, 6, 4];
      for(let i = 0; i < numEqBars; i++) {
        let typeIndex = eqPattern[i % eqPattern.length];
        eqHtml += `<div class="bar bar${typeIndex}"></div>`;
      }
      
      return `
        <style>
          /* Génération dynamique des keyframes d'animation CSS (Marquee) */
          @keyframes sync-marquee-artist {
            0% { transform: translateX(0); }
            ${artistPercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes sync-marquee-subtitle {
            0% { transform: translateX(0); }
            ${subtitlePercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes fadeBlink { 
            0% { opacity: 0.2; } 
            100% { opacity: 1; } 
          }
        </style>
        <!-- 
          [ZONE MODIFIABLE] : ESPACEMENT (PADDING) DE L'ÉCRAN LCD
          Si vous voulez que l'écran soit moins haut, vous devez réduire le 'padding' 
          (actuellement à 8px 12px) dans la balise div ci-dessous, ET réduire la taille 
          des polices HTML (voir plus bas).
        -->
        <div style="box-sizing: border-box; width: 100%; height: 100%; background: var(--bg-screen-lcd); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); border-radius: var(--radius-md); padding: 8px 12px; display: flex; flex-direction: column; justify-content: space-between; text-align: left; overflow: hidden; box-shadow: var(--shadow-encastrement); position: relative;">
          <!-- Reflet vitré -->
          <div style="position: absolute; top: 0; left: 0; right: 0; height: 50%; background: linear-gradient(to bottom, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.02) 100%); pointer-events: none; z-index: 2;"></div>
          
          ${isPaused ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-strong); animation: fadeBlink 1.5s alternate infinite ease-in-out;">
              [ ${textStatus} ]
            </div>
          </div>
          ` : `
          <div style="display: flex; justify-content: space-between; width: 100%; position: relative; z-index: 1;">
            
            <!-- Left Column: Artist + Subtitle -->
            <div style="display: flex; flex-direction: column; overflow: hidden; flex-grow: 1;">
              
              <!-- [ZONE MODIFIABLE] TEXTE ARTISTE (Modifiez le font-size: 16px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; position: relative;">
                <div style="${artistNeedsScroll ? `animation: sync-marquee-artist ${maxDuration}s linear infinite;` : ''} font-size: 16px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-soft); display: inline-flex; width: max-content;">
                  <span style="padding-right: ${artistNeedsScroll ? '50px' : '0'};">${artist}</span>
                  ${artistNeedsScroll ? `<span style="padding-right: 50px;">${artist}</span>` : ''}
                </div>
              </div>

              <!-- [ZONE MODIFIABLE] TEXTE SOUS-TITRE (Modifiez le font-size: 12px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; margin-top: 4px; position: relative;">
                <div style="${subtitleNeedsScroll ? `animation: sync-marquee-subtitle ${maxDuration}s linear infinite;` : ''} font-size: 12px; font-family: var(--font-family-lcd); color: var(--color-dos-green); display: inline-flex; width: max-content; opacity: 0.7;">
                  <span style="padding-right: ${subtitleNeedsScroll ? '50px' : '0'};">${subtitle}</span>
                  ${subtitleNeedsScroll ? `<span style="padding-right: 50px;">${subtitle}</span>` : ''}
                </div>
              </div>
            </div>

          </div>
          `}
          


          <div style="margin-top: 12px; display: flex; justify-content: space-between; align-items: flex-end; width: 100%; height: var(--size-eq-height, 20px);">
            <div class="winamp-eq ${isPlaying ? 'playing' : ''}">
              ${eqHtml}
            </div>

            <div style="display: flex; gap: 8px; align-items: center; font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: var(--font-weight-lcd); margin-bottom: 2px;">
              <div style="color: ${shuffle ? 'var(--color-dos-green)' : 'rgba(var(--color-dos-green-rgb), 0.2)'}; filter: ${shuffle ? 'drop-shadow(0 0 2px rgba(var(--color-dos-green-rgb), 0.8))' : 'none'}; display: flex;">
                <ha-icon icon="mdi:shuffle" style="--mdc-icon-size: 14px;"></ha-icon>
              </div>
              <div style="color: ${repeat && repeat !== 'off' ? 'var(--color-dos-green)' : 'rgba(var(--color-dos-green-rgb), 0.2)'}; filter: ${repeat && repeat !== 'off' ? 'drop-shadow(0 0 2px rgba(var(--color-dos-green-rgb), 0.8))' : 'none'}; display: flex;">
                <ha-icon icon="${repeat === 'one' ? 'mdi:repeat-once' : 'mdi:repeat'}" style="--mdc-icon-size: 14px;"></ha-icon>
              </div>
              <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); margin-left: 8px;">OUT: ${source}</div>
            </div>
          </div>

        </div>
      `;
    ]]]
  progress: >
    [[[
      // --- 3. BARRE DE PROGRESSION (SLIDER) ---
      const attr = entity ? entity.attributes : {};
      let pct = 0;
      let duration = attr.media_duration || 100;
      let position = attr.media_position || 0;
      
      // Calcul du pourcentage d'avancement
      if (duration && position) {
        pct = (position / duration) * 100;
      }
      
      // Génération d'un ID unique pour éviter les conflits HTML si plusieurs lecteurs
      const sliderId = 'slider-' + Math.random().toString(36).substr(2, 9);
      
      return `
        <!-- Input natif 'range' maquillé en curseur Winamp -->
        <input type="range" class="glass-slider" id="${sliderId}" min="0" max="${duration}" value="${position}" step="1"
          ${!entity ? 'disabled' : ''}
          style="--slider-pct: ${pct}%;"
          onpointerdown="event.stopPropagation();"
          onclick="event.stopPropagation();"
          oninput="
            /* Met à jour visuellement la barre de progression de couleur instantanément */
            this.style.setProperty('--slider-pct', (this.value / this.max) * 100 + '%');
          "
          onchange="
            /* Envoie la nouvelle position au service HA (media_seek) lors du relâchement */
            if (!'${entity ? entity.entity_id : ''}') return;
            document.querySelector('home-assistant').hass.callService('media_player', 'media_seek', {
              entity_id: '${entity ? entity.entity_id : ''}',
              seek_position: parseFloat(this.value)
            });
          "
        >
      `;
    ]]]
  controls: >
    [[[
      // --- 4. BOUTONS DE CONTRÔLE MULTIMÉDIA ---
      // Détection de l'état actuel pour afficher dynamiquement l'icône Play ou Pause
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const eid = entity ? entity.entity_id : '';
      const attr = entity ? entity.attributes : {};
      
      // Détection de la fonction STOP matérielle
      // Le chiffre '4096' est le code officiel (Bitmask SUPPORT_STOP) de Home Assistant
      // Il permet de vérifier mathématiquement si l'enceinte possède la capacité d'être complètement stoppée.
      const supportsStop = attr.supported_features ? (attr.supported_features & 4096) !== 0 : false;
      const stopService = supportsStop ? 'media_stop' : 'media_pause'; // Repli sur 'pause' si stop n'est pas géré
      
      // Gestion de l'état "Shuffle" (Aléatoire)
      const isShuffle = attr.shuffle === true;
      
      // Gestion intelligente du mode boucle (Repeat)
      // Cycle standard : off -> all -> one -> off
      const repeatMode = attr.repeat || 'off';
      const isRepeat = repeatMode !== 'off';
      const repeatIcon = repeatMode === 'one' ? 'mdi:repeat-once' : (repeatMode === 'all' ? 'mdi:repeat' : 'mdi:repeat-off');
      
      // Calcul du prochain état de boucle lors du clic sur le bouton Repeat
      let nextRepeat = 'all';
      if (repeatMode === 'all') nextRepeat = 'one';
      if (repeatMode === 'one') nextRepeat = 'off';

      return `
        <!-- Conteneur des boutons alignés horizontalement -->
        <div style="display: flex; gap: 8px; align-items: center; justify-content: flex-start; padding-bottom: 4px;">
          
          <!-- Bouton PRÉCÉDENT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_previous_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-previous"></ha-icon>
          </div>
          
          <!-- Bouton LECTURE (S'illumine via la classe 'active' si la musique joue) -->
          <div class="glass-btn ${isPlaying ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_play', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:play"></ha-icon>
          </div>

          <!-- Bouton PAUSE -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_pause', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:pause"></ha-icon>
          </div>

          <!-- Bouton STOP (Dynamique : appelle 'media_stop' si géré par le lecteur, sinon 'media_pause' en secours) -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', '${stopService}', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:stop"></ha-icon>
          </div>
          
          <!-- Bouton SUIVANT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_next_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-next"></ha-icon>
          </div>

          <!-- Bouton ALÉATOIRE (Plus petit via 'glass-btn-sm', s'illumine si actif) -->
          <div class="glass-btn glass-btn-sm ${isShuffle ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'shuffle_set', {entity_id: '${eid}', shuffle: ${!isShuffle}})">
            <ha-icon icon="mdi:shuffle"></ha-icon>
          </div>
          
          <!-- Bouton BOUCLE (Envoie la commande avec la variable calculée 'nextRepeat') -->
          <div class="glass-btn glass-btn-sm ${isRepeat ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'repeat_set', {entity_id: '${eid}', repeat: '${nextRepeat}'})">
            <ha-icon icon="${repeatIcon}"></ha-icon>
          </div>
        </div>
      `;
    ]]]

extra_styles: |
  /* ==========================================================================
     [ZONES MODIFIABLES] : STYLES CSS (COULEURS & THÈMES)
     C'est ici que vous pouvez changer les couleurs globales du lecteur Winamp.
     Remplacez les codes (#XXXXXX) ou les noms de couleurs pour adapter le thème.
     ========================================================================== */
  :host {
    /* --- COULEURS DU CHÂSSIS --- 
       Jeux d'ombres et lumières pour créer l'effet 3D "encastré" */
    --color-chassis-base: #1c1c1c;
    --color-chassis-shadow-deep: #0a0a0a;
    --color-chassis-shadow-light: #111111;
    --color-chassis-highlight-deep: #333333;
    --color-chassis-highlight-light: #2b2b2b;
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES TEXTES ET BOUTONS
       --------------------------------------------------------- */
    --color-dos-green: #00FF00; /* Le fameux vert Winamp (Textes de l'écran) */
    --color-dos-green-rgb: 0, 255, 0; /* Mettez les valeurs RGB de votre couleur ci-dessus (ex: pour le vert) */
    --color-neon-active: rgb(138, 43, 226); /* Couleur des boutons physiques (Violet par défaut) */
    --color-success: rgb(46, 139, 87); /* Couleur du bouton actif (Lecture en cours) */
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES DÉGRADÉS (FOND ET ÉGALISEUR)
       --------------------------------------------------------- */
    --bg-screen-lcd: radial-gradient(ellipse at center, #0f240f 0%, #030803 80%, #000000 100%); /* Fond de l'écran LCD */
    
    /* 
       Couleurs de l'égaliseur animé :
       - Actuellement en "Dégradé doux" : Vert de 0%, Jaune progressif jusqu'à 60%, Rouge à 100%.
       - Si vous voulez que le rouge arrive plus bas, baissez les pourcentages (ex: #FFFF00 30%, #FF0000 60%).
       - Si vous voulez des "Blocs LED" nets (sans mélange baveux), doublez les pourcentages au point de rupture :
         Exemple LED : linear-gradient(to top, #00FF00 0%, #00FF00 60%, #FFFF00 60%, #FFFF00 85%, #FF0000 85%, #FF0000 100%)
       - Vous pouvez empiler autant de couleurs que vous le souhaitez.
    */
    --bg-eq-bars: linear-gradient(to top, #00FF00 0%, #FFFF00 60%, #FF0000 100%);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : TYPOGRAPHIE LCD
       Ajustez la taille de police si l'écran LCD est trop petit chez vous
       --------------------------------------------------------- */
    --font-family-lcd: monospace;
    --font-weight-lcd: bold;
    --font-size-lcd-large: 24px;
    --font-size-lcd-xs: 10px;
    --letter-spacing-lcd: 4px;
    
    /* --- EFFETS DE LUEUR (NÉON) --- */
    --text-shadow-lcd-strong: 0 0 8px rgba(0, 255, 0, 0.8);
    --text-shadow-lcd-soft: 0 0 5px rgba(0, 255, 0, 0.5);
    --shadow-neon-glow-active: drop-shadow(0 0 5px var(--color-neon-active)); /* Aura autour du bouton physique pressé */
    --shadow-neon-glow-success: drop-shadow(0 0 6px var(--color-success)); /* Aura autour du bouton "Lecture" actif */
    --opacity-lcd-dim: 0.8;
    --opacity-lcd-dimmer: 0.7;
    
    /* ---------------------------------------------------------
       EFFETS "VERRE BOMBÉ" (BOUTONS PHYSIQUES)
       Construits avec de multiples couches d'ombres et reflets
       --------------------------------------------------------- */
    /* Fonds sphériques pour créer le volume du bouton */
    --bg-verre-bombe: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.02) 50%, rgba(0,0,0,0.4) 100%);
    --bg-verre-bombe-pressed: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0.6) 100%);
    --bg-verre-bombe-active: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.08) 50%, rgba(0,0,0,0.2) 100%);
    
    /* Ombres externes (portée sur le châssis) et internes (relief bombé) */
    --shadow-verre-bombe: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 3px rgba(255,255,255,0.3), inset -2px -2px 6px rgba(0,0,0,0.7);
    --shadow-verre-bombe-pressed: 0px 1px 2px rgba(0,0,0,0.8), -1px -1px 1px rgba(255,255,255,0.08), inset 2px 2px 8px rgba(0,0,0,0.8), inset -1px -1px 2px rgba(255,255,255,0.1);
    --shadow-verre-bombe-active: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 4px rgba(255,255,255,0.5), inset -2px -2px 6px rgba(0,0,0,0.7);
    
    /* --- COULEURS ET OMBRES DE LA BARRE DE PROGRESSION (SLIDER) --- */
    --slider-track-bg-off: rgba(0, 0, 0, 0.6); /* Couleur du fond de la piste */
    --slider-track-border: 1px solid rgba(255, 255, 255, 0.05);
    --slider-track-shadow: inset 0 1px 3px rgba(0,0,0,0.8);
    --slider-thumb-bg: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 40%, rgba(0,0,0,0.2) 100%); /* Effet verre du curseur */
    --slider-thumb-border: 1px solid rgba(255, 255, 255, 0.1);
    --slider-thumb-border-top: 1px solid rgba(255, 255, 255, 0.5);
    --slider-thumb-border-left: 1px solid rgba(255, 255, 255, 0.3);
    --slider-thumb-shadow: 2px 4px 6px rgba(0,0,0,0.5), inset 1px 1px 2px rgba(255,255,255,0.5), inset -1px -1px 2px rgba(0,0,0,0.4);
    --slider-thumb-backdrop: blur(12px) brightness(1.15); /* Floutage derrière le curseur */
    
    /* Ombre d'encastrement globale (L'écran entier semble creusé dans le plastique) */
    --shadow-encastrement: inset 2px 2px 5px rgba(255, 255, 255, 0.25), inset -3px -3px 8px rgba(0,0,0,1), inset 0 0 20px rgba(0,0,0,0.8);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : DIMENSIONS ET TAILLES GLOBALES
       Ajustez ces valeurs pour redimensionner globalement les éléments
       --------------------------------------------------------- */
    --size-art-cover: 190px; /* Largeur de la pochette (La grille s'ajustera automatiquement) */
    --size-btn-physique: 28px; /* Diamètre des boutons de contrôle principaux */
    --size-btn-physique-sm: 20px; /* Diamètre des petits boutons (Shuffle/Repeat) */
    --size-eq-height: 20px; /* [ZONE MODIFIABLE] Hauteur de l'égaliseur dynamique */
    --radius-md: 12px; /* Arrondi des angles de la pochette et de l'écran */
  }

  /* --- ANIMATION GLOBALE DE LA CARTE --- */
  /* Style de base de la carte : bordures subtiles et ombres complexes pour l'effet de profondeur */
  ha-card {
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.02) 100%) !important;
    border: 1.5px solid rgba(255, 255, 255, 0.15) !important;
    border-radius: 24px !important;
    box-shadow: 0 16px 36px #000000CC, 0 0 1px #FFFFFF1A, 0 4px 12px #FFFFFF08, inset 0 1.5px 1.5px #FFFFFF59, inset 0 3px 10px #FFFFFF1F, inset 0 -3px 6px #00000099 !important;
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
  }

  /* 
     [ZONE MODIFIABLE] : EFFET AU SURVOL (HOVER) 
     La carte "monte" de 4px vers le haut quand on passe la souris. 
     Mettez translateY(0px) si vous ne voulez pas d'animation.
  */
  ha-card:hover {
    transform: translateY(-4px) !important;
    border-color: rgba(255, 255, 255, 0.25) !important;
    box-shadow: 0 24px 48px #000000D9, 0 0 2px #FFFFFF26, 0 6px 16px #FFFFFF0D, inset 0 1.5px 1.5px #FFFFFF73, inset 0 3px 10px #FFFFFF2E, inset 0 -3px 6px #000000B3 !important;
  }

  /* --- STYLES DE LA BARRE DE PROGRESSION --- */
  .glass-slider {
    -webkit-appearance: none;
    width: 100%;
    height: 32px;
    background: transparent;
    outline: none;
    cursor: pointer;
    margin: 0;
  }
  
  /* Piste du slider (La rigole creusée dans le châssis) */
  .glass-slider::-webkit-slider-runnable-track {
    width: 100%;
    height: 8px;
    background: linear-gradient(to right, var(--color-success) var(--slider-pct, 0%), var(--slider-track-bg-off) var(--slider-pct, 0%));
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }
  
  /* 
     ==========================================================================
     [ZONE MODIFIABLE] : APPARENCE DU CURSEUR (Slider Thumb)
     ⚠️ RÈGLE D'OR DU WEB : Modifiez TOUJOURS les DEUX blocs ci-dessous à l'identique !
     - Le 1er bloc (-webkit) contrôle : Chrome, Edge, Brave, Safari, Appli Mobile.
     - Le 2ème bloc (-moz) contrôle : Firefox.
     Si vous modifiez la taille (width: 14px; height: 24px) dans l'un, faites-le dans l'autre !
     ========================================================================== 
  */
  .glass-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 24px;
    margin-top: -8px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }
  
  /* --- COMPATIBILITÉ FIREFOX --- */
  /* Piste du slider (Version Firefox) */
  .glass-slider::-moz-range-track {
    width: 100%;
    height: 8px;
    background: linear-gradient(to right, var(--color-success) var(--slider-pct, 0%), var(--slider-track-bg-off) var(--slider-pct, 0%));
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }
  
  /* [ZONE MODIFIABLE] : Curseur effet verre (Version Firefox - Gardez les mêmes dimensions que -webkit ci-dessus !) */
  .glass-slider::-moz-range-thumb {
    width: 14px;
    height: 24px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }

  /* --- STYLES DES BOUTONS PHYSIQUES (.glass-btn) --- */
  .glass-btn {
    width: var(--size-btn-physique);
    height: var(--size-btn-physique);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-verre-bombe);
    border: 2px solid var(--color-chassis-base);
    box-shadow: var(--shadow-verre-bombe);
    cursor: pointer;
    transition: transform 0.1s ease, box-shadow 0.1s ease;
  }

  .glass-btn-sm {
    width: var(--size-btn-physique-sm) !important;
    height: var(--size-btn-physique-sm) !important;
  }

  /* [ZONE MODIFIABLE] : APPARENCE DES ICÔNES DES BOUTONS */
  .glass-btn ha-icon {
    --mdc-icon-size: 18px; /* Taille des icônes à l'intérieur du bouton */
    /* La couleur pointe vers la variable globale '--color-neon-active'.
       Vous pouvez soit changer la variable tout en haut, soit écrire votre couleur ici (ex: red) */
    color: var(--color-neon-active);
    opacity: 0.95;
    filter: var(--shadow-neon-glow-active);
    transition: all 0.2s ease;
  }

  .glass-btn-sm ha-icon {
    --mdc-icon-size: 14px !important;
  }

  /* Effet d'enfoncement physique au clic (active) */
  .glass-btn:active {
    transform: translateY(2px);
    background: var(--bg-verre-bombe-pressed);
    box-shadow: var(--shadow-verre-bombe-pressed);
  }

  /* L'icône s'illumine lors de la pression */
  .glass-btn:active ha-icon {
    color: var(--color-success) !important;
    filter: var(--shadow-neon-glow-success) !important;
    opacity: 1 !important;
  }

  .glass-btn.active {
    background: var(--bg-verre-bombe-active);
    box-shadow: var(--shadow-verre-bombe-active);
  }

  .glass-btn.active ha-icon {
    color: var(--color-success);
    opacity: 1;
    filter: var(--shadow-neon-glow-success);
  }

  /* --- EFFET MARQUEE (DÉFILEMENT DE TEXTE) --- */
  /* La classe marquee applique une translation continue de droite à gauche */
  .marquee {
    animation: marquee 15s linear infinite;
  }
  
  /* L'animation translate de 0 à -50% (car le texte est dupliqué 2 fois dans le HTML pour boucler) */
  @keyframes marquee {
    0% { transform: translateX(0); }
    100% { transform: translateX(-50%); }
  }
  
  /* --- STYLES DE L'ÉGALISEUR --- */
  /* Conteneur principal des barres */
  .winamp-eq {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: var(--size-eq-height, 20px);
  }
  
  /* Style de base d'une seule barre de l'égaliseur */
  .winamp-eq .bar {
    width: 4px;
    height: 2px;
    background: var(--bg-eq-bars);
    background-size: 100% var(--size-eq-height, 20px);
    background-position: bottom;
    border-radius: 1px 1px 0 0;
    opacity: 0.85;
  }
  
  /* Attribution des animations individuelles aux 10 types de barres (uniquement si classe 'playing' active) */
  .winamp-eq.playing .bar1 { animation: eq1 0.4s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar2 { animation: eq2 0.6s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar3 { animation: eq3 0.5s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar4 { animation: eq4 0.7s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar5 { animation: eq5 0.45s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar6 { animation: eq6 0.65s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar7 { animation: eq7 0.55s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar8 { animation: eq8 0.75s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar9 { animation: eq9 0.4s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar10 { animation: eq10 0.8s infinite alternate ease-in-out; }

  /* Animations relatives (%) pour s'adapter dynamiquement à var(--size-eq-height) */
  @keyframes eq1 { 0% { height: 10%; } 100% { height: 80%; } }
  @keyframes eq2 { 0% { height: 20%; } 100% { height: 100%; } }
  @keyframes eq3 { 0% { height: 10%; } 100% { height: 70%; } }
  @keyframes eq4 { 0% { height: 30%; } 100% { height: 90%; } }
  @keyframes eq5 { 0% { height: 15%; } 100% { height: 75%; } }
  @keyframes eq6 { 0% { height: 25%; } 100% { height: 95%; } }
  @keyframes eq7 { 0% { height: 10%; } 100% { height: 60%; } }
  @keyframes eq8 { 0% { height: 20%; } 100% { height: 85%; } }
  @keyframes eq9 { 0% { height: 10%; } 100% { height: 80%; } }
  @keyframes eq10 { 0% { height: 25%; } 100% { height: 100%; } }

J'espère que ce petit bond nostalgique dans le passé plaira aux anciens (et aux plus jeunes) ! N'hésitez pas si vous avez des questions ou des suggestions. :blush:


":police_car_light: SPOILER ALERT : La suite arrive...
Si cette carte vous plaît et qu'il y a de la demande, je suis en train de peaufiner l'intégration d'un véritable Fader de Volume vertical. Toujours fidèle à l'esprit Winamp et au design Glassmorphisme !
volume fader

Hello,

Un code simple comme on aime :grin:

cdt

ca ....
environ 600 lignes, commentaires inclus, ca reste raisonnable.
j'en connais qui font pire :smiley:

Aucune idée de qui tu parles :upside_down_face:

j'ai avancé un peu dans ma vue multimedia pour y rajouter la gestion du volumes, un petit screenshot du rendu

prochaine etape les playlists :smiley:

dans la lancé la liste des morceau en file d'attente, interactive
carte playlist queue

Salut,
Ça aurait été pas mal de mettre le temps restant et total du morceau dans la barre d'avancement. Perso, je trouve qu'avoir le temps du morceau avec le titre du morceau dans l'écran en scrolling, c'est un peu long pour voir l'information.

Les GIF son petit, on voit pas trop. Il me faut des lunettes, j'y vois plus de près lol.

Salut,

c'est que je n'y avais pas pensé, mais c'est faisable, merci pour l'idée

Plutot que dans la barre d'avancement, j'utilise l'ecran


visible facilement et ca rentre bien dans l'idée

le code modifier

# ==============================================================================
# 🎵 LECTEUR WINAMP STANDALONE - HOME ASSISTANT
# ==============================================================================
# Type : custom:button-card (Nécessite l'installation via HACS)
# Description : Carte générique reproduisant l'esthétique et la logique d'un lecteur Winamp classique.
# Auteur : Gaël avec l'aide de l'IA Antigravity
# Prérequis :
#   - Remplacer l'entité ci-dessous par votre propre media_player.
# ==============================================================================
type: custom:button-card
entity: media_player.spotifyplus_gael
show_name: false
show_icon: false
show_state: false
show_ripple: false
tap_action:
  action: none
hold_action:
  action: none
double_tap_action:
  action: none

styles:
  card:
    - padding: "12px"
    - color: "#ffffff"
    - overflow: "visible"
    - pointer-events: "auto"
    - box-sizing: "border-box"
    - width: "100%"
  grid:
    - padding: 0
    - width: 100% !important
    - min-width: 100% !important
    - flex: 1
    - justify-self: stretch
    - align-self: stretch
    # --- ARCHITECTURE DE LA CARTE (CSS GRID) --- 
    # Le 'grid-template-areas' définit l'ossature (les fondations).
    # Ici : la pochette ("art") prend toute la colonne de gauche.
    # À droite s'empilent l'écran ("info"), la barre ("progress") et les boutons ("controls").
    - grid-template-areas: |
        "art info"
        "art progress"
        "art controls"
    - grid-template-columns: var(--size-art-cover) calc(100% - var(--size-art-cover) - 24px)
    - grid-template-rows: 1fr auto auto
    - column-gap: 24px
    - row-gap: 8px
  custom_fields:
    art:
      - justify-self: start
      - align-self: start
      - grid-row: 1 / -1
    info:
      - justify-self: stretch
      - align-self: stretch
      - width: 100%
      - height: 100%
    progress:
      - justify-self: stretch
      - align-self: center
      - pointer-events: auto
    controls:
      - justify-self: start
      - align-self: center
      - display: flex
      - gap: 4px
      - width: 100%

custom_fields:
  art: >
    [[[
      // --- 1. LOGIQUE DE LA POCHETTE (ART COVER) ---
      // Extraction des attributs du media_player
      const attr = entity ? entity.attributes : {};
      
      // La pochette n'est visible que si la musique est EN COURS (playing).
      // Cela évite d'afficher une ancienne pochette restée en cache lorsque le lecteur est arrêté.
      const isVisible = entity && entity.state === 'playing';
      const image = isVisible ? (attr.entity_picture || '') : '';
      
      return `
        <!-- 
          [ZONE MODIFIABLE] : DIMENSIONS DE LA POCHETTE (HTML)
          ⚠️ ATTENTION : Si vous modifiez 'width' et 'height' ici (ex: 100px), 
          vous DEVEZ obligatoirement aller tout en bas du fichier et modifier la 
          variable globale '--size-art-cover' avec la même valeur.
        -->
        <div style="width: 190px; height: 190px; border-radius: var(--radius-md); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); background: radial-gradient(ellipse at center, #1a1a1a 0%, #000000 100%); box-shadow: var(--shadow-encastrement); position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center;">
          ${image ? `<img src="${image}" style="width: 100%; height: 100%; object-fit: cover;">` : `<ha-icon icon="mdi:music-circle" style="width: 50%; height: 50%; color: var(--color-chassis-highlight-deep);"></ha-icon>`}
          <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%, rgba(0,0,0,0.3) 100%); pointer-events: none;"></div>
        </div>
      `;
    ]]]
  info: >
    [[[
      // --- 2. LOGIQUE DE L'ÉCRAN LCD ---
      const attr = entity ? entity.attributes : {};
      
      // [ZONE MODIFIABLE] : TEXTES PAR DÉFAUT (Quand aucune musique n'est lancée)
      // Modifiez ces textes ("Stopped", "Paused") par ce que vous préférez voir à l'arrêt
      const title = attr.media_title || (entity ? 'Stopped' : 'En attente...');
      const artist = attr.media_artist || (entity ? 'Paused' : 'Veuillez configurer un lecteur');
      const album = attr.media_album_name || '';
      
      // Conversion de la durée totale (secondes) en format lisible (MM:SS)
      let timeString = '';
      if (attr.media_duration) {
        const mins = Math.floor(attr.media_duration / 60);
        const secs = Math.floor(attr.media_duration % 60);
        timeString = `${mins}:${secs.toString().padStart(2, '0')}`;
      } else if (!entity) {
        timeString = '0:00';
      }

      // Construction du sous-titre complet (Titre - Album)
      let subtitle = title;
      if (album) subtitle += ` - ${album}`;
      
      // --- ANIMATION DE DÉFILEMENT (MARQUEE) ---
      // On vérifie si les textes dépassent la largeur de l'écran LCD
      let artistNeedsScroll = artist.length > 18;
      let subtitleNeedsScroll = subtitle.length > 28;
      
      // [ZONE MODIFIABLE] : VITESSE DE DÉFILEMENT DU TEXTE (MARQUEE)
      // Modificateurs de vitesse : Plus le chiffre est grand, plus le texte défile LENTEMENT.
      const speedArtist = 0.55;   // Vitesse de la ligne 1 (Artiste)
      const speedSubtitle = 0.40; // Vitesse de la ligne 2 (Titre et Album)
      
      // La vitesse de défilement s'adapte automatiquement au nombre de caractères
      let artistDuration = artist.length * speedArtist;
      let subtitleDuration = subtitle.length * speedSubtitle;
      
      // Synchronisation des deux lignes pour qu'elles tournent en boucle ensemble
      let maxDuration = 0;
      if (artistNeedsScroll && subtitleNeedsScroll) {
        maxDuration = Math.max(artistDuration, subtitleDuration);
      } else if (artistNeedsScroll) {
        maxDuration = artistDuration;
      } else if (subtitleNeedsScroll) {
        maxDuration = subtitleDuration;
      }
      
      let artistPercentage = maxDuration > 0 ? (artistDuration / maxDuration) * 100 : 100;
      let subtitlePercentage = maxDuration > 0 ? (subtitleDuration / maxDuration) * 100 : 100;
      
      artistPercentage = Math.min(100, Math.max(0, artistPercentage));
      subtitlePercentage = Math.min(100, Math.max(0, subtitlePercentage));
      
      // --- ÉTATS DU LECTEUR ---
      const isPaused = entity ? (entity.state === 'paused' || entity.state === 'idle') : true;
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const shuffle = attr.shuffle || false;
      const repeat = attr.repeat || 'off';
      
      // [ZONE MODIFIABLE] : TEXTES D'ARRÊT (LCD)
      // Remplacez les mots ci-dessous si vous préférez du français (ex: "EN PAUSE", "ARRÊTÉ")
      const textStop = "STOP";
      const textPause = "PAUSE";
      const textStatus = entity ? (entity.state === 'idle' ? textStop : textPause) : 'NO ENTITY';
      
      // Source du lecteur (Fallbacks : attribut source > spécificité Spotify > Nom de l'enceinte > Inconnu)
      const source = attr.source || attr.sp_device_name || attr.friendly_name || 'Inconnu';
      
      // --- GÉNÉRATEUR DYNAMIQUE D'ÉGALISEUR ---
      const numEqBars = 10; // [ZONE MODIFIABLE] : Nombre de barres de l'égaliseur (ex: 5, 10, 16)
      let eqHtml = '';
      // Séquence d'animations déterministe pour éviter les sauts graphiques
      const eqPattern = [1, 5, 9, 3, 7, 2, 6, 10, 4, 8];
      for(let i = 0; i < numEqBars; i++) {
        let typeIndex = eqPattern[i % eqPattern.length];
        eqHtml += `<div class="bar bar${typeIndex}"></div>`;
      }
      
      return `
        <style>
          /* Génération dynamique des keyframes d'animation CSS (Marquee) */
          @keyframes sync-marquee-artist {
            0% { transform: translateX(0); }
            ${artistPercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes sync-marquee-subtitle {
            0% { transform: translateX(0); }
            ${subtitlePercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes fadeBlink { 
            0% { opacity: 0.2; } 
            100% { opacity: 1; } 
          }
        </style>
        <!-- 
          [ZONE MODIFIABLE] : ESPACEMENT (PADDING) DE L'ÉCRAN LCD
          Si vous voulez que l'écran soit moins haut, vous devez réduire le 'padding' 
          (actuellement à 8px 12px) dans la balise div ci-dessous, ET réduire la taille 
          des polices HTML (voir plus bas).
        -->
        <div style="box-sizing: border-box; width: 100%; height: 100%; background: var(--bg-screen-lcd); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); border-radius: var(--radius-md); padding: 8px 12px; display: flex; flex-direction: column; justify-content: space-between; text-align: left; overflow: hidden; box-shadow: var(--shadow-encastrement); position: relative;">
          <!-- Reflet vitré -->
          <div style="position: absolute; top: 0; left: 0; right: 0; height: 50%; background: linear-gradient(to bottom, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.02) 100%); pointer-events: none; z-index: 2;"></div>
          
          ${isPaused ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-strong); animation: fadeBlink 1.5s alternate infinite ease-in-out;">
              [ ${textStatus} ]
            </div>
          </div>
          ` : `
          <div style="display: flex; justify-content: space-between; width: 100%; position: relative; z-index: 1;">
            
            <!-- Left Column: Artist + Subtitle -->
            <div style="display: flex; flex-direction: column; overflow: hidden; flex-grow: 1;">
              
              <!-- [ZONE MODIFIABLE] TEXTE ARTISTE (Modifiez le font-size: 16px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; position: relative;">
                <div style="${artistNeedsScroll ? `animation: sync-marquee-artist ${maxDuration}s linear infinite;` : ''} font-size: 16px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-soft); display: inline-flex; width: max-content;">
                  <span style="padding-right: ${artistNeedsScroll ? '50px' : '0'};">${artist}</span>
                  ${artistNeedsScroll ? `<span style="padding-right: 50px;">${artist}</span>` : ''}
                </div>
              </div>

              <!-- [ZONE MODIFIABLE] TEXTE SOUS-TITRE (Modifiez le font-size: 12px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; margin-top: 4px; position: relative;">
                <div style="${subtitleNeedsScroll ? `animation: sync-marquee-subtitle ${maxDuration}s linear infinite;` : ''} font-size: 12px; font-family: var(--font-family-lcd); color: var(--color-dos-green); display: inline-flex; width: max-content; opacity: 0.7;">
                  <span style="padding-right: ${subtitleNeedsScroll ? '50px' : '0'};">${subtitle}</span>
                  ${subtitleNeedsScroll ? `<span style="padding-right: 50px;">${subtitle}</span>` : ''}
                </div>
              </div>
            </div>

          </div>
          `}
          


          <div style="margin-top: 12px; display: flex; justify-content: space-between; align-items: flex-end; width: 100%; height: var(--size-eq-height, 20px);">
            <div class="winamp-eq ${isPlaying ? 'playing' : ''}">
              ${eqHtml}
            </div>

            <!-- Compteur Digital & Ticker JS -->
            <div class="winamp-timer" style="font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: normal; color: var(--color-dos-green); opacity: 0.7; white-space: nowrap; margin-bottom: 2px;">
              --:-- <span style="opacity:0.5; font-size: 0.9em;">/</span> --:--
            </div>
            
            <img src="x" style="display:none;" onerror="
              if (this.hasAttribute('ticker-started')) return;
              this.setAttribute('ticker-started', 'true');
              const img = this;
              
              let calcPos = ${attr.media_position || 0};
              const updated_at = '${attr.media_position_updated_at || ''}';
              if (updated_at && '${entity ? entity.state : ''}' === 'playing') {
                const updatedTime = new Date(updated_at).getTime();
                if (!isNaN(updatedTime)) calcPos += (new Date().getTime() - updatedTime) / 1000;
              }
              
              let pos = calcPos;
              const dur = ${attr.media_duration || 1};
              let isPlaying = ('${entity ? entity.state : ''}' === 'playing');
              
              function updateDOM() {
                const root = img.getRootNode();
                if (!root || !root.querySelector) return;
                
                const timerEl = root.querySelector('.winamp-timer');
                if (timerEl) {
                  const fm = Math.floor(pos / 60);
                  const fs = Math.floor(pos % 60).toString().padStart(2, '0');
                  const dm = Math.floor(dur / 60);
                  const ds = Math.floor(dur % 60).toString().padStart(2, '0');
                  timerEl.innerHTML = fm + ':' + fs + ' <span style=\\'opacity:0.5; font-size: 0.9em;\\'>/</span> ' + dm + ':' + ds;
                }
                
                const sliderEl = root.querySelector('.glass-slider');
                if (sliderEl && !sliderEl.matches(':active')) {
                  sliderEl.value = pos;
                  sliderEl.style.setProperty('--slider-pct', (pos / dur) * 100 + '%');
                }
              }
              
              updateDOM();
              
              const intervalId = setInterval(() => {
                if (!img.isConnected) { clearInterval(intervalId); return; }
                if (isPlaying && pos < dur) pos++;
                updateDOM();
              }, 1000);
            ">

            <div style="display: flex; gap: 8px; align-items: center; font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: var(--font-weight-lcd); margin-bottom: 2px;">
              <div style="color: ${shuffle ? 'var(--color-dos-green)' : 'rgba(var(--color-dos-green-rgb), 0.2)'}; filter: ${shuffle ? 'drop-shadow(0 0 2px rgba(var(--color-dos-green-rgb), 0.8))' : 'none'}; display: flex;">
                <ha-icon icon="mdi:shuffle" style="--mdc-icon-size: 14px;"></ha-icon>
              </div>
              <div style="color: ${repeat && repeat !== 'off' ? 'var(--color-dos-green)' : 'rgba(var(--color-dos-green-rgb), 0.2)'}; filter: ${repeat && repeat !== 'off' ? 'drop-shadow(0 0 2px rgba(var(--color-dos-green-rgb), 0.8))' : 'none'}; display: flex;">
                <ha-icon icon="${repeat === 'one' ? 'mdi:repeat-once' : 'mdi:repeat'}" style="--mdc-icon-size: 14px;"></ha-icon>
              </div>
              <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); margin-left: 8px;">OUT: ${source}</div>
            </div>
          </div>

        </div>
      `;
    ]]]
  progress: >
    [[[
      // --- 3. BARRE DE PROGRESSION (SLIDER) ---
      const attr = entity ? entity.attributes : {};
      let pct = 0;
      let duration = attr.media_duration || 100;
      let position = attr.media_position || 0;
      
      // Calcul du pourcentage d'avancement
      if (duration && position) {
        pct = (position / duration) * 100;
      }
      
      // Génération d'un ID unique pour éviter les conflits HTML si plusieurs lecteurs
      const sliderId = 'slider-' + Math.random().toString(36).substr(2, 9);
      
      return `
        <!-- Input natif 'range' maquillé en curseur Winamp -->
        <input type="range" class="glass-slider" id="${sliderId}" min="0" max="${duration}" value="${position}" step="1"
          ${!entity ? 'disabled' : ''}
          style="--slider-pct: ${pct}%;"
          onpointerdown="event.stopPropagation();"
          onclick="event.stopPropagation();"
          oninput="
            /* Met à jour visuellement la barre de progression de couleur instantanément */
            this.style.setProperty('--slider-pct', (this.value / this.max) * 100 + '%');
            const root = this.getRootNode();
            if (root && root.querySelector) {
              const tEl = root.querySelector('.winamp-timer');
              if(tEl) {
                const fm = Math.floor(this.value/60);
                const fs = Math.floor(this.value%60).toString().padStart(2,'0');
                const dm = Math.floor(this.max/60);
                const ds = Math.floor(this.max%60).toString().padStart(2,'0');
                tEl.innerHTML = fm+':'+fs+' <span style=\\'opacity:0.5; font-size: 0.9em;\\'>/</span> '+dm+':'+ds;
              }
            }
          "
          onchange="
            /* Envoie la nouvelle position au service HA (media_seek) lors du relâchement */
            if (!'${entity ? entity.entity_id : ''}') return;
            document.querySelector('home-assistant').hass.callService('media_player', 'media_seek', {
              entity_id: '${entity ? entity.entity_id : ''}',
              seek_position: parseFloat(this.value)
            });
          "
        >
      `;
    ]]]
  controls: >
    [[[
      // --- 4. BOUTONS DE CONTRÔLE MULTIMÉDIA ---
      // Détection de l'état actuel pour afficher dynamiquement l'icône Play ou Pause
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const eid = entity ? entity.entity_id : '';
      const attr = entity ? entity.attributes : {};
      
      // Détection de la fonction STOP matérielle
      // Le chiffre '4096' est le code officiel (Bitmask SUPPORT_STOP) de Home Assistant
      // Il permet de vérifier mathématiquement si l'enceinte possède la capacité d'être complètement stoppée.
      const supportsStop = attr.supported_features ? (attr.supported_features & 4096) !== 0 : false;
      const stopService = supportsStop ? 'media_stop' : 'media_pause'; // Repli sur 'pause' si stop n'est pas géré
      
      // Gestion de l'état "Shuffle" (Aléatoire)
      const isShuffle = attr.shuffle === true;
      
      // Gestion intelligente du mode boucle (Repeat)
      // Cycle standard : off -> all -> one -> off
      const repeatMode = attr.repeat || 'off';
      const isRepeat = repeatMode !== 'off';
      const repeatIcon = repeatMode === 'one' ? 'mdi:repeat-once' : (repeatMode === 'all' ? 'mdi:repeat' : 'mdi:repeat-off');
      
      // Calcul du prochain état de boucle lors du clic sur le bouton Repeat
      let nextRepeat = 'all';
      if (repeatMode === 'all') nextRepeat = 'one';
      if (repeatMode === 'one') nextRepeat = 'off';

      return `
        <!-- Conteneur des boutons alignés horizontalement -->
        <div style="display: flex; gap: 8px; align-items: center; justify-content: flex-start; padding-bottom: 4px;">
          
          <!-- Bouton PRÉCÉDENT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_previous_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-previous"></ha-icon>
          </div>
          
          <!-- Bouton LECTURE (S'illumine via la classe 'active' si la musique joue) -->
          <div class="glass-btn ${isPlaying ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_play', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:play"></ha-icon>
          </div>

          <!-- Bouton PAUSE -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_pause', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:pause"></ha-icon>
          </div>

          <!-- Bouton STOP (Dynamique : appelle 'media_stop' si géré par le lecteur, sinon 'media_pause' en secours) -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', '${stopService}', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:stop"></ha-icon>
          </div>
          
          <!-- Bouton SUIVANT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_next_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-next"></ha-icon>
          </div>

          <!-- Bouton ALÉATOIRE (Plus petit via 'glass-btn-sm', s'illumine si actif) -->
          <div class="glass-btn glass-btn-sm ${isShuffle ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'shuffle_set', {entity_id: '${eid}', shuffle: ${!isShuffle}})">
            <ha-icon icon="mdi:shuffle"></ha-icon>
          </div>
          
          <!-- Bouton BOUCLE (Envoie la commande avec la variable calculée 'nextRepeat') -->
          <div class="glass-btn glass-btn-sm ${isRepeat ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'repeat_set', {entity_id: '${eid}', repeat: '${nextRepeat}'})">
            <ha-icon icon="${repeatIcon}"></ha-icon>
          </div>
        </div>
      `;
    ]]]

extra_styles: |
  /* ==========================================================================
     [ZONES MODIFIABLES] : STYLES CSS (COULEURS & THÈMES)
     C'est ici que vous pouvez changer les couleurs globales du lecteur Winamp.
     Remplacez les codes (#XXXXXX) ou les noms de couleurs pour adapter le thème.
     ========================================================================== */
  :host {
    /* --- COULEURS DU CHÂSSIS --- 
       Jeux d'ombres et lumières pour créer l'effet 3D "encastré" */
    --color-chassis-base: #1c1c1c;
    --color-chassis-shadow-deep: #0a0a0a;
    --color-chassis-shadow-light: #111111;
    --color-chassis-highlight-deep: #333333;
    --color-chassis-highlight-light: #2b2b2b;
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES TEXTES ET BOUTONS
       --------------------------------------------------------- */
    --color-dos-green: #00FF00; /* Le fameux vert Winamp (Textes de l'écran) */
    --color-dos-green-rgb: 0, 255, 0; /* Mettez les valeurs RGB de votre couleur ci-dessus (ex: pour le vert) */
    --color-neon-active: rgb(138, 43, 226); /* Couleur des boutons physiques (Violet par défaut) */
    --color-success: rgb(46, 139, 87); /* Couleur du bouton actif (Lecture en cours) */
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES DÉGRADÉS (FOND ET ÉGALISEUR)
       --------------------------------------------------------- */
    --bg-screen-lcd: radial-gradient(ellipse at center, #0f240f 0%, #030803 80%, #000000 100%); /* Fond de l'écran LCD */
    
    /* 
       Couleurs de l'égaliseur animé :
       - Actuellement en "Dégradé doux" : Vert de 0%, Jaune progressif jusqu'à 60%, Rouge à 100%.
       - Si vous voulez que le rouge arrive plus bas, baissez les pourcentages (ex: #FFFF00 30%, #FF0000 60%).
       - Si vous voulez des "Blocs LED" nets (sans mélange baveux), doublez les pourcentages au point de rupture :
         Exemple LED : linear-gradient(to top, #00FF00 0%, #00FF00 60%, #FFFF00 60%, #FFFF00 85%, #FF0000 85%, #FF0000 100%)
       - Vous pouvez empiler autant de couleurs que vous le souhaitez.
    */
    --bg-eq-bars: linear-gradient(to top, #00FF00 0%, #FFFF00 60%, #FF0000 100%);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : TYPOGRAPHIE LCD
       Ajustez la taille de police si l'écran LCD est trop petit chez vous
       --------------------------------------------------------- */
    --font-family-lcd: monospace;
    --font-weight-lcd: bold;
    --font-size-lcd-large: 24px;
    --font-size-lcd-xs: 10px;
    --letter-spacing-lcd: 4px;
    
    /* --- EFFETS DE LUEUR (NÉON) --- */
    --text-shadow-lcd-strong: 0 0 8px rgba(0, 255, 0, 0.8);
    --text-shadow-lcd-soft: 0 0 5px rgba(0, 255, 0, 0.5);
    --shadow-neon-glow-active: drop-shadow(0 0 5px var(--color-neon-active)); /* Aura autour du bouton physique pressé */
    --shadow-neon-glow-success: drop-shadow(0 0 6px var(--color-success)); /* Aura autour du bouton "Lecture" actif */
    --opacity-lcd-dim: 0.8;
    --opacity-lcd-dimmer: 0.7;
    
    /* ---------------------------------------------------------
       EFFETS "VERRE BOMBÉ" (BOUTONS PHYSIQUES)
       Construits avec de multiples couches d'ombres et reflets
       --------------------------------------------------------- */
    /* Fonds sphériques pour créer le volume du bouton */
    --bg-verre-bombe: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.02) 50%, rgba(0,0,0,0.4) 100%);
    --bg-verre-bombe-pressed: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0.6) 100%);
    --bg-verre-bombe-active: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.08) 50%, rgba(0,0,0,0.2) 100%);
    
    /* Ombres externes (portée sur le châssis) et internes (relief bombé) */
    --shadow-verre-bombe: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 3px rgba(255,255,255,0.3), inset -2px -2px 6px rgba(0,0,0,0.7);
    --shadow-verre-bombe-pressed: 0px 1px 2px rgba(0,0,0,0.8), -1px -1px 1px rgba(255,255,255,0.08), inset 2px 2px 8px rgba(0,0,0,0.8), inset -1px -1px 2px rgba(255,255,255,0.1);
    --shadow-verre-bombe-active: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 4px rgba(255,255,255,0.5), inset -2px -2px 6px rgba(0,0,0,0.7);
    
    /* --- COULEURS ET OMBRES DE LA BARRE DE PROGRESSION (SLIDER) --- */
    --slider-track-bg-off: rgba(0, 0, 0, 0.6); /* Couleur du fond de la piste */
    --slider-track-border: 1px solid rgba(255, 255, 255, 0.05);
    --slider-track-shadow: inset 0 1px 3px rgba(0,0,0,0.8);
    --slider-thumb-bg: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 40%, rgba(0,0,0,0.2) 100%); /* Effet verre du curseur */
    --slider-thumb-border: 1px solid rgba(255, 255, 255, 0.1);
    --slider-thumb-border-top: 1px solid rgba(255, 255, 255, 0.5);
    --slider-thumb-border-left: 1px solid rgba(255, 255, 255, 0.3);
    --slider-thumb-shadow: 2px 4px 6px rgba(0,0,0,0.5), inset 1px 1px 2px rgba(255,255,255,0.5), inset -1px -1px 2px rgba(0,0,0,0.4);
    --slider-thumb-backdrop: blur(12px) brightness(1.15); /* Floutage derrière le curseur */
    
    /* Ombre d'encastrement globale (L'écran entier semble creusé dans le plastique) */
    --shadow-encastrement: inset 2px 2px 5px rgba(255, 255, 255, 0.25), inset -3px -3px 8px rgba(0,0,0,1), inset 0 0 20px rgba(0,0,0,0.8);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : DIMENSIONS ET TAILLES GLOBALES
       Ajustez ces valeurs pour redimensionner globalement les éléments
       --------------------------------------------------------- */
    --size-art-cover: 190px; /* Largeur de la pochette (La grille s'ajustera automatiquement) */
    --size-btn-physique: 28px; /* Diamètre des boutons de contrôle principaux */
    --size-btn-physique-sm: 20px; /* Diamètre des petits boutons (Shuffle/Repeat) */
    --size-eq-height: 20px; /* [ZONE MODIFIABLE] Hauteur de l'égaliseur dynamique */
    --radius-md: 12px; /* Arrondi des angles de la pochette et de l'écran */
  }

  /* --- ANIMATION GLOBALE DE LA CARTE --- */
  /* Style de base de la carte : bordures subtiles et ombres complexes pour l'effet de profondeur */
  ha-card {
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.02) 100%) !important;
    border: 1.5px solid rgba(255, 255, 255, 0.15) !important;
    border-radius: 24px !important;
    box-shadow: 0 16px 36px #000000CC, 0 0 1px #FFFFFF1A, 0 4px 12px #FFFFFF08, inset 0 1.5px 1.5px #FFFFFF59, inset 0 3px 10px #FFFFFF1F, inset 0 -3px 6px #00000099 !important;
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
  }

  /* 
     [ZONE MODIFIABLE] : EFFET AU SURVOL (HOVER) 
     La carte "monte" de 4px vers le haut quand on passe la souris. 
     Mettez translateY(0px) si vous ne voulez pas d'animation.
  */
  ha-card:hover {
    transform: translateY(-4px) !important;
    border-color: rgba(255, 255, 255, 0.25) !important;
    box-shadow: 0 24px 48px #000000D9, 0 0 2px #FFFFFF26, 0 6px 16px #FFFFFF0D, inset 0 1.5px 1.5px #FFFFFF73, inset 0 3px 10px #FFFFFF2E, inset 0 -3px 6px #000000B3 !important;
  }

  /* --- STYLES DE LA BARRE DE PROGRESSION --- */
  .glass-slider {
    -webkit-appearance: none;
    width: 100%;
    height: 32px;
    background: transparent;
    outline: none;
    cursor: pointer;
    margin: 0;
  }
  
  /* Piste du slider (La rigole creusée dans le châssis) */
  .glass-slider::-webkit-slider-runnable-track {
    width: 100%;
    height: 8px;
    background: linear-gradient(to right, var(--color-success) var(--slider-pct, 0%), var(--slider-track-bg-off) var(--slider-pct, 0%));
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }
  
  /* 
     ==========================================================================
     [ZONE MODIFIABLE] : APPARENCE DU CURSEUR (Slider Thumb)
     ⚠️ RÈGLE D'OR DU WEB : Modifiez TOUJOURS les DEUX blocs ci-dessous à l'identique !
     - Le 1er bloc (-webkit) contrôle : Chrome, Edge, Brave, Safari, Appli Mobile.
     - Le 2ème bloc (-moz) contrôle : Firefox.
     Si vous modifiez la taille (width: 14px; height: 24px) dans l'un, faites-le dans l'autre !
     ========================================================================== 
  */
  .glass-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 24px;
    margin-top: -8px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }
  
  /* --- COMPATIBILITÉ FIREFOX --- */
  /* Piste du slider (Version Firefox) */
  .glass-slider::-moz-range-track {
    width: 100%;
    height: 8px;
    background: linear-gradient(to right, var(--color-success) var(--slider-pct, 0%), var(--slider-track-bg-off) var(--slider-pct, 0%));
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }
  
  /* [ZONE MODIFIABLE] : Curseur effet verre (Version Firefox - Gardez les mêmes dimensions que -webkit ci-dessus !) */
  .glass-slider::-moz-range-thumb {
    width: 14px;
    height: 24px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }

  /* --- STYLES DES BOUTONS PHYSIQUES (.glass-btn) --- */
  .glass-btn {
    width: var(--size-btn-physique);
    height: var(--size-btn-physique);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-verre-bombe);
    border: 2px solid var(--color-chassis-base);
    box-shadow: var(--shadow-verre-bombe);
    cursor: pointer;
    transition: transform 0.1s ease, box-shadow 0.1s ease;
  }

  .glass-btn-sm {
    width: var(--size-btn-physique-sm) !important;
    height: var(--size-btn-physique-sm) !important;
  }

  /* [ZONE MODIFIABLE] : APPARENCE DES ICÔNES DES BOUTONS */
  .glass-btn ha-icon {
    --mdc-icon-size: 18px; /* Taille des icônes à l'intérieur du bouton */
    /* La couleur pointe vers la variable globale '--color-neon-active'.
       Vous pouvez soit changer la variable tout en haut, soit écrire votre couleur ici (ex: red) */
    color: var(--color-neon-active);
    opacity: 0.95;
    filter: var(--shadow-neon-glow-active);
    transition: all 0.2s ease;
  }

  .glass-btn-sm ha-icon {
    --mdc-icon-size: 14px !important;
  }

  /* Effet d'enfoncement physique au clic (active) */
  .glass-btn:active {
    transform: translateY(2px);
    background: var(--bg-verre-bombe-pressed);
    box-shadow: var(--shadow-verre-bombe-pressed);
  }

  /* L'icône s'illumine lors de la pression */
  .glass-btn:active ha-icon {
    color: var(--color-success) !important;
    filter: var(--shadow-neon-glow-success) !important;
    opacity: 1 !important;
  }

  .glass-btn.active {
    background: var(--bg-verre-bombe-active);
    box-shadow: var(--shadow-verre-bombe-active);
  }

  .glass-btn.active ha-icon {
    color: var(--color-success);
    opacity: 1;
    filter: var(--shadow-neon-glow-success);
  }

  /* --- EFFET MARQUEE (DÉFILEMENT DE TEXTE) --- */
  /* La classe marquee applique une translation continue de droite à gauche */
  .marquee {
    animation: marquee 15s linear infinite;
  }
  
  /* L'animation translate de 0 à -50% (car le texte est dupliqué 2 fois dans le HTML pour boucler) */
  @keyframes marquee {
    0% { transform: translateX(0); }
    100% { transform: translateX(-50%); }
  }
  
  /* --- STYLES DE L'ÉGALISEUR --- */
  /* Conteneur principal des barres */
  .winamp-eq {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: var(--size-eq-height, 20px);
  }
  
  /* Style de base d'une seule barre de l'égaliseur */
  .winamp-eq .bar {
    width: 4px;
    height: 2px;
    background: var(--bg-eq-bars);
    background-size: 100% var(--size-eq-height, 20px);
    background-position: bottom;
    border-radius: 1px 1px 0 0;
    opacity: 0.85;
  }
  
  /* Attribution des animations individuelles aux 10 types de barres (uniquement si classe 'playing' active) */
  .winamp-eq.playing .bar1 { animation: eq1 0.4s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar2 { animation: eq2 0.6s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar3 { animation: eq3 0.5s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar4 { animation: eq4 0.7s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar5 { animation: eq5 0.45s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar6 { animation: eq6 0.65s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar7 { animation: eq7 0.55s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar8 { animation: eq8 0.75s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar9 { animation: eq9 0.4s infinite alternate ease-in-out; }
  .winamp-eq.playing .bar10 { animation: eq10 0.8s infinite alternate ease-in-out; }

  /* Animations relatives (%) pour s'adapter dynamiquement à var(--size-eq-height) */
  @keyframes eq1 { 0% { height: 10%; } 100% { height: 80%; } }
  @keyframes eq2 { 0% { height: 20%; } 100% { height: 100%; } }
  @keyframes eq3 { 0% { height: 10%; } 100% { height: 70%; } }
  @keyframes eq4 { 0% { height: 30%; } 100% { height: 90%; } }
  @keyframes eq5 { 0% { height: 15%; } 100% { height: 75%; } }
  @keyframes eq6 { 0% { height: 25%; } 100% { height: 95%; } }
  @keyframes eq7 { 0% { height: 10%; } 100% { height: 60%; } }
  @keyframes eq8 { 0% { height: 20%; } 100% { height: 85%; } }
  @keyframes eq9 { 0% { height: 10%; } 100% { height: 80%; } }
  @keyframes eq10 { 0% { height: 25%; } 100% { height: 100%; } }

Comment tu allumes/éteins le media player ?
Je ne vois pas de bouton power.

EDIT:
J'y ai trop cru, mais l'équaliseur est fictif. :sweat_smile:
Top, comment tu as détaillé le code, bien plus facile pour modifier ce qu'on veut. :+1:

EDIT2;
Afficher le nom de la playlist et numéro de chanson en cours ?
PLAYLIST: xxxx | TRACK: x

EDIT3:
j'ai des bug de largeur de la carte, elle est en pleine largeur et des fois elle se rétrécit (genre en pause).
le bouton de la barre d'avancement, fais des aller/retour sur place en fin de lecture du morceau.


C'est bon pour le playlist / Track :blush:

      // Playlist (Fallbacks : attribut media_playlist > Inconnu)
      const playlist = attr.media_playlist || 'No';

      // Track (Fallbacks : attribut media_track > Inconnu)
      const track = attr.media_track || '0';
            <!-- Playlist & Track -->          
            <div style="display: flex; gap: 8px; align-items: center;  font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: var(--font-weight-lcd); margin-bottom: 2px;">
              <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); margin-left: 0px;">PLAYLIST: ${playlist} / TRACK: ${track}</div>
            </div>

Comme je suis en vue section, j'ai régler le problème en ajoutant :

section_mode: true
grid_options:
  rows: auto
  columns: full

j'ai ajouté l'état OFF pour le media_player :

      // --- ÉTATS DU LECTEUR ---
      const isOff = entity ? (entity.state === 'off' || entity.state === 'unavailable') : false;
      const isPaused = entity ? (entity.state === 'paused' || entity.state === 'idle') : true;
      const isPlaying = entity ? (entity.state === 'playing') : false;
      // [ZONE MODIFIABLE] : TEXTES D'ARRÊT (LCD)
      // Remplacez les mots ci-dessous si vous préférez du français (ex: "EN PAUSE", "ARRÊTÉ")
      const textStop = "STOP";
      const textPause = "PAUSE";
      const textOff = "OFF";
      const textStatus = entity
        ? (entity.state === 'off' || entity.state === 'unavailable'
            ? textOff
            : (entity.state === 'idle' ? textStop : textPause))
        : 'NO ENTITY';
          ${isOff ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: rgba(var(--color-dos-green-rgb), 0.3); text-shadow: none;">
              [ ${textStatus} ]
            </div>
          </div>
          ` : isPaused ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-strong); animation: fadeBlink 1.5s alternate infinite ease-in-out;">
              [ ${textStatus} ]
            </div>
          </div>

j'ai pas mis de bouton power, en general je demarre a la voix la lecture

c'est le but des commentaires, sinon c'est imbuvable ce code pour retrouver ce qu'on cherche

jamais fait attention a ce comportement, j'y preterai attention, peut etre la mise a jour du slider le soucis

de mon coté, j'ai fait aussi les playlist
pour les playlist et la queue, j'ai dut passer par des sensors templates pour bien tout avoir depuis spotify plus

On


Off ,j'ai tout masqué.

Je vais ajouter volume - et + et le niveau du volume, sûrement à la place des boutons aléatoire et répétition que je n'utilise pas.

code avec tous les ajouts et modifications :

type: custom:button-card
entity: media_player.sejour_spotifyplus_xxxxx
show_name: false
show_icon: false
show_state: false
show_ripple: false
tap_action:
  action: none
hold_action:
  action: more-info
double_tap_action:
  action: none
styles:
  card:
    - padding: 12px
    - color: "#ffffff"
    - overflow: visible
    - pointer-events: auto
    - box-sizing: border-box
    - width: 100%
  grid:
    - padding: 0
    - width: 100% !important
    - min-width: 100% !important
    - flex: 1
    - justify-self: stretch
    - align-self: stretch
    - grid-template-areas: |
        "art info"
        "art progress"
        "art controls"
    - grid-template-columns: var(--size-art-cover) calc(100% - var(--size-art-cover) - 24px)
    - grid-template-rows: 1fr auto auto
    - column-gap: 24px
    - row-gap: 8px
  custom_fields:
    art:
      - justify-self: start
      - align-self: start
      - grid-row: 1 / -1
    info:
      - justify-self: stretch
      - align-self: stretch
      - width: 100%
      - height: 100%
    progress:
      - justify-self: stretch
      - align-self: center
      - pointer-events: auto
    controls:
      - justify-self: start
      - align-self: center
      - display: flex
      - gap: 4px
      - width: 100%
custom_fields:
  art: |
    [[[
      // --- 1. LOGIQUE DE LA POCHETTE (ART COVER) ---
      // Extraction des attributs du media_player
      const attr = entity ? entity.attributes : {};
      
      // La pochette n'est visible que si la musique est EN COURS (playing).
      // Cela évite d'afficher une ancienne pochette restée en cache lorsque le lecteur est arrêté.
      const isVisible = entity && entity.state === 'playing';
      const image = isVisible ? (attr.entity_picture || '') : '';
      
      return `
        <!-- 
          [ZONE MODIFIABLE] : DIMENSIONS DE LA POCHETTE (HTML)
          ⚠️ ATTENTION : Si vous modifiez 'width' et 'height' ici (ex: 100px), 
          vous DEVEZ obligatoirement aller tout en bas du fichier et modifier la 
          variable globale '--size-art-cover' avec la même valeur.
        -->
        <div style="width: 190px; height: 190px; border-radius: var(--radius-md); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); background: radial-gradient(ellipse at center, #1a1a1a 0%, #000000 100%); box-shadow: var(--shadow-encastrement); position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center;">
          ${image ? `<img src="${image}" style="width: 100%; height: 100%; object-fit: cover;">` : `<ha-icon icon="mdi:music-circle" style="width: 50%; height: 50%; color: var(--color-chassis-highlight-deep);"></ha-icon>`}
          <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%, rgba(0,0,0,0.3) 100%); pointer-events: none;"></div>
        </div>
      `;
    ]]]
  info: |
    [[[
      // --- 2. LOGIQUE DE L'ÉCRAN LCD ---
      const attr = entity ? entity.attributes : {};
      
      // [ZONE MODIFIABLE] : TEXTES PAR DÉFAUT (Quand aucune musique n'est lancée)
      // Modifiez ces textes ("Stopped", "Paused") par ce que vous préférez voir à l'arrêt
      const title = attr.media_title || (entity ? 'Stopped' : 'En attente...');
      const artist = attr.media_artist || (entity ? 'Paused' : 'Veuillez configurer un lecteur');
      const album = attr.media_album_name || '';
      
      // Conversion de la durée totale (secondes) en format lisible (MM:SS)
      let timeString = '';
      if (attr.media_duration) {
        const mins = Math.floor(attr.media_duration / 60);
        const secs = Math.floor(attr.media_duration % 60);
        timeString = `${mins}:${secs.toString().padStart(2, '0')}`;
      } else if (!entity) {
        timeString = '0:00';
      }

      // Construction du sous-titre complet (Titre - Album)
      let subtitle = title;
      if (album) subtitle += ` - ${album}`;
      
      // --- ANIMATION DE DÉFILEMENT (MARQUEE) ---
      // On vérifie si les textes dépassent la largeur de l'écran LCD
      let artistNeedsScroll = artist.length > 18;
      let subtitleNeedsScroll = subtitle.length > 28;
      
      // [ZONE MODIFIABLE] : VITESSE DE DÉFILEMENT DU TEXTE (MARQUEE)
      // Modificateurs de vitesse : Plus le chiffre est grand, plus le texte défile LENTEMENT.
      const speedArtist = 0.55;   // Vitesse de la ligne 1 (Artiste)
      const speedSubtitle = 0.40; // Vitesse de la ligne 2 (Titre et Album)
      
      // La vitesse de défilement s'adapte automatiquement au nombre de caractères
      let artistDuration = artist.length * speedArtist;
      let subtitleDuration = subtitle.length * speedSubtitle;
      
      // Synchronisation des deux lignes pour qu'elles tournent en boucle ensemble
      let maxDuration = 0;
      if (artistNeedsScroll && subtitleNeedsScroll) {
        maxDuration = Math.max(artistDuration, subtitleDuration);
      } else if (artistNeedsScroll) {
        maxDuration = artistDuration;
      } else if (subtitleNeedsScroll) {
        maxDuration = subtitleDuration;
      }
      
      let artistPercentage = maxDuration > 0 ? (artistDuration / maxDuration) * 100 : 100;
      let subtitlePercentage = maxDuration > 0 ? (subtitleDuration / maxDuration) * 100 : 100;
      
      artistPercentage = Math.min(100, Math.max(0, artistPercentage));
      subtitlePercentage = Math.min(100, Math.max(0, subtitlePercentage));
      
      // --- ÉTATS DU LECTEUR ---
      const isOff = entity ? (entity.state === 'off' || entity.state === 'unavailable') : false;
      const isPaused = entity ? (entity.state === 'paused' || entity.state === 'idle') : true;
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const shuffle = attr.shuffle || false;
      const repeat = attr.repeat || 'off';
      
      // [ZONE MODIFIABLE] : TEXTES D'ARRÊT (LCD)
      // Remplacez les mots ci-dessous si vous préférez du français (ex: "EN PAUSE", "ARRÊTÉ")
      const textStop = "STOP";
      const textPause = "PAUSE";
      const textOff = "OFF";
      const textStatus = entity
        ? (entity.state === 'off' || entity.state === 'unavailable'
            ? textOff
            : (entity.state === 'idle' ? textStop : textPause))
        : 'NO ENTITY';
      
      // Source du lecteur (Fallbacks : attribut source > spécificité Spotify > Nom de l'enceinte > Inconnu)
      const source = attr.source || attr.sp_device_name || attr.friendly_name || 'Inconnu';

      // Playlist (Fallbacks : attribut media_playlist > Inconnu)
      const playlist = attr.media_playlist || 'No';

      // Track (Fallbacks : attribut media_track > Inconnu)
      const track = attr.media_track || '0';

      // --- GÉNÉRATEUR DYNAMIQUE D'ÉGALISEUR ---
      const numEqBars = 10; // [ZONE MODIFIABLE] : Nombre de barres de l'égaliseur (ex: 5, 10, 16)
      let eqHtml = '';
      // Séquence d'animations déterministe pour éviter les sauts graphiques
      const eqPattern = [1, 5, 9, 3, 7, 2, 6, 10, 4, 8];
      for(let i = 0; i < numEqBars; i++) {
        let typeIndex = eqPattern[i % eqPattern.length];
        eqHtml += `<div class="bar bar${typeIndex}"></div>`;
      }
      
      return `
        <style>
          /* Génération dynamique des keyframes d'animation CSS (Marquee) */
          @keyframes sync-marquee-artist {
            0% { transform: translateX(0); }
            ${artistPercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes sync-marquee-subtitle {
            0% { transform: translateX(0); }
            ${subtitlePercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes fadeBlink { 
            0% { opacity: 0.2; } 
            100% { opacity: 1; } 
          }
        </style>
        <!-- 
          [ZONE MODIFIABLE] : ESPACEMENT (PADDING) DE L'ÉCRAN LCD
          Si vous voulez que l'écran soit moins haut, vous devez réduire le 'padding' 
          (actuellement à 8px 12px) dans la balise div ci-dessous, ET réduire la taille 
          des polices HTML (voir plus bas).
        -->
        <div style="box-sizing: border-box; width: 100%; height: 100%; background: var(--bg-screen-lcd); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); border-radius: var(--radius-md); padding: 8px 12px; display: flex; flex-direction: column; justify-content: space-between; text-align: left; overflow: hidden; box-shadow: var(--shadow-encastrement); position: relative;">
          <!-- Reflet vitré -->
          <div style="position: absolute; top: 0; left: 0; right: 0; height: 50%; background: linear-gradient(to bottom, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.02) 100%); pointer-events: none; z-index: 2;"></div>
          
          ${isOff ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: rgba(var(--color-dos-green-rgb), 0.3); text-shadow: none;">
              [ ${textStatus} ]
            </div>
          </div>
          ` : isPaused ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-strong); animation: fadeBlink 1.5s alternate infinite ease-in-out;">
              [ ${textStatus} ]
            </div>
          </div>
          ` : `
          <div style="display: flex; justify-content: space-between; width: 100%; position: relative; z-index: 1;">
            
            <!-- Left Column: Artist + Subtitle -->
            <div style="display: flex; flex-direction: column; overflow: hidden; flex-grow: 1;">
              
              <!-- [ZONE MODIFIABLE] TEXTE ARTISTE (Modifiez le font-size: 16px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; position: relative;">
                <div style="${artistNeedsScroll ? `animation: sync-marquee-artist ${maxDuration}s linear infinite;` : ''} font-size: 16px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-soft); display: inline-flex; width: max-content;">
                  <span style="padding-right: ${artistNeedsScroll ? '50px' : '0'};">${artist}</span>
                  ${artistNeedsScroll ? `<span style="padding-right: 50px;">${artist}</span>` : ''}
                </div>
              </div>

              <!-- [ZONE MODIFIABLE] TEXTE SOUS-TITRE (Modifiez le font-size: 12px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; margin-top: 4px; position: relative;">
                <div style="${subtitleNeedsScroll ? `animation: sync-marquee-subtitle ${maxDuration}s linear infinite;` : ''} font-size: 12px; font-family: var(--font-family-lcd); color: var(--color-dos-green); display: inline-flex; width: max-content; opacity: 0.7;">
                  <span style="padding-right: ${subtitleNeedsScroll ? '50px' : '0'};">${subtitle}</span>
                  ${subtitleNeedsScroll ? `<span style="padding-right: 50px;">${subtitle}</span>` : ''}
                </div>
              </div>
            </div>

          </div>
          `}
          ${!isOff ? `
          <!-- Playlist & Track -->          
          <div style="display: flex; gap: 8px; align-items: center;  font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: var(--font-weight-lcd); margin-bottom: 2px;">
            <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); margin-left: 0px;">PLAYLIST: ${playlist} / TRACK: ${track}</div>
          </div>
          ` : ''}

          ${!isOff ? `
          <div style="margin-top: 0px; display: flex; justify-content: space-between; align-items: flex-end; width: 100%; height: var(--size-eq-height, 20px);">
            <div class="winamp-eq ${isPlaying ? 'playing' : ''}">
              ${eqHtml}
            </div>
          
            <!-- Compteur Digital & Ticker JS -->
            <div class="winamp-timer" style="font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: normal; color: var(--color-dos-green); opacity: 0.7; white-space: nowrap; margin-bottom: 0px;">
              --:-- <span style="opacity:0.5; font-size: 0.9em;">/</span> --:--
            </div>
            
            <img src="x" style="display:none;" onerror="
              if (this.hasAttribute('ticker-started')) return;
              this.setAttribute('ticker-started', 'true');
              const img = this;
              
              let calcPos = ${attr.media_position || 0};
              const updated_at = '${attr.media_position_updated_at || ''}';
              if (updated_at && '${entity ? entity.state : ''}' === 'playing') {
                const updatedTime = new Date(updated_at).getTime();
                if (!isNaN(updatedTime)) calcPos += (new Date().getTime() - updatedTime) / 1000;
              }
              
              let pos = calcPos;
              const dur = ${attr.media_duration || 1};
              let isPlaying = ('${entity ? entity.state : ''}' === 'playing');
              
              function updateDOM() {
                const root = img.getRootNode();
                if (!root || !root.querySelector) return;
                
                const timerEl = root.querySelector('.winamp-timer');
                if (timerEl) {
                  const fm = Math.floor(pos / 60);
                  const fs = Math.floor(pos % 60).toString().padStart(2, '0');
                  const dm = Math.floor(dur / 60);
                  const ds = Math.floor(dur % 60).toString().padStart(2, '0');
                  timerEl.innerHTML = fm + ':' + fs + '<span style=\\'opacity:0.5; font-size: 0.9em;\\'>|</span>' + dm + ':' + ds;
                }
                
                const sliderEl = root.querySelector('.glass-slider');
                if (sliderEl && !sliderEl.matches(':active')) {
                  sliderEl.value = pos;
                  sliderEl.style.setProperty('--slider-pct', (pos / dur) * 100 + '%');
                }
              }
              
              updateDOM();
              
              const intervalId = setInterval(() => {
                if (!img.isConnected) { clearInterval(intervalId); return; }
                if (isPlaying && pos < dur) pos++;
                updateDOM();
              }, 1000);
            ">
            ` : ''}
            ${!isOff ? `
            <div style="display: flex; gap: 8px; align-items: center; font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: var(--font-weight-lcd); margin-bottom: 0px;">
              <div style="color: ${shuffle ? 'var(--color-dos-green)' : 'rgba(var(--color-dos-green-rgb), 0.2)'}; filter: ${shuffle ? 'drop-shadow(0 0 2px rgba(var(--color-dos-green-rgb), 0.8))' : 'none'}; display: flex;">
                <ha-icon icon="mdi:shuffle" style="--mdc-icon-size: 14px;"></ha-icon>
              </div>
              <div style="color: ${repeat && repeat !== 'off' ? 'var(--color-dos-green)' : 'rgba(var(--color-dos-green-rgb), 0.2)'}; filter: ${repeat && repeat !== 'off' ? 'drop-shadow(0 0 2px rgba(var(--color-dos-green-rgb), 0.8))' : 'none'}; display: flex;">
                <ha-icon icon="${repeat === 'one' ? 'mdi:repeat-once' : 'mdi:repeat'}" style="--mdc-icon-size: 14px;"></ha-icon>
              </div>
              <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); margin-left: 8px;">OUT: ${source}</div>
            </div>
            ` : ''}
          </div>

        </div>
      `;
    ]]]
  progress: |
    [[[
      // --- 3. BARRE DE PROGRESSION (SLIDER) ---
      const attr = entity ? entity.attributes : {};
      let pct = 0;
      let duration = attr.media_duration || 100;
      let position = attr.media_position || 0;
      
      // Calcul du pourcentage d'avancement
      if (duration && position) {
        pct = (position / duration) * 100;
      }
      
      // Génération d'un ID unique pour éviter les conflits HTML si plusieurs lecteurs
      const sliderId = 'slider-' + Math.random().toString(36).substr(2, 9);
      
      return `
        <!-- Input natif 'range' maquillé en curseur Winamp -->
        <input type="range" class="glass-slider" id="${sliderId}" min="0" max="${duration}" value="${position}" step="1"
          ${!entity ? 'disabled' : ''}
          style="--slider-pct: ${pct}%;"
          onpointerdown="event.stopPropagation();"
          onclick="event.stopPropagation();"
          oninput="
            /* Met à jour visuellement la barre de progression de couleur instantanément */
            this.style.setProperty('--slider-pct', (this.value / this.max) * 100 + '%');
            const root = this.getRootNode();
            if (root && root.querySelector) {
              const tEl = root.querySelector('.winamp-timer');
              if(tEl) {
                const fm = Math.floor(this.value/60);
                const fs = Math.floor(this.value%60).toString().padStart(2,'0');
                const dm = Math.floor(this.max/60);
                const ds = Math.floor(this.max%60).toString().padStart(2,'0');
                tEl.innerHTML = fm+':'+fs+' <span style=\\'opacity:0.5; font-size: 0.9em;\\'>/</span> '+dm+':'+ds;
              }
            }
          "
          onchange="
            /* Envoie la nouvelle position au service HA (media_seek) lors du relâchement */
            if (!'${entity ? entity.entity_id : ''}') return;
            document.querySelector('home-assistant').hass.callService('media_player', 'media_seek', {
              entity_id: '${entity ? entity.entity_id : ''}',
              seek_position: parseFloat(this.value)
            });
          "
        >
      `;
    ]]]
  controls: |
    [[[
      // --- 4. BOUTONS DE CONTRÔLE MULTIMÉDIA ---
      // Détection de l'état actuel pour afficher dynamiquement l'icône Play ou Pause
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const eid = entity ? entity.entity_id : '';
      const attr = entity ? entity.attributes : {};
      
      // Détection de la fonction STOP matérielle
      // Le chiffre '4096' est le code officiel (Bitmask SUPPORT_STOP) de Home Assistant
      // Il permet de vérifier mathématiquement si l'enceinte possède la capacité d'être complètement stoppée.
      const supportsStop = attr.supported_features ? (attr.supported_features & 4096) !== 0 : false;
      const stopService = supportsStop ? 'media_stop' : 'media_pause'; // Repli sur 'pause' si stop n'est pas géré
      
      // Gestion de l'état "Shuffle" (Aléatoire)
      const isShuffle = attr.shuffle === true;
      
      // Gestion intelligente du mode boucle (Repeat)
      // Cycle standard : off -> all -> one -> off
      const repeatMode = attr.repeat || 'off';
      const isRepeat = repeatMode !== 'off';
      const repeatIcon = repeatMode === 'one' ? 'mdi:repeat-once' : (repeatMode === 'all' ? 'mdi:repeat' : 'mdi:repeat-off');
      
      // Calcul du prochain état de boucle lors du clic sur le bouton Repeat
      let nextRepeat = 'all';
      if (repeatMode === 'all') nextRepeat = 'one';
      if (repeatMode === 'one') nextRepeat = 'off';

      return `
        <!-- Conteneur des boutons alignés horizontalement -->
        <div style="display: flex; gap: 8px; align-items: center; justify-content: flex-start; padding-bottom: 4px;">
          
          <!-- Bouton PRÉCÉDENT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_previous_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-previous"></ha-icon>
          </div>
          
          <!-- Bouton LECTURE (S'illumine via la classe 'active' si la musique joue) -->
          <div class="glass-btn ${isPlaying ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_play', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:play"></ha-icon>
          </div>

          <!-- Bouton PAUSE -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_pause', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:pause"></ha-icon>
          </div>

          <!-- Bouton STOP (Dynamique : appelle 'media_stop' si géré par le lecteur, sinon 'media_pause' en secours) -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', '${stopService}', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:stop"></ha-icon>
          </div>
          
          <!-- Bouton SUIVANT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_next_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-next"></ha-icon>
          </div>

          <!-- Bouton ALÉATOIRE (Plus petit via 'glass-btn-sm', s'illumine si actif) -->
          <div class="glass-btn glass-btn-sm ${isShuffle ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'shuffle_set', {entity_id: '${eid}', shuffle: ${!isShuffle}})">
            <ha-icon icon="mdi:shuffle"></ha-icon>
          </div>
          
          <!-- Bouton BOUCLE (Envoie la commande avec la variable calculée 'nextRepeat') -->
          <div class="glass-btn glass-btn-sm ${isRepeat ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'repeat_set', {entity_id: '${eid}', repeat: '${nextRepeat}'})">
            <ha-icon icon="${repeatIcon}"></ha-icon>
          </div>
        </div>
      `;
    ]]]
extra_styles: >
  /* ==========================================================================
     [ZONES MODIFIABLES] : STYLES CSS (COULEURS & THÈMES)
     C'est ici que vous pouvez changer les couleurs globales du lecteur Winamp.
     Remplacez les codes (#XXXXXX) ou les noms de couleurs pour adapter le thème.
     ========================================================================== */
  :host {
    display: block;
    width: 100%;
    box-sizing: border-box;  
    /* --- COULEURS DU CHÂSSIS --- 
       Jeux d'ombres et lumières pour créer l'effet 3D "encastré" */
    --color-chassis-base: #1c1c1c;
    --color-chassis-shadow-deep: #0a0a0a;
    --color-chassis-shadow-light: #111111;
    --color-chassis-highlight-deep: #333333;
    --color-chassis-highlight-light: #2b2b2b;
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES TEXTES ET BOUTONS
       --------------------------------------------------------- */
    --color-dos-green: #00FF00; /* Le fameux vert Winamp (Textes de l'écran) */
    --color-dos-green-rgb: 0, 255, 0; /* Mettez les valeurs RGB de votre couleur ci-dessus (ex: pour le vert) */
    --color-neon-active: rgb(68,115,158); /* Couleur des boutons physiques (Violet par défaut) */
    --color-success: rgb(46, 139, 87); /* Couleur du bouton actif (Lecture en cours) */
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES DÉGRADÉS (FOND ET ÉGALISEUR)
       --------------------------------------------------------- */
    --bg-screen-lcd: radial-gradient(ellipse at center, #0f240f 0%, #030803 80%, #000000 100%); /* Fond de l'écran LCD */
    
    /* 
       Couleurs de l'égaliseur animé :
       - Actuellement en "Dégradé doux" : Vert de 0%, Jaune progressif jusqu'à 60%, Rouge à 100%.
       - Si vous voulez que le rouge arrive plus bas, baissez les pourcentages (ex: #FFFF00 30%, #FF0000 60%).
       - Si vous voulez des "Blocs LED" nets (sans mélange baveux), doublez les pourcentages au point de rupture :
         Exemple LED : linear-gradient(to top, #00FF00 0%, #00FF00 60%, #FFFF00 60%, #FFFF00 85%, #FF0000 85%, #FF0000 100%)
       - Vous pouvez empiler autant de couleurs que vous le souhaitez.
    */
    --bg-eq-bars: linear-gradient(to top, #00FF00 0%, #FFFF00 60%, #FF0000 100%);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : TYPOGRAPHIE LCD
       Ajustez la taille de police si l'écran LCD est trop petit chez vous
       --------------------------------------------------------- */
    --font-family-lcd: monospace;
    --font-weight-lcd: bold;
    --font-size-lcd-large: 24px;
    --font-size-lcd-xs: 10px;
    --letter-spacing-lcd: 4px;
    
    /* --- EFFETS DE LUEUR (NÉON) --- */
    --text-shadow-lcd-strong: 0 0 8px rgba(0, 255, 0, 0.8);
    --text-shadow-lcd-soft: 0 0 5px rgba(0, 255, 0, 0.5);
    --shadow-neon-glow-active: drop-shadow(0 0 5px var(--color-neon-active)); /* Aura autour du bouton physique pressé */
    --shadow-neon-glow-success: drop-shadow(0 0 6px var(--color-success)); /* Aura autour du bouton "Lecture" actif */
    --opacity-lcd-dim: 0.8;
    --opacity-lcd-dimmer: 0.7;
    
    /* ---------------------------------------------------------
       EFFETS "VERRE BOMBÉ" (BOUTONS PHYSIQUES)
       Construits avec de multiples couches d'ombres et reflets
       --------------------------------------------------------- */
    /* Fonds sphériques pour créer le volume du bouton */
    --bg-verre-bombe: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.02) 50%, rgba(0,0,0,0.4) 100%);
    --bg-verre-bombe-pressed: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0.6) 100%);
    --bg-verre-bombe-active: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.08) 50%, rgba(0,0,0,0.2) 100%);
    
    /* Ombres externes (portée sur le châssis) et internes (relief bombé) */
    --shadow-verre-bombe: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 3px rgba(255,255,255,0.3), inset -2px -2px 6px rgba(0,0,0,0.7);
    --shadow-verre-bombe-pressed: 0px 1px 2px rgba(0,0,0,0.8), -1px -1px 1px rgba(255,255,255,0.08), inset 2px 2px 8px rgba(0,0,0,0.8), inset -1px -1px 2px rgba(255,255,255,0.1);
    --shadow-verre-bombe-active: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 4px rgba(255,255,255,0.5), inset -2px -2px 6px rgba(0,0,0,0.7);
    
    /* --- COULEURS ET OMBRES DE LA BARRE DE PROGRESSION (SLIDER) --- */
    --slider-track-bg-off: rgba(0, 0, 0, 0.6); /* Couleur du fond de la piste */
    --slider-track-border: 1px solid rgba(255, 255, 255, 0.05);
    --slider-track-shadow: inset 0 1px 3px rgba(0,0,0,0.8);
    --slider-thumb-bg: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 40%, rgba(0,0,0,0.2) 100%); /* Effet verre du curseur */
    --slider-thumb-border: 1px solid rgba(255, 255, 255, 0.1);
    --slider-thumb-border-top: 1px solid rgba(255, 255, 255, 0.5);
    --slider-thumb-border-left: 1px solid rgba(255, 255, 255, 0.3);
    --slider-thumb-shadow: 2px 4px 6px rgba(0,0,0,0.5), inset 1px 1px 2px rgba(255,255,255,0.5), inset -1px -1px 2px rgba(0,0,0,0.4);
    --slider-thumb-backdrop: blur(12px) brightness(1.15); /* Floutage derrière le curseur */
    
    /* Ombre d'encastrement globale (L'écran entier semble creusé dans le plastique) */
    --shadow-encastrement: inset 2px 2px 5px rgba(255, 255, 255, 0.25), inset -3px -3px 8px rgba(0,0,0,1), inset 0 0 20px rgba(0,0,0,0.8);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : DIMENSIONS ET TAILLES GLOBALES
       Ajustez ces valeurs pour redimensionner globalement les éléments
       --------------------------------------------------------- */
    --size-art-cover: 190px; /* Largeur de la pochette (La grille s'ajustera automatiquement) */
    --size-btn-physique: 28px; /* Diamètre des boutons de contrôle principaux */
    --size-btn-physique-sm: 28px; /* Diamètre des petits boutons (Shuffle/Repeat) */
    --size-eq-height: 20px; /* [ZONE MODIFIABLE] Hauteur de l'égaliseur dynamique */
    --radius-md: 12px; /* Arrondi des angles de la pochette et de l'écran */
  }


  /* --- ANIMATION GLOBALE DE LA CARTE --- */

  /* Style de base de la carte : bordures subtiles et ombres complexes pour
  l'effet de profondeur */

  ha-card {
    width: 100%;
    border: 1.5px solid rgba(255, 255, 255, 0.15) !important;
    border-radius: 12px !important;
    box-shadow: 0 16px 36px #000000CC, 0 0 1px #FFFFFF1A, 0 4px 12px #FFFFFF08, inset 0 1.5px 1.5px #FFFFFF59, inset 0 3px 10px #FFFFFF1F, inset 0 -3px 6px #00000099 !important;
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
  }


  /* 
     [ZONE MODIFIABLE] : EFFET AU SURVOL (HOVER) 
     La carte "monte" de 4px vers le haut quand on passe la souris. 
     Mettez translateY(0px) si vous ne voulez pas d'animation.
  */

  ha-card:hover {
    border-color: rgba(255, 255, 255, 0.25) !important;
    box-shadow: 0 24px 48px #000000D9, 0 0 2px #FFFFFF26, 0 6px 16px #FFFFFF0D, inset 0 1.5px 1.5px #FFFFFF73, inset 0 3px 10px #FFFFFF2E, inset 0 -3px 6px #000000B3 !important;
  }


  /* --- STYLES DE LA BARRE DE PROGRESSION --- */

  .glass-slider {
    -webkit-appearance: none;
    width: 100%;
    height: 32px;
    background: transparent;
    outline: none;
    cursor: pointer;
    margin: 0;
  }


  /* Piste du slider (La rigole creusée dans le châssis) */

  .glass-slider::-webkit-slider-runnable-track {
    width: 100%;
    height: 8px;
    background: linear-gradient(to right, var(--color-success) var(--slider-pct, 0%), var(--slider-track-bg-off) var(--slider-pct, 0%));
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }


  /* 
     ==========================================================================
     [ZONE MODIFIABLE] : APPARENCE DU CURSEUR (Slider Thumb)
     ⚠️ RÈGLE D'OR DU WEB : Modifiez TOUJOURS les DEUX blocs ci-dessous à l'identique !
     - Le 1er bloc (-webkit) contrôle : Chrome, Edge, Brave, Safari, Appli Mobile.
     - Le 2ème bloc (-moz) contrôle : Firefox.
     Si vous modifiez la taille (width: 14px; height: 24px) dans l'un, faites-le dans l'autre !
     ========================================================================== 
  */

  .glass-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 24px;
    margin-top: -8px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }


  /* --- COMPATIBILITÉ FIREFOX --- */

  /* Piste du slider (Version Firefox) */

  .glass-slider::-moz-range-track {
    width: 100%;
    height: 8px;
    background: linear-gradient(to right, var(--color-success) var(--slider-pct, 0%), var(--slider-track-bg-off) var(--slider-pct, 0%));
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }


  /* [ZONE MODIFIABLE] : Curseur effet verre (Version Firefox - Gardez les mêmes
  dimensions que -webkit ci-dessus !) */

  .glass-slider::-moz-range-thumb {
    width: 14px;
    height: 24px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }


  /* --- STYLES DES BOUTONS PHYSIQUES (.glass-btn) --- */

  .glass-btn {
    width: var(--size-btn-physique);
    height: var(--size-btn-physique);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-verre-bombe);
    border: 2px solid var(--color-chassis-base);
    box-shadow: var(--shadow-verre-bombe);
    cursor: pointer;
    transition: transform 0.1s ease, box-shadow 0.1s ease;
  }


  .glass-btn-sm {
    width: var(--size-btn-physique-sm) !important;
    height: var(--size-btn-physique-sm) !important;
  }


  /* [ZONE MODIFIABLE] : APPARENCE DES ICÔNES DES BOUTONS */

  .glass-btn ha-icon {
    --mdc-icon-size: 18px; /* Taille des icônes à l'intérieur du bouton */
    /* La couleur pointe vers la variable globale '--color-neon-active'.
       Vous pouvez soit changer la variable tout en haut, soit écrire votre couleur ici (ex: red) */
    color: var(--color-neon-active);
    opacity: 0.95;
    filter: var(--shadow-neon-glow-active);
    transition: all 0.2s ease;
  }


  .glass-btn-sm ha-icon {
    --mdc-icon-size: 14px !important;
  }


  /* Effet d'enfoncement physique au clic (active) */

  .glass-btn:active {
    transform: translateY(2px);
    background: var(--bg-verre-bombe-pressed);
    box-shadow: var(--shadow-verre-bombe-pressed);
  }


  /* L'icône s'illumine lors de la pression */

  .glass-btn:active ha-icon {
    color: var(--color-success) !important;
    filter: var(--shadow-neon-glow-success) !important;
    opacity: 1 !important;
  }


  .glass-btn.active {
    background: var(--bg-verre-bombe-active);
    box-shadow: var(--shadow-verre-bombe-active);
  }


  .glass-btn.active ha-icon {
    color: var(--color-success);
    opacity: 1;
    filter: var(--shadow-neon-glow-success);
  }


  /* --- EFFET MARQUEE (DÉFILEMENT DE TEXTE) --- */

  /* La classe marquee applique une translation continue de droite à gauche */

  .marquee {
    animation: marquee 15s linear infinite;
  }


  /* L'animation translate de 0 à -50% (car le texte est dupliqué 2 fois dans le
  HTML pour boucler) */

  @keyframes marquee {
    0% { transform: translateX(0); }
    100% { transform: translateX(-50%); }
  }


  /* --- STYLES DE L'ÉGALISEUR --- */

  /* Conteneur principal des barres */

  .winamp-eq {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: var(--size-eq-height, 20px);
  }


  /* Style de base d'une seule barre de l'égaliseur */

  .winamp-eq .bar {
    width: 4px;
    height: 2px;
    background: var(--bg-eq-bars);
    background-size: 100% var(--size-eq-height, 20px);
    background-position: bottom;
    border-radius: 1px 1px 0 0;
    opacity: 0.85;
  }


  /* Attribution des animations individuelles aux 10 types de barres (uniquement
  si classe 'playing' active) */

  .winamp-eq.playing .bar1 { animation: eq1 0.4s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar2 { animation: eq2 0.6s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar3 { animation: eq3 0.5s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar4 { animation: eq4 0.7s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar5 { animation: eq5 0.45s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar6 { animation: eq6 0.65s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar7 { animation: eq7 0.55s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar8 { animation: eq8 0.75s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar9 { animation: eq9 0.4s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar10 { animation: eq10 0.8s infinite alternate
  ease-in-out; }


  /* Animations relatives (%) pour s'adapter dynamiquement à
  var(--size-eq-height) */

  @keyframes eq1 { 0% { height: 10%; } 100% { height: 80%; } }

  @keyframes eq2 { 0% { height: 20%; } 100% { height: 100%; } }

  @keyframes eq3 { 0% { height: 10%; } 100% { height: 70%; } }

  @keyframes eq4 { 0% { height: 30%; } 100% { height: 90%; } }

  @keyframes eq5 { 0% { height: 15%; } 100% { height: 75%; } }

  @keyframes eq6 { 0% { height: 25%; } 100% { height: 95%; } }

  @keyframes eq7 { 0% { height: 10%; } 100% { height: 60%; } }

  @keyframes eq8 { 0% { height: 20%; } 100% { height: 85%; } }

  @keyframes eq9 { 0% { height: 10%; } 100% { height: 80%; } }

  @keyframes eq10 { 0% { height: 25%; } 100% { height: 100%; } }
section_mode: true
grid_options:
  rows: auto
  columns: full

si tu veux, j'ai les sliders sous la mains

enfin, il faut que je fasse une carte standalone, mais c'est pas le plus chiant

Re,

Va encore falloir ajouter une carte à mon dashboard :smiley:

cdt

Merci, mais ça met pas utile. Juste des boutons me suffiront.

Et une petite derniere avec les info de plex/tautulli/android adb

encore un peu de travail sur celle la, a voir si je fusionne pas avec le player music
mais pour aujourd'hui ca suffit

dernière modification:

  • bouton play/pause sur le même.
  • Ajoute du bouton mute, volume + et +. Avec information dans la fenêtre (affiche MUTE ou le volume)
  • Correction d'un bug qui pouvait afficher --:-- / --:-- à la place du temps du morceau (en cours de lecture ou pause)
  • Correction du curseur de la barre d'avancement, qui faisait un recul en fin de lecture.
  • Correction du trait en fin de barre d'avancement (quand j'ai modifié du code précédemment, on le voit sur les images))
  • Ajout du media player pour YouTube Music avec les informations de media player différentes de Spotify ou autre.
  • Changement automatique de l'icône suivant Spotify ou YouTube, basé sur le nom de l'entité du media_player.
  • Ajout d'un clic sur la pochette, pour ouvrir le more-info la première fois, puis accéder à la playlist (parcourir les médias). Au deuxième clic sur la pochette, c'est directement l'accès à la playlist (Media library). Il faut que ça précharge une fois l'information. Pas trouvé d'autre solution avec l'IA.

Spotify


YouTube

Icône suivant l'entité


Curseur OK
popup9

Code
type: custom:button-card
entity: media_player.sejour_spotifyplus_xxxxxxx
show_name: false
show_icon: false
show_state: false
show_ripple: false
tap_action:
  action: none
hold_action:
  action: none
double_tap_action:
  action: none
styles:
  card:
    - padding: 12px
    - color: "#ffffff"
    - overflow: visible
    - pointer-events: auto
    - box-sizing: border-box
    - width: 100%
  grid:
    - padding: 0
    - width: 100% !important
    - min-width: 100% !important
    - flex: 1
    - justify-self: stretch
    - align-self: stretch
    - grid-template-areas: |
        "art info"
        "art progress"
        "art controls"
    - grid-template-columns: var(--size-art-cover) calc(100% - var(--size-art-cover) - 24px)
    - grid-template-rows: 1fr auto auto
    - column-gap: 24px
    - row-gap: 8px
  custom_fields:
    art:
      - justify-self: start
      - align-self: start
      - grid-row: 1 / -1
    info:
      - justify-self: stretch
      - align-self: stretch
      - width: 100%
      - height: 100%
    progress:
      - justify-self: stretch
      - align-self: center
      - pointer-events: auto
    controls:
      - justify-self: start
      - align-self: center
      - display: flex
      - gap: 4px
      - width: 100%
      - margin-left: "-2.5px"
      - padding-right: 8px
custom_fields:
  art: |
    [[[
      // --- 1. LOGIQUE DE LA POCHETTE (ART COVER) ---
      // Extraction des attributs du media_player    
      const attr = entity ? entity.attributes : {};
      const eid = entity ? entity.entity_id : '';

      // La pochette n'est visible que si la musique est EN COURS (playing).
      // Cela évite d'afficher une ancienne pochette restée en cache lorsque le lecteur est arrêté.    
      const isVisible = entity && entity.state === 'playing';
      const image = isVisible ? (attr.entity_picture || '') : '';
      
      // --- Détection de la source pour choisir l'icône par défaut ---
      const entityId = entity ? entity.entity_id : '';
      
      const isSpotify = entityId.includes('spotify');
      const isYoutube = entityId.includes('ytube_music') || entityId.includes('youtube');
      
      const defaultIcon = isYoutube ? 'mdi:youtube' : (isSpotify ? 'mdi:spotify' : 'mdi:music-circle');    
     
      return `
        <!-- 
          [ZONE MODIFIABLE] : DIMENSIONS DE LA POCHETTE (HTML)
          ⚠️ ATTENTION : Si vous modifiez 'width' et 'height' ici (ex: 100px), 
          vous DEVEZ obligatoirement aller tout en bas du fichier et modifier la 
          variable globale '--size-art-cover' avec la même valeur.
        -->
        <div onclick="
          event.stopPropagation();
          const haEl = document.querySelector('home-assistant');
          let opened = false;
        
          const tryDirectBrowse = () => {
            if (customElements.get('dialog-media-player-browse')) {
              opened = true;
              const evt = new CustomEvent('show-dialog', {
                bubbles: true,
                composed: true,
                detail: {
                  dialogTag: 'dialog-media-player-browse',
                  dialogImport: () => customElements.whenDefined('dialog-media-player-browse'),
                  dialogParams: {
                    action: 'play',
                    entityId: '${eid}',
                    mediaPickedCallback: (pickedMedia) => {
                      haEl.hass.callService('media_player', 'play_media', {
                        entity_id: '${eid}',
                        media_content_id: pickedMedia.item.media_content_id,
                        media_content_type: pickedMedia.item.media_content_type
                      });
                    }
                  }
                }
              });
              haEl.dispatchEvent(evt);
            }
          };
        
          tryDirectBrowse();
        
          // Si le composant n'était pas encore chargé, on ouvre le more-info classique
          // en repli (qui contient le bouton 'Parcourir les médias' natif)
          setTimeout(() => {
            if (!opened) {
              const evtInfo = new CustomEvent('hass-more-info', {
                bubbles: true,
                composed: true,
                detail: { entityId: '${eid}' }
              });
              haEl.dispatchEvent(evtInfo);
            }
          }, 300);
        " style="width: 190px; height: 190px; border-radius: var(--radius-md); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); background: radial-gradient(ellipse at center, #1a1a1a 0%, #000000 100%); box-shadow: var(--shadow-encastrement); position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; cursor: pointer;">
          ${image ? `<img src="${image}" style="width: 100%; height: 100%; object-fit: cover;">` : `<ha-icon icon="${defaultIcon}" style="width: 50%; height: 50%; color: var(--color-chassis-highlight-deep);"></ha-icon>`}
          <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 50%, rgba(0,0,0,0.3) 100%); pointer-events: none;"></div>
        </div>
      `;
    ]]]
  info: |
    [[[
      // --- 2. LOGIQUE DE L'ÉCRAN LCD ---
      const attr = entity ? entity.attributes : {};
      
      // [ZONE MODIFIABLE] : TEXTES PAR DÉFAUT (Quand aucune musique n'est lancée)
      // Modifiez ces textes ("Stopped", "Paused") par ce que vous préférez voir à l'arrêt
      const title = attr.media_title || (entity ? 'Stopped' : 'En attente...');
      const artist = attr.media_artist || (entity ? 'Paused' : 'Veuillez configurer un lecteur');
      const album = attr.media_album_name || '';
      
      // Conversion de la durée totale (secondes) en format lisible (MM:SS)
      let timeString = '';
      if (attr.media_duration) {
        const mins = Math.floor(attr.media_duration / 60);
        const secs = Math.floor(attr.media_duration % 60);
        timeString = `${mins}:${secs.toString().padStart(2, '0')}`;
      } else if (!entity) {
        timeString = '0:00';
      }

      // Construction du sous-titre complet (Titre - Album)
      let subtitle = title;
      if (album) subtitle += ` - ${album}`;
      
      // --- ANIMATION DE DÉFILEMENT (MARQUEE) ---
      // On vérifie si les textes dépassent la largeur de l'écran LCD
      let artistNeedsScroll = artist.length > 18;
      let subtitleNeedsScroll = subtitle.length > 28;
      
      // [ZONE MODIFIABLE] : VITESSE DE DÉFILEMENT DU TEXTE (MARQUEE)
      // Modificateurs de vitesse : Plus le chiffre est grand, plus le texte défile LENTEMENT.
      const speedArtist = 0.55;   // Vitesse de la ligne 1 (Artiste)
      const speedSubtitle = 0.40; // Vitesse de la ligne 2 (Titre et Album)
      
      // La vitesse de défilement s'adapte automatiquement au nombre de caractères
      let artistDuration = artist.length * speedArtist;
      let subtitleDuration = subtitle.length * speedSubtitle;
      
      // Synchronisation des deux lignes pour qu'elles tournent en boucle ensemble
      let maxDuration = 0;
      if (artistNeedsScroll && subtitleNeedsScroll) {
        maxDuration = Math.max(artistDuration, subtitleDuration);
      } else if (artistNeedsScroll) {
        maxDuration = artistDuration;
      } else if (subtitleNeedsScroll) {
        maxDuration = subtitleDuration;
      }
      
      let artistPercentage = maxDuration > 0 ? (artistDuration / maxDuration) * 100 : 100;
      let subtitlePercentage = maxDuration > 0 ? (subtitleDuration / maxDuration) * 100 : 100;
      
      artistPercentage = Math.min(100, Math.max(0, artistPercentage));
      subtitlePercentage = Math.min(100, Math.max(0, subtitlePercentage));
      
      // --- ÉTATS DU LECTEUR ---
      const isOff = entity ? (entity.state === 'off' || entity.state === 'unavailable') : false;
      const isPaused = entity ? (entity.state === 'paused' || entity.state === 'idle') : true;
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const shuffle = attr.shuffle || false;
      const repeat = attr.repeat || 'off';
      window.__winampVolumeLocks = window.__winampVolumeLocks || {};
      const volKey = entity ? entity.entity_id : 'none';
      const volLock = window.__winampVolumeLocks[volKey];
      
      let volumePct = typeof attr.volume_level === 'number' ? Math.round(attr.volume_level * 100) : null;
      let isMuted = attr.is_volume_muted === true;
      
      // Le verrou reste actif pendant toute sa durée de vie, sans libération anticipée :
      // ça évite qu'un rendu intermédiaire (ancienne valeur renvoyée par l'appareil avant
      // confirmation finale) ne s'affiche entre deux rendus qui matchaient déjà notre verrou.
      if (volLock && (Date.now() - volLock.time) < 3000) {
        volumePct = volLock.pct;
        isMuted = volLock.muted;
      }   
      
      // [ZONE MODIFIABLE] : TEXTES D'ARRÊT (LCD)
      // Remplacez les mots ci-dessous si vous préférez du français (ex: "EN PAUSE", "ARRÊTÉ")
      const textStop = "STOP";
      const textPause = "PAUSE";
      const textOff = "OFF";
      const textStatus = entity
        ? (entity.state === 'off' || entity.state === 'unavailable'
            ? textOff
            : (entity.state === 'idle' ? textStop : textPause))
        : 'NO ENTITY';
      
      // Source du lecteur (Fallbacks : attribut source > spécificité Spotify > Ytube music > Inconnu)
      const rawSource = attr.source || attr.sp_device_name || attr.remote_player_id || 'Inconnu';
      const source = rawSource
        .replace(/^media_player\./, '')
        .replace(/_/g, ' ')
        .replace(/\b\w/g, c => c.toUpperCase()); // Majuscule en début de chaque mot

      // Playlist (Fallbacks : attribut media_playlist > Inconnu)
      const playlist = attr.media_playlist || attr.current_playlist_title || 'No';

      // Track (Fallbacks : attribut media_track > Ytube music > Inconnu)
      const track = attr.media_track || attr.current_track || '0';

      // Échappe les apostrophes pour éviter de casser les chaînes JS dans le ticker
      const escJs = (s) => (s || '').replace(/'/g, "\\'").replace(/"/g, '\\"');    

      // --- Calcul de la position initiale pour affichage immédiat (anti-clignotement) ---
      const dur = Math.round(attr.media_duration || 0);
      let initialPos = Math.round(attr.media_position || 0);
      if (attr.media_position_updated_at && entity && entity.state === 'playing') {
        const updatedTime = new Date(attr.media_position_updated_at).getTime();
        if (!isNaN(updatedTime)) initialPos += Math.round((new Date().getTime() - updatedTime) / 1000);
      }
      initialPos = Math.min(Math.max(initialPos, 0), dur);
      
      const fm = Math.floor(initialPos / 60);
      const fs = Math.floor(initialPos % 60).toString().padStart(2, '0');
      const dm = Math.floor(dur / 60);
      const ds = Math.floor(dur % 60).toString().padStart(2, '0');
      const initialTimeDisplay = `${fm}:${fs}<span style="opacity:1; font-size: 0.9em;">|</span>${dm}:${ds}`;    

      // --- GÉNÉRATEUR DYNAMIQUE D'ÉGALISEUR ---
      const numEqBars = 10; // [ZONE MODIFIABLE] : Nombre de barres de l'égaliseur (ex: 5, 10, 16)
      let eqHtml = '';
      // Séquence d'animations déterministe pour éviter les sauts graphiques
      const eqPattern = [1, 5, 9, 3, 7, 2, 6, 10, 4, 8];
      for(let i = 0; i < numEqBars; i++) {
        let typeIndex = eqPattern[i % eqPattern.length];
        eqHtml += `<div class="bar bar${typeIndex}"></div>`;
      }
      
      return `
        <style>
          /* Génération dynamique des keyframes d'animation CSS (Marquee) */
          @keyframes sync-marquee-artist {
            0% { transform: translateX(0); }
            ${artistPercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes sync-marquee-subtitle {
            0% { transform: translateX(0); }
            ${subtitlePercentage}% { transform: translateX(-50%); }
            100% { transform: translateX(-50%); }
          }
          @keyframes fadeBlink { 
            0% { opacity: 0.2; } 
            100% { opacity: 1; } 
          }
        </style>
        <!-- 
          [ZONE MODIFIABLE] : ESPACEMENT (PADDING) DE L'ÉCRAN LCD
          Si vous voulez que l'écran soit moins haut, vous devez réduire le 'padding' 
          (actuellement à 8px 12px) dans la balise div ci-dessous, ET réduire la taille 
          des polices HTML (voir plus bas).
        -->
        <div style="box-sizing: border-box; width: 100%; height: 100%; background: var(--bg-screen-lcd); border: 4px solid var(--color-chassis-base); border-top-color: var(--color-chassis-shadow-deep); border-left-color: var(--color-chassis-shadow-light); border-bottom-color: var(--color-chassis-highlight-deep); border-right-color: var(--color-chassis-highlight-light); border-radius: var(--radius-md); padding: 4px 10px; display: flex; flex-direction: column; justify-content: space-between; text-align: left; overflow: hidden; box-shadow: var(--shadow-encastrement); position: relative;">
          <!-- Reflet vitré -->
          <div style="position: absolute; top: 0; left: 0; right: 0; height: 50%; background: linear-gradient(to bottom, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.02) 100%); pointer-events: none; z-index: 2;"></div>
          
          ${isOff ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: rgba(var(--color-dos-green-rgb), 0.3); text-shadow: none;">
              [ ${textStatus} ]
            </div>
          </div>
          ` : isPaused ? `
          <div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; position: relative; z-index: 1;">
            <div style="font-size: 24px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-strong); animation: fadeBlink 1.5s alternate infinite ease-in-out;">
              [ ${textStatus} ]
            </div>
          </div>
          ` : `
          <div style="display: flex; justify-content: space-between; width: 100%; position: relative; z-index: 1;">
            
            <!-- Left Column: Artist + Subtitle -->
            <div style="display: flex; flex-direction: column; overflow: hidden; flex-grow: 1;">
              
              <!-- [ZONE MODIFIABLE] TEXTE ARTISTE (Modifiez le font-size: 16px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; position: relative;">
                <div style="${artistNeedsScroll ? `animation: sync-marquee-artist ${maxDuration}s linear infinite;` : ''} font-size: 16px; font-family: var(--font-family-lcd); font-weight: var(--font-weight-lcd); color: var(--color-dos-green); text-shadow: var(--text-shadow-lcd-soft); display: inline-flex; width: max-content;">
                  <span style="padding-right: ${artistNeedsScroll ? '50px' : '0'};">${artist}</span>
                  ${artistNeedsScroll ? `<span style="padding-right: 50px;">${artist}</span>` : ''}
                </div>
              </div>

              <!-- [ZONE MODIFIABLE] TEXTE SOUS-TITRE (Modifiez le font-size: 12px ci-dessous si besoin) -->
              <div style="overflow: hidden; width: 100%; white-space: nowrap; margin-top: 4px; position: relative;">
                <div style="${subtitleNeedsScroll ? `animation: sync-marquee-subtitle ${maxDuration}s linear infinite;` : ''} font-size: 12px; font-family: var(--font-family-lcd); color: var(--color-dos-green); display: inline-flex; width: max-content; opacity: 0.7;">
                  <span style="padding-right: ${subtitleNeedsScroll ? '50px' : '0'};">${subtitle}</span>
                  ${subtitleNeedsScroll ? `<span style="padding-right: 50px;">${subtitle}</span>` : ''}
                </div>
              </div>
            </div>

          </div>
          `}
          ${!isOff ? `
          <!-- Playlist & Track & Volume -->          
          <div style="display: flex; gap: 8px; align-items: center; justify-content: space-between; font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: var(--font-weight-lcd); margin-bottom: 2px;">
            <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); margin-left: 0px;">PLAYLIST: ${playlist} | TRACK: ${track}</div>
            ${volumePct !== null ? `
            <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); white-space: nowrap;">
              VOL: ${isMuted ? 'MUTE' : volumePct + '%'}
            </div>
            ` : ''}
          </div>
          ` : ''}

          ${!isOff ? `
          <div style="margin-top: 0px; display: flex; justify-content: space-between; align-items: flex-end; width: 100%; height: var(--size-eq-height, 20px);">
            <div class="winamp-eq ${isPlaying ? 'playing' : ''}">
              ${eqHtml}
            </div>
          
            <!-- Compteur Digital & Ticker JS -->
            <div class="winamp-timer" style="font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: normal; color: var(--color-dos-green); opacity: 1; white-space: nowrap; margin-bottom: -0.5px;">
              ${initialTimeDisplay}
            </div>
            
            <img src="x" style="display:none;" onerror="
              (() => {
                window.__winampTickers = window.__winampTickers || {};
                const key = '${entity ? entity.entity_id : 'none'}';
                const trackId = '${escJs(attr.media_title) + '|' + escJs(attr.media_artist) + '|' + dur}';
                const dur = ${dur};
                const isPlaying = ${isPlaying ? 'true' : 'false'};
                const rawPos = ${initialPos};
                const img = this;
            
                let t = window.__winampTickers[key];
            
                if (!t || t.trackId !== trackId) {
                  // Vrai changement de piste (ou premier rendu) : on repart de la position brute
                  if (t && t.intervalId) clearInterval(t.intervalId);
                  t = { trackId: trackId, pos: rawPos, dur: dur, isPlaying: isPlaying, img: img, intervalId: null };
                  window.__winampTickers[key] = t;
                } else {
                  // Même piste : on ne recule JAMAIS. On n'accepte qu'une vraie avance
                  // ou un écart important (>3s = vrai seek), jamais un micro-recul (jitter réseau).
                  t.dur = dur;
                  t.isPlaying = isPlaying;
                  t.img = img;
                  if (rawPos > t.pos || (t.pos - rawPos) > 3) {
                    t.pos = rawPos;
                  }
                }
            
                function paint() {
                  const root = t.img.getRootNode();
                  if (!root || !root.querySelector) return;
            
                  const timerEl = root.querySelector('.winamp-timer');
                  if (timerEl) {
                    const fm = Math.floor(t.pos / 60);
                    const fs = Math.floor(t.pos % 60).toString().padStart(2, '0');
                    const dm = Math.floor(t.dur / 60);
                    const ds = Math.floor(t.dur % 60).toString().padStart(2, '0');
                    timerEl.innerHTML = fm + ':' + fs + '<span style=\\'opacity:1; font-size: 0.9em;\\'>|</span>' + dm + ':' + ds;
                  }
            
                  const sliderEl = root.querySelector('.glass-slider');
                  if (sliderEl && !sliderEl.matches(':active')) {
                    sliderEl.value = t.pos;
                    const fillEl = root.querySelector('.slider-progress-fill');
                    if (fillEl) fillEl.style.width = (t.pos / t.dur) * 100 + '%';
                  }
                }
            
                paint();
            
                // Un seul intervalle vit en continu pour cette entité, jamais tué par un re-rendu
                if (!t.intervalId) {
                  t.intervalId = setInterval(() => {
                    if (t.isPlaying && t.pos < t.dur) t.pos++;
                    paint();
                  }, 1000);
                }
              })();
            ">
            ` : ''}
            ${!isOff ? `
            <div style="display: flex; gap: 8px; align-items: center; font-family: var(--font-family-lcd); font-size: var(--font-size-lcd-xs); font-weight: var(--font-weight-lcd); margin-bottom: -2px;">
              <div style="color: ${repeat && repeat !== 'off' ? 'var(--color-dos-green)' : 'rgba(var(--color-dos-green-rgb), 0.4)'}; filter: ${repeat && repeat !== 'off' ? 'drop-shadow(0 0 2px rgba(var(--color-dos-green-rgb), 0.8))' : 'none'}; display: flex; transform: translateY(-0.4px);">
                <ha-icon icon="${repeat === 'one' ? 'mdi:repeat-once' : 'mdi:repeat'}" style="--mdc-icon-size: 14px;"></ha-icon>
              </div>
              <div style="color: var(--color-dos-green); opacity: var(--opacity-lcd-dim); margin-left: 8px;">OUT: ${source}</div>
            </div>
            ` : ''}
          </div>

        </div>
      `;
    ]]]
  progress: |
    [[[
      // --- 3. BARRE DE PROGRESSION (SLIDER) ---
      const attr = entity ? entity.attributes : {};
      let pct = 0;
      let duration = Math.round(attr.media_duration || 100);
      let position = Math.round(attr.media_position || 0);
      
      // --- Utilise la position déjà avancée par le ticker (info) si disponible
      const tickerKey = entity ? entity.entity_id : 'none';
      const trackId = (attr.media_title || '') + '|' + (attr.media_artist || '') + '|' + duration;
      if (typeof window !== 'undefined' && window.__winampTickers) {
        const ticker = window.__winampTickers[tickerKey];
        if (ticker && ticker.trackId === trackId && typeof ticker.pos === 'number') {
          position = ticker.pos;
        }
      }
      
      if (duration && position) {
        pct = (position / duration) * 100;
      }
      
      const sliderId = 'slider-' + Math.random().toString(36).substr(2, 9);
      
      return `
        <div style="position: relative; width: 100%; height: 32px; display: flex; align-items: center;">
          <!-- Piste de fond (grise, fixe) -->
          <div style="position: absolute; left: 0; right: 0; height: 8px; background: var(--slider-track-bg-off); border-radius: 10px; border: var(--slider-track-border); box-shadow: var(--slider-track-shadow); pointer-events: none;"></div>

          <!-- Remplissage vert (élément séparé, pas de pseudo-élément) -->
          <div class="slider-progress-fill" style="position: absolute; left: 0; height: 8px; width: ${pct}%; background: var(--color-success); border-radius: 10px; pointer-events: none;"></div>

          <input type="range" class="glass-slider" id="${sliderId}" min="0" max="${duration}" value="${position}" step="1"
            ${!entity ? 'disabled' : ''}
            style="position: relative; z-index: 1; background: transparent;"
            onpointerdown="event.stopPropagation();"
            onclick="event.stopPropagation();"
            oninput="
              const root = this.getRootNode();
              if (root && root.querySelector) {
                const fillEl = root.querySelector('.slider-progress-fill');
                if (fillEl) fillEl.style.width = (this.value / this.max) * 100 + '%';

                const tEl = root.querySelector('.winamp-timer');
                if(tEl) {
                  const fm = Math.floor(this.value/60);
                  const fs = Math.floor(this.value%60).toString().padStart(2,'0');
                  const dm = Math.floor(this.max/60);
                  const ds = Math.floor(this.max%60).toString().padStart(2,'0');
                  tEl.innerHTML = fm + ':' + fs + '<span style=\\'opacity:1; font-size: 0.9em;\\'>|</span>' + dm + ':' + ds;
                }
              }
            "
            onchange="
              if (!'${entity ? entity.entity_id : ''}') return;
              document.querySelector('home-assistant').hass.callService('media_player', 'media_seek', {
                entity_id: '${entity ? entity.entity_id : ''}',
                seek_position: parseFloat(this.value)
              });
            "
          >
        </div>
      `;
    ]]]
  controls: |
    [[[
      // --- 4. BOUTONS DE CONTRÔLE MULTIMÉDIA ---
      // Détection de l'état actuel pour afficher dynamiquement l'icône Play ou Pause
      const isPlaying = entity ? (entity.state === 'playing') : false;
      const eid = entity ? entity.entity_id : '';
      const attr = entity ? entity.attributes : {};
        
      // --- GESTION DU VOLUME (lit le même verrou que l'affichage pour rester cohérent) ---
      window.__winampVolumeLocks = window.__winampVolumeLocks || {};
      const volLockCtrl = window.__winampVolumeLocks[eid];
      
      let isMuted = attr.is_volume_muted === true;
      let volumeLevel = typeof attr.volume_level === 'number' ? attr.volume_level : 0;
      
      if (volLockCtrl && (Date.now() - volLockCtrl.time) < 3000) {
        volumeLevel = volLockCtrl.pct / 100;
        isMuted = volLockCtrl.muted;
      }

      // --- ÉTAT POWER ---
      const isOff = entity ? (entity.state === 'off' || entity.state === 'unavailable') : true;    

      // Gestion intelligente du mode boucle (Repeat)
      // Cycle standard : off -> all -> one -> off
      const repeatMode = attr.repeat || 'off';
      const isRepeat = repeatMode !== 'off';
      const repeatIcon = repeatMode === 'one' ? 'mdi:repeat-once' : (repeatMode === 'all' ? 'mdi:repeat' : 'mdi:repeat-off');
      
      // Calcul du prochain état de boucle lors du clic sur le bouton Repeat
      let nextRepeat = 'all';
      if (repeatMode === 'all') nextRepeat = 'one';
      if (repeatMode === 'one') nextRepeat = 'off';

      return `
        <!-- Conteneur des boutons alignés horizontalement -->
        <div style="display: flex; gap: 6px; align-items: center; justify-content: flex-start; padding-bottom: 4px;">
          
          <!-- Bouton PRÉCÉDENT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_previous_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-previous"></ha-icon>
          </div>
          
          <!-- Bouton LECTURE/PAUSE (S'illumine via la classe 'active' si la musique joue) -->
          <div class="glass-btn ${isPlaying ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', '${isPlaying ? 'media_pause' : 'media_play'}', {entity_id: '${eid}'})">
            <ha-icon icon="${isPlaying ? 'mdi:pause' : 'mdi:play'}"></ha-icon>
          </div>
        
          <!-- Bouton SUIVANT -->
          <div class="glass-btn" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'media_next_track', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:skip-next"></ha-icon>
          </div>
        
          <!-- Bouton BOUCLE (Envoie la commande avec la variable calculée 'nextRepeat') -->
          <div class="glass-btn glass-btn-sm ${isRepeat ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'repeat_set', {entity_id: '${eid}', repeat: '${nextRepeat}'})">
            <ha-icon icon="${repeatIcon}"></ha-icon>
          </div>

          <!-- Bouton VOLUME MOINS -->
          <div class="glass-btn glass-btn-sm" onclick="
            event.stopPropagation();
            window.__winampVolumeLocks = window.__winampVolumeLocks || {};
            const cur = ${Math.round(volumeLevel * 100)};
            const next = Math.max(0, cur - 10);
            window.__winampVolumeLocks['${eid}'] = { pct: next, muted: ${isMuted}, time: Date.now() };
            if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'volume_down', {entity_id: '${eid}'})
          ">
            <ha-icon icon="mdi:volume-minus"></ha-icon>
          </div>
          
          <!-- Bouton MUTE (s'illumine si coupé) -->
          <div class="glass-btn glass-btn-sm ${isMuted ? 'active' : ''}" onclick="
            event.stopPropagation();
            window.__winampVolumeLocks = window.__winampVolumeLocks || {};
            const cur = ${Math.round(volumeLevel * 100)};
            window.__winampVolumeLocks['${eid}'] = { pct: cur, muted: ${!isMuted}, time: Date.now() };
            if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'volume_mute', {entity_id: '${eid}', is_volume_muted: ${!isMuted}})
          ">
            <ha-icon icon="${isMuted ? 'mdi:volume-off' : 'mdi:volume-high'}"></ha-icon>
          </div>
          
          <!-- Bouton VOLUME PLUS -->
          <div class="glass-btn glass-btn-sm" onclick="
            event.stopPropagation();
            window.__winampVolumeLocks = window.__winampVolumeLocks || {};
            const cur = ${Math.round(volumeLevel * 100)};
            const next = Math.min(100, cur + 10);
            window.__winampVolumeLocks['${eid}'] = { pct: next, muted: ${isMuted}, time: Date.now() };
            if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', 'volume_up', {entity_id: '${eid}'})
          ">
            <ha-icon icon="mdi:volume-plus"></ha-icon>
          </div>

          <!-- Bouton POWER (s'illumine en rouge/vert selon l'état) -->
          <div class="glass-btn glass-btn-power ${!isOff ? 'active' : ''}" onclick="event.stopPropagation(); if('${eid}') document.querySelector('home-assistant').hass.callService('media_player', '${isOff ? 'turn_on' : 'turn_off'}', {entity_id: '${eid}'})">
            <ha-icon icon="mdi:power"></ha-icon>
          </div>    
        </div>
      `;
    ]]]
extra_styles: >
  /* ==========================================================================
     [ZONES MODIFIABLES] : STYLES CSS (COULEURS & THÈMES)
     C'est ici que vous pouvez changer les couleurs globales du lecteur Winamp.
     Remplacez les codes (#XXXXXX) ou les noms de couleurs pour adapter le thème.
     ========================================================================== */
  :host {
    display: block;
    width: 100%;
    box-sizing: border-box;  
    /* --- COULEURS DU CHÂSSIS --- 
       Jeux d'ombres et lumières pour créer l'effet 3D "encastré" */
    --color-chassis-base: #1c1c1c;
    --color-chassis-shadow-deep: #0a0a0a;
    --color-chassis-shadow-light: #111111;
    --color-chassis-highlight-deep: #333333;
    --color-chassis-highlight-light: #2b2b2b;
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES TEXTES ET BOUTONS
       --------------------------------------------------------- */
    --color-dos-green: #00FF00; /* Le fameux vert Winamp (Textes de l'écran) */
    --color-dos-green-rgb: 0, 255, 0; /* Mettez les valeurs RGB de votre couleur ci-dessus (ex: pour le vert) */
    --color-neon-active: rgb(68,115,158); /* Couleur des boutons physiques (Violet par défaut) */
    --color-success: rgb(46, 139, 87); /* Couleur du bouton actif (Lecture en cours) */
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : COULEURS DES DÉGRADÉS (FOND ET ÉGALISEUR)
       --------------------------------------------------------- */
    --bg-screen-lcd: radial-gradient(ellipse at center, #0f240f 0%, #030803 80%, #000000 100%); /* Fond de l'écran LCD */
    
    /* 
       Couleurs de l'égaliseur animé :
       - Actuellement en "Dégradé doux" : Vert de 0%, Jaune progressif jusqu'à 60%, Rouge à 100%.
       - Si vous voulez que le rouge arrive plus bas, baissez les pourcentages (ex: #FFFF00 30%, #FF0000 60%).
       - Si vous voulez des "Blocs LED" nets (sans mélange baveux), doublez les pourcentages au point de rupture :
         Exemple LED : linear-gradient(to top, #00FF00 0%, #00FF00 60%, #FFFF00 60%, #FFFF00 85%, #FF0000 85%, #FF0000 100%)
       - Vous pouvez empiler autant de couleurs que vous le souhaitez.
    */
    --bg-eq-bars: linear-gradient(to top, #00FF00 0%, #FFFF00 60%, #FF0000 100%);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : TYPOGRAPHIE LCD
       Ajustez la taille de police si l'écran LCD est trop petit chez vous
       --------------------------------------------------------- */
    --font-family-lcd: monospace;
    --font-weight-lcd: bold;
    --font-size-lcd-large: 24px;
    --font-size-lcd-xs: 10px;
    --letter-spacing-lcd: 4px;
    
    /* --- EFFETS DE LUEUR (NÉON) --- */
    --text-shadow-lcd-strong: 0 0 8px rgba(0, 255, 0, 0.8);
    --text-shadow-lcd-soft: 0 0 5px rgba(0, 255, 0, 0.5);
    --shadow-neon-glow-active: drop-shadow(0 0 5px var(--color-neon-active)); /* Aura autour du bouton physique pressé */
    --shadow-neon-glow-success: drop-shadow(0 0 6px var(--color-success)); /* Aura autour du bouton "Lecture" actif */
    --opacity-lcd-dim: 0.8;
    --opacity-lcd-dimmer: 0.7;
    
    /* ---------------------------------------------------------
       EFFETS "VERRE BOMBÉ" (BOUTONS PHYSIQUES)
       Construits avec de multiples couches d'ombres et reflets
       --------------------------------------------------------- */
    /* Fonds sphériques pour créer le volume du bouton */
    --bg-verre-bombe: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.02) 50%, rgba(0,0,0,0.4) 100%);
    --bg-verre-bombe-pressed: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0.6) 100%);
    --bg-verre-bombe-active: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.08) 50%, rgba(0,0,0,0.2) 100%);
    
    /* Ombres externes (portée sur le châssis) et internes (relief bombé) */
    --shadow-verre-bombe: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 3px rgba(255,255,255,0.3), inset -2px -2px 6px rgba(0,0,0,0.7);
    --shadow-verre-bombe-pressed: 0px 1px 2px rgba(0,0,0,0.8), -1px -1px 1px rgba(255,255,255,0.08), inset 2px 2px 8px rgba(0,0,0,0.8), inset -1px -1px 2px rgba(255,255,255,0.1);
    --shadow-verre-bombe-active: 2px 4px 6px rgba(0,0,0,0.6), -1px -1px 1px rgba(255,255,255,0.08), inset 1px 1px 4px rgba(255,255,255,0.5), inset -2px -2px 6px rgba(0,0,0,0.7);
    
    /* --- COULEURS ET OMBRES DE LA BARRE DE PROGRESSION (SLIDER) --- */
    --slider-track-bg-off: rgba(0, 0, 0, 0.6); /* Couleur du fond de la piste */
    --slider-track-border: 1px solid rgba(255, 255, 255, 0.05);
    --slider-track-shadow: inset 0 1px 3px rgba(0,0,0,0.8);
    --slider-thumb-bg: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 40%, rgba(0,0,0,0.2) 100%); /* Effet verre du curseur */
    --slider-thumb-border: 1px solid rgba(255, 255, 255, 0.1);
    --slider-thumb-border-top: 1px solid rgba(255, 255, 255, 0.5);
    --slider-thumb-border-left: 1px solid rgba(255, 255, 255, 0.3);
    --slider-thumb-shadow: 2px 4px 6px rgba(0,0,0,0.5), inset 1px 1px 2px rgba(255,255,255,0.5), inset -1px -1px 2px rgba(0,0,0,0.4);
    --slider-thumb-backdrop: blur(12px) brightness(1.15); /* Floutage derrière le curseur */
    
    /* Ombre d'encastrement globale (L'écran entier semble creusé dans le plastique) */
    --shadow-encastrement: inset 2px 2px 5px rgba(255, 255, 255, 0.25), inset -3px -3px 8px rgba(0,0,0,1), inset 0 0 20px rgba(0,0,0,0.8);
    
    /* ---------------------------------------------------------
       [ZONE MODIFIABLE] : DIMENSIONS ET TAILLES GLOBALES
       Ajustez ces valeurs pour redimensionner globalement les éléments
       --------------------------------------------------------- */
    --size-art-cover: 190px; /* Largeur de la pochette (La grille s'ajustera automatiquement) */
    --size-btn-physique: 28px; /* Diamètre des boutons de contrôle principaux */
    --size-btn-physique-sm: 28px; /* Diamètre des petits boutons (Shuffle/Repeat) */
    --size-eq-height: 20px; /* [ZONE MODIFIABLE] Hauteur de l'égaliseur dynamique */
    --radius-md: 12px; /* Arrondi des angles de la pochette et de l'écran */
  }


  /* --- ANIMATION GLOBALE DE LA CARTE --- */

  /* Style de base de la carte : bordures subtiles et ombres complexes pour
  l'effet de profondeur */

  ha-card {
    width: 100%;
    border: 1.5px solid rgba(255, 255, 255, 0.15) !important;
    border-radius: 12px !important;
    box-shadow: 0 16px 36px #000000CC, 0 0 1px #FFFFFF1A, 0 4px 12px #FFFFFF08, inset 0 1.5px 1.5px #FFFFFF59, inset 0 3px 10px #FFFFFF1F, inset 0 -3px 6px #00000099 !important;
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
  }


  /* 
     [ZONE MODIFIABLE] : EFFET AU SURVOL (HOVER) 
     La carte "monte" de 4px vers le haut quand on passe la souris. 
     Mettez translateY(0px) si vous ne voulez pas d'animation.
  */

  ha-card:hover {
    border-color: rgba(255, 255, 255, 0.25) !important;
    box-shadow: 0 24px 48px #000000D9, 0 0 2px #FFFFFF26, 0 6px 16px #FFFFFF0D, inset 0 1.5px 1.5px #FFFFFF73, inset 0 3px 10px #FFFFFF2E, inset 0 -3px 6px #000000B3 !important;
  }


  /* --- STYLES DE LA BARRE DE PROGRESSION --- */

  .glass-slider {
    -webkit-appearance: none;
    width: 100%;
    height: 32px;
    background: transparent;
    outline: none;
    cursor: pointer;
    margin: 0;
  }


  /* Piste du slider (La rigole creusée dans le châssis) */

  .glass-slider::-webkit-slider-runnable-track {
    width: 100%;
    height: 8px;
    background: none;
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }


  /* 
     ==========================================================================
     [ZONE MODIFIABLE] : APPARENCE DU CURSEUR (Slider Thumb)
     ⚠️ RÈGLE D'OR DU WEB : Modifiez TOUJOURS les DEUX blocs ci-dessous à l'identique !
     - Le 1er bloc (-webkit) contrôle : Chrome, Edge, Brave, Safari, Appli Mobile.
     - Le 2ème bloc (-moz) contrôle : Firefox.
     Si vous modifiez la taille (width: 14px; height: 24px) dans l'un, faites-le dans l'autre !
     ========================================================================== 
  */

  .glass-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 24px;
    margin-top: -8px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }


  /* --- COMPATIBILITÉ FIREFOX --- */

  /* Piste du slider (Version Firefox) */

  .glass-slider::-moz-range-track {
    width: 100%;
    height: 8px;
    background: none;
    border-radius: 10px;
    border: var(--slider-track-border);
    box-shadow: var(--slider-track-shadow);
  }


  /* [ZONE MODIFIABLE] : Curseur effet verre (Version Firefox - Gardez les mêmes
  dimensions que -webkit ci-dessus !) */

  .glass-slider::-moz-range-thumb {
    width: 14px;
    height: 24px;
    background: var(--slider-thumb-bg);
    backdrop-filter: var(--slider-thumb-backdrop);
    -webkit-backdrop-filter: var(--slider-thumb-backdrop);
    border: var(--slider-thumb-border);
    border-top: var(--slider-thumb-border-top);
    border-left: var(--slider-thumb-border-left);
    border-radius: 5px;
    box-shadow: var(--slider-thumb-shadow);
    cursor: pointer;
  }


  /* --- STYLES DES BOUTONS PHYSIQUES (.glass-btn) --- */

  .glass-btn {
    width: var(--size-btn-physique);
    height: var(--size-btn-physique);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-verre-bombe);
    border: 2px solid var(--color-chassis-base);
    box-shadow: var(--shadow-verre-bombe);
    cursor: pointer;
    transition: transform 0.1s ease, box-shadow 0.1s ease;
  }

  .glass-btn-power ha-icon {
    color: rgba(255, 80, 80, 0.7);
    filter: drop-shadow(0 0 4px rgba(255, 80, 80, 0.5));
  }

  .glass-btn-power.active ha-icon {
    color: var(--color-success) !important;
    filter: var(--shadow-neon-glow-success) !important;
  }  


  .glass-btn-sm {
    width: var(--size-btn-physique-sm) !important;
    height: var(--size-btn-physique-sm) !important;
  }


  /* [ZONE MODIFIABLE] : APPARENCE DES ICÔNES DES BOUTONS */

  .glass-btn ha-icon {
    --mdc-icon-size: 18px; /* Taille des icônes à l'intérieur du bouton */
    /* La couleur pointe vers la variable globale '--color-neon-active'.
       Vous pouvez soit changer la variable tout en haut, soit écrire votre couleur ici (ex: red) */
    color: var(--color-neon-active);
    opacity: 0.95;
    filter: var(--shadow-neon-glow-active);
    transition: all 0.2s ease;
  }


  .glass-btn-sm ha-icon {
    --mdc-icon-size: 14px !important;
  }


  /* Effet d'enfoncement physique au clic (active) */

  .glass-btn:active {
    transform: translateY(2px);
    background: var(--bg-verre-bombe-pressed);
    box-shadow: var(--shadow-verre-bombe-pressed);
  }


  /* L'icône s'illumine lors de la pression */

  .glass-btn:active ha-icon {
    color: var(--color-success) !important;
    filter: var(--shadow-neon-glow-success) !important;
    opacity: 1 !important;
  }


  .glass-btn.active {
    background: var(--bg-verre-bombe-active);
    box-shadow: var(--shadow-verre-bombe-active);
  }


  .glass-btn.active ha-icon {
    color: var(--color-success);
    opacity: 1;
    filter: var(--shadow-neon-glow-success);
  }


  /* --- EFFET MARQUEE (DÉFILEMENT DE TEXTE) --- */

  /* La classe marquee applique une translation continue de droite à gauche */

  .marquee {
    animation: marquee 15s linear infinite;
  }


  /* L'animation translate de 0 à -50% (car le texte est dupliqué 2 fois dans le
  HTML pour boucler) */

  @keyframes marquee {
    0% { transform: translateX(0); }
    100% { transform: translateX(-50%); }
  }


  /* --- STYLES DE L'ÉGALISEUR --- */

  /* Conteneur principal des barres */

  .winamp-eq {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: var(--size-eq-height, 20px);
  }


  /* Style de base d'une seule barre de l'égaliseur */

  .winamp-eq .bar {
    width: 4px;
    height: 2px;
    background: var(--bg-eq-bars);
    background-size: 100% var(--size-eq-height, 20px);
    background-position: bottom;
    border-radius: 1px 1px 0 0;
    opacity: 0.85;
  }


  /* Attribution des animations individuelles aux 10 types de barres (uniquement
  si classe 'playing' active) */

  .winamp-eq.playing .bar1 { animation: eq1 0.4s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar2 { animation: eq2 0.6s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar3 { animation: eq3 0.5s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar4 { animation: eq4 0.7s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar5 { animation: eq5 0.45s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar6 { animation: eq6 0.65s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar7 { animation: eq7 0.55s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar8 { animation: eq8 0.75s infinite alternate
  ease-in-out; }

  .winamp-eq.playing .bar9 { animation: eq9 0.4s infinite alternate ease-in-out;
  }

  .winamp-eq.playing .bar10 { animation: eq10 0.8s infinite alternate
  ease-in-out; }


  /* Animations relatives (%) pour s'adapter dynamiquement à
  var(--size-eq-height) */

  @keyframes eq1 { 0% { height: 10%; } 100% { height: 80%; } }

  @keyframes eq2 { 0% { height: 20%; } 100% { height: 100%; } }

  @keyframes eq3 { 0% { height: 10%; } 100% { height: 70%; } }

  @keyframes eq4 { 0% { height: 30%; } 100% { height: 90%; } }

  @keyframes eq5 { 0% { height: 15%; } 100% { height: 75%; } }

  @keyframes eq6 { 0% { height: 25%; } 100% { height: 95%; } }

  @keyframes eq7 { 0% { height: 10%; } 100% { height: 60%; } }

  @keyframes eq8 { 0% { height: 20%; } 100% { height: 85%; } }

  @keyframes eq9 { 0% { height: 10%; } 100% { height: 80%; } }

  @keyframes eq10 { 0% { height: 25%; } 100% { height: 100%; } }
section_mode: true
grid_options:
  rows: auto
  columns: full

ca va tu t'amuse bien a ce que je vois :smiley: faudra que je regarde ce que tu as fais et voir pour integrer

Oui, j'ai la tête en 4 d'avoir passé du temps sur le code. ( tester, modifier)