/* ============================================================
   ÉCRANS — aide, paramètres, liste, flashcard, lecteur audio
   ============================================================
   Le style propre à chaque écran de l'app, dans l'ordre où on les rencontre. La géométrie verticale de la flashcard y est figée : voir CLAUDE.md avant d'y toucher.

   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.
   ============================================================ */

/* --- Page d'aide --- */
.info-content {
    text-align: left;
    font-size: 14px;
    color: #444;
    line-height: 1.55;
}

.info-content h3 {
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #999;
    margin: 22px 0 8px;
}

.info-content h3:first-child {
    margin-top: 0;
}

.info-content ul {
    padding-left: 20px;
    margin: 0;
}

.info-content li {
    margin-bottom: 6px;
}

/* Les 4 notes : une réplique du vrai bouton, puis sa description. Le bouton
   garde ses classes .btn-grade/.grade-* pour hériter exactement des couleurs
   et de la géométrie de la carte, y compris son max-width de 130px ; seule sa
   base flex est reprise ici, parce qu'il n'est plus dans une rangée de quatre.
   Cette base reproduit ce que la vraie rangée lui donne : la largeur utile
   moins les trois gouttières de .buttons, divisée par quatre. La hauteur, elle,
   ne se règle pas, elle tombe des padding, font-size et line-height hérités.

   Toutes ces règles sont préfixées par .info-content, y compris celle du
   palier mobile : sans ce préfixe elles seraient moins spécifiques que le
   « .info-content ul » ci-dessus, qui leur réimposerait son retrait de 20px. */
.info-content .aide-notes {
    --aide-note-largeur: calc((100% - 30px) / 4);
    list-style: none;
    padding-left: 0;
    margin-top: 8px;
}

.info-content .aide-notes li {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 8px;
}

.info-content .aide-notes .btn-grade {
    flex: 0 0 var(--aide-note-largeur);
    pointer-events: none; /* illustration : ni survol ni clic */
}

.info-content .golden-rule {
    margin-top: 10px;
    padding: 10px 14px;
    background: #f0f4ff;
    border-left: 3px solid #667eea;
    border-radius: 4px;
    color: #333;
}

h1 {
    color: #333;
    margin-bottom: 5px;
    font-size: 32px;
}

.subtitle {
    color: #666;
    margin-bottom: 25px;
    font-size: 14px;
}

/* Le serif japonais. La pile « Songti » est simplifiée-chinoise : appliquée
   au japonais, elle dessine les kana et les kanji au trait chinois. */
body.matiere-ja.hanzi-songti .hanzi:not(.text-prompt),
body.matiere-ja.hanzi-songti .hanzi-answer,
body.matiere-ja.hanzi-songti td.col-hanzi,
body.matiere-ja.hanzi-songti .explorer-hanzi {
    font-family: 'Hiragino Mincho ProN', 'Yu Mincho', YuMincho, 'MS Mincho',
                 'Noto Serif JP', serif;
}

/* --- Page paramètres --- */
.mode-toggle,
.interval-toggle,
.style-toggle,
.theme-toggle,
.pinyin-toggle,
.audio-toggle,
.lang-toggle,
.matiere-toggle {
    display: flex;
    gap: 8px;
    justify-content: center;
    flex-wrap: wrap;
}

/* Sur l'accueil, le mode de jeu est au-dessus de la liste des decks */
.mode-toggle {
    margin-bottom: 20px;
}

/* Libellé court des boutons de mode : réservé au mobile, où les trois
   libellés complets ne tiennent pas sur une ligne (voir css/responsive.css). */
.mode-court {
    display: none;
}

.setting-row {
    margin-bottom: 24px;
}

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

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

/* Note explicative sous un réglage. Même rendu que .danger-text, mais un nom
   distinct : ce texte n'annonce rien d'irréversible, et emprunter la classe
   des avertissements rendrait la feuille de style trompeuse. */
.setting-note {
    font-size: 13px;
    color: #666;
    line-height: 1.5;
    max-width: 420px;
    margin: 12px auto 0;
}

.mode-btn {
    padding: 8px 16px;
    font-size: 14px;
    background: #f0f4ff;
    color: #667eea;
    border: 2px solid #e0e7ff;
}

.mode-btn:hover {
    border-color: #2c2e40;
}

.mode-btn.active {
    background: #2c2e40;
    border-color: #2c2e40;
    color: white;
}

/* Mention « (défaut) » : annotation discrète, détachée du libellé */
.mode-btn small {
    margin-left: 7px;
    font-size: 0.7em;
    font-style: italic;
    font-weight: 400;
    opacity: 0.7;
}

/* Les matières ne sont pas un réglage parmi d'autres : c'est le premier
   choix, celui qui décide de tout le reste. D'où deux grandes tuiles carrées,
   drapeau au-dessus et nom en dessous, plutôt que la rangée de boutons des
   autres écrans. Elles se passent d'un titre au-dessus, le sous-titre de
   l'écran le disant déjà.

   Direction column, là où tous les autres toggles alignent leur contenu sur
   une ligne. */
.matiere-toggle {
    gap: 16px;
}

.matiere-toggle .mode-btn {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    width: 150px;
    height: 150px;
    padding: 0;
    font-size: 16px;
}

/* 60px pour une grille de dix cellules : exactement 6px par cellule. Une
   taille non divisible par dix les rendrait inégales sous
   shape-rendering: crispEdges, et le drapeau s'en trouverait déformé. */
.matiere-toggle .mode-btn .deck-icon {
    width: 60px;
    height: 60px;
    margin: 0;
}

.style-toggle .mode-btn .sample {
    font-size: 20px;
    margin-right: 6px;
    vertical-align: -2px;
}

/* Dans les paramètres et l'écran Apparence, tous les boutons de
   choix ont les mêmes dimensions et s'alignent en colonnes */
#settingsScreen .mode-btn,
#appearanceScreen .mode-btn {
    width: 150px;
    height: 42px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* Préférences : une simple liste de boutons, qui n'a pas besoin de l'air des
   24px par défaut (l'écran Apparence, lui, garde les siens : ses rangées
   portent des groupes de plusieurs boutons) */
#settingsScreen .setting-row {
    margin-bottom: 14px;
}

/* Sépare les boutons d'écran (« Thème et affichage », « Statistiques ») du
   groupe « Compte ». Gris neutre semi-transparent : lisible tel quel sur les
   quatre thèmes (clair, sombre, pixel, pixel sombre), donc rien à surcharger
   plus bas. */
#settingsScreen .setting-row-sep {
    border-top: 1px solid rgba(128, 128, 128, 0.28);
    padding-top: 18px;
    margin-top: 22px;
}

/* Sur les Préférences, chaque rangée n'a qu'un seul bouton : à 150px ils
   laissaient la page presque vide, et chacun tombait sur une largeur
   différente (libellés de longueurs inégales). On les aligne tous sur la
   largeur qu'occupe une rangée de deux boutons ailleurs (2 × 150 + 8 de
   gouttière), pour que Préférences et « Thème et affichage » se lisent à la
   même largeur. Plus assez large pour ne rien tronquer, donc plus besoin de
   la largeur automatique qu'avait « Thème et affichage ». */
#settingsScreen .mode-btn,
#settingsScreen .btn-google {
    width: 100%;
    max-width: 308px;
}

/* « Compte » est la seule rangée à empiler deux boutons : sans cela ils se
   touchent, alors que les rangées d'un seul bouton respirent grâce au
   margin-bottom des .setting-row. On reprend donc le même écart (14px), en
   passant le bouton du haut en flex (donc de niveau bloc) pour que la marge
   soit exacte, sans l'interligne d'un élément inline. */
#settingsScreen #accountBtn {
    display: flex;
    margin: 0 auto 14px;
}

/* Police Songti (serif chinois classique) appliquée quand le body
   porte la classe hanzi-songti. Le prompt texte (.text-prompt)
   affiche du français : il garde la police normale. */
.hanzi-songti .hanzi:not(.text-prompt),
.hanzi-songti .hanzi-answer,
.hanzi-songti td.col-hanzi,
.hanzi-songti .explorer-hanzi,
.songti-font {
    font-family: 'Songti SC', 'STSong', 'SimSun', 'Noto Serif SC', 'Source Han Serif SC', serif;
}

.deck-list {
    display: flex;
    flex-direction: column;
    gap: 10px;

    /* --- Réglages du fondu des bords (les règles sont plus bas) --- */
    --fondu: 80px;
    /* Largeur réservée à l'ascenseur dans le masque : c'est la GOUTTIÈRE
       entière, pas le pouce, sinon le fondu mordrait sur son retrait. */
    --barre: 20px;

    /* Plafond RELATIF, et pas un simple confort : à cette hauteur, sur un
       écran court, les deux fondus se chevaucheraient. Les positions du
       dégradé sortiraient alors de l'ordre croissant, le navigateur les
       ramènerait à la précédente, et la bande pleinement opaque du milieu
       disparaîtrait : la liste serait estompée de bout en bout, illisible.
       35 % de chaque côté laissent toujours 30 % de liste nette. */
    --fondu-eff: min(var(--fondu), 35%);

    /* Le masque est posé en DEUX couches, et ce n'est pas un raffinement :
       un masque s'applique AUSSI à l'ascenseur, donc un simple dégradé
       l'effacerait aux deux extrémités, c'est-à-dire précisément là où il
       doit se voir. La première couche porte le dégradé sur la colonne du
       contenu, la seconde un aplat opaque sur celle de l'ascenseur.
       Ces trois propriétés ne varient pas d'une combinaison à l'autre :
       elles sont donc mises en facteur ici. */
    -webkit-mask-size: calc(100% - var(--barre)) 100%, var(--barre) 100%;
    mask-size: calc(100% - var(--barre)) 100%, var(--barre) 100%;
    -webkit-mask-position: top left, top right;
    mask-position: top left, top right;
    -webkit-mask-repeat: no-repeat, no-repeat;
    mask-repeat: no-repeat, no-repeat;

    /* Intensité du fondu de chaque côté, entre 0 et 1, posée par
       majFondu() dans js/decks.js selon la position du défilement.
       0 par défaut : tant que rien n'a défilé, aucun fondu. */
    --k-haut: 0;
    --k-bas: 0;

    /* Hauteur EFFECTIVE de chaque fondu : elle croît avec le défilement.
       À 0, les deux étapes du dégradé se superposent et il ne reste
       qu'un aplat opaque, donc aucun fondu visible — ce qui est
       exactement ce qu'on veut aux butées. */
    --haut-eff: calc(var(--fondu-eff) * var(--k-haut));
    --bas-eff: calc(var(--fondu-eff) * var(--k-bas));

    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--haut-eff), #000 calc(100% - var(--bas-eff)), transparent 100%), linear-gradient(#000, #000);
    mask-image: linear-gradient(to bottom, transparent 0, #000 var(--haut-eff), #000 calc(100% - var(--bas-eff)), transparent 100%), linear-gradient(#000, #000);
}

/* --- Menus à hauteur d'écran, liste défilante ---
   Sans cela, un dossier chargé fait grandir le cadre au-delà de la fenêtre
   et c'est la PAGE qui défile : le titre et les boutons de coin sortent du
   champ, le cadre perd son ancrage.

   Tout est cantonné aux classes que showScreen() pose déjà sur le body, de
   sorte que l'écran de carte, dont la géométrie verticale est figée (voir
   CLAUDE.md), n'est jamais touché.

   Le débordement est porté par la LISTE, jamais par le conteneur : celui-ci
   doit rester sans overflow pour laisser dépasser le panda pixel. */

/* Le cadre des trois menus tient dans la fenêtre, exactement.

   Ce n'est pas une précaution, c'est la correction d'un défaut visible. Le body
   porte min-height: 100vh (css/base.css). Sur mobile, 100vh vaut la hauteur du
   viewport « large », barres du navigateur rétractées, donc TOUJOURS plus que
   la zone réellement visible. Le body dépassait ainsi l'écran de la hauteur de
   ces barres, et comme il centre son contenu (align-items: center), le cadre se
   trouvait centré dans une boîte trop haute : il sortait de l'écran en haut et
   en bas, et la page défilait par-dessus le défilement de la liste.

   Ce sont height (et non min-height) et dvh qui corrigent cela, à eux seuls :
   le body fait exactement la hauteur visible, donc la boîte de centrage est
   juste.

   Il y avait ici un overflow: hidden en plus. Il a été RETIRÉ, volontairement :
   tant que le body ne peut pas défiler, aucun geste ne se propage à lui et le
   navigateur désactive son « tirer pour rafraîchir ». Or c'est précisément ce
   qu'on veut au bout de la liste des decks (voir .deck-list). Le body ne
   déborde de toute façon pas : le cadre est plafonné à la hauteur visible
   moins --marge-page et --reserve-haut, qui sont exactement ce que lui prennent
   les marges de la page et le panda du thème Pixel.

   Cantonné aux classes de showScreen(), donc sans effet sur l'écran de carte. */
body.on-home,
body.on-folders,
body.on-decks {
    height: 100vh;    /* repli pour les navigateurs sans dvh */
    height: 100dvh;
    /* Indispensable, malgré les apparences : css/base.css pose
       min-height: 100vh sur le body, et un min-height supérieur l'emporte sur
       height. Or 100vh est justement PLUS grand que 100dvh sur mobile, la
       cause même du défaut corrigé ici. Sans cette ligne, la hauteur fixée
       ci-dessus ne s'appliquerait pas et le body dépasserait toujours. */
    min-height: 0;

    /* Ce que le cadre doit laisser libre. Deux quantités que des feuilles
       différentes possèdent, d'où les variables plutôt que des calc recopiés :
       --marge-page  les padding haut + bas du body (css/base.css), que le
                     responsive réduit à 20px sur téléphone ;
       --reserve-haut  la place d'un élément qui dépasse du cadre par le haut.
                     Nulle par défaut : seul le panda du thème Pixel en demande
                     (css/theme-pixel.css), et sur les menus seuls. */
    --marge-page: 40px;
    --reserve-haut: 0px;
}

body.on-home .container,
body.on-folders .container,
body.on-decks .container {
    display: flex;
    flex-direction: column;
    /* dvh plutôt que vh : les barres de Safari mobile apparaissent et
       disparaissent au défilement, et avec vh le cadre se ferait couper.
       La première ligne est le repli pour les navigateurs sans dvh. */
    max-height: calc(100vh - var(--marge-page) - var(--reserve-haut));
    max-height: calc(100dvh - var(--marge-page) - var(--reserve-haut));
}

body.on-home #homeScreen.show,
body.on-folders #folderScreen.show,
body.on-decks #menuScreen.show {
    display: flex;
    flex-direction: column;
    flex: 1;
    /* Pas décoratif : sans min-height, un enfant flex refuse de rétrécir
       sous la taille de son contenu, et rien ne défilerait. */
    min-height: 0;
}

/* Les boutons de mode gardent leur taille : c'est la liste qui absorbe la
   contrainte de hauteur, pas eux. */
body.on-decks .mode-toggle {
    flex-shrink: 0;
}

body.on-folders .deck-list,
body.on-decks .deck-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    /* PAS d'overscroll-behavior ici, et c'est délibéré : arrivé en bout de
       liste, le geste DOIT se propager à la page, pour que le doigt obtienne
       le rebond élastique aux deux bords et le « tirer pour rafraîchir » en
       haut. La page de ces écrans tient dans la fenêtre et ne défile pas
       vraiment : le mouvement est décoratif, c'est la continuité du geste qui
       est recherchée. Il a fallu pour cela lever l'overflow: hidden du body
       (voir la règle de hauteur des menus, plus haut) : sans lui, la valeur
       posée ici ne changerait rien.

       Deux zones défilantes gardent au contraire « contain », chacune pour sa
       raison : .dict-entries (css/stats.css), où le geste propagé referme la
       bulle du dictionnaire, et .explorer-resultats (plus bas), dont l'écran
       n'a pas la contrainte de hauteur des menus et dont la page défile pour
       de bon. Ne pas uniformiser les quatre. */
    /* L'ascenseur lui-même est stylé globalement dans css/base.css : même
       allure pour toutes les zones défilantes de l'app, celle-ci comprise.
       Ne rien poser ici, en particulier pas scrollbar-width ni
       scrollbar-color, qui désactiveraient ::-webkit-scrollbar sur Chrome. */
}

/* Icônes pixel des decks (grilles 10×10 rendues en SVG par app.js) */
.deck-icon {
    width: 17px;
    height: 17px;
    vertical-align: -3px;
    margin-right: 6px;
    shape-rendering: crispEdges;
}

/* Le caractère 文 tient lieu d'icône sur le bouton Langue : le seul
   pictogramme de l'app rendu par une police plutôt que dessiné, faute d'un
   dessin pixel convaincant à cette taille. Calé sur .deck-icon, dont il prend
   la place. Largeur fixe : sans elle, il décalerait le libellé d'une police
   système à l'autre. Marge un cran plus large que celle des icônes SVG : un
   caractère chinois remplit sa boîte jusqu'aux bords, là où un dessin garde
   son air autour de lui, et à 6 px il touchait presque le libellé. */
.icone-hanzi {
    display: inline-block;
    width: 17px;
    margin-right: 10px;
    font-size: 17px;
    line-height: 1;
    vertical-align: -2px;
}

/* Icône pixel d'un bouton de coin (engrenage des paramètres) :
   currentColor suit la couleur du bouton dans les deux thèmes */
/* Le drapeau de la matière remplit son bouton plus franchement qu'une icône
   de coin : c'est un dessin plein, pas un pictogramme au trait. Il porte ses
   couleurs en dur et non currentColor, comme le logo Google, seul autre
   pictogramme couleur de l'app. En thème sombre son liseré d'encre
   s'éclaircit tout seul, par la règle de css/theme-pixel.css sur .deck-icon. */
.btn-matiere .deck-icon {
    width: 22px;
    height: 22px;
    margin: auto;
    display: block;
}

/* Matière dont la « traduction » n'est que la lecture (les kana) : la colonne
   répéterait mot pour mot celle d'à côté. Masquée en CSS et non en JS, sinon
   il faudrait retirer les <td> et les colonnes se décaleraient. La règle vaut
   pour la liste des cartes comme pour l'explorateur, en-tête comprise. */
body.sans-traduction .col-translation {
    display: none;
}

.corner-icon {
    width: 18px;
    height: 18px;
    display: block;
    margin: auto;
    shape-rendering: crispEdges;
}

.deck-btn {
    cursor: pointer;
    user-select: none;
    /* Les decks sont des <div> construits en JavaScript, pas des <button> :
       la règle générique plus bas ne les atteint pas. Même raison qu'elle,
       le double-tap de zoom du navigateur. */
    touch-action: manipulation;
    padding: 8px 16px;
    font-size: 17px;
    border: 2px solid #e0e7ff;
    border-radius: 8px;
    background: #f0f4ff;
    color: #333;
    transition: all 0.2s ease;
    font-weight: 600;
    text-align: left;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.deck-btn:hover {
    border-color: #667eea;
    background: #e0e7ff;
}

.deck-btn .count {
    font-size: 13px;
    color: #667eea;
    font-weight: normal;
    padding: 4px 10px;
    border-radius: 12px;
    background: white;
    border: 1px solid #c7d2f9;
    transition: all 0.2s ease;
}

.deck-btn .count:hover {
    background: #2c2e40;
    color: white;
}

/* Dans un bouton de dossier, la pastille « N decks » est purement
   informative (tout le bouton ouvre le dossier) */
.folder-btn .count:hover {
    background: white;
    color: #667eea;
}

/* --- Menu principal ---------------------------------------------------
   Trois tuiles, et une hiérarchie : Decks tient toute la largeur, les deux
   annexes se partagent la rangée du dessous. Elles gardent l'ossature du
   bouton de deck (padding, transition, liseré) mais pas sa mise en ligne :
   le libellé est centré, seul, et sans compteur.
   Ce ne sont pas des <div> construits en JS mais de vrais <button>, d'où la
   police à redire.
   La couleur de chaque tuile est posée dans css/theme-pixel.css. */
.home-menu {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    /* Le bloc se pose au MILIEU de la hauteur libre plutôt que de se coller
       sous le sous-titre : trois tuiles ne remplissent pas le cadre, et la
       note du jour lui réserve déjà sa bande en bas. */
    align-content: center;
    /* Sur un écran très court (téléphone couché), les trois tuiles ne
       tiennent plus : elles défilent alors plutôt que d'être rognées, et
       « safe » rend la main au haut du bloc dans ce cas précis, sans quoi le
       centrage mettrait la première tuile hors d'atteinte. La ligne
       précédente est le repli des moteurs qui ignorent « safe ». */
    align-content: safe center;
    overflow-y: auto;
    /* Comme .deck-list : le geste se propage aux bords. Les trois menus se
       comportent pareil, passer de l'un à l'autre ne doit pas changer la
       réponse au doigt. */
    flex: 1;
    min-height: 0;
}

.home-btn {
    font-family: inherit;   /* un bouton n'hérite pas de la police du corps */
    flex-direction: column;
    justify-content: center;
    text-align: center;
    min-height: 68px;
}

/* L'entrée principale : elle tient les deux colonnes, et c'est tout ce qui
   la distingue en structure. Le reste (corps du texte, taille de l'icône)
   suit cette largeur, sans quoi elle paraîtrait étirée plutôt que grande. */
.home-btn-grand {
    grid-column: 1 / -1;
    min-height: 116px;
    font-size: 26px;
}

.home-btn-grand .deck-icon {
    width: 26px;
    height: 26px;
    vertical-align: -5px;
    margin-right: 10px;
}

/* --- Note du jour -----------------------------------------------------
   Une bande d'écran contre le bord inférieur du cadre, sur le seul menu
   principal : un message qui rayonne et glisse en boucle. Deux écrans en
   réalité, un par thème : dalle blanc écran à peine bleutée, encre noire
   et bavure verte le jour ; dalle sombre, encre blanche et halo bleuté la
   nuit (la palette sombre est dans css/theme-pixel.css).
   Aucune police n'est chargée (c'est un invariant du projet), l'allure
   vient donc de trois choses : une pile monospace locale, une trame de
   pixels en dégradés et le halo du texte. */
body {
    --motd-hauteur: 32px;   /* dalle et liseré compris */
    --motd-ecart: 40px;     /* l'air qui la sépare du dernier bouton */

    /* La palette de la dalle, redéclarée EN BLOC par la variante sombre
       (css/theme-pixel.css) : c'est le seul endroit à toucher pour changer
       une couleur de la note. Le halo est porté entier par une variable,
       rayons compris, les deux écrans ne rayonnant pas de la même façon. */
    --motd-fond: #f5f8ff;
    --motd-bord: #2c2e40;
    --motd-trame: rgba(44, 46, 64, 0.09);
    --motd-encre: #14161c;
    --motd-halo: 0 0 4px rgba(76, 175, 80, 0.42), 0 0 10px rgba(76, 175, 80, 0.25);
}

/* Le cadre lui réserve sa bande, plus l'air qui la sépare du dernier
   bouton : la note est posée en absolu, donc sans cette réserve elle
   passerait purement et simplement par-dessus lui.
   --motd-ecart n'est pas un ajustement mais ce qui la détache du menu : au
   ras des boutons, la note se lisait comme une quatrième ligne de la liste.
   Elle vaut ici la marge intérieure du cadre lui-même (40px), pour que la
   note paraisse posée dans le cadre et non accrochée aux boutons.
   C'est la seule place qu'elle coûte, et elle ne coûte rien ailleurs :
   partout sauf sur ce menu, elle n'existe pas. */
body.on-home .container {
    padding-bottom: calc(var(--motd-hauteur) + var(--motd-ecart));
}

/* Posée en absolu contre le bord inférieur du cadre, exactement comme la
   bande signature l'est contre le bord supérieur (css/theme-pixel.css).
   left et right à zéro la portent d'un bord intérieur à l'autre, la marge
   intérieure du cadre ne s'appliquant pas à un élément hors flux : c'est ce
   qui la colle au cadre au lieu de la faire flotter à 40px du bas. */
.motd-rail {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    /* Affichée sur le seul menu principal (règle suivante) */
    display: none;
    align-items: center;
    overflow: hidden;
    height: var(--motd-hauteur);
    background: var(--motd-fond);
    border-top: 2px solid var(--motd-bord);
}

/* :not([hidden]) n'est pas décoratif : une règle d'auteur l'emporte sur
   l'attribut hidden, et une dalle vide s'afficherait le temps que
   rendreMotd() (js/accueil.js) remplisse la piste. Ce délai est réel dans
   les langues autres que le français, dont le paquet de traductions est
   chargé de façon asynchrone avant le premier rendu des menus. */
body.on-home .motd-rail:not([hidden]) {
    display: flex;
}

/* La trame d'écran : deux grilles croisées, un pixel sombre tous les
   trois, posées PAR-DESSUS le texte. C'est elle qui fait la dalle. Elle
   est discrète : sur gris, la trame dense du panneau d'aéroport se lisait
   comme un grillage noir. Sa couleur suit le thème, et ce n'est pas un
   détail : le noir qui va au fond sombre salirait la dalle claire en gris. */
.motd-rail::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
        repeating-linear-gradient(0deg, var(--motd-trame) 0 1px, transparent 1px 3px),
        repeating-linear-gradient(90deg, var(--motd-trame) 0 1px, transparent 1px 3px);
}

/* La piste est remplie par rendreMotd() (js/accueil.js) : assez de copies
   du message pour couvrir la fenêtre, puis le ruban entier en double. Elle
   se déplace jusqu'à -50 % de sa largeur, donc la seconde moitié arrive
   exactement là où la première était : la boucle ne se voit pas.
   La durée est posée par le même code, calculée sur la largeur réelle du
   ruban : une durée fixe ferait filer les longs messages et traîner les
   courts. */
.motd-piste {
    display: flex;
    width: max-content;
    animation: motd-defile var(--motd-duree, 24s) linear infinite;
}

/* Le survol ne fige plus le ruban ici : animation-play-state ne connaît que
   le tout ou rien, et l'arrêt sec cassait le geste. C'est js/accueil.js qui
   fait désormais tomber la vitesse de lecture sur une rampe, puis la remonte
   au départ du curseur. */

.motd-texte {
    /* L'écart entre la fin du message et sa reprise. Large, et c'est LE
       réglage qui espace les répétitions : le message doit repasser de temps
       en temps, pas se répéter en litanie sur toute la largeur du cadre.
       En ch, donc proportionnel au corps du texte : le téléphone, qui lit ce
       bloc avec une police plus petite, garde la même respiration. */
    padding-right: 30ch;
    font-family: 'Courier New', Consolas, monospace;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.04em;
    white-space: nowrap;
    color: var(--motd-encre);
    /* La seule ombre du projet, et elle est assumée : ce n'est pas une
       ombre portée de mise en page mais la bavure d'un écran allumé. */
    text-shadow: var(--motd-halo);
}

@keyframes motd-defile {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
}

/* Rien ne défile si l'utilisateur a demandé moins d'animations : le
   message est alors posé et centré, et les copies retirées, qui ne
   servaient que la boucle et la largeur. */
@media (prefers-reduced-motion: reduce) {
    .motd-piste {
        animation: none;
        width: 100%;
        justify-content: center;
    }

    .motd-texte {
        overflow: hidden;
        padding-right: 0;
        text-overflow: ellipsis;
    }

    .motd-texte ~ .motd-texte {
        display: none;
    }
}

/* --- Explorer les caractères ------------------------------------------
   Le tableau est celui de la liste d'un deck, une colonne de plus. Seule
   la zone des résultats défile : neuf cents lignes feraient sinon défiler
   la page entière, cadre et titre compris. */
.explorer-barre {
    position: relative;
    margin-bottom: 10px;
}

.explorer-loupe {
    position: absolute;
    top: 50%;
    left: 12px;
    width: 18px;
    height: 18px;
    transform: translateY(-50%);
    color: #667eea;
    pointer-events: none;
    shape-rendering: crispEdges;
}

.explorer-champ {
    width: 100%;
    padding: 10px 14px 10px 40px;
    font-family: inherit;
    /* 16 px et pas moins : en dessous, iOS zoome sur le champ à la mise au
       point, et ne dézoome jamais. */
    font-size: 16px;
    color: #333;
    background: #f0f4ff;
    border: 2px solid #e0e7ff;
    border-radius: 8px;
}

.explorer-champ:focus {
    outline: none;
    border-color: #667eea;
}

.explorer-compte {
    padding: 0 12px 6px;
    font-size: 12px;
    color: #999;
    text-align: left;
}

.explorer-resultats {
    max-height: 60vh;
    overflow-y: auto;
    /* Aucun défilement horizontal, jamais : les règles ci-dessous font tenir
       le tableau dans la largeur, et ceci garantit qu'un contenu imprévu ne
       fera pas réapparaître une barre en bas de la liste. */
    overflow-x: hidden;
    overscroll-behavior: contain;
}

/* --- Les entrées du Dictionnaire ---
   Une liste, pas un tableau. À quatre colonnes, le mot et sa lecture
   n'avaient chacun qu'un quart des 540 px du cadre, et la traduction se
   coupait en trois lignes. Ici le mot dispose de la largeur qu'il lui faut,
   et la traduction de toute la ligne suivante.

   Rien ne peut déborder horizontalement : le mot est le seul élément
   insécable, il pèse une centaine de pixels au pire, et les deux autres
   éléments de la première ligne se rétractent avant lui. */
.explorer-liste {
    list-style: none;
    margin: 0;
    padding: 0;
    text-align: left;
}

.explorer-entree {
    padding: 5px 10px;
    border-bottom: 1px solid #eee;
}

.explorer-entree:last-child {
    border-bottom: none;
}

/* Première ligne : le mot, sa lecture, et le deck repoussé au bout.
   baseline et non center : le mot est deux fois plus haut que sa lecture,
   et c'est sur la ligne d'écriture qu'ils doivent s'aligner. */
.explorer-tete {
    display: flex;
    align-items: baseline;
    gap: 10px;
}

/* Le mot question ne se coupe JAMAIS et ne se rétracte pas : c'est lui qu'on
   vient lire. flex-shrink à 0, contrairement aux deux autres.

   Il est CLIQUABLE, et d'un seul tenant : un appui le prononce en entier et
   le surligne en entier. Ailleurs dans l'app, l'unité est le caractère, qui
   ouvre sa fiche de dictionnaire ; ici c'est le mot, parce qu'on parcourt une
   liste de mots. touch-action : un appui répété pour réécouter est
   littéralement un double-tap, que le navigateur mobile prendrait pour un
   zoom, comme sur .hz et sur les boutons. */
.explorer-hanzi {
    flex: 0 0 auto;
    font-size: 22px;
    line-height: 1.25;
    white-space: nowrap;
    cursor: pointer;
    touch-action: manipulation;
    transition: color 0.12s;
}

/* La couleur ne dit qu'UNE chose : ce mot est en train d'être joué. Elle
   s'allume à l'appui et s'éteint à la fin du son (js/decks.js).

   Pas de :hover ici, et c'est délibéré. À la souris, le curseur reste sur le
   mot après le clic : la couleur de survol survivait donc à la fin du son et
   masquait l'extinction. Au doigt, c'est pire, le navigateur garde :hover sur
   le dernier élément touché jusqu'à ce qu'on touche ailleurs, ce qui faisait
   durer la surbrillance « jusqu'au clic à un autre endroit ».
   L'affordance reste portée par le curseur (cursor: pointer, ci-dessus). */
.explorer-hanzi-actif {
    color: #667eea;
}

.explorer-pinyin {
    flex: 0 1 auto;
    min-width: 0;
    color: #667eea;
    font-weight: 600;
    font-size: 15px;
}

/* Le deck d'origine : utile pour situer une carte, jamais au point de
   pousser le reste. Il se coupe d'un trois-points plutôt que de passer à la
   ligne, ce qui ajouterait de la hauteur à chaque entrée. */
.explorer-deck {
    flex: 0 1 auto;
    min-width: 0;
    margin-left: auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 11px;
    color: #999;
}

/* Seconde ligne : la traduction entière, sur toute la largeur. */
.explorer-trad {
    color: #666;
    font-style: italic;
    font-size: 13px;
    line-height: 1.35;
    overflow-wrap: anywhere;
}

/* Matière dont la traduction n'est que la lecture (les kana) : elle
   répéterait mot pour mot la ligne du dessus. */
body.sans-traduction .explorer-trad {
    display: none;
}

/* --- Liste des cartes d'un deck --- */
.card-table {
    width: 100%;
    border-collapse: collapse;
    text-align: left;
}

.card-table th {
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #999;
    padding: 8px 12px;
    border-bottom: 2px solid #e0e7ff;
}

.card-table td {
    padding: 8px 12px;
    border-bottom: 1px solid #eee;
    color: #333;
}

.card-table tr:last-child td {
    border-bottom: none;
}

/* Ces trois règles portent sur « td » et NON sur la classe seule : les <th>
   portent les mêmes classes, parce que leurs libellés dépendent de la matière
   et que majLibellesMode() (js/decks.js) doit les viser. Sans le « td », le
   titre « Hanzi » héritait de la taille du CONTENU et s'affichait trois fois
   plus gros que « Pinyin » et « Traduction » à côté de lui. */
.card-table td.col-hanzi {
    font-size: 26px;
    /* Le mot question ne se coupe JAMAIS : un mot chinois ou japonais admet
       une coupure entre deux caractères, et l'écran Dictionnaire autorise en
       plus la césure partout (overflow-wrap), ce qui renverrait « 同...一样 »
       à la ligne au milieu. Une ligne du tableau = un mot sur une ligne.
       Sans risque pour la largeur : le plus long tient en six caractères, et
       la colonne réclame ainsi une centaine de pixels sur les 540 du cadre. */
    white-space: nowrap;
}

.card-table td.col-pinyin {
    color: #667eea;
    font-weight: 600;
    font-size: 17px;
}

.card-table td.col-translation {
    color: #666;
    font-style: italic;
    font-size: 14px;
}

/* Le deck d'origine, sur l'écran « Explorer » : utile pour situer une
   carte, mais c'est la colonne qu'on sacrifie en premier quand la largeur
   manque (css/responsive.css). La règle d'affichage reste sur la classe
   seule, pour emporter l'en-tête avec la colonne. */
.card-table td.col-deck {
    font-size: 12px;
    color: #999;
}

/* --- Flashcard ---
   Le conteneur est centré verticalement dans la fenêtre : toute variation
   de hauteur déplacerait toute la carte. Les trois blocs de l'écran (mot
   question, zone de réponse, rangée de boutons) réservent donc d'avance
   leur hauteur maximale, pour que rien ne bouge au retournement, d'une
   carte à l'autre, entre les deux modes, ni selon les réglages. */
.hanzi {
    --hanzi-size: 160px;
    font-size: var(--hanzi-size);
    line-height: 1.2;
    color: #333;
    /* Hauteur réservée = celle d'un hanzi (160 × 1.2). Le mot question
       occupe la même boîte dans les deux modes : le prompt texte, plus
       petit, s'y centre au lieu de remonter la carte. min-height plutôt
       que height : un prompt exceptionnellement long s'étend au lieu de
       déborder sur les boutons. */
    min-height: 192px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 25px;
    user-select: none;
}

/* Un mot de plusieurs caractères passerait sur deux lignes et doublerait
   la hauteur de la boîte : il tient sur une seule ligne, sa taille
   descendant juste ce qu'il faut pour rentrer dans la largeur de la carte
   (un hanzi occupe une chasse pleine, donc 1 em). --hanzi-len, le nombre
   de caractères, est posé par showCard() ; 96cqw laisse une marge sur la
   largeur de #cardScreen. */
.hanzi:not(.text-prompt) {
    white-space: nowrap;
    font-size: min(var(--hanzi-size), calc(96cqw / var(--hanzi-len, 1)));
}

/* Prompt texte (mode traduction → pinyin). Pas de padding : la boîte de
   .hanzi fournit déjà la hauteur, et le texte s'y centre. */
.hanzi.text-prompt {
    font-size: 34px;
    font-weight: 600;
    font-style: italic;
}

.result .hanzi-answer {
    font-size: 72px;
    line-height: 1.2;
    color: #333;
    margin-bottom: 8px;
}

.result .hanzi-answer:empty {
    display: none;
}

.result .translation:empty {
    display: none;
}

/* Hauteur réservée pour la réponse la plus haute (mode traduction) :
   audio 24 + 25 de marge, hanzi 72 × 1.2 + 8 de marge, pinyin 42 × 1.2
   (sa marge basse fusionne avec celle de la zone) = 194. Le mode hanzi y
   tient largement, traduction sur deux lignes comprise. */
.answer-zone {
    min-height: 194px;
    margin-bottom: 20px;
}

.result {
    display: none;
}

.result.show {
    display: block;
}

/* line-height explicite ici et sur .translation : la hauteur réservée par
   .answer-zone se calcule à partir de ces valeurs, elle ne doit pas
   dépendre de la police retenue par le navigateur */
.result .pinyin {
    font-size: 42px;
    line-height: 1.2;
    font-weight: 600;
    color: #4caf50;
    margin-bottom: 8px;
}

.result .pinyin.revealed {
    color: #ff9800;
}

/* En mode hanzi → pinyin, la traduction est l'élément principal de la
   réponse : affichée au-dessus du pinyin, en plus gros */
.result .translation {
    font-size: 38px;
    line-height: 1.2;
    font-weight: 600;
    color: #333;
    margin-bottom: 8px;
}

/* Commentaires entre parenthèses dans la traduction : plus petits et grisés */
.paren-note {
    font-size: 0.65em;
    font-weight: 400;
    color: #999;
}

/* Le pinyin passe en second plan quand la traduction est affichée */
.result.hanzi-mode .pinyin {
    font-size: 28px;
    margin-bottom: 10px;
}

/* Réglage « Afficher le pinyin » : masqué dans la réponse retournée
   et dans l'aperçu de l'écran Apparence */
body.hide-pinyin .result .pinyin,
body.hide-pinyin .preview-pinyin {
    display: none;
}

/* --- Lecteur audio (bouton play + barre de progression) --- */
/* Toujours dans le flux, même masqué : sa place est réservée, donc la
   réponse s'affiche à la même hauteur que la carte soit retournée ou non,
   et que le son soit actif ou coupé */
.audio-player {
    display: flex;
    visibility: hidden; /* invisible tant que la carte n'est pas retournée */
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-bottom: 25px; /* même distance que .hanzi avant sa réponse */
}

.audio-player.show {
    visibility: visible;
}

/* Réglage « Lecture audio » : reflété en direct dans l'aperçu Apparence.
   Comme sur la vraie carte, la ligne garde sa place. */
body.audio-muted .preview-audio-player {
    visibility: hidden;
}

.btn-audio-play {
    width: 24px;
    height: 24px;
    min-width: 24px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    color: #000;
    border: 1.5px solid #000 !important;
    border-radius: 50% !important;
    cursor: pointer;
    transition: background 0.15s ease, transform 0.1s ease;
    flex-shrink: 0;
    font-size: 0;
    line-height: 0;
}

.btn-audio-play svg {
    width: 14px;
    height: 14px;
}

/* Centrage optique : seul le triangle est décentré, le carré d'arrêt est
   symétrique et n'a rien à corriger */
.btn-audio-play .icone-play {
    margin-left: 2px;
}

/* L'icône dit l'état : triangle à l'arrêt, carré pendant la lecture */
.btn-audio-play .icone-stop {
    display: none;
}

.btn-audio-play.en-lecture .icone-play {
    display: none;
}

.btn-audio-play.en-lecture .icone-stop {
    display: block;
}

.btn-audio-play:hover {
    background: rgba(0, 0, 0, 0.08);
}

.btn-audio-play:active {
    transform: scale(0.92);
}

.audio-progress-bar {
    width: 90px;
    height: 4px;
    background: #ddd;
    border-radius: 2px;
    overflow: hidden;
    flex-shrink: 0;
}

/* Le remplissage occupe toute la barre et se met à l'échelle : une width en
   pourcentage relance une mise en page à chaque frame d'animation, un transform
   part au compositeur. L'écrasement du border-radius est invisible sur 4 px de
   haut, et la barre parente rogne déjà les coins (overflow: hidden).

   AUCUNE TRANSITION ici, volontairement : la progression est jouée par une
   animation (js/audio.js, animerBarre), et une transition par-dessus serait
   relancée à chaque écriture de la propriété, ce qui saccadait sur mobile. Elle
   n'a plus rien à lisser non plus, l'animation étant continue.

   will-change promet la propriété au compositeur avant même le premier clip :
   sans lui, la promotion se ferait au démarrage de l'animation, et se verrait. */
.audio-progress-fill {
    height: 100%;
    width: 100%;
    background: #000;
    border-radius: 2px;
    transform: scaleX(0);
    transform-origin: left center;
    will-change: transform;
}

/* Thème sombre */
body.theme-pixel.theme-dark .btn-audio-play {
    color: #e6e6ec;
    border-color: #e6e6ec;
}

body.theme-pixel.theme-dark .btn-audio-play:hover {
    background: rgba(255, 255, 255, 0.1);
}

body.theme-pixel.theme-dark .audio-progress-bar {
    background: #3a3a42;
}

body.theme-pixel.theme-dark .audio-progress-fill {
    background: #e6e6ec;
}

/* Hauteur réservée pour la rangée la plus haute, la note FSRS avec aperçu
   d'intervalle (10 + 16 × 1.2 + 2 + 12 × 1.2 + 10 = 56) : « Retourner la
   carte » et les quatre notes ne se remplacent plus en changeant la
   hauteur de la carte. align-items : chaque bouton garde sa hauteur
   naturelle et se centre dans la rangée. */
.buttons {
    display: flex;
    gap: 10px;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    min-height: 56px;
    margin-bottom: 20px;
}

button {
    padding: 12px 24px;
    font-size: 16px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
    font-weight: 600;

    /* Sur téléphone, deux appuis rapprochés sont pris pour un double-tap de
       zoom : en enchaînant les notes FSRS, le navigateur zoomait au lieu de
       transmettre le second clic. « manipulation » annonce que l'élément
       n'attend que l'appui et le défilement, ce qui désactive ce double-tap
       SANS toucher au pincement pour zoomer — celui-ci reste disponible, et
       l'accessibilité avec (WCAG 1.4.4 demande un agrandissement jusqu'à
       200 %, ce qu'un user-scalable=no dans la balise viewport aurait
       supprimé). Supprime au passage le délai d'environ 300 ms que certains
       navigateurs s'accordaient pour distinguer appui et double-tap.
       Volontairement posé sur les commandes et non sur body : le double-tap
       de zoom reste utile sur le contenu, en particulier sur un hanzi. */
    touch-action: manipulation;

    /* Les appuis rapprochés déclenchaient aussi la sélection du libellé et
       sa bulle contextuelle. */
    user-select: none;
}

/* line-height explicite ici et sur .btn-grade : la hauteur réservée par
   .buttons se calcule à partir de ces valeurs */
.btn-reveal {
    background-color: #ff9800;
    color: white;
    line-height: 1.2;
}

.btn-reveal:hover {
    background-color: #f57c00;
}

/* Retour visuel pendant le maintien de la touche Espace */
.btn-reveal.holding {
    background-color: #e65100;
    transform: scale(1.08);
    transition: all 0.35s linear;
}

/* Auto-évaluation en mode Flashcard */

/* Notes FSRS (entraînement) : boutons Anki avec aperçu d'intervalle */
.btn-grade {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 10px 8px;
    line-height: 1.2;
    color: white;
    flex: 1 1 0;
    max-width: 130px;
}

.btn-grade small {
    font-weight: 400;
    font-size: 12px;
    opacity: 0.85;
}

.grade-again { background-color: #f44336; }
.grade-again:hover { background-color: #e53935; }
.grade-hard { background-color: #ff9800; }
.grade-hard:hover { background-color: #f57c00; }
.grade-good { background-color: #4caf50; }
.grade-good:hover { background-color: #43a047; }
.grade-easy { background-color: #2196f3; }
.grade-easy:hover { background-color: #1e88e5; }

/* Badge « X dues · Y nouvelles » sur la liste des decks */
.deck-side {
    display: flex;
    gap: 10px;
    align-items: center;
}

/* Deck entièrement à jour (aucune carte due ni nouvelle) */
.deck-btn.deck-done {
    background: #e8f5e9;
    border-color: #c8e6c9;
    color: #7cb096;
}

.deck-btn.deck-done:hover {
    background: #dcefdd;
    border-color: #4caf50;
}

.deck-btn.deck-done .count {
    border-color: #c8e6c9;
    color: #7cb096;
}

.btn-restart {
    background-color: #667eea;
    color: white;
}

.btn-restart:hover {
    background-color: #5568d3;
}

.btn-menu {
    background-color: #e0e7ff;
    color: #667eea;
}

.btn-menu:hover {
    background-color: #c7d2f9;
}

