/* ============================================================
   STATISTIQUES ET ANNEXES
   ============================================================
   Heatmap d'assiduité, courbe de progression, écran du compte, connexion, aperçu de l'écran Apparence, et panneau de survol du dictionnaire.

   Chargé par index.html. L'ORDRE DES BALISES <link> COMPTE :
   base → ecrans → stats → responsive → theme-pixel. Le responsive
   et les thèmes surchargent ce qui précède par la cascade.
   ============================================================ */

/* --- Statistiques (heatmap, temps passé, progression) --- */

/* L'écran vit de sa largeur : la heatmap étale une année sur 53 colonnes et
   n'a pas le droit de défiler, donc chaque pixel gagné agrandit les cases.
   On reprend la largeur de l'écran Apparence plutôt que d'introduire une
   dimension de plus (620 par défaut → 866 ici, soit ~15px par case). */
body.on-stats .container {
    max-width: 866px;
}

/* Grand écran : même palier que l'écran Apparence. Pas de condition de
   hauteur ici, contrairement à lui : rien dans les stats ne risque de
   déborder verticalement, la page défile. */
@media (min-width: 1180px) {
    body.on-stats .container {
        max-width: 1007px;
    }
}

.stats-section {
    margin-bottom: 28px;
}

.stats-notice {
    background-color: #f0f4ff;
    border: 1px solid #e0e7ff;
    border-radius: 6px;
    padding: 14px 16px;
    margin-bottom: 20px;
    font-size: 14px;
    color: #666;
    line-height: 1.5;
}

.stats-empty {
    font-size: 14px;
    color: #999;
    padding: 10px 0;
}

/* Tuiles de résumé : flexbox comme partout, aucune grille dans ce fichier */
.stats-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 28px;
}

.stat-tile {
    flex: 1 1 110px;
    background-color: #f0f4ff;
    border: 1px solid #e0e7ff;
    border-radius: 8px;
    padding: 14px 10px;
    text-align: center;
}

.stat-tile b {
    display: block;
    font-size: 22px;
    color: #667eea;
}

.stat-tile span {
    display: block;
    margin-top: 4px;
    font-size: 12px;
    color: #999;
}

/* Le bloc porte l'infobulle et NE DÉFILE PAS. C'est tout l'intérêt de la
   séparer du cadre : posée en absolu, elle sort de son repère (au-dessus
   d'une case de la rangée du haut, en dessous d'une case du bas), et dans un
   conteneur défilant elle y déclenchait un ascenseur vertical au survol.
   Même montage que .progress-host pour l'infobulle de la courbe. */
.heatmap-bloc {
    position: relative;
    padding-bottom: 6px;
}

/* Le cadre défilant, et le seul. Sa hauteur est FIXE et ne dépend ni de la
   fenêtre choisie ni de la présence d'un débordement : c'est ce qui empêche
   la page de sauter quand on change de période.
   overflow-y est dit explicitement, et ce n'est pas une redondance : en CSS,
   « visible » sur un axe est calculé en « auto » dès que l'autre ne vaut plus
   « visible ». Sans cette ligne, le cadre défilerait aussi verticalement. */
.heatmap-host {
    height: 118px;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
}

/* Pas d'ascenseur visible : ses 20 px (css/base.css) se prendraient sur la
   hauteur du bloc, qui cesserait d'être la même d'une période à l'autre.
   Le fondu des bords ci-dessous dit à sa place qu'il reste de l'historique.
   Même parti que la liste des decks sur téléphone (css/responsive.css). */
.heatmap-host::-webkit-scrollbar {
    height: 0;
}

/* Firefox : la propriété standard ne doit JAMAIS sortir de cette bascule,
   sous peine de désactiver ::-webkit-scrollbar sur Chrome (piège documenté
   en tête de css/base.css). */
@supports not selector(::-webkit-scrollbar) {
    .heatmap-host {
        scrollbar-width: none;
    }
}

/* Fondu des bords, à la place de l'ascenseur. Une seule couche de dégradé,
   contrairement à la liste des decks : elle en a deux pour épargner sa barre,
   qui est ici masquée. Les deux intensités sont posées par majFondu(zone,'x')
   (js/decks.js) selon la position du défilement ; à 0, les deux étapes du
   dégradé se superposent et il ne reste qu'un aplat opaque. */
.heatmap-host {
    --fondu: 60px;
    --k-gauche: 0;
    --k-droite: 0;
    --gauche-eff: calc(min(var(--fondu), 35%) * var(--k-gauche));
    --droite-eff: calc(min(var(--fondu), 35%) * var(--k-droite));
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--gauche-eff), #000 calc(100% - var(--droite-eff)), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 var(--gauche-eff), #000 calc(100% - var(--droite-eff)), transparent 100%);
}

/* Infobulle du survol, partagée par la heatmap et la courbe : placée en JS,
   translatée d'une demi-largeur pour être centrée sur la case ou sur le point.
   pointer-events none, sinon elle s'interposerait entre le curseur et le
   graphe et ferait clignoter le survol. */
.hm-tip,
.pc-tip {
    position: absolute;
    transform: translateX(-50%);
    z-index: 2;
    padding: 5px 9px;
    background: #5a5d75; /* gris du projet (#6b6e8a) assombri : blanc lisible dessus */
    color: white;
    border-radius: 6px;
    font-size: 12px;
    white-space: nowrap;
    pointer-events: none;
}

.hm-tip[hidden],
.pc-tip[hidden] {
    display: none;
}

/* Aucune dimension ici : le SVG porte ses width et height en pixels, calculés
   par hmGeometrie() (js/stats.js) et égaux à son viewBox. Il fait donc
   exactement la largeur du cadre, sauf quand ses cases y tomberaient sous
   HM_COTE_MIN : il déborde alors, et le cadre le fait défiler.

   La hauteur du cadre (118px ci-dessus) et HM_HAUTEUR (js/stats.js) sont le
   même nombre écrit à deux endroits : si l'un change, l'autre doit suivre. */
.heatmap {
    display: block;
    shape-rendering: crispEdges;
}

/* TOUTES les cases portent leur contour en permanence, transparent au repos :
   le survol ne fait que le colorer. C'est ce qui garantit qu'il désigne une
   case et une seule. Un contour n'apparaissant qu'au survol s'AJOUTERAIT à la
   case, et comme il est centré sur l'arête il déborderait de la moitié de son
   épaisseur sur la gouttière, jusqu'à toucher la case voisine.
   hmCase() (js/stats.js) rétrécit le rectangle d'autant, et --hm-trait est
   posée sur le SVG : l'épaisseur suit la taille des cases, que le CSS ne peut
   pas connaître.

   geometricPrecision, et non le crispEdges hérité de .heatmap : celui-ci sert
   les aplats, mais il aligne les arêtes sur la grille de pixels alors que les
   cases tombent à des coordonnées fractionnaires, ce qui hache le contour
   d'une case à l'autre. .hm-label fait déjà le même écart. */
.hm-cell {
    cursor: pointer;
    stroke: transparent;
    stroke-width: var(--hm-trait, 1px);
    shape-rendering: geometricPrecision;
}

/* Survol : contour discret, qui laisse lire l'intensité de la case. Le jour
   choisi porte le même contour en encre pleine, pour rester distinguable du
   simple survol. Seule la COULEUR les sépare, jamais l'épaisseur. */
.hm-cell:hover {
    stroke: rgba(44, 46, 64, 0.45);
}

/* Libellés de mois et de jours. shape-rendering repasse en auto : le
   crispEdges de .heatmap est fait pour les cases, il abîmerait le texte. */
.hm-label {
    fill: #999;
    shape-rendering: auto;
}

.hm-selected,
.hm-selected:hover {
    stroke: #2c2e40;
}

/* Un niveau = une couleur, posée à la fois en fill (rects du SVG) et en
   background (pastilles de légende en HTML). Rampe rouge s'assombrissant
   régulièrement, sur un vide neutre pour que le rouge ressorte. */
.hm-l0 { fill: #ebedf0; background-color: #ebedf0; }
.hm-l1 { fill: #ffcdd2; background-color: #ffcdd2; }
.hm-l2 { fill: #f19891; background-color: #f19891; }
.hm-l3 { fill: #e53935; background-color: #e53935; }
.hm-l4 { fill: #9d1c1c; background-color: #9d1c1c; }

.hm-legend {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 4px;
    margin-top: 8px;
    font-size: 12px;
    color: #999;
}

.hm-key {
    display: inline-block;
    width: 11px;
    height: 11px;
}

/* Détail du jour choisi */
.day-detail {
    margin-bottom: 28px;
}

.day-detail-title {
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #999;
    margin-bottom: 10px;
}

.day-deck {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid #eee;
    font-size: 14px;
}

.day-deck-side {
    color: #999;
    font-size: 13px;
    white-space: nowrap;
}

/* Courbe : deux polylignes, étirées à la largeur disponible */
.progress-chart {
    width: 100%;
    height: 140px;
    display: block;
    border-bottom: 1px solid #e0e7ff;
}

.pc-line {
    fill: none;
    stroke-width: 2;
    vector-effect: non-scaling-stroke; /* sinon l'étirement déforme le trait */
}

/* Les découvertes en rouge, la même encre que le palier 3 de la grille
   d'assiduité juste au-dessus et que le survol du bouton « Encore » ; les
   cartes mûres en vert. L'aire remplie est celle des mûres. */
.pc-decouvertes { stroke: #e53935; }
.pc-mures { stroke: #4caf50; }
.pc-area { fill: rgba(76, 175, 80, 0.12); stroke: none; }

.pc-legend {
    display: flex;
    gap: 16px;
    justify-content: flex-end;
    margin-top: 8px;
    font-size: 12px;
}

.pc-key::before {
    content: '';
    display: inline-block;
    width: 10px;
    height: 10px;
    margin-right: 5px;
}

.pc-key.pc-decouvertes { color: #e53935; }
.pc-key.pc-decouvertes::before { background-color: #e53935; }
.pc-key.pc-mures { color: #4caf50; }
.pc-key.pc-mures::before { background-color: #4caf50; }

/* Repère de placement de l'infobulle et du trait de survol */
.progress-host {
    position: relative;
}

/* Trait vertical du survol, sur le point dont la bulle donne les chiffres.
   pointer-events none comme la bulle : il ne doit jamais s'interposer entre
   le curseur et la courbe. */
.pc-guide {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: rgba(44, 46, 64, 0.35);
    pointer-events: none;
}

.pc-guide[hidden] {
    display: none;
}

/* Bornes de l'axe du temps, sous le graphe : le SVG est étiré à la largeur
   disponible (preserveAspectRatio="none"), donc ses libellés seraient déformés
   et ils sont écrits en HTML */
.pc-axis {
    display: flex;
    justify-content: space-between;
    margin-top: 6px;
    font-size: 12px;
    color: #999;
}

/* En-tête de bloc : titre à gauche, filtre de fenêtre à droite. Le filtre
   passe sous le titre sur petit écran plutôt que d'y comprimer ses boutons. */
.stats-section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 10px;
}

/* L'espacement est porté par l'en-tête */
.stats-section-head .setting-label {
    margin-bottom: 0;
}

.stats-range-toggle {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

/* Plus compacts que les boutons de réglage : ils accompagnent un graphe, ils
   ne sont pas le sujet de l'écran */
.stats-range-toggle .mode-btn {
    padding: 5px 10px;
    font-size: 12px;
}

/* --- Paramètres du compte (actions irréversibles) --- */

/* Même resserrement que les Préférences, pour que les deux écrans de réglages
   aient le même rythme */
#accountScreen .setting-row {
    margin-bottom: 14px;
}

#accountScreen .danger-text {
    margin-bottom: 8px;
}

.danger-text {
    font-size: 13px;
    color: #666;
    line-height: 1.5;
    margin-bottom: 12px;
    max-width: 420px;
    margin-left: auto;
    margin-right: auto;
}

.btn-danger {
    padding: 8px 16px;
    font-size: 14px;
    background: white;
    color: #f44336;
    border: 2px solid #f44336;
    border-radius: 6px;
    transition: all 0.2s ease;
}

.btn-danger:hover {
    background: #f44336;
    color: white;
}

.btn-danger:disabled {
    color: #bbb;
    border-color: #ddd;
    background: white;
    cursor: not-allowed;
}

/* Le rappel de l'irréversibilité, découvert au premier clic */
.danger-confirm {
    max-width: 420px;
    margin: 0 auto;
    padding: 12px 14px;
    background: #fff2f1;
    border: 2px solid #f44336;
    border-radius: 6px;
    font-size: 13px;
    color: #b71c1c;
}

.danger-confirm[hidden] {
    display: none;
}

.danger-actions {
    display: flex;
    gap: 8px;
    justify-content: center;
    flex-wrap: wrap;
    margin-top: 10px;
}

/* --- Connexion / synchronisation (écran paramètres) --- */

/* Bouton « Se connecter avec Google » conforme à la charte :
   fond blanc, bordure grise, icône « G » officielle, Roboto */
.btn-google {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    height: 40px;
    padding: 0 16px;
    background: white;
    color: #3c4043;
    border: 1px solid #dadce0;
    border-radius: 4px;
    font-family: 'Roboto', 'Segoe UI', Tahoma, sans-serif;
    font-size: 14px;
    font-weight: 500;
    transition: background 0.2s ease, box-shadow 0.2s ease;
}

.btn-google:hover {
    background: #f8faff;
    box-shadow: 0 1px 3px rgba(60, 64, 67, 0.15);
}

.btn-google .g-icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

/* Cette ligne sépare le bouton de connexion de « Paramètres du compte » :
   sa marge est symétrique, pour que le message ne colle ni à l'un ni à
   l'autre une fois affiché.
   min-height : la ligne garde sa place même vide, sinon l'apparition du
   message ferait sauter le bouton du dessous au moment où on le vise. */
.sync-status {
    margin: 5px 0;
    font-size: 12px;
    color: #4caf50;
    min-height: 15px;
}

.sync-status.error {
    color: #f44336;
}

/* Les réglages restent resserrés par rapport aux Paramètres, mais
   assez grands pour se lire d'un coup d'œil à côté de l'aperçu */
#appearanceScreen .setting-row {
    margin-bottom: 16px;
}

#appearanceScreen .setting-label {
    margin-bottom: 7px;
}

/* La largeur est portée par la LIGNE, et les boutons se la partagent. C'est
   l'inverse de ce qui se faisait, et ce n'est pas un détail de style : la
   ligne Thème a TROIS boutons quand les autres en ont deux, et une largeur
   posée sur le bouton lui donnerait forcément une ligne plus large. Ici les
   lignes à deux boutons retrouvent exactement leurs 152px, celle du thème
   divise la même largeur par trois, et les deux paliers de css/responsive.css
   n'ont qu'une variable à changer au lieu de trois règles. */
#appearanceScreen {
    --toggle-largeur: 310px;   /* 152 × 2 + 6 de gouttière */
}

#appearanceScreen .interval-toggle,
#appearanceScreen .style-toggle,
#appearanceScreen .theme-toggle,
#appearanceScreen .pinyin-toggle,
#appearanceScreen .audio-toggle {
    gap: 6px;
    width: var(--toggle-largeur);
}

#appearanceScreen .mode-btn {
    height: 38px;
    /* min-width: 0 lève le minimum implicite d'un élément flex, qui refuserait
       sinon de descendre sous la largeur de son contenu */
    flex: 1;
    width: auto;
    min-width: 0;
    font-size: 14px;
}

/* Trois boutons dans la largeur de deux : le padding de 16px ne laisserait que
   44px au libellé, or « Système » en demande une soixantaine. On resserre, et
   l'icône rapproche sa marge. */
#appearanceScreen .theme-toggle .mode-btn {
    padding-left: 6px;
    padding-right: 6px;
}

#appearanceScreen .theme-toggle .deck-icon {
    margin-right: 4px;
}

#appearanceScreen .setting-row:last-child {
    margin-bottom: 0;
}

/* L'écran Apparence est en deux colonnes : les réglages à gauche, la
   carte d'exemple à droite. La carte ne s'étire pas : sa hauteur est
   celle d'une vraie carte mise à l'échelle (voir --preview-scale) */
body.on-appearance .container {
    max-width: 866px;   /* 320 réglages + 32 gap + 434 carte + 80 padding */
}

body.on-appearance #appearanceScreen.show {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 32px;
}

/* Le titre et le sous-titre du conteneur passent dans la carte d'exemple :
   ceux d'en haut sont masqués pour ne pas faire doublon */
body.on-appearance .container > h1,
body.on-appearance .container > .subtitle {
    display: none;
}

body.on-appearance .appearance-settings {
    flex: 0 0 320px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

body.on-appearance .preview-card {
    flex: 0 0 auto;
    margin: 0;
}

/* --- Écran Apparence : carte d'exemple --- */

/* L'aperçu est une miniature fidèle de la carte d'entraînement : toutes
   les tailles de texte et les distances verticales sont celles de la vraie
   carte multipliées par --preview-scale. Les variables --card-* reprennent
   les valeurs de la flashcard (voir « Flashcard » plus haut) : si l'une
   d'elles change là-bas, la répercuter ici. */
.preview-card {
    --preview-scale: 0.7;
    --card-width: 620px;           /* .container max-width */
    --card-pad-top: 40px;          /* .container padding-top */
    --card-pad-x: 40px;            /* .container padding gauche/droite */
    --card-pad-bottom: 40px;       /* .container padding-bottom */
    --card-title: 32px;            /* h1 font-size */
    --card-title-gap: 5px;         /* h1 margin-bottom */
    --card-subtitle: 14px;         /* .subtitle font-size */
    --card-subtitle-gap: 25px;     /* .subtitle margin-bottom */
    --card-corner: 40px;           /* .btn-back / .btn-mute côté */
    --card-corner-inset: 15px;     /* .btn-back / .btn-mute top et left */
    --card-corner-icon: 18px;      /* .corner-icon côté */
    --card-hanzi: 160px;           /* .hanzi --hanzi-size */
    --card-hanzi-box: 192px;       /* .hanzi min-height */
    --card-hanzi-gap: 25px;        /* .hanzi margin-bottom */
    --card-audio-gap: 25px;        /* .audio-player margin-bottom */
    --card-audio-btn: 24px;        /* .btn-audio-play côté */
    --card-audio-icon: 14px;       /* .btn-audio-play svg */
    --card-audio-inner: 8px;       /* .audio-player gap */
    --card-audio-bar-w: 90px;      /* .audio-progress-bar largeur */
    --card-audio-bar-h: 4px;       /* .audio-progress-bar hauteur */
    --card-translation: 38px;      /* .result .translation font-size */
    --card-translation-gap: 8px;   /* .result .translation margin-bottom */
    --card-pinyin: 28px;           /* .result.hanzi-mode .pinyin font-size */
    --card-pinyin-gap: 10px;       /* .result.hanzi-mode .pinyin margin-bottom */
    --card-zone-min: 194px;        /* .answer-zone min-height */
    --card-zone-gap: 20px;         /* .answer-zone margin-bottom */
    --card-buttons-gap: 10px;      /* .buttons gap */
    --card-buttons-min: 56px;      /* .buttons min-height */
    --card-buttons-end: 20px;      /* .buttons margin-bottom */
    --card-grade-pad-y: 10px;      /* .btn-grade padding vertical */
    --card-grade-pad-x: 8px;       /* .btn-grade padding horizontal */
    --card-grade-font: 16px;       /* .btn-grade font-size (base button) */
    --card-grade-small: 12px;      /* .btn-grade small font-size */
    --card-grade-gap: 2px;         /* .btn-grade gap */
    --card-grade-max: 130px;       /* .btn-grade max-width */

    position: relative;
    width: min(100%, calc(var(--card-width) * var(--preview-scale)));
    margin: 0 auto 18px;
    padding: calc(var(--card-pad-top) * var(--preview-scale))
             calc(var(--card-pad-x) * var(--preview-scale))
             calc(var(--card-pad-bottom) * var(--preview-scale));
    border: 1px solid #565a7a;
    border-radius: 8px;
    background: white;
}

.preview-card .preview-title {
    font-size: calc(var(--card-title) * var(--preview-scale));
    margin-bottom: calc(var(--card-title-gap) * var(--preview-scale));
}

.preview-card .preview-subtitle {
    font-size: calc(var(--card-subtitle) * var(--preview-scale));
    margin-bottom: calc(var(--card-subtitle-gap) * var(--preview-scale));
}

/* min-height obligatoire : sans elle, la classe .hanzi imposerait à
   l'aperçu la hauteur de boîte de la vraie carte, non mise à l'échelle */
.preview-card .preview-hanzi {
    font-size: calc(var(--card-hanzi) * var(--preview-scale));
    min-height: calc(var(--card-hanzi-box) * var(--preview-scale));
    margin-bottom: calc(var(--card-hanzi-gap) * var(--preview-scale));
}

.preview-answer-zone {
    min-height: calc(var(--card-zone-min) * var(--preview-scale));
    margin-bottom: calc(var(--card-zone-gap) * var(--preview-scale));
}

.preview-card .preview-audio-player {
    gap: calc(var(--card-audio-inner) * var(--preview-scale));
    margin-bottom: calc(var(--card-audio-gap) * var(--preview-scale));
}

.preview-card .btn-audio-play {
    width: calc(var(--card-audio-btn) * var(--preview-scale));
    height: calc(var(--card-audio-btn) * var(--preview-scale));
    min-width: calc(var(--card-audio-btn) * var(--preview-scale));
    border-width: calc(1.5px * var(--preview-scale)) !important;
    pointer-events: none; /* aperçu : non cliquable */
}

.preview-card .btn-audio-play svg {
    width: calc(var(--card-audio-icon) * var(--preview-scale));
    height: calc(var(--card-audio-icon) * var(--preview-scale));
}

.preview-card .btn-audio-play .icone-play {
    margin-left: calc(2px * var(--preview-scale));
}

.preview-card .audio-progress-bar {
    width: calc(var(--card-audio-bar-w) * var(--preview-scale));
    height: calc(var(--card-audio-bar-h) * var(--preview-scale));
}

/* Boutons de coin décoratifs : reproduisent l'apparence pixel des vrais
   boutons retour/mute, non cliquables (aperçu uniquement) */
.preview-corner-btn {
    position: absolute;
    top: calc(var(--card-corner-inset) * var(--preview-scale));
    width: calc(var(--card-corner) * var(--preview-scale));
    height: calc(var(--card-corner) * var(--preview-scale));
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 0;
    border: 2px solid #2c2e40;
    background: white;
    color: #2c2e40;
    pointer-events: none;
}

.preview-corner-btn .corner-icon {
    width: calc(var(--card-corner-icon) * var(--preview-scale));
    height: calc(var(--card-corner-icon) * var(--preview-scale));
}

.preview-corner-back {
    left: calc(var(--card-corner-inset) * var(--preview-scale));
}

.preview-corner-mute {
    right: calc(var(--card-corner-inset) * var(--preview-scale));
}

body.theme-pixel.theme-dark .preview-corner-btn {
    background: #2e2e34;
    border-color: #4d4d57;
    color: #d4d4dc;
}

.preview-translation {
    font-size: calc(var(--card-translation) * var(--preview-scale));
    font-weight: 600;
    color: #333;
    margin-bottom: calc(var(--card-translation-gap) * var(--preview-scale));
}

.preview-pinyin {
    font-size: calc(var(--card-pinyin) * var(--preview-scale));
    font-weight: 600;
    color: #4caf50;
    margin-bottom: calc(var(--card-pinyin-gap) * var(--preview-scale));
}

.preview-buttons {
    gap: calc(var(--card-buttons-gap) * var(--preview-scale));
    min-height: calc(var(--card-buttons-min) * var(--preview-scale));
    margin-bottom: calc(var(--card-buttons-end) * var(--preview-scale));
}

.preview-card .btn-grade {
    gap: calc(var(--card-grade-gap) * var(--preview-scale));
    padding: calc(var(--card-grade-pad-y) * var(--preview-scale))
             calc(var(--card-grade-pad-x) * var(--preview-scale));
    font-size: calc(var(--card-grade-font) * var(--preview-scale));
    max-width: calc(var(--card-grade-max) * var(--preview-scale));
    pointer-events: none; /* aperçu : non cliquable */
}

.preview-card .btn-grade small {
    font-size: calc(var(--card-grade-small) * var(--preview-scale));
}

body.theme-pixel.theme-dark .preview-card {
    background: #2e2e34;
    border-color: #45454f;
}

body.theme-pixel.theme-dark .preview-translation {
    color: #e8e8f0;
}

/* --- Survol d'un caractère : surbrillance + panneau dictionnaire --- */

.hz {
    cursor: help;
    transition: color 0.12s;
    /* « Appuyer une nouvelle fois sur le hanzi » pour rejouer le son est
       littéralement un double-tap, que le navigateur mobile interpréterait
       comme un zoom. Même correctif que sur les boutons (css/ecrans.css). */
    touch-action: manipulation;
}

/* .hz-actif est le caractère auquel la bulle est ancrée, posé en JS. Il ne
   double pas :hover, il le remplace là où :hover ne tient pas : sur tactile,
   le survol « colle » au dernier élément touché, donc appuyer sur la bulle le
   transférait à la bulle et éteignait le caractère alors qu'elle était encore
   ouverte. Tant que la bulle est là, son caractère reste allumé. */
.hz:hover,
.hz.hz-actif {
    color: #667eea;
}

/* Panneau fixe en colonne de gauche, à côté du conteneur central.
   Affiché uniquement sur écran large (sinon il chevaucherait l'app ;
   au toucher le survol n'existe pas de toute façon). */
.dict-panel {
    display: none;
    position: fixed;
    left: 24px;
    top: 50%;
    transform: translateY(-50%) translateX(-8px);
    width: 280px;
    max-height: 80vh;
    /* Le panneau ne défile PAS lui-même : le thème Pixel le rogne
       (overflow: hidden, css/theme-pixel.css) pour ranger sa bande signature
       dans ses coins arrondis, et cette bande, posée en absolu, partirait
       avec le contenu. C'est .dict-entries qui défile (plus bas), le panneau
       n'étant plus qu'une colonne flex : en-tête et licence épinglés, seules
       les définitions bougent. */
    flex-direction: column;
    /* Devant TOUT le reste, les quatre boutons de coin compris. Rien ne doit
       pouvoir être touché au travers de la bulle : un bouton qui passerait
       devant recevrait l'appui à sa place, et l'utilisateur, qui visait une
       définition, déclencherait un retour ou une coupure du son sans avoir vu
       ce qu'il touchait. La comparaison est valide : .container est en
       position: relative SANS z-index ni transform, il ne crée donc pas de
       contexte d'empilement qui isolerait ses boutons. */
    z-index: 60;
    background: white;
    border-radius: 12px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
    padding: 22px;
    text-align: left;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
}

.dict-panel.show {
    opacity: 1;
    visibility: visible;
    transform: translateY(-50%) translateX(0);
}

@media (min-width: 1240px) {
    .dict-panel {
        display: flex;
    }
}

/* --- La même définition, en bulle ancrée sous le caractère --- */

/* Sous 1240 px le panneau de gauche ne s'affiche pas, et il n'y a pas de
   survol au doigt : c'est la bulle qui prend le relais, ouverte au clic.
   Le même élément sert aux deux, ce qui évite de dupliquer le rendu des
   entrées et tout l'habillage du thème Pixel (css/theme-pixel.css).
   Les coordonnées sont posées en JS (positionnerBulle), la classe étant plus
   spécifique que la règle du palier ci-dessus. */
.dict-panel.dict-bulle {
    display: flex;
    width: min(320px, calc(100vw - 24px));
    max-height: none;   /* plafond réel posé en JS d'après la place libre */
    padding: 16px 18px;
    /* left/top pilotent la position : le translateY du panneau fixe ne doit
       pas s'y ajouter, il ne reste que la petite entrée en glissé */
    transform: translateY(-6px);
}

.dict-panel.dict-bulle.show {
    transform: translateY(0);
}

.dict-bulle .dict-hanzi {
    font-size: 40px;
}

/* Les quatre boutons de coin (retour, son, préférences, aide) restent à
   z-index automatique, DERRIÈRE la bulle. Retour et son ont porté un
   z-index: 62 qui les faisait passer devant elle, pour qu'un appui direct les
   conserve quand elle les recouvrait : c'était un piège, l'appui partait au
   bouton alors que la bulle était visible dessous et qu'on croyait toucher une
   définition. Ils redeviennent atteignables dès que la bulle se ferme, ce que
   fait le premier appui n'importe où en dehors d'elle (js/dictionnaire.js). */

/* La pointe est un élément à part, pas un ::before de la bulle : le thème
   Pixel rogne le panneau (overflow: hidden) et lui a déjà pris son ::before
   pour sa bande signature. Elle est placée sur le CARACTÈRE, indépendamment
   de la bulle, qu'un bord de l'écran a pu décaler. */
.dict-arrow {
    display: none;
    position: fixed;
    z-index: 61;
    width: 20px;
    height: 9px;
    margin-left: -10px;   /* centrée sur la coordonnée posée en JS */
    background: var(--bulle-bord, transparent);
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s;
}

/* Le liseré : triangle de fond posé sur celui de bordure, décalé vers
   l'intérieur. Sans bordure de thème, les deux se confondent. */
.dict-arrow::after {
    content: '';
    position: absolute;
    inset: 3px 0 0;
    background: var(--bulle-fond, #fff);
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

.dict-arrow.show {
    display: block;
    opacity: 1;
}

/* Bulle basculée au-dessus du caractère (pas la place dessous) : la pointe
   se retourne pour continuer de le désigner */
.dict-arrow.pointe-bas {
    clip-path: polygon(0 0, 100% 0, 50% 100%);
}

.dict-arrow.pointe-bas::after {
    inset: 0 0 3px;
    clip-path: polygon(0 0, 100% 0, 50% 100%);
}

/* La zone défilante, de la bulle comme du panneau fixe. Le plafond de hauteur
   est posé sur le PANNEAU (max-height: 80vh ici, valeur calculée en JS pour la
   bulle) ; c'est cet enfant-là qui absorbe le débordement, pour que le
   caractère en tête et la mention de licence restent visibles pendant la
   lecture. Cette dernière n'est pas décorative : CFDICT et CC-CEDICT exigent
   toutes deux d'être citées. */
.dict-entries {
    overflow-y: auto;
    /* Arrivé en bout de définitions, le geste s'arrête là : sans quoi il se
       propage à la page, et le gestionnaire de défilement de
       js/dictionnaire.js refermerait la bulle au moment précis où l'on finit
       de lire. Même raison que sur .deck-list (css/ecrans.css). */
    overscroll-behavior: contain;
    /* Un élément flex refuse par défaut de descendre sous la hauteur de son
       contenu : sans ceci le plafond du panneau serait ignoré et rien ne
       défilerait, la bulle débordant simplement de l'écran. */
    min-height: 0;
    /* L'ascenseur est stylé globalement dans css/base.css, variante sombre
       dans css/theme-pixel.css : ne rien poser ici, en particulier pas
       scrollbar-width ni scrollbar-color, qui désactiveraient
       ::-webkit-scrollbar sur Chrome. */
}

/* Les deux bords du panneau ne bougent pas : seul le milieu défile */
.dict-hanzi,
.dict-source {
    flex-shrink: 0;
}

.dict-hanzi {
    font-size: 56px;
    line-height: 1.2;
    color: #333;
    margin-bottom: 10px;
}

.hanzi-songti .dict-hanzi {
    font-family: "Songti SC", "Noto Serif SC", serif;
}

.dict-reading + .dict-reading {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid #eee;
}

.dict-pinyin {
    font-size: 20px;
    font-weight: 600;
    color: #667eea;
    margin-bottom: 4px;
}

.dict-defs {
    list-style: none;
    font-size: 14px;
    color: #444;
    line-height: 1.5;
}

.dict-defs li + li {
    margin-top: 2px;
}

.dict-missing {
    font-size: 14px;
    color: #999;
    font-style: italic;
}

.dict-source {
    margin-top: 16px;
    font-size: 11px;
    color: #bbb;
}

.screen {
    display: none;
}

/* Référence de largeur pour la taille du mot question (voir .hanzi) */
#cardScreen {
    container-type: inline-size;
}

/* Fin de session (« Tout est révisé ») : il n'y a plus ni réponse ni
   boutons, donc plus rien à réserver ; la carte se resserre autour du
   message au lieu de garder une grande zone vide. */
#cardScreen.done .answer-zone,
#cardScreen.done .buttons {
    min-height: 0;
}

#cardScreen.done .audio-player {
    display: none;
}

.screen.show {
    display: block;
}

