/* ============================================
   Reusable UI Components
   ============================================ */

/* ============================================
   Cards
   ============================================ */
.card {
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: 1.5rem;
    margin-bottom: 1.5rem;
    box-shadow: var(--shadow);
}
.card-title {
    font-family: var(--police-titre);
    font-size: 1.6rem;
    font-weight: 400;
    line-height: 1.1;
    margin-bottom: 1rem;
    color: var(--text-primary);
}
.card-title-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1rem;
}

/* ============================================
   Buttons
   ============================================ */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1.25rem;
    border: none;
    border-radius: var(--radius);
    font-size: 0.9rem;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s, transform 0.1s;
    text-decoration: none;
}
.btn:active:not(:disabled) { transform: scale(0.98); }
.btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}
.btn-spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    flex-shrink: 0;
}
.btn-primary {
    background: var(--primary);
    color: var(--primary-contrast);
}
.btn-primary:hover { background: var(--primary-hover); }
.btn-secondary {
    background: var(--bg-secondary);
    color: var(--text-primary);
}
.btn-secondary:hover { background: var(--bg-tertiary); }
.btn-outline-muted {
    background: transparent;
    color: var(--text-secondary);
    border: 1px solid var(--border-color);
    font-size: 0.82rem;
}
.btn-outline-muted:hover {
    background: var(--bg-secondary);
    color: var(--text-primary);
}
.btn-sm {
    font-size: 0.75rem;
    padding: 0.25rem 0.5rem;
}
.btn-warning {
    background: var(--warning);
    color: var(--bg-body);
    font-weight: 600;
}
.btn-warning:hover { background: #eab308; }
.btn-danger {
    background: var(--danger);
    color: #fff;
}
.btn-danger:hover { background: #ef4444; }
.btn-link {
    background: none;
    border: none;
    color: var(--primary);
    font-size: 0.75rem;
    cursor: pointer;
    padding: 0.2rem 0.3rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-weight: 500;
}
.btn-link:hover {
    color: var(--primary-hover);
    text-decoration: underline;
}
.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    background: var(--bg-secondary);
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.15s;
}
.btn-icon:hover:not(:disabled) {
    border-color: var(--primary);
    color: var(--primary);
    background: rgba(var(--primary-rgb), 0.1);
}
.btn-icon:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.btn-icon svg {
    width: 18px;
    height: 18px;
}
/* Variante « action principale » d'un bouton icône : plein, sur la couleur
   d'accent, pour qu'un geste se détache de ses voisins dans une barre.
   À NE PAS CONFONDRE avec .btn-icon--actif (mediatheque.css), qui dit qu'un
   INTERRUPTEUR est enclenché (shuffle, muet) et se lit en creux, fond
   translucide et texte coloré. Celle-ci ne décrit aucun état, elle hiérarchise :
   d'où le fond PLEIN, seule forme qui ne puisse pas se lire comme « allumé ».
   Les deux ne doivent jamais se retrouver sur le même bouton. */
.btn-icon--principal {
    border-color: var(--primary);
    background: var(--primary);
    color: var(--bg-body);
}
.btn-icon--principal:hover:not(:disabled) {
    border-color: var(--primary-hover);
    background: var(--primary-hover);
    color: var(--bg-body);
}

/* ============================================
   Forms
   ============================================ */
.form-group { margin-bottom: 1rem; }
.form-label {
    display: block;
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-bottom: 0.35rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-weight: 500;
}
.form-control, .form-select {
    width: 100%;
    padding: 0.5rem 0.75rem;
    background: var(--bg-body);
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 0.9rem;
    transition: border-color 0.2s, box-shadow 0.2s;
}
.form-control:focus, .form-select:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(var(--primary-rgb), .2);
}
/* Aide posée SOUS un champ (ce que le champ accepte, ce qu'il refuse). Sous le
   champ et non dans son placeholder : un placeholder disparaît à la frappe,
   exactement quand l'aide sert. */
.form-aide {
    margin-top: 0.4rem;
    font-size: 0.8rem;
    line-height: 1.5;
    color: var(--text-secondary);
}
.controls-row {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    align-items: end;
    margin-bottom: 1rem;
}
.controls-row .form-group { margin-bottom: 0; }
/* Une aide posée SOUS la ligne plutôt que dans le groupe de son champ (Phase 36 :
   dans le groupe, sa phrase dimensionnait le conteneur flexible et étirait le
   <select>, mesuré à 1045 px contre 83). La ligne rend alors sa marge basse à
   l'aide, sinon la phrase reste collée à l'étiquette du bloc suivant. */
.controls-row:has(+ .form-aide) { margin-bottom: 0; }
.controls-row + .form-aide { margin-bottom: 1rem; }

/* ============================================
   Cases à cocher (Phase 32)

   La case était le SEUL contrôle du projet encore laissé au navigateur. Sur un
   thème ardoise qui ne déclare nulle part `color-scheme: dark`, Chrome la
   dessinait en mode clair : un petit carré BLANC, posé la plupart du temps sur
   une vignette. `accent-color` ne colorait que l'état coché, c'est-à-dire
   précisément celui où la case gênait le moins.

   Ce reste un <input type="checkbox"> : `form=`, `disabled`, `data-role`, les
   compteurs de sélection et tous les sélecteurs :checked du projet sont
   inchangés. Seul le DESSIN change.
   ============================================ */
.case-a-cocher {
    appearance: none;
    -webkit-appearance: none;
    /* Une case ne se laisse jamais écraser par le texte qu'elle accompagne. */
    flex-shrink: 0;
    width: 1.05rem;
    height: 1.05rem;
    margin: 0;
    border: 1.5px solid var(--text-muted);
    /* 4px et non var(--radius) : sur une boîte de 17 px, l'arrondi de 8 px des
       cartes rend un cercle, et un cercle veut dire « bouton radio », donc un
       choix exclusif. Or on en coche plusieurs. La forme doit dire le geste. */
    border-radius: 4px;
    background: var(--bg-body);
    display: grid;
    place-content: center;
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease,
                opacity 0.12s ease;
}

/* La coche est un carré DÉCOUPÉ et non un caractère : un ✓ typographique change
   de dessin et d'aplomb d'une police à l'autre, et ne se centre jamais dans une
   boîte de 17 px. Elle est toujours dans le DOM, à l'échelle 0 au repos, pour
   que cocher soit une transition et non une apparition. */
.case-a-cocher::before {
    content: "";
    width: 0.62rem;
    height: 0.62rem;
    background: var(--bg-body);
    clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
    transform: scale(0);
    transition: transform 0.12s ease;
}

.case-a-cocher:hover:not(:disabled) {
    border-color: var(--primary);
}

.case-a-cocher:checked {
    background: var(--primary);
    border-color: var(--primary);
}

.case-a-cocher:checked::before {
    transform: scale(1);
}

/* L'état désactivé se dit en COULEUR et jamais en `opacity`, contrairement aux
   cases natives qu'il remplace : `opacity` est la propriété qui révèle la case
   au survol (voir mediatheque.css), et deux propriétaires d'une même propriété
   finissent par se marcher dessus. La case grisée d'une playlist vide serait
   redevenue une case ordinaire au premier survol. */
.case-a-cocher:disabled {
    border-color: var(--border-color);
    background: transparent;
    cursor: not-allowed;
}

.case-a-cocher:disabled:checked {
    background: var(--bg-tertiary);
    border-color: var(--bg-tertiary);
}

.case-a-cocher:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* Posée sur une VIGNETTE : sous une image quelconque, le fond de page ne dit
   plus rien. Il faut une bordure claire et une ombre pour que la case existe
   aussi bien sur un ciel blanc que sur une scène de nuit.

   color-mix plutôt que des rgba() en dur : la charte reste la seule source des
   couleurs (règle de CLAUDE.md), on n'en prend que la transparence. */
.case-a-cocher--sur-image {
    border-color: color-mix(in srgb, var(--text-primary) 60%, transparent);
    background: color-mix(in srgb, var(--bg-body) 65%, transparent);
    box-shadow: var(--shadow);
}

.case-a-cocher--sur-image:disabled {
    border-color: color-mix(in srgb, var(--text-primary) 22%, transparent);
    background: color-mix(in srgb, var(--bg-body) 45%, transparent);
}

/* ============================================
   Chips
   ============================================ */
.chip-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    max-height: 180px;
    overflow-y: auto;
    padding: 0.5rem;
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    background: var(--bg-body);
}
.chip {
    display: inline-flex;
    align-items: center;
    padding: 0.25rem 0.65rem;
    font-size: 0.78rem;
    border-radius: 999px;
    border: 1px solid var(--border-color);
    background: var(--bg-secondary);
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.15s;
    white-space: nowrap;
    user-select: none;
}
.chip:hover {
    border-color: var(--primary);
    color: var(--text-primary);
}
.chip.active {
    background: rgba(var(--primary-rgb), 0.15);
    border-color: var(--primary);
    color: var(--primary);
    font-weight: 500;
}

/* ============================================
   Tabs
   ============================================ */
.tabs {
    display: flex;
    gap: 0.25rem;
    padding: 0.5rem;
    background: var(--bg-secondary);
    border-radius: var(--radius-lg);
    margin-bottom: 1.5rem;
    box-shadow: var(--shadow);
}
.tab {
    flex: 1;
    padding: 0.7rem 1.25rem;
    cursor: pointer;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--text-secondary);
    border: none;
    background: transparent;
    border-radius: var(--radius);
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
}
.tab:hover {
    color: var(--text-primary);
    background: rgba(255,255,255,.05);
}
.tab.active {
    background: var(--primary);
    color: var(--primary-contrast);
    font-weight: 600;
}
.tab svg { width: 18px; height: 18px; flex-shrink: 0; }
.tab-panel { display: none; }
.tab-panel.active {
    display: block;
    animation: fadeInUp 0.3s ease;
}
@keyframes fadeInUp {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}

/* ============================================
   Alerts
   ============================================ */
.alert {
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    margin-bottom: 1rem;
    font-size: 0.9rem;
}
.alert-error {
    background: rgba(248,113,113,.1);
    border: 1px solid var(--danger);
    color: var(--danger);
}
.alert-success {
    background: rgba(52,211,153,.1);
    border: 1px solid var(--accent);
    color: var(--accent);
}
.alert-warning {
    background: rgba(251,191,36,.1);
    border: 1px solid var(--warning);
    color: var(--warning);
}
/* Django émet les tags success|error|warning|info|debug ; info complète la
   famille (CustomLogoutView en pose un). Calqué sur .message.info de login.html. */
.alert-info,
.alert-debug {
    background: rgba(var(--primary-rgb), .1);
    border: 1px solid var(--primary);
    color: var(--primary-light);
}

/* ============================================
   Toast notifications
   ============================================ */
.toast-container {
    position: fixed;
    top: 4.5rem;
    right: 1rem;
    z-index: 99;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    pointer-events: none;
}
.toast {
    pointer-events: auto;
    padding: 0.75rem 1.25rem;
    border-radius: var(--radius);
    font-size: 0.85rem;
    box-shadow: var(--shadow-md);
    animation: toastIn 0.3s ease, toastOut 0.3s ease 3.5s forwards;
    max-width: 400px;
}
.toast--success {
    background: rgba(52, 211, 153, 0.15);
    border: 1px solid var(--accent);
    color: var(--accent);
}
.toast--error {
    background: rgba(248, 113, 113, 0.15);
    border: 1px solid var(--danger);
    color: var(--danger);
}
.toast--warning {
    background: rgba(250, 204, 21, 0.15);
    border: 1px solid var(--warning);
    color: var(--warning);
}
@keyframes toastIn {
    from { opacity: 0; transform: translateX(50px); }
    to { opacity: 1; transform: translateX(0); }
}
@keyframes toastOut {
    from { opacity: 1; }
    to { opacity: 0; }
}

/* ============================================
   Confirm Modal (shared)
   ============================================ */
.confirm-modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(var(--bg-body-rgb), 0.85);
    z-index: 2000;
    justify-content: center;
    align-items: center;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}
.confirm-modal-overlay.active {
    display: flex;
    animation: confirmFadeIn 0.18s ease-out;
}
@keyframes confirmFadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}
.confirm-modal {
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: 1.75rem;
    max-width: 420px;
    width: 90%;
    box-shadow: 0 8px 32px rgba(0,0,0,.5);
    text-align: center;
    animation: confirmSlideIn 0.18s ease-out;
}
@keyframes confirmSlideIn {
    from { opacity: 0; transform: scale(0.95) translateY(-8px); }
    to   { opacity: 1; transform: scale(1) translateY(0); }
}
.confirm-modal-icon {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto 1rem;
}
.confirm-modal-icon svg { width: 24px; height: 24px; }
.confirm-modal-icon--danger {
    background: rgba(248, 113, 113, 0.12);
    color: var(--danger);
}
.confirm-modal-icon--warning {
    background: rgba(250, 204, 21, 0.12);
    color: var(--warning);
}
.confirm-modal-icon--info {
    background: rgba(var(--primary-rgb), 0.12);
    color: var(--primary);
}
.confirm-modal h3 {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 0.5rem;
}
.confirm-modal p {
    color: var(--text-secondary);
    font-size: 0.85rem;
    line-height: 1.5;
    margin-bottom: 0;
}
.confirm-modal-actions {
    display: flex;
    gap: 0.75rem;
    justify-content: center;
    margin-top: 1.25rem;
}
.confirm-modal-actions .btn { min-width: 100px; }

/* ============================================
   Menu contextuel (Phase 31)

   Un bouton qui ouvre un panneau d'entrées, ancré sur lui. Premier composant
   flottant CLIQUABLE du projet : .tooltip-content s'ouvre au survol et porte
   pointer-events: none, il ne pouvait pas servir ici.

   Le panneau est un frère du déclencheur dans un .menu en position: relative,
   jamais son enfant : un panneau dans un <button> serait du HTML invalide que le
   parseur re-parenterait (même raison qui tient les formulaires hors de
   .playlist-card, qui est un <a>).
   ============================================ */
.menu {
    position: relative;
    display: inline-flex;
}
.menu-panneau {
    position: absolute;
    /* Ancré sous le déclencheur et aligné à DROITE : le menu vit dans le coin
       droit de l'en-tête, s'aligner à gauche le ferait déborder de la page. */
    top: calc(100% + 6px);
    right: 0;
    display: flex;
    flex-direction: column;
    min-width: 16rem;
    padding: 0.35rem;
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    box-shadow: var(--shadow-md);
    /* Au-dessus de la bulle du lecteur (900), sous les modales (1500) et sous la
       confirmation (2000). Cliquer une entrée qui ouvre une modale doit voir la
       modale passer devant le menu, même si le menu n'était pas encore fermé. */
    z-index: 1200;
}
/* Le display: flex d'auteur ci-dessus bat le [hidden] du navigateur : sans cette
   règle, le panneau resterait visible en permanence. Même piège que
   .episode-list li[hidden] et .playlist-card[hidden]. */
.menu-panneau[hidden] {
    display: none;
}
.menu-item {
    /* Un bouton, jamais un <a> : aucune de ces entrées ne mène à une URL, et un
       lien sans href n'est pas atteignable au clavier. */
    display: block;
    width: 100%;
    padding: 0.5rem 0.75rem;
    background: none;
    border: none;
    border-radius: var(--radius);
    color: var(--text-primary);
    font-family: inherit;
    font-size: 0.85rem;
    text-align: left;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}
.menu-item:hover:not(:disabled) {
    background: var(--bg-secondary);
}
.menu-item:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}
.menu-item:disabled {
    color: var(--text-muted);
    cursor: not-allowed;
}
/* L'entrée dont on ne revient pas (Phase 38 : supprimer la playlist). La COULEUR
   du texte et jamais l'opacité, qui appartient ailleurs à la révélation d'une
   case (Phase 32) et ne dirait ici qu'« un peu moins présent ». Le fond ne
   s'allume qu'au survol : une entrée rouge pleine, en permanence, ferait de la
   destruction le geste le plus voyant d'un menu qui sert d'abord à ranger. */
.menu-item--danger {
    color: var(--danger);
}
/* Un VOILE de danger, jamais l'aplat : `--text-primary` sur `--danger` plein ne
   donne que ~2,5:1, donc l'entrée devenait MOINS lisible au moment précis où on
   la vise, et moins lisible qu'au repos (~5,3:1). Le texte garde donc sa couleur
   et c'est le fond qui bouge, ce qui laisse le contraste au-dessus du seuil. */
.menu-item--danger:hover:not(:disabled) {
    background: rgba(248, 113, 113, 0.16);
}
.menu-separateur {
    height: 1px;
    margin: 0.35rem 0.25rem;
    background: var(--border-color);
}

/* ============================================
   Modale de formulaire (Phase 31)

   Distincte de .confirm-modal, qui ne porte qu'un titre, un message et deux
   boutons. Celle-ci accueille un formulaire entier (sélecteur de dossier à
   onglets, liste de renommage défilante), d'où une largeur plus généreuse et un
   corps qui défile chez lui.

   z-index 1500, et c'est le point à NE PAS remonter : la confirmation globale
   (2000) doit passer PAR-DESSUS, puisqu'un formulaire d'ici peut la déclencher en
   se soumettant (renommage.js). C'est aussi pourquoi ce n'est pas un <dialog>
   natif : showModal() place l'élément dans le top layer, au-dessus de tout
   z-index, et la confirmation se serait affichée derrière lui.
   ============================================ */
.modale-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(var(--bg-body-rgb), 0.85);
    z-index: 1500;
    justify-content: center;
    align-items: center;
    padding: 1rem;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}
.modale-overlay.active {
    display: flex;
    animation: confirmFadeIn 0.18s ease-out;
}
.modale {
    display: flex;
    flex-direction: column;
    width: min(560px, 92vw);
    max-height: 90vh;
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    box-shadow: 0 8px 32px rgba(0,0,0,.5);
    animation: confirmSlideIn 0.18s ease-out;
}
/* La liste de renommage porte jusqu'à 42 lignes de « ancien devient nouveau » :
   à 560 px elles se cassaient toutes en deux. */
.modale--large {
    width: min(760px, 92vw);
}
.modale-entete {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.25rem 1.5rem 0.75rem;
}
.modale-titre {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--text-primary);
}
.modale-fermer {
    flex-shrink: 0;
    padding: 0 0.25rem;
    background: none;
    border: none;
    color: var(--text-secondary);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
}
.modale-fermer:hover {
    color: var(--text-primary);
}
/* Le corps défile, pas la page : l'overlay est en position fixed, une modale
   plus haute que l'écran deviendrait inatteignable par le bas. */
.modale-corps {
    padding: 0 1.5rem 1.5rem;
    overflow-y: auto;
}

/* ============================================
   Tooltip
   ============================================ */
.tooltip {
    position: relative;
    cursor: help;
}
.tooltip-content {
    position: absolute;
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 9999;
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    padding: 0.6rem 0.8rem;
    min-width: 160px;
    box-shadow: var(--shadow-md);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s, visibility 0.2s;
    text-align: left;
    font-size: 0.75rem;
    pointer-events: none;
    white-space: nowrap;
}
.tooltip-content::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 5px solid transparent;
    border-top-color: var(--border-color);
}
.tooltip:hover .tooltip-content {
    opacity: 1;
    visibility: visible;
}

/* ============================================
   Demo-specific: response viewer
   ============================================ */
.response-viewer {
    background: var(--bg-body);
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    padding: 1rem;
    font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
    font-size: 0.85rem;
    color: var(--text-primary);
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 320px;
    overflow-y: auto;
}
.response-viewer.empty {
    color: var(--text-muted);
    font-style: italic;
}
