/*
 * Meteal : feuille unique, pensee pour un telephone tenu a bout de bras.
 * Regles de base : rien sous 17px, cibles tactiles a 56px, une seule colonne.
 */

/* --- Jetons -------------------------------------------------------------- */

:root {
    color-scheme: light;

    --page: #f9f9f7;
    --surface-1: #fcfcfb;
    --surface-2: #f2f1ee;

    --text-primary: #0b0b0b;
    --text-secondary: #52514e;
    /* #898781 ne donnait que 3,4:1 sur --page, sous le seuil AA de 4,5:1 pour
       les petits textes (quantiemes, axes) qu'il sert. #706e69 =
       4,83:1, calcule sur --page clair. Le muted sombre reste inchange : plus
       clair sur fond sombre, il contraste deja. */
    --text-muted: #706e69;

    --grid: #e1e0d9;
    --axis: #c3c2b7;
    --border: rgba(11, 11, 11, 0.10);

    /* Bleu : la pluie, et la serie de reference partout ailleurs. */
    --series-1: #2a78d6;
    /* Rouge : le chaud. Avec le bleu, c'est la paire divergente validee. */
    --series-hot: #e34948;
    /* Les deux courbes du detail : l'exterieur et l'interieur. Un vert et un
       ambre, le jardin dehors et la lumiere chaude dedans, plutot qu'un bleu et
       un rouge qui se liraient "froid" et "chaud" : ce sont deux endroits, pas
       les deux bouts d'une echelle de temperature. Le vert et l'ambre se
       ressemblent pour un daltonien rouge-vert ; c'est alors l'aire pleine de
       l'exterieur contre le trait epais de l'interieur, et non la seule teinte,
       qui les departage. */
    --series-outside: #0ca30c;
    --series-inside: #cf7d0a;
    /* Donnees estimees depuis une source exterieure (Margny, ERA5). */
    --series-external: #898781;
    /* Les deux points remarquables d'une courbe, son mini et son maxi. Ce ne
       sont ni --series-1 ni --series-hot, qui disent la pluie et le chaud :
       ceux-la ne disent que "le plus bas" et "le plus haut", fut-ce sur un
       barometre. Le bleu s'eclaircit en sombre : un bleu nuit sur du noir ne
       se voit pas, et toutes les rampes bleues de l'app s'inversent deja. */
    --marker-min: #0d366b;
    --marker-max: #e34948;

    /* Le cran extreme de l'echelle « chaud » des verdicts : un rouge plus
       profond que series-hot, pour dire « exceptionnel » sans virer au brun.
       (Le cran extreme « froid » reutilise --marker-min, deja bien contraste.) */
    --verdict-warm: #a3231f;

    /* Rampe sequentielle bleue : cumuls de pluie du calendrier. */
    --blue-100: #cde2fb;
    --blue-200: #9ec5f4;
    --blue-300: #6da7ec;
    --blue-450: #2a78d6;
    --blue-550: #1c5cab;
    --blue-700: #0d366b;

    /* Rampe divergente des annees (froid -> chaud). */
    --div-cold-3: #184f95;
    --div-cold-2: #3987e5;
    --div-cold-1: #9ec5f4;
    --div-mid: #f0efec;
    --div-warm-1: #f3b0af;
    --div-warm-2: #e34948;
    --div-warm-3: #8f1f1f;

    /* Nuit tropicale (mini >= 20 C) : un violet, la chaleur (rouge) posee sur la
       nuit (bleu). Il tranche sur la rampe chaude des jours et sur les bleus du
       froid. Une seule valeur pour les deux modes, comme la rampe chaude, calee
       assez soutenue pour tenir sur une barre etroite au clair comme au sombre. */
    --night-tropical: #7a4fc0;

    /* L'axe sec <-> arrose de la reglette pluie. Le mouille reprend le bleu de
       la pluie (series-1, marker-min pour l'extreme) ; le sec prend un ambre,
       pour ne pas reutiliser le rouge « chaud » qui dirait autre chose. */
    --rain-dry: #9a6a12;
    --rain-dry-strong: #6e4a0c;

    /* Ambre : ce qui demande un geste, sans etre une erreur. */
    --warn: #8a5200;
    --warn-surface: #fbf0dc;

    /* Le ciel : le soleil chaud, la lune froide. La paire se lit d'un coup, et
       tient pour un daltonien la ou deux teintes proches se confondraient. */
    --sun: #e0930a;
    --moon: #6f7ea6;
    /* Les deux faces du disque de phase : la part eclairee et l'ombre. */
    --moon-lit: #f6f2e5;
    --moon-shadow: #3f4860;

    --radius: 14px;
    --tap: 56px;
}

/*
 * Sombre. Deux chemins y menent : « auto » (le defaut) suit le telephone via la
 * requete media ; « sombre » le force quel que soit le reglage systeme. Le CSS
 * ne sait pas partager un meme bloc de jetons entre une requete media et un
 * selecteur simple : les deux listes ci-dessous sont donc identiques et doivent
 * le rester. « clair » n'a rien a faire ici : la base :root est deja claire, et
 * les deux selecteurs epargnent explicitement data-theme='light'.
 */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']):not([data-theme='dark']) {
        color-scheme: dark;

        --page: #0d0d0d;
        --surface-1: #1a1a19;
        --surface-2: #232322;

        --text-primary: #ffffff;
        --text-secondary: #c3c2b7;
        --text-muted: #898781;

        --grid: #2c2c2a;
        --axis: #383835;
        --border: rgba(255, 255, 255, 0.10);

        --series-1: #3987e5;
        --series-hot: #e66767;
        /* Vert et ambre eclaircis sur fond noir, comme les autres tons de l'app
           en sombre. */
        --series-outside: #3fbf3f;
        --series-inside: #f0a94a;
        --series-external: #898781;
        --marker-min: #9ec5f4;
        --marker-max: #e66767;
        /* Sur fond noir, l'extreme « chaud » s'eclaircit comme les autres tons
           chauds, pour rester lisible tout en pesant plus que series-hot. */
        --verdict-warm: #f2a3a0;

        --blue-100: #104281;
        --blue-200: #184f95;
        --blue-300: #1c5cab;
        --blue-450: #3987e5;
        --blue-550: #6da7ec;
        --blue-700: #b7d3f6;

        --div-mid: #383835;

        /* Ambre du sec, eclairci sur fond noir comme le reste de la palette. */
        --rain-dry: #d6a94e;
        --rain-dry-strong: #ecc77e;

        --warn: #f0b64a;
        --warn-surface: #2b2114;

        --sun: #f2b53f;
        --moon: #a6b3d4;
        --moon-lit: #f1ebd8;
        --moon-shadow: #262b38;
    }
}

/* Jumeau du bloc ci-dessus : « sombre » force, sans egard au reglage systeme.
   Garder les deux listes de jetons rigoureusement identiques. */
:root[data-theme='dark'] {
    color-scheme: dark;

    --page: #0d0d0d;
    --surface-1: #1a1a19;
    --surface-2: #232322;

    --text-primary: #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted: #898781;

    --grid: #2c2c2a;
    --axis: #383835;
    --border: rgba(255, 255, 255, 0.10);

    --series-1: #3987e5;
    --series-hot: #e66767;
    --series-outside: #3fbf3f;
    --series-inside: #f0a94a;
    --series-external: #898781;
    --marker-min: #9ec5f4;
    --marker-max: #e66767;
    --verdict-warm: #f2a3a0;

    --blue-100: #104281;
    --blue-200: #184f95;
    --blue-300: #1c5cab;
    --blue-450: #3987e5;
    --blue-550: #6da7ec;
    --blue-700: #b7d3f6;

    --div-mid: #383835;

    --rain-dry: #d6a94e;
    --rain-dry-strong: #ecc77e;

    --warn: #f0b64a;
    --warn-surface: #2b2114;

    --sun: #f2b53f;
    --moon: #a6b3d4;
    --moon-lit: #f1ebd8;
    --moon-shadow: #262b38;
}

/* --- Base ---------------------------------------------------------------- */

*, *::before, *::after {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--page);
    color: var(--text-primary);
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: 17px;
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
}

/* Tirer-pour-rafraichir de la PWA installee (voir pull_refresh_controller.js).
   Position et opacite sont pilotees en JS pour suivre le doigt ; on ne fixe ici
   que l'allure et l'animation de rotation. */
.pull-refresh {
    position: fixed;
    top: calc(env(safe-area-inset-top) + 0.25rem);
    left: 50%;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    background: var(--surface-1);
    border: 1px solid var(--border);
    box-shadow: 0 2px 10px rgba(11, 11, 11, 0.18);
    opacity: 0;
    /* Au repos, garee au-dessus de l'encoche, hors champ. */
    transform: translate(-50%, -40px);
    pointer-events: none;
}

/* Pose seulement au relachement : pendant le tirage, l'indicateur colle au
   doigt sans transition, sinon il traine. */
.pull-refresh.is-animating {
    transition: transform 0.2s ease, opacity 0.2s ease;
}

.pull-refresh__spinner {
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 50%;
    border: 2px solid var(--axis);
    /* Un seul cran colore : l'anneau se lit comme une aiguille qui tourne. */
    border-top-color: var(--text-secondary);
}

/* Seuil franchi : l'anneau prend la couleur d'un geste qui va aboutir. */
.pull-refresh.is-ready .pull-refresh__spinner {
    border-top-color: var(--series-1);
}

.pull-refresh.is-refreshing .pull-refresh__spinner {
    animation: pull-refresh-spin 0.7s linear infinite;
}

@keyframes pull-refresh-spin {
    to {
        transform: rotate(360deg);
    }
}

/* La barre de progression de Turbo, en haut de l'ecran quand une visite traine.
   Sa feuille embarquee la peint en #0076ff, un bleu etranger a la palette : on
   la ramene au bleu de reference, qui existe en clair comme en sombre. Les
   couleurs sont des jetons, jamais des valeurs en dur. La hauteur reprend celle
   du vendor, deja discrete sur un telephone ; la redire ici la met a l'abri
   d'une mise a jour de leur feuille. */
.turbo-progress-bar {
    height: 3px;
    background: var(--series-1);
}

h1 {
    font-size: 1.5rem;
    margin: 0 0 0.25rem;
}

h2 {
    font-size: 1.15rem;
    margin: 0 0 0.5rem;
}

a {
    color: var(--series-1);
}

.page {
    /* Ancre la bascule de theme, garee en absolu dans le coin haut droit. */
    position: relative;
    max-width: 720px;
    margin: 0 auto;
    /* Place pour la barre d'onglets, encoche du telephone comprise. */
    padding: 1rem 1rem calc(var(--tap) + 2.5rem + env(safe-area-inset-bottom));
}

.page-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1rem;
    /* Reserve la colonne des commandes du coin (cloche + "?" + theme) : un titre
       long, comme la date pleine de l'accueil, ne passe jamais dessous. Mesure
       du groupe rendu : deux ronds de 2.5rem, le "?", leurs ecarts et le retrait. */
    padding-inline-end: 7rem;
}

.page-header .subtitle {
    color: var(--text-secondary);
    font-size: 0.95rem;
}

/* --- Commandes du coin haut droit : notifications + theme ---------------- */

/* Garees dans le coin haut droit de chaque page, a la hauteur du titre. En
   absolu plutot qu'en fixe : elles vivent dans le fil de la page, sous la
   banniere du capteur muet, et ne la chevauchent donc jamais. Deux boutons
   ronds cote a cote, la cloche des notifications puis la bascule du theme, avec
   le "?" des regles entre eux. */
.top-controls {
    position: absolute;
    top: 1rem;
    inset-inline-end: 1rem;
    z-index: 4;
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

/* Un seul bouton de theme qui cycle entre les trois modes, plus court a
   atteindre au pouce qu'un menu deroule. Meme rond pour la cloche des
   notifications, posee dans la section dediee de Reglages. */
.theme-toggle,
.notify-toggle,
.news-bell {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    color: var(--text-secondary);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
}

.theme-toggle:hover,
.theme-toggle:focus-visible,
.notify-toggle:hover,
.notify-toggle:focus-visible,
.news-bell:hover,
.news-bell:focus-visible {
    color: var(--text-primary);
    border-color: var(--axis);
}

.theme-toggle svg,
.notify-toggle svg,
.news-bell svg {
    width: 22px;
    height: 22px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Notifications actives : cloche pleine, teintee du bleu de reference, pour
   dire "en marche" d'un coup d'oeil. Eteintes : cloche barree, neutre. Les deux
   icones sont dans le bouton, on montre celle de l'etat courant (aria-pressed). */
.notify-toggle .icon {
    display: none;
}

.notify-toggle .icon-bell-off {
    display: block;
}

.notify-toggle[aria-pressed='true'] .icon-bell-off {
    display: none;
}

.notify-toggle[aria-pressed='true'] .icon-bell {
    display: block;
}

.notify-toggle[aria-pressed='true'] {
    color: var(--series-1);
    border-color: var(--series-1);
}

.notify-toggle:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* La cloche des nouveautes vit dans le flux du .top-controls ; le span qui
   l'enveloppe (bouton + popover, pour le controleur Stimulus) ne doit pas
   s'interposer dans la flexbox. */
.news-bell-wrap {
    display: contents;
}

/* Ancre la bulle compteur au coin du bouton rond. */
.news-bell {
    position: relative;
}

/* Compteur des groupes de nouveautes non lus. Bleu de reference, comme la
   cloche des push actives : c'est une information, pas une alerte. Plancher
   de 0.75rem (12px), le meme que le "?" des bulles d'aide. */
.news-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 18px;
    color: #fff;
    background: var(--series-1);
    border-radius: 999px;
}

/* Le popover de la cloche reprend la bulle d'aide (couche superieure,
   fermeture native), en colonne de lecture alignee a gauche. */
.news-popover {
    width: min(320px, calc(100vw - 2.5rem));
    /* Plusieurs groupes non lus (ex. six au premier lancement) depassent la
       hauteur de l'ecran : sans plafond, la bulle se centre sur sa propre
       hauteur et deborde des deux cotes, croix et titre compris, hors champ.
       Mesure sur iPhone 13 (844px de haut). */
    max-height: calc(100dvh - 2.5rem);
    overflow-y: auto;
    text-align: left;
}

.news-popover h2 {
    margin: 0 0 0.2rem;
    font-size: 1.05rem;
}

.news-popover h3 {
    margin: 0.7rem 0 0.2rem;
    font-size: 0.85rem;
    color: var(--text-secondary);
}

.news-all {
    display: inline-block;
    margin-top: 0.8rem;
    color: var(--series-1);
}

/* La moitie pleine du cercle « auto » : le cercle reste un trait, ce demi-disque
   est plein pour figurer le passage clair vers sombre. */
.theme-toggle .icon-auto .fill-half {
    fill: currentColor;
    stroke: none;
}

/* Le serveur pose deja data-theme d'apres le cookie : la bonne icone s'affiche
   des le rendu, sans clignotement. On les tient toutes cachees, sauf celle du
   mode courant. « auto » (aucun attribut, ou sa valeur explicite) montre l'ecran. */
.theme-toggle .icon {
    display: none;
}

.theme-toggle .icon-auto {
    display: block;
}

:root[data-theme='light'] .theme-toggle .icon-auto,
:root[data-theme='dark'] .theme-toggle .icon-auto {
    display: none;
}

:root[data-theme='light'] .theme-toggle .icon-light,
:root[data-theme='dark'] .theme-toggle .icon-dark {
    display: block;
}

/* --- Cartes -------------------------------------------------------------- */

.card {
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1rem;
    margin-bottom: 1rem;
}

.card > h2 {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

/* Titre et son "?" restent solidaires a gauche ; la note file a droite. */
.card > h2 .title {
    min-width: 0;
}

/* La note d'en-tete (la periode : "S29 · 13-19 juil 2026") ne se coupe jamais
   en son milieu. Atomique (nowrap), elle tient sur la ligne du titre tant qu'ils
   y logent tous deux, sinon bascule entiere sur la ligne suivante, poussee a
   droite par la marge auto. Mesure au telephone : derriere "Une semaine aux
   extremes", cette note deborde sous 400px ; sans nowrap le retour tombait avant
   "2026", qui restait seul sur une ligne. La regle ne vise que le span en-tete,
   pas le paragraphe .note de couverture qui suit. */
.card > h2 .note {
    white-space: nowrap;
    margin-left: auto;
}

.card .note {
    color: var(--text-secondary);
    font-size: 0.9rem;
    margin: 0.5rem 0 0;
}

/* --- Tuiles de stats ----------------------------------------------------- */

.tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.tile {
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.85rem 1rem;
}

.tile .label {
    color: var(--text-secondary);
    font-size: 0.9rem;
    display: block;
}

.tile .value {
    font-size: 1.9rem;
    font-weight: 600;
    line-height: 1.15;
    margin-top: 0.15rem;
    display: block;
    /* "1013 hPa" ne doit pas se couper entre le nombre et son unite. */
    white-space: nowrap;
}

.tile .unit {
    font-size: 1rem;
    font-weight: 400;
    color: var(--text-secondary);
}

.tile .delta {
    font-size: 0.9rem;
    color: var(--text-secondary);
    display: block;
}

/* Mini a gauche, maxi a droite : la paire est fixe, donc deux
   colonnes egales tenues meme sur telephone plutot que l'empilement de
   `auto-fit`. La date peut alors passer a la ligne dans sa tuile etroite. */
.tiles.extremes {
    grid-template-columns: 1fr 1fr;
}

/* Froid en bleu, chaud en rouge : series-1 et series-hot, la paire divergente
   validee du projet (et au validateur du skill dataviz : contraste et ecart
   daltonien larges, en clair comme en sombre). Deux canaux, comme pour une stat
   tile : un fond en lavis (~6 %) qui teinte sans peser, et une pastille pleine
   couleur qui ancre la teinte. Un lavis seul, dilue, virait au gris ; c'est la
   pastille, pas un fond sursature, qui fait lire "bleu"/"rouge" franchement, et
   le texte reste en encre. color-mix garde le lavis clair en clair, sombre en
   sombre, la ou un aplat fixe blanchirait le texte en mode sombre. */
.tile.extreme-cold {
    background: color-mix(in srgb, var(--series-1) 6%, var(--surface-1));
}

.tile.extreme-warm {
    background: color-mix(in srgb, var(--series-hot) 6%, var(--surface-1));
}

/* « Mini »/« Maxi » plus lisibles que la note discrete des autres tuiles :
   deux mots courts qui nomment le coeur du bloc, on les grossit au-dela du
   0,9rem herite de `.tile .label`. */
.tiles.extremes .label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 1.15rem;
}

.tiles.extremes .label::before {
    content: "";
    width: 9px;
    height: 9px;
    border-radius: 50%;
    flex: none;
}

.tile.extreme-cold .label::before {
    background: var(--series-1);
}

.tile.extreme-warm .label::before {
    background: var(--series-hot);
}

/* Le chiffre que la page raconte : un seul par ecran. */
.hero {
    font-size: 3.2rem;
    font-weight: 600;
    line-height: 1;
    margin: 0;
}

.hero-unit {
    font-size: 1.5rem;
    font-weight: 400;
    color: var(--text-secondary);
}

/* --- Reglette « un jour normal » ----------------------------------------- */

/* Le releve du jour pose sur l'ecart frais -> doux de la meme date. Les
   positions viennent du serveur (NormalDayBuilder) ; ici on ne fait que
   dessiner le rail, la bande, la mediane et le point. */
.pos-row { margin-top: 1.35rem; }

.pos-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}

/* La date de l'extreme, calee a droite du sous-titre. Note effacee (le sous-titre
   porte deja le sens), atomique : sur telephone elle tient a cote du sous-titre et
   ne se coupe jamais en son milieu, sinon elle bascule entiere sous lui. */
.pos-date {
    color: var(--text-secondary);
    font-size: 0.9rem;
    white-space: nowrap;
    margin-left: auto;
}

/* Le sous-titre de la ligne (Mini, Matin, Maxi) : un vrai intertitre, gros et
   gras, pas une etiquette effacee. */
.pos-label {
    color: var(--text-primary);
    font-size: 1.1rem;
    font-weight: 600;
}

/* Le verdict n'est plus une pastille a lui seul : c'est le mot colore dans la
   pastille du releve (voir .pos-measure). Il ne porte que sa couleur et son
   poids ; le contour et le fond sont ceux du drapeau qui l'englobe. */
.pos-verdict {
    font-size: 0.78rem;
    font-weight: 600;
    white-space: nowrap;
}

/* Echelle de neuf nuances, du plus frais au plus chaud. Le mot porte le detail ;
   la couleur, l'ampleur de l'ecart, en trois paliers par cote : « un peu »
   adouci vers le gris, la nuance franche en couleur pleine (bleu de series-1,
   rouge de series-hot), l'extreme en teinte profonde. « normal » reste le gris
   de texte : "rien de particulier". Frais et doux gardent une couleur franche,
   seul « un peu » s'efface, pour ne pas crier a cote du bleu de la pluie. */
.pos-verdict.normal { color: var(--text-secondary); }

.pos-verdict.un-peu-frais { color: color-mix(in srgb, var(--series-1) 58%, var(--text-secondary)); }
.pos-verdict.frais,
.pos-verdict.froid { color: var(--series-1); }
.pos-verdict.tres-froid { color: var(--marker-min); }

.pos-verdict.un-peu-doux { color: color-mix(in srgb, var(--series-hot) 58%, var(--text-secondary)); }
.pos-verdict.doux,
.pos-verdict.chaud { color: var(--series-hot); }
.pos-verdict.tres-chaud { color: var(--verdict-warm); }

/* La reglette pluie parle sec (ambre) <-> arrose (bleu de la pluie), memes
   paliers que la temperature : « un peu » adouci vers le gris, la nuance franche
   en couleur pleine, l'extreme en teinte profonde. */
.pos-verdict.un-peu-sec { color: color-mix(in srgb, var(--rain-dry) 58%, var(--text-secondary)); }
.pos-verdict.sec { color: var(--rain-dry); }
.pos-verdict.tres-sec { color: var(--rain-dry-strong); }

.pos-verdict.un-peu-arrose { color: color-mix(in srgb, var(--series-1) 58%, var(--text-secondary)); }
.pos-verdict.arrose { color: var(--series-1); }
.pos-verdict.tres-arrose { color: var(--marker-min); }

/* Le drapeau du releve, pose a l'aplomb de son point : une seule pastille qui
   tient le chiffre et son verdict, pour qu'on lise d'un bloc que le mot qualifie
   ce chiffre-la. L'ecart au rail reste lisible : colle au point, le drapeau
   semblait en sortir. */
.pos-flag {
    position: relative;
    height: 1.9rem;
    margin-bottom: 0.55rem;
}

.pos-measure {
    position: absolute;
    bottom: 0;
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--surface-2);
    line-height: 1;
    white-space: nowrap;
}

/* Le chiffre garde sa presence, il reste le sujet ; le verdict est la note qui
   l'accompagne, plus petite, seule sa couleur porte l'ampleur de l'ecart. */
.pos-value {
    font-size: 1.25rem;
    font-weight: 600;
}

.pos-value .unit {
    font-size: 0.8rem;
    font-weight: 400;
    color: var(--text-secondary);
}

/* Ancrage horizontal du drapeau sur son point : centre au milieu du rail, colle
   par une extremite pres d'un bord pour ne jamais deborder (bornes cote serveur,
   NormalStripBuilder). */
.pos-measure.anchor-center { transform: translateX(-50%); }
.pos-measure.anchor-start { transform: translateX(0); }
.pos-measure.anchor-end { transform: translateX(-100%); }

.pos-track {
    position: relative;
    height: 10px;
    border-radius: 5px;
    background: var(--surface-2);
    border: 1px solid var(--border);
}

/* La fourchette habituelle : du frais (p20) au doux (p80). Un fond, pas un
   aplat : opacite basse pour que la mediane et le point priment. */
.pos-band {
    position: absolute;
    top: -1px;
    bottom: -1px;
    border-radius: 5px;
    background: linear-gradient(90deg, var(--div-cold-1), var(--div-mid) 50%, var(--div-warm-1));
    opacity: 0.68;
}

/* La reglette pluie va du sec (ambre) a l'arrose (bleu), au lieu du froid ->
   chaud. Un peu plus pale : l'ambre plein pesait plus lourd que la bande temp. */
.reglette-rain .pos-band {
    background: linear-gradient(90deg, var(--rain-dry), var(--div-mid) 50%, var(--series-1));
    opacity: 0.55;
}

/* La mediane : le repere "normal". */
.pos-median {
    position: absolute;
    top: -4px;
    bottom: -4px;
    width: 2px;
    background: var(--text-secondary);
    transform: translateX(-1px);
}

/* Le releve du jour : "vous etes ici". Contour de la couleur de la carte pour
   qu'il se detache par-dessus la bande, quelle qu'en soit la teinte. */
.pos-dot {
    position: absolute;
    top: 50%;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--text-primary);
    border: 2px solid var(--surface-1);
    box-shadow: 0 0 0 1px var(--text-primary);
    transform: translate(-50%, -50%);
}

.pos-scale {
    position: relative;
    height: 1.9rem;
    margin-top: 0.4rem;
    /* Plancher a 0.75rem (12px), voir M16. */
    font-size: 0.75rem;
    color: var(--text-muted);
}

.pos-scale span {
    position: absolute;
    transform: translateX(-50%);
    text-align: center;
    line-height: 1.2;
    white-space: nowrap;
}

.pos-scale span b {
    display: block;
    color: var(--text-secondary);
    font-weight: 600;
    font-size: 0.8rem;
}

/* Aux deux bords, les libelles s'alignent vers l'interieur pour ne pas deborder
   du cadre. */
.pos-scale span.edge-l { transform: translateX(0); text-align: left; }
.pos-scale span.edge-r { transform: translateX(-100%); text-align: right; }

/* Repli sans fourchette (station seule) : la valeur brute, sans point. */
.pos-plain {
    font-size: 1.25rem;
    font-weight: 600;
    margin: 0;
}

/* --- Graphiques ---------------------------------------------------------- */

/* Sous-ligne du graphique temp-main : le titre porte la periode, cette ligne
   nomme la courbe ("Heure par heure") et porte sa bulle "?". Secondaire au
   titre, donc plus discrete, mais posee juste au-dessus de la courbe. */
.chart-subheading {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    margin: 0 0 0.5rem;
    font-size: 1rem;
    font-weight: 600;
    color: var(--text-secondary);
}

.chart {
    position: relative;
    /* Hauteur fixe, bande des libelles d'axe comprise. */
    height: 260px;
    /* Garder le doigt sur la courbe pour lire une valeur ne doit pas armer la
       selection du navigateur, qui empoignait alors tout le graphique et le
       texte autour. On ne touche pas au touch-action par defaut : la page
       defile toujours quand on glisse par-dessus (l'exception zoomable ci-apres
       la garde en pan-y). */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

/* Courbe dense (voir ZOOM_MIN_POINTS cote JS) : pincer a deux doigts pour
   zoomer, et deplacement a un doigt maintenu (gere cote JS). pan-y preserve le
   defilement vertical a un doigt de la page, tout en cedant a l'element le
   pincer et le glisse horizontal (touch-action: auto detournerait le pincer
   vers le zoom natif de la page, et le glisse horizontal empecherait le
   deplacement). */
.chart.zoomable {
    touch-action: pan-y;
}

/* Retour a la vue d'ensemble apres un zoom. Petit bouton flottant en haut a
   droite, cree cote JS et masque tant que le graphique n'est pas zoome. Fond
   plein pour rester lisible par-dessus la courbe. Cible tactile a 44px, le
   plancher du projet. */
.chart-reset {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    font-size: 1.35rem;
    line-height: 1;
    color: var(--text-secondary);
    background: var(--page);
    border: 1px solid var(--axis);
    border-radius: 50%;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
    cursor: pointer;
}

.chart-reset[hidden] {
    display: none;
}

.chart.tall {
    height: 320px;
}

.legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1rem;
    margin-top: 0.6rem;
    color: var(--text-secondary);
    font-size: 0.9rem;
}

.legend .item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.legend .key {
    width: 18px;
    height: 3px;
    border-radius: 2px;
    background: var(--series-1);
    flex: none;
}

.legend .key.band {
    height: 12px;
    border-radius: 3px;
    opacity: 0.28;
}

.legend .key.hot { background: var(--series-hot); }

/* Bande saisonniere du gel : un pave plein (pas un fil), plus haut que la cle
   par defaut pour que trois nuances proches de series-1 se distinguent. La
   couleur arrive en ligne, calee sur la meme opacite que le graphique. */
.legend .key.season { height: 12px; border-radius: 3px; }

/* Les deux fils fins qui soulignent les bords de la bande : le maxi teinte
   chaud, le mini teinte froid, dans les memes lavis que les tuiles d'extremes
   (series-hot et series-1). Palis pour rester en retrait du trait franc du 7h. */
.legend .key.line-max { background: color-mix(in srgb, var(--series-hot) 45%, transparent); }
.legend .key.line-min { background: color-mix(in srgb, var(--series-1) 45%, transparent); }

/* Un point, et non un trait : la cle doit ressembler a ce qu'elle nomme, et le
   releve de 7h se pose sur la courbe horaire au lieu d'en tracer une. */
.legend .key.dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--text-primary);
}

/* Trait gris plein : les annees passees d'une journee type, tracees en retrait
   derriere l'annee en cours. La cle doit ressembler au trait qu'elle nomme. */
.legend .key.muted { background: var(--text-muted); }

/* L'annee mise en avant porte un trait plus large dans la figure ; sa cle le
   dit en etant plus epaisse, les autres plus fines. */
.legend .key.lead { height: 5px; }
.legend .key.thin { height: 2px; }

.legend .key.faded { background: color-mix(in srgb, var(--series-1) 22%, transparent); }

.legend .key.muted-band {
    background: color-mix(in srgb, var(--text-muted) 15%, transparent);
    opacity: 1;
}

/* Trait pointille : ce qui sert de repere (normale) ou n'a pas ete mesure ici. */
.legend .key.muted-dash,
.legend .key.external {
    background: repeating-linear-gradient(
        90deg, var(--series-external) 0 5px, transparent 5px 9px
    );
}

/* --- Reordonner les blocs ------------------------------------------------ */

/* Repere pour les fleches, qui se posent en absolu sur le coin du bloc. */
[data-block-order-target~="block"] {
    position: relative;
}

/* Les deux fleches chevauchent le bord haut du bloc, a droite. Elles se tiennent
   dans la marge et le rembourrage du haut, au-dessus du titre : un titre long
   passe dessous sans les toucher, et un bloc sans titre (les tuiles) les porte
   aussi bien. */
.reorder {
    position: absolute;
    top: -0.7rem;
    right: 0.8rem;
    z-index: 2;
    display: inline-flex;
    /* Un peu d'ecart entre les deux fleches : leurs debords cliquables
       ci-dessous peuvent alors s'etendre vers l'interieur sans se chevaucher. */
    gap: 0.45rem;
}

.reorder[hidden] {
    display: none;
}

.reorder .move {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
    color: var(--text-secondary);
    background: var(--surface-1);
    border: 1px solid var(--axis);
    border-radius: 999px;
    cursor: pointer;
}

/* Le rond reste discret (25,6px) mais la cible atteint 44px en hauteur, le sens
   du geste au pouce. En largeur, le debord s'arrete dans l'ecart entre les deux
   fleches, sans deborder sur la voisine. */
.reorder .move::before {
    content: "";
    position: absolute;
    inset: -9px -3px;
}

/* L'attribut [hidden] du navigateur ne bat pas la regle ci-dessus : on le dit. */
.reorder .move[hidden] {
    display: none;
}

.reorder .move svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Le bloc qui vient d'etre deplace : un halo bleu qui s'estompe. Le glissement
   dit qu'il a change de place, ce halo dit lequel c'etait. Passe au-dessus de
   son voisin pendant qu'ils se croisent. */
@keyframes block-moved {
    from { box-shadow: 0 0 0 2px var(--series-1); }
    to { box-shadow: 0 0 0 2px transparent; }
}

.just-moved {
    z-index: 3;
    animation: block-moved 0.8s ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .just-moved {
        animation: none;
    }
}

/* --- Calendrier de la pluie ---------------------------------------------- */

/* Mois en colonnes, jours en lignes : la grille epouse un ecran de telephone
   au lieu de le deborder. */
.calendar-row {
    display: grid;
    grid-template-columns: 1.5rem repeat(12, 1fr);
    gap: 3px;
    align-items: center;
}

.calendar-row + .calendar-row {
    margin-top: 3px;
}

.calendar-row.head {
    margin-bottom: 4px;
}

.calendar-month {
    font-size: 0.7rem;
    color: var(--text-secondary);
    text-align: center;
    line-height: 1.2;
}

.calendar-daynum {
    /* Plancher a 0.75rem (12px) : 0.65rem = 10,4px etait sous le seuil de
       lisibilite pour un public familial sur telephone. */
    font-size: 0.75rem;
    color: var(--text-muted);
    text-align: right;
    padding-right: 0.2rem;
    line-height: 1.2;
}

.calendar-cell {
    height: 15px;
    border-radius: 3px;
    background: var(--surface-2);
}

.calendar-cell.empty {
    background: transparent;
}

.calendar-cell.unknown {
    background: repeating-linear-gradient(
        45deg, var(--surface-2) 0 2px, transparent 2px 4px
    );
}

/* Rampe sequentielle : une seule teinte, plus il pleut plus c'est sombre. */
.calendar-cell.level-0, .swatch.level-0 { background: var(--surface-2); }
.calendar-cell.level-1, .swatch.level-1 { background: var(--blue-100); }
.calendar-cell.level-2, .swatch.level-2 { background: var(--blue-200); }
.calendar-cell.level-3, .swatch.level-3 { background: var(--blue-300); }
.calendar-cell.level-4, .swatch.level-4 { background: var(--blue-450); }
.calendar-cell.level-5, .swatch.level-5 { background: var(--blue-700); }

/* Jour estime depuis une source exterieure : cerne, jamais confondu avec un
   releve fait dans le jardin. */
.calendar-cell.external,
.swatch.external-key {
    box-shadow: inset 0 0 0 1.5px var(--series-external);
}

/* Une case porte la pluie de son jour : le curseur le laisse deviner au bureau,
   une bulle la donne au doigt. */
.calendar-cell:not(.empty) {
    cursor: pointer;
}

/* La case dont la bulle est ouverte : un cerne net, en couleur de texte pour
   trancher sur n'importe quel niveau de pluie. Au-dessus de ses voisines pour
   que le cerne ne soit pas rogne. */
.calendar-cell.is-selected {
    position: relative;
    z-index: 1;
    outline: 2px solid var(--text-primary);
    outline-offset: 1px;
}

/* Petite bulle ancree a l'element touche (le controleur tip pose left/top). On
   defait le centrage du style UA (inset 0 + margin auto) pour la caler
   nous-memes ; pas de fond assombri, contrairement a la bulle d'aide centree. */
.tip-bubble {
    margin: 0;
    inset: auto;
    max-width: 12rem;
    padding: 0.4rem 0.6rem;
    font-size: 0.85rem;
    line-height: 1.35;
    color: var(--text-primary);
    background: var(--surface-1);
    border: 1px solid var(--axis);
    border-radius: var(--radius);
    box-shadow: 0 6px 20px rgb(0 0 0 / 0.18);
}

.calendar-legend {
    gap: 0.35rem 0.75rem;
}

.legend .swatch {
    width: 14px;
    height: 14px;
    border-radius: 3px;
    border: 1px solid var(--border);
    flex: none;
}

/* --- Bandes de rechauffement --------------------------------------------- */

/* Les bandes sont un graphique Chart.js : survol au bureau, appui maintenu au
   doigt, l'infobulle native comme sur les autres figures. Seule la couleur code
   l'ecart a la moyenne (l'ordonnee ne se lit pas), d'ou un canvas plus mince que
   les autres graphiques. Les colonnes montent aux deux tiers (plafond de l'axe a
   1,75) : le tiers haut degage recoit l'infobulle, posee au-dessus de la colonne
   visee plutot qu'en plein milieu. 150 px : ~86 px de colonne, ~64 px libres
   au-dessus, mesures dans le navigateur pour loger les deux lignes de la bulle. */
.chart.stripes-chart {
    height: 150px;
}

.stripes-axis {
    display: flex;
    justify-content: space-between;
    margin-top: 0.3rem;
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* Rampe divergente : deux teintes opposees, gris au centre. Le milieu doit se
   lire "rien de particulier", ce qu'aucune couleur ne ferait. Les bandes memes
   sont peintes sur le canvas depuis ces jetons ; ici, seulement la cle sous la
   figure. */
.swatch.cold-3 { background: var(--div-cold-3); }
.swatch.cold-2 { background: var(--div-cold-2); }
.swatch.cold-1 { background: var(--div-cold-1); }
.swatch.mid    { background: var(--div-mid); }
.swatch.warm-1 { background: var(--div-warm-1); }
.swatch.warm-2 { background: var(--div-warm-2); }
.swatch.warm-3 { background: var(--div-warm-3); }

/* --- Tuile temps reel ---------------------------------------------------- */

/* Le temps que la station reponde : toutes les mesures s'effacent ensemble, on
   garde la place, sans clignotement. Les valeurs affichees datent du releve
   precedent : en laisser une seule vive la ferait passer pour fraiche.
   Le .hero vise ici est celui de la station muette, seul a la racine de la
   tuile ; ceux de .live-temps s'effacent avec leur bloc, sans quoi les deux
   opacites se multiplieraient. */
.live > .hero,
.live .live-temps,
.live .tiles {
    transition: opacity 0.2s;
}

.live.is-loading > .hero,
.live.is-loading .live-temps,
.live.is-loading .tiles {
    opacity: 0.25;
}

/* Huit mesures d'un coup d'oeil : des tuiles plus serrees que les tuiles de
   page, sans quoi la carte occuperait tout l'ecran. */
.live .tiles {
    grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
    gap: 0.5rem;
    margin: 0.85rem 0 0;
}

.live .tiles .tile {
    padding: 0.6rem 0.7rem;
}

.live .tiles .value {
    font-size: 1.35rem;
}

.live .tiles .label {
    font-size: 0.8rem;
}

.live .tiles .delta {
    font-size: 0.75rem;
}

/* --- Records ------------------------------------------------------------- */

.records {
    margin: 0;
}

.record {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--grid);
}

.record:last-child {
    border-bottom: 0;
}

.record dt {
    color: var(--text-secondary);
    font-size: 0.95rem;
    flex: 1;
}

.record dd {
    margin: 0;
    text-align: right;
}

.record-value {
    display: block;
    font-size: 1.25rem;
    font-weight: 600;
    white-space: nowrap;
}

.record-detail {
    display: block;
    font-size: 0.8rem;
    color: var(--text-muted);
    white-space: nowrap;
}

/* Colonne de texte libre : elle peut passer a la ligne, contrairement aux
   colonnes de chiffres. */
td.wrap {
    white-space: normal;
    text-align: left;
    font-size: 0.85rem;
    line-height: 1.35;
}

/* --- Listes de precisions ------------------------------------------------ */

.facts {
    margin: 0;
    padding-left: 1.1rem;
    color: var(--text-secondary);
    font-size: 0.92rem;
    line-height: 1.5;
}

.facts li + li {
    margin-top: 0.5rem;
}

/* --- Tableaux ------------------------------------------------------------ */

.table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

table {
    border-collapse: collapse;
    width: 100%;
    font-size: 0.92rem;
}

th, td {
    text-align: right;
    padding: 0.5rem 0.25rem;
    border-bottom: 1px solid var(--grid);
    white-space: nowrap;
}

th:first-child, td:first-child {
    padding-left: 0;
}

th:last-child, td:last-child {
    padding-right: 0;
}

th:first-child, td:first-child {
    text-align: left;
}

thead th {
    color: var(--text-secondary);
    font-weight: 500;
    font-size: 0.85rem;
}

tbody td {
    font-variant-numeric: tabular-nums;
}

/* --- Marque de provenance ------------------------------------------------ */

.src {
    display: inline-block;
    font-size: 0.75rem;
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    color: var(--text-secondary);
    vertical-align: 0.1em;
}

.src.external {
    border-style: dashed;
}

/* --- Bandeau du capteur exterieur ---------------------------------------- */

/* Colle en haut plutot que fixe : il garde sa place dans le flux, et aucune
   page n'a a se decaler pour ne pas passer dessous. */
.sensor-banner {
    position: sticky;
    top: 0;
    z-index: 11;
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.7rem 1rem calc(0.7rem + env(safe-area-inset-top));
    background: var(--warn-surface);
    border-bottom: 1px solid var(--warn);
    color: var(--text-primary);
    font-size: 0.9rem;
    line-height: 1.35;
}
.sensor-banner svg {
    flex: none;
    width: 20px;
    height: 20px;
    margin-top: 0.1rem;
    fill: none;
    stroke: var(--warn);
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.sensor-banner strong {
    color: var(--warn);
}

/* --- Barre d'onglets ----------------------------------------------------- */

.bottom-nav {
    position: fixed;
    inset: auto 0 0 0;
    display: flex;
    background: var(--surface-1);
    border-top: 1px solid var(--border);
    padding-bottom: env(safe-area-inset-bottom);
    z-index: 10;
}

.bottom-nav a {
    flex: 1;
    min-height: var(--tap);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.1rem;
    padding: 0.4rem 0.2rem;
    color: var(--text-secondary);
    text-decoration: none;
    font-size: 0.8rem;
}

.bottom-nav a.active {
    color: var(--series-1);
    font-weight: 600;
}

.bottom-nav svg {
    width: 26px;
    height: 26px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* --- Formulaires --------------------------------------------------------- */

.field {
    display: block;
    width: 100%;
    min-height: var(--tap);
    padding: 0.6rem 0.9rem;
    font-size: 1.5rem;
    font-family: inherit;
    text-align: center;
    letter-spacing: 0.35em;
    color: var(--text-primary);
    background: var(--surface-1);
    border: 2px solid var(--axis);
    border-radius: var(--radius);
}

.field:focus {
    outline: 3px solid var(--series-1);
    outline-offset: 1px;
    border-color: var(--series-1);
}

/* .field vise d'abord le code d'acces (chiffres espaces, centres) : une
   question libre veut du texte a gauche, sans les 0,35em d'espacement entre
   lettres qui le rendraient illisible. font-size en 17px pleins, le corps du
   texte, plutot que le 1.5rem herite. Pas 1rem : le rem se mesure sur <html>,
   qui reste a 16px, quand le 17px de Meteal est pose sur <body> ; mesure au
   navigateur, le champ calculait bien 16px et passait donc sous le plancher de
   17px que la feuille s'impose. A cette taille, un texte de question tient sur
   deux lignes sans deborder du bloc sur un iPhone 13. */
textarea.field {
    min-height: calc(var(--tap) * 2);
    padding: 0.7rem 0.9rem;
    font-size: 17px;
    letter-spacing: normal;
    text-align: left;
    resize: none;
}

.btn {
    display: block;
    width: 100%;
    min-height: var(--tap);
    margin-top: 1rem;
    padding: 0.8rem 1rem;
    font-size: 1.15rem;
    font-family: inherit;
    font-weight: 600;
    color: #fff;
    background: var(--series-1);
    border: 0;
    border-radius: var(--radius);
    cursor: pointer;
}

/* L'attribut [hidden] du navigateur ne bat pas display: block ci-dessus : on le
   dit, sinon le bouton « Autoriser » resterait visible une fois abonné. */
.btn[hidden] {
    display: none;
}

/*
 * Les periodes : des rangees qui defilent, plutot qu'un pave de boutons qui
 * pousserait les courbes hors de l'ecran. Une vingtaine d'annees ne tient sur
 * la largeur d'aucun telephone. Les douze mois, si : voir plus bas.
 */
/*
 * Le choix de la periode reste sous le pouce quand la page defile : sur un
 * long graphique, changer de semaine ne demande plus de remonter tout en haut.
 *
 * Barre pleine largeur : elle deborde le padding lateral de .page pour que son
 * fond, et le trait qui la separe, aillent d'un bord a l'autre de l'ecran. Ce
 * fond opaque est indispensable, sans quoi les courbes defileraient au travers.
 */
.period-picker {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    flex-direction: column;
    /* Serre : quatre rangees collees en haut, chaque espace se paie en place. */
    gap: 0.3rem;
    margin: 0 -1rem 1rem;
    padding: 0.4rem 1rem;
    background: var(--page);
    border-bottom: 1px solid var(--border);
}

/* Detachee : la barre defile avec la page. "relative" et non "static", pour
   rester le repere des fleches posees en absolu. */
.period-picker.is-unpinned {
    position: relative;
    top: auto;
}

/*
 * La poignee sous la barre : une languette large qui pend de son bord bas, du
 * meme fond que la barre, comme si le bord se prolongeait en un tiret qu'on
 * saisit pour la lacher. Elle epouse le bord (haut plat, sans trait, bas arrondi)
 * et le prolonge au lieu de flotter dessous en pastille : plus large, plus haute,
 * et cousine de la languette de rappel plutot que des fleches rondes qui
 * reordonnent les blocs, avec qui l'ancienne se confondait.
 *
 * Cachee par defaut : elle ne parait que lorsque la barre s'est accrochee en
 * haut (is-stuck), pas quand on est en haut de page ou la barre est deja a sa
 * place. Detachee, plus de poignee : c'est la fleche de rappel qui prend le
 * relais.
 */
.sticky-toggle {
    position: absolute;
    left: 50%;
    top: 100%;
    transform: translateX(-50%);
    z-index: 11;
    display: none;
    align-items: center;
    justify-content: center;
    width: 3.5rem;
    height: 1.5rem;
    padding: 0;
    color: var(--text-secondary);
    background: var(--page);
    border: 1px solid var(--border);
    border-top: none;
    border-radius: 0 0 999px 999px;
    cursor: pointer;
}

.period-picker.is-stuck:not(.is-unpinned) .sticky-toggle {
    display: flex;
}

/* Un tiret epais aux bouts ronds : la prise qu'on tire, pas un chevron de plus. */
.sticky-toggle svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/*
 * Le rappel : une languette fixee en haut de l'ecran, qui pend du bord. Cachee
 * par defaut, le controleur ne la montre que lorsque la barre detachee a quitte
 * l'ecran. Son "fixed" la tient au bord haut quoi qu'il defile.
 *
 * Devenue une pilule a trois segments : le centre reprend l'ancienne languette
 * entiere (rattacher la barre), les bouts sont un pas vers la periode voisine,
 * comme la rangee .period-step au-dessus, pour changer de periode sans rouvrir
 * la barre. Largeur libre pour accueillir le libelle, mais bornee a la largeur
 * utile de l'ecran ; un libelle trop long (une semaine datee) se tronque plutot
 * que de deborder.
 */
.sticky-restore {
    position: fixed;
    left: 50%;
    top: 0;
    transform: translateX(-50%);
    z-index: 20;
    display: none;
    align-items: stretch;
    max-width: calc(100vw - 2rem);
    min-height: 1.55rem;
    /* Pleine et bleue, comme la periode choisie dans sa rangee : la languette
       rappelle la selection avec la meme couleur d'accent, texte et fleches en
       clair pour tenir le contraste dans les deux modes. */
    color: var(--page);
    background: var(--series-1);
    border: 1px solid var(--series-1);
    border-top: none;
    border-radius: 0 0 999px 999px;
    /* Les coins des segments ne doivent pas deborder l'arrondi de la pilule. */
    overflow: hidden;
}

.sticky-restore.is-visible {
    display: flex;
}

/* Le segment central : l'ancienne languette entiere, qui rattache la barre. */
.restore-main {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    min-width: 0;
    padding: 0.3rem 0.8rem 0.35rem;
    font-family: inherit;
    color: inherit;
    background: none;
    border: 0;
    cursor: pointer;
}

/* Les bouts : un pas vers la periode voisine, separes du centre d'un filet. */
.restore-step {
    display: flex;
    align-items: center;
    padding: 0 0.7rem;
    color: inherit;
}

.restore-step:first-child {
    border-right: 1px solid color-mix(in srgb, var(--page) 35%, transparent);
}

.restore-step:last-child {
    border-left: 1px solid color-mix(in srgb, var(--page) 35%, transparent);
}

.restore-step.off {
    opacity: 0.45;
}

/* L'anneau de focus par defaut se dessine hors de la boite et serait
   rogne par l'overflow hidden de la pilule : on le ramene a l'interieur. */
.restore-main:focus-visible,
.restore-step:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: -3px;
}

/* Le libelle : a peine sous le corps des annees, pour rester lisible sur la
   languette bleue. Tronque d'un point de suspension quand il touche son bord. */
.sticky-label {
    /* Sans min-width: 0, un item flex refuse de descendre sous son contenu et
       la troncature ne se declenche jamais : le libelle deborderait la languette. */
    min-width: 0;
    overflow: hidden;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--page);
    white-space: nowrap;
    text-overflow: ellipsis;
}

.sticky-restore svg {
    flex: none;
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Le raccourci "aujourd'hui" et la rangee des annees partagent une ligne : le
   bouton reste epingle a gauche, les annees defilent a sa droite sans jamais
   l'emporter hors de l'ecran. */
.periods-row {
    display: flex;
    gap: 0.5rem;
    align-items: stretch;
}

/* min-width: 0, sinon la rangee refuse de retrecir sous son contenu et son
   defilement horizontal cesse de fonctionner. */
.periods-row > .periods {
    flex: 1;
    min-width: 0;
}

/* Une pastille d'action, distinguee des annees par l'accent bleu des liens. */
.today {
    flex: none;
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 0.9rem;
    color: var(--series-1);
    background: var(--surface-1);
    border: 1px solid var(--series-1);
    border-radius: 999px;
    font-size: 1rem;
    font-weight: 600;
    white-space: nowrap;
    text-decoration: none;
}

/* On y est deja : le raccourci se remplit, comme la periode choisie dans sa rangee. */
.today.active {
    color: var(--page);
    background: var(--series-1);
    border-color: var(--series-1);
}

/* La rangee de pas : les fleches menent a la periode relevee voisine, le
   libelle redit la selection entre elles. Memes pastilles que "Aujourd'hui",
   en neutre : l'accent bleu reste a la periode choisie dans ses rangees. */
.period-step {
    display: flex;
    align-items: stretch;
    gap: 0.5rem;
}

.period-step .step {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 56px;
    min-height: 44px;
    color: var(--series-1);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: 999px;
}

/* Au bout de l'historique : la fleche reste en place, eteinte. */
.period-step .step.off {
    color: var(--text-secondary);
    opacity: 0.4;
}

.period-step .step svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.step-label {
    flex: 1;
    min-width: 0;
    align-self: center;
    overflow: hidden;
    text-align: center;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-secondary);
    white-space: nowrap;
    text-overflow: ellipsis;
}

.periods {
    display: flex;
    gap: 0.5rem;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    /* Repere des offsetLeft que le controleur lit pour centrer la periode. */
    position: relative;
}

/* Le selecteur de series d'un graphique (extérieur / intérieur / les deux) :
   des pastilles comme celles des periodes, mais au-dessus du graphique et un
   cran plus basses, en controle secondaire. */
.chart-toggle {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 0.9rem;
}

.chart-toggle .pill {
    flex: none;
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0 0.9rem;
    color: var(--text-secondary);
    background: var(--surface-1);
    border: 1px solid var(--axis);
    border-radius: 999px;
    font: inherit;
    white-space: nowrap;
    cursor: pointer;
}

/* Pleine et sombre : la serie choisie se reconnait a sa forme, pas a sa seule
   couleur, comme la periode active. */
.chart-toggle .pill.active {
    color: var(--page);
    background: var(--text-primary);
    border-color: var(--text-primary);
    font-weight: 600;
}

/* Les enfants directs seulement : une pastille peut porter du balisage, qui ne
   doit pas devenir une pastille a son tour. */
.periods > a,
.periods > span {
    /* Sans quoi les libelles se compriment au lieu de faire defiler. */
    flex: none;
    display: inline-flex;
    align-items: center;
    /* 44px : le plancher tactile confortable, l'axe principal des annees. */
    min-height: 44px;
    padding: 0 0.9rem;
    color: var(--text-secondary);
    background: var(--surface-1);
    border: 1px solid var(--axis);
    border-radius: 999px;
    font-size: 1rem;
    white-space: nowrap;
    text-decoration: none;
}

/* Pleine et bleue, comme le raccourci "Aujourd'hui" quand on y est : la periode
   choisie se reconnait a sa forme et a la meme couleur d'accent partout, et le
   contraste tient dans les deux modes. */
.periods > a.active {
    color: var(--page);
    background: var(--series-1);
    border-color: var(--series-1);
    font-weight: 600;
}

/* Les rangees du dessous decoupent celle du dessus : plus petites, elles se
   lisent comme une precision et non comme une rangee rivale. */
.periods.sub > a,
.periods.sub > span {
    /* Un cran sous les annees : plus bas descendrait sous la cible du pouce,
       les jours etant deja etroits. */
    min-height: 36px;
    padding: 0 0.7rem;
    font-size: 0.9rem;
}

/*
 * L'annee entiere d'un seul coup d'oeil : douze colonnes egales, pas une
 * rangee qui defile. Un mois qu'il faut aller chercher hors de l'ecran ne se
 * compare plus a ses voisins, et les mois manquants ne se comptent plus.
 *
 * A douze de front la colonne ne fait qu'une vingtaine de pixels : le libelle
 * (jusqu'a "Juil"/"Août", 4 lettres) doit rapetisser pour y tenir sans passer
 * a la ligne (measure : iPhone 13, 390px, e2e/shot.mjs). Le nom entier reste
 * dans le title et l'aria-label, pour le doigt qui hesite et le lecteur
 * d'ecran.
 */
.periods.months {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: 0.25rem;
    overflow: visible;
}

.periods.months > a,
.periods.months > span {
    font-size: clamp(0.55rem, 2.1vw, 0.7rem);
}

/*
 * Les semaines d'un mois : quatre a six, jamais plus. Trop peu pour justifier
 * une rangee qui defile, assez pour tenir la ligne d'un bord a l'autre. Leur
 * nombre change d'un mois a l'autre, d'ou les colonnes implicites plutot qu'un
 * compte fige : chaque semaine cree sa colonne, toutes se partagent la largeur.
 */
.periods.weeks {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: 0.25rem;
    overflow: visible;
}

.periods.weeks > a,
.periods.weeks > span {
    padding: 0;
    justify-content: center;
}

/*
 * Les sept jours d'une semaine, meme principe : une grille, jamais un defile.
 * Chaque pastille porte le nom court du jour et son quantieme ("Lun 16") : sept
 * quantiemes nus ne diraient pas ou tombe le week-end.
 *
 * minmax(0, 1fr) et non 1fr : sans le minimum a zero, une colonne refuse de
 * descendre sous la largeur de son texte et la rangee deborde de l'ecran.
 * Ici les sept colonnes se partagent la largeur a parts strictement egales, et
 * c'est le texte qui se plie a la case, pas l'inverse.
 */
.periods.days {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 0.2rem;
    overflow: visible;
}

/* Apres .periods.sub, a specificite egale : c'est l'ordre qui tranche. */
.periods.months > a,
.periods.months > span,
.periods.days > a,
.periods.days > span {
    padding: 0;
    justify-content: center;
}

/*
 * "Lun 16" sur un septieme d'un telephone etroit : la place est comptee. La
 * taille suit la largeur de l'ecran, bornee pour rester lisible sans jamais
 * forcer la rangee a deborder. Le nom du jour, plus accessoire que le chiffre,
 * s'ecrit un cran plus petit et efface.
 */
.periods.days > a,
.periods.days > span {
    gap: 0.28em;
    font-size: clamp(0.72rem, 2.5vw, 0.9rem);
}

.periods.days .wd {
    font-size: 0.82em;
    color: var(--text-muted);
}

/* Sur la pastille choisie, tout passe en couleur de page : le nom du jour ne
   doit pas rester gris sur fond sombre, il s'y perdrait. */
.periods.days > a.active .wd {
    color: inherit;
}

/* Un mois sans le moindre releve garde sa place, mais creuse et en pointilles :
   il se donne a lire comme une absence, pas comme un bouton qui ne marche pas. */
.periods > span.off {
    color: var(--text-muted);
    background: transparent;
    border-style: dashed;
}


.alert {
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    background: var(--surface-2);
    border-left: 4px solid var(--series-hot);
    margin-bottom: 1rem;
}

/* --- Connexion ----------------------------------------------------------- */

.login {
    /* Ancre la bascule de theme, comme .page. */
    position: relative;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 0.5rem;
    max-width: 380px;
    margin: 0 auto;
    padding: 1.5rem;
}

/* Sans cela, la largeur intrinseque du champ (attribut size + interlettrage)
   empeche le formulaire de retrecir et fait deborder la page. */
.login > * {
    min-width: 0;
}

.login h1 {
    text-align: center;
    font-size: 1.8rem;
}

.login p {
    text-align: center;
    color: var(--text-secondary);
    margin-top: 0;
}

/* --- Choix de reglage ---------------------------------------------------- */

.choice {
    display: flex;
    align-items: flex-start;
    gap: 0.7rem;
    padding: 0.85rem;
    margin-top: 0.6rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    cursor: pointer;
}

.choice.is-current {
    border-color: var(--series-1);
    box-shadow: inset 0 0 0 1px var(--series-1);
}

.choice input {
    width: 22px;
    height: 22px;
    margin: 0.1rem 0 0;
    accent-color: var(--series-1);
    flex: none;
}

.choice-body {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.choice-title {
    font-weight: 600;
}

.choice-desc {
    color: var(--text-secondary);
    font-size: 0.9rem;
    line-height: 1.4;
}

.choice-stats {
    color: var(--text-muted);
    font-size: 0.8rem;
}

/* Un reglage a bascule : le libelle a gauche, la cloche ronde a droite, alignes
   sur une ligne comme un interrupteur. La cloche garde sa taille (aucun etirement). */
.toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.9rem;
    margin: 0.4rem 0 0.7rem;
}

.toggle-label {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

/* Le cadre qui dit l'etat en toutes lettres, sous la bascule : la cloche seule
   ne parle qu'a qui connait l'icone. Bleu de reference quand c'est actif (le
   meme "en marche" que la cloche pleine), neutre sinon, plus efface quand la
   plateforme ne sait pas notifier. */
.notify-status {
    /* Colle au controle au-dessus (le cadre lit son etat), respire avant la
       liste des conditions en dessous, qui est un propos distinct. */
    margin: 0 0 1rem;
    padding: 0.6rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--text-secondary);
    font-size: 0.9rem;
}

.notify-status[data-state='on'] {
    color: var(--series-1);
    border-color: var(--series-1);
}

.notify-status[data-state='unavailable'] {
    color: var(--text-muted);
}

/* Un envoi partiel (des echecs, des abonnements caducs) attire l'oeil sans
   crier : le rouge du chaud, deja au theme, dit "regarde" mieux qu'un neutre. */
.notify-status[data-state='warn'] {
    color: var(--series-hot);
    border-color: var(--series-hot);
}

/* Le bouton de test, tout en bas de Reglages : un utilitaire discret, pas le
   grand bouton bleu d'une action principale. Bordure et fond neutres, mais la
   meme cible tactile de 56 px que partout, et centre car il ne commande rien
   au-dessus de lui. */
.page-footer {
    margin-top: 2rem;
    text-align: center;
}

.btn-quiet {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap);
    padding: 0.6rem 1.1rem;
    font-size: 1rem;
    font-family: inherit;
    font-weight: 600;
    color: var(--text-secondary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    cursor: pointer;
}

.btn-quiet:hover,
.btn-quiet:focus-visible {
    color: var(--text-primary);
    border-color: var(--text-muted);
}

.btn-quiet:disabled {
    opacity: 0.55;
    cursor: default;
}

/* Le cadre d'etat reprend .notify-status, mais centre et resserre sous le
   bouton : il n'appartient a aucune section, juste au verdict de l'envoi. */
.page-footer .notify-status {
    margin: 0.7rem auto 0;
    max-width: 26rem;
    text-align: center;
}

/* --- Bulles d'explication ------------------------------------------------ */

/* Un "?" discret dans la case concernee : la precision est a portee de doigt
   sans encombrer ceux qui la connaissent deja. */
.help {
    width: 18px;
    height: 18px;
    padding: 0;
    margin-left: 0.1rem;
    font: inherit;
    /* Plancher a 0.75rem (12px), voir M16 : le « ? » de 18px l'accueille. */
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1;
    color: var(--text-secondary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
    vertical-align: 0.05em;
    /* Ancre le debord cliquable ci-dessous. */
    position: relative;
}

/* La cible tactile deborde le rond sans pousser la mise en page. Un box-shadow
   (l'ancienne approche) ne participe JAMAIS au hit-testing : la zone cliquable
   restait ~18px, sous le minimum WCAG 2.5.5. Un ::before en absolu,
   lui, se clique. inset -13px porte 18px a 44px : (44 - 18) / 2 = 13. */
.help::before {
    content: "";
    position: absolute;
    inset: -13px;
}

.help:hover,
.help:focus-visible {
    color: var(--text-primary);
    border-color: var(--axis);
}

/* API Popover : le navigateur gere la couche superieure, la fermeture au clic
   exterieur et la touche Echap. Rien a coder. */
.bubble {
    max-width: min(320px, calc(100vw - 2.5rem));
    margin: auto;
    /* La marge droite reserve la colonne de la croix : le texte n'y passe
       jamais dessous. Le popover reste `position: fixed` (style UA) : cela
       suffit a caler la croix en absolu, sans redefinir la position. */
    padding: 0.9rem 2.6rem 0.9rem 1rem;
    font-size: 0.9rem;
    line-height: 1.45;
    color: var(--text-primary);
    background: var(--surface-1);
    border: 1px solid var(--axis);
    border-radius: var(--radius);
    box-shadow: 0 8px 30px rgb(0 0 0 / 0.18);
}

.bubble::backdrop {
    background: rgb(0 0 0 / 0.2);
}

/* Une bulle centree n'a pas de bord evident ou fermer : la croix le dit. Le
   clic exterieur et Echap restent, mais au pouce la croix est le geste sur. */
.bubble-close {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    width: 36px;
    height: 36px;
    padding: 0;
    font-size: 1.25rem;
    line-height: 1;
    color: var(--text-secondary);
    background: none;
    border: none;
    border-radius: 999px;
    cursor: pointer;
}

/* Porte la cible de 36 a 44px sans grossir le rond visible (WCAG 2.5.5). */
.bubble-close::before {
    content: "";
    position: absolute;
    inset: -4px;
}

.bubble-close:hover,
.bubble-close:focus-visible {
    color: var(--text-primary);
    background: var(--surface-2);
}

/* --- Bouton de relecture ------------------------------------------------- */

.live-head {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}

.refresh {
    /* Cible tactile confortable, rond discret : c'est un appoint, le releve
       automatique fait le travail. */
    position: relative;
    width: 34px;
    height: 34px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
}

/* Porte la cible de 34 a 44px sans grossir le rond visible (WCAG 2.5.5). */
.refresh::before {
    content: "";
    position: absolute;
    inset: -5px;
}

.refresh svg {
    width: 17px;
    height: 17px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.refresh:hover:not(:disabled),
.refresh:focus-visible {
    color: var(--text-primary);
    border-color: var(--axis);
}

.refresh:disabled {
    cursor: progress;
    opacity: 0.5;
}

/* Tourne tant que la station repond. */
.live.is-loading .refresh svg {
    animation: refresh-spin 1s linear infinite;
}

@keyframes refresh-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .live.is-loading .refresh svg { animation: none; }
}

/* --- Dehors / dedans ----------------------------------------------------- */

.live-temps {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
    margin-top: 0.25rem;
}

.live-temp .label {
    color: var(--text-secondary);
    font-size: 0.9rem;
    display: block;
}

/* Deux chiffres cote a cote : un peu moins hauts qu'un chiffre unique, pour
   tenir dans la largeur d'un telephone sans se couper de leur unite. */
.live-temp .hero {
    font-size: 2.5rem;
    white-space: nowrap;
}

.live-temp .hero-unit {
    font-size: 1.1rem;
}

/* --- Cadran soleil / lune ------------------------------------------------- */

.sky-dial-wrap {
    display: flex;
    justify-content: center;
    margin-top: 0.25rem;
}

.sky-dial {
    width: 100%;
    max-width: 320px;
    height: auto;
}

/* L'anneau des 24 heures : la nuit, sur laquelle le jour et la lune se posent. */
.sky-track {
    fill: none;
    stroke: var(--grid);
    stroke-width: 8;
}

/* Le jour : du lever au coucher du soleil, par-dessus la nuit. Bouts arrondis,
   pour que l'arc se lise comme une bande et non comme un tuyau coupe net. */
.sky-day {
    fill: none;
    stroke: var(--sun);
    stroke-width: 8;
    stroke-linecap: round;
}

/* La lune levee, sur un anneau interieur : elle ne recouvre jamais le jour. */
.sky-moon {
    fill: none;
    stroke: var(--moon);
    stroke-width: 5;
    stroke-linecap: round;
}

/* L'heure courante : une aiguille d'horloge, du bord de la lune vers l'anneau. */
.sky-now {
    fill: var(--text-primary);
    stroke: var(--surface-1);
    stroke-width: 0.6;
    stroke-linejoin: round;
}

.sky-now-hub {
    fill: var(--text-primary);
    stroke: var(--surface-1);
    stroke-width: 1;
}

/* Le disque de phase, au centre. */
.sky-phase-dark {
    fill: var(--moon-shadow);
}

.sky-phase-lit {
    fill: var(--moon-lit);
}

.sky-phase-ring {
    stroke: var(--border);
    stroke-width: 1;
}

/* Les quatre reperes : un point sur l'anneau, a la couleur de son astre. */
.sky-dot--sun {
    fill: var(--sun);
}

.sky-dot--moon {
    fill: var(--moon);
}

/* Le groupe porte la couleur ; la fleche (currentColor) la reprend. */
.sky-mark--sun {
    color: var(--sun);
}

.sky-mark--moon {
    color: var(--moon);
}

.sky-glyph-fill {
    fill: currentColor;
}

.sky-glyph-stroke {
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
}

.sky-arrow {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* L'heure reprend la couleur de son astre (currentColor, porte par le groupe) :
   orange pour le soleil, bleu pour la lune, comme les arcs du cercle. */
.sky-time {
    fill: currentColor;
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.sky-summary {
    margin: 0.6rem 0 0;
    text-align: center;
    color: var(--text-secondary);
    font-size: 0.9rem;
}

/* --- Feuille semestrielle imprimable ------------------------------------- */

/* A4 paysage : la feuille est dimensionnee en millimetres, taille deterministe
   independante du viewport du telephone. Deux garde-fous contre l'impression
   ratee vue sur mobile : le graphique est fige en image (voir freezeToImage),
   qui ne s'etire jamais et ne se re-rasterise pas a contretemps ; et la feuille
   garde sa vraie largeur (pas de max-width), pour que cette image soit dessinee
   au format paysage du papier, jamais au format rabote de l'ecran. Ainsi la
   grille de stats reste alignee sous les barres de mois. Sur un ordinateur et le
   plus souvent sur Chrome Android, @page donne une vraie page paysage ; iOS
   Safari l'ignore et reduit la feuille sur une page portrait (lisible, une page,
   non etiree). */
@page {
    size: A4 landscape;
    margin: 8mm;
}

.print-page {
    margin: 0;
    padding: 1.5rem 1rem;
    color: var(--text-primary);
    background: #e9e9e6;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
}

.print-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1rem;
    width: 281mm;
    max-width: 100%;
    margin: 0 auto 1rem;
}

.print-toolbar .field {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    font-size: 0.85rem;
    color: var(--text-secondary);
}

.print-toolbar select {
    min-height: var(--tap);
    padding: 0.4rem 0.6rem;
    font: inherit;
    color: var(--text-primary);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.print-toolbar .semester-choice {
    display: inline-flex;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.print-toolbar .semester-choice label {
    position: relative;
    padding: 0.5rem 0.9rem;
    cursor: pointer;
}

.print-toolbar .semester-choice label.active {
    color: #fff;
    background: var(--series-1);
}

/* La radio reste dans le flux pour recevoir le clic via son libelle, mais
   invisible : c'est le libelle actif qui montre le choix. */
.print-toolbar .semester-choice input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}

.print-toolbar .semester-choice label:has(input:focus-visible) {
    outline: 2px solid var(--series-1);
    outline-offset: 2px;
}

.print-toolbar .btn {
    display: inline-block;
    width: auto;
    margin: 0 0 0 auto;
    padding: 0.5rem 1.1rem;
    font-size: 1rem;
}

/* La feuille prend exactement la zone imprimable d'une page A4 paysage (297 x
   210 mm moins les 8 mm de marge de @page, soit 281 x 194 mm). Ainsi tout tient
   sur une seule page : le graphique occupe l'espace laisse par la grille de
   stats (flex), il ne peut plus pousser le tableau sur une deuxieme page. 1 mm de
   moins que la zone reelle, marge de securite contre un arrondi qui relancerait
   une page. Pas de max-width : la feuille garde sa vraie largeur en mm meme sur un
   telephone (elle deborde alors, on la fait defiler), pour que le graphique soit
   dessine et fige a la taille du papier, jamais rabote a celle de l'ecran. */
.sheet {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    width: 280mm;
    height: 193mm;
    margin: 0 auto;
    padding: 6mm;
    background: #fff;
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.15);
}

.sheet-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin: 0 0 2mm;
}

.sheet-head h1 {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 600;
}

.sheet-period {
    margin: 0;
    font-size: 1rem;
    color: var(--text-secondary);
}

.sheet-period strong {
    color: var(--text-primary);
}

/* Le graphique occupe l'espace laisse par la grille de stats (fixee par son
   contenu) dans la feuille a hauteur figee : c'est lui qui s'adapte, jamais la
   feuille qui grandit. min-height 0 : sans cela un enfant flex ne descend pas
   sous sa hauteur de contenu et deborderait la page. */
.sheet-chart {
    flex: 1 1 auto;
    min-height: 0;
}

/* Le canvas ne sert que d'atelier : des qu'il a peint, le controleur le remplace
   par une image PNG (.chart-frozen) de meme boite. L'image est dessinee au format
   exact de sa boite, donc width/height 100% la montrent au 1:1 sans deformation. */
.sheet-chart canvas,
.sheet-chart .chart-frozen {
    display: block;
    width: 100% !important;
    height: 100% !important;
}

.sheet-legend {
    justify-content: center;
    margin: 1mm 0 2mm;
}

.legend .key.key-mean {
    background: var(--text-secondary);
}

.sheet-stats {
    display: grid;
    font-variant-numeric: tabular-nums;
    border-top: 1px solid var(--border);
}

.sheet-stats > div {
    padding: 0.5mm 1.2mm;
    border-bottom: 1px solid var(--border);
}

/* Un filet plus marque sous les noms de mois et la ligne des annees. */
.sheet-stats .corner,
.sheet-stats .month-name {
    border-bottom: 1px solid var(--text-muted);
}

/* Le nom du mois coiffe ses deux sous-colonnes (annee choisie + annee d'avant). */
.sheet-stats .month-name {
    grid-column: span 2;
    font-weight: 600;
    text-align: center;
    border-left: 1px solid var(--border);
}

.sheet-stats .row-label {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-secondary);
    text-align: right;
    white-space: nowrap;
}

/* La ligne des annees : quelle sous-colonne est le semestre choisi, laquelle est
   l'annee d'avant. Petite, l'annee choisie un rien plus dense. */
.sheet-stats .year-head {
    font-size: 0.6rem;
    text-align: center;
    color: var(--text-muted);
}

.sheet-stats .year-head.cur {
    font-weight: 600;
    color: var(--text-secondary);
}

.sheet-stats .stat {
    font-size: 0.72rem;
    text-align: center;
    white-space: nowrap;
}

/* La barre de mois passe a gauche de la colonne de l'annee choisie : c'est la
   frontiere entre deux mois, calee sur les barres verticales du graphique.
   Teinte text-muted, comme le filet horizontal sous les noms de mois et comme
   les barres du graphique, plus marquee que les filets de lignes. La paire
   annee / annee-1 d'un meme mois n'est pas separee, pour se lire ensemble. */
.sheet-stats .month-name,
.sheet-stats .year-head.cur,
.sheet-stats .stat.cur {
    border-left: 1px solid var(--text-muted);
}

/* L'annee choisie en gras, l'annee d'avant en retrait : l'oeil va d'abord au
   semestre imprime, la comparaison vient ensuite. */
.sheet-stats .stat.cur {
    font-weight: 600;
}

.sheet-stats .stat.prev {
    color: var(--text-muted);
}

@media print {
    /* On garde .sheet a l'identique (taille en mm) : seuls le decor d'ecran et
       la barre d'outils disparaissent, le canvas n'est donc pas redimensionne. */
    .no-print {
        display: none !important;
    }

    /* Marges du navigateur a zero : sans cela, les ~8 px de marge par defaut du
       body s'ajoutent aux 281 mm de la feuille, qui deborde alors la largeur
       imprimable et pousse une seconde page blanche. La feuille tient sur une
       seule page A4 paysage. */
    html,
    body {
        margin: 0;
        padding: 0;
    }

    .print-page {
        padding: 0;
        background: #fff;
    }

    .sheet {
        box-shadow: none;
    }
}

/* Assistant : fil de discussion une colonne, bulles sobres. */
.assistant-thread { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
.assistant-message { padding: 12px 14px; border-radius: var(--radius); max-width: 92%; }
.assistant-question { align-self: flex-end; background: var(--page); border: 1px solid var(--border); }
.assistant-answer { align-self: flex-start; background: var(--surface-1); border: 1px solid var(--border); }
.assistant-pending { opacity: 0.7; }
.assistant-degraded, .assistant-error { border-left: 3px solid var(--text-muted); }

/* Un fil sans message ne creuse rien : ni sa marge basse, ni la part de
   hauteur libre que lui donne le panneau flottant (voir plus bas). Le
   formulaire remonte alors juste sous le titre, au lieu de tomber au fond d'un
   vide de 220 px (mesure au navigateur le 27/08/2026, panneau ouvert au
   gabarit iPhone 13). Le gabarit ne laisse volontairement aucune espace dans
   le fil vide, sans quoi `:empty` ne mordrait jamais. */
.assistant-thread:empty {
    flex: none;
    margin-bottom: 0;
}

/* Le lien d'une reponse (« Voir la journée ») garde la cible de 56 px, mais
   pas la pleine largeur ni le corps d'un bouton de page : dans une bulle qui
   plafonne deja a 92 % de la colonne, un pave bleu de bord a bord pesait plus
   lourd que la phrase qu'il prolonge. */
.assistant-answer .btn {
    /* Un flex de niveau bloc, pas un inline-flex : le lien est colle au texte
       de la reponse (un noeud texte et un <a>, sans paragraphe autour quand le
       JS ecrit la bulle), et en ligne il s'invitait au milieu de la derniere
       phrase. Bloc, il passe dessous ; `fit-content`, il ne s'etale pas. */
    display: flex;
    align-items: center;
    width: fit-content;
    margin-top: 10px;
    padding: 0 1rem;
    font-size: 17px;
    font-weight: 400;
    /* Un aplat bleu se lit deja comme un bouton : le souligne du lien, en
       blanc sur bleu, n'ajoutait qu'un trait. */
    text-decoration: none;
}

/* La ligne de saisie, comme dans une messagerie : le champ prend ce qui reste,
   « Demander » se range a cote. Les deux tiennent la cible de 56 px, et la
   ligne entiere en fait 56 au lieu des 184 px que coutaient le champ de deux
   lignes, sa marge et le bouton pleine largeur empiles (mesure au navigateur
   le 27/08/2026). Alignement en bas : quand le champ grandit sous la frappe,
   le bouton reste sur la derniere ligne, la ou se termine la phrase. */
.assistant-ask {
    display: flex;
    align-items: flex-end;
    gap: 8px;
}

/* Meme piege que .btn plus haut : le `display: none` de l'agent utilisateur
   pour [hidden] ne bat pas une regle d'auteur. Sans ce rappel, le formulaire
   de signalement resterait affiche sous « Merci, c'est noté » une fois envoye. */
.assistant-ask[hidden] {
    display: none;
}

/* Le champ de question repasse a une ligne au repos (textarea.field impose
   deux hauteurs de cible aux zones de texte de l'app) et grandit ensuite sous
   la frappe, jusqu'a quatre lignes (voir MAX_INPUT_HEIGHT du controleur). */
.assistant-ask .field {
    flex: 1;
    min-width: 0;
    min-height: var(--tap);
}

/* « Demander » : le bleu de l'action, mais en corps de texte et sans graisse.
   Les 130 px sont mesures au navigateur (27/08/2026) : le bouton fait 111 px
   au repos et 129 px quand le controleur y met « Je cherche… », et sans
   plancher le champ perdait 18 px de large a chaque envoi, le temps de la
   reponse. Le plancher fige la ligne sur le plus large des deux libelles. */
.assistant-send {
    flex: none;
    min-width: 130px;
    min-height: var(--tap);
    padding: 0 1rem;
    font-size: 17px;
    font-family: inherit;
    color: #fff;
    background: var(--series-1);
    border: 0;
    border-radius: var(--radius);
    cursor: pointer;
}

.assistant-send:disabled {
    opacity: 0.55;
    cursor: default;
}

/* Les deux commandes secondaires, sur une ligne sous la saisie. Du texte, pas
   des blocs : elles ne servent qu'apres coup et ne doivent pas concurrencer la
   question. La cible de 56 px vient de la hauteur, pas d'un cadre visible.
   .assistant-reset est cache par defaut, le controleur bascule [hidden] des
   qu'un fil existe ; le declencheur du signalement reste toujours visible,
   seule sa <details> s'ouvre ou se referme. */
.assistant-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* 24 px d'ecart, et les deux libelles tiennent sur la ligne la plus etroite
       des deux emplacements : 324 px de colonne sur la page Assistant (390 px
       d'ecran moins deux fois le retrait de page et celui de la carte), pour
       110 px de « Recommencer » et 162 px de « Signaler un probleme » mesures
       au navigateur le 27/08/2026, soit 296 px. « Nouvelle conversation », a
       168 px, faisait deborder la ligne : le libelle entier reste pour les
       lecteurs d'ecran, comme dans les en-tetes de bloc. */
    gap: 0 24px;
    margin-top: 4px;
}

.assistant-reset,
.assistant-report summary {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0;
    font-size: 17px;
    font-family: inherit;
    font-weight: 400;
    color: var(--text-secondary);
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-color: var(--axis);
    background: none;
    border: 0;
    cursor: pointer;
}

.assistant-reset:hover,
.assistant-reset:focus-visible,
.assistant-report summary:hover,
.assistant-report summary:focus-visible {
    color: var(--text-primary);
    text-decoration-color: currentColor;
}

.assistant-reset[hidden] {
    display: none;
}

/* Signalement d'une conversation (<details> natif, replie par defaut, tache
   17) : la puce native de <summary> disparait, le libelle explicite la rend
   inutile. Deplie, la <details> prend la ligne entiere : son formulaire a
   besoin de toute la colonne, quand son seul declencheur se contentait de sa
   moitie a cote de « Recommencer ». */
.assistant-report summary { list-style: none; }
.assistant-report summary::-webkit-details-marker { display: none; }
.assistant-report[open] { flex: 1 0 100%; }
.assistant-report form { margin-bottom: 12px; }
.assistant-report-done { margin-top: 12px; }

/* --- Teo flottant -------------------------------------------------------- */

/* Le bouton d'appel, fixe en bas a droite, juste au-dessus de la barre de
   navigation. Les 73 px se mesurent : la barre fait 61 px de haut au gabarit
   iPhone 13 (relevé au navigateur le 27/08/2026 : la cible de 56 px, plus son
   trait du haut et l'interligne du libelle), et 12 px d'air suffisent a
   detacher le rond sans le noyer. La marge de securite iOS s'ajoute par-dessus,
   comme pour la barre elle-meme. Le rond reste sous elle en z-index (10) : si
   les deux se croisaient un jour, la navigation resterait maitresse. */
.assistant-fab {
    position: fixed;
    right: 1rem;
    bottom: calc(73px + env(safe-area-inset-bottom));
    z-index: 9;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    padding: 0;
    color: #fff;
    background: var(--series-1);
    border: none;
    border-radius: 999px;
    box-shadow: 0 6px 18px rgb(0 0 0 / 0.22);
    cursor: pointer;
}

/* Meme trait que les icones de la navigation basse : c'est la meme famille de
   commandes, elles doivent se ressembler. */
.assistant-fab svg {
    width: 26px;
    height: 26px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Le panneau : un Popover natif `manual` (voir le gabarit), qui prend
   l'essentiel de l'ecran du telephone en une colonne. Le `display` ne se pose
   que sur :popover-open : une regle d'auteur battrait le `display: none` que
   l'agent utilisateur applique au popover ferme, et le panneau resterait
   ouvert en permanence. */
.assistant-panel {
    width: min(560px, 100vw - 1rem);
    /* Le panneau prend la hauteur de ce qu'il a a montrer, jusqu'a l'ecran
       entier : a l'ouverture, la conversation est vide et une hauteur fixe
       laissait 400 px de blanc sous le champ (mesure au navigateur le
       27/08/2026, panneau de 632 px au gabarit iPhone 13). Il grandit ensuite
       avec le fil, puis c'est le fil qui defile pour lui-meme. */
    height: auto;
    max-height: calc(100dvh - 2rem);
    /* Ancre en bas plutot qu'au centre : le panneau s'ouvre depuis le rond
       flottant du coin bas droit et se remplit par le haut, comme une
       messagerie. Le champ tombe ainsi toujours a la meme place, sous le
       pouce, que la conversation soit vide ou longue. Le `top: auto` n'est pas
       decoratif : l'agent utilisateur cale les popovers avec `inset: 0`, et
       une boite tenue par ses deux bords voit sa hauteur resolue a l'espace
       disponible, `height: auto` ou non. Sans lui, le panneau restait plein
       ecran et le blanc revenait. */
    top: auto;
    bottom: 1rem;
    margin: auto;
    padding: 0;
    overflow: hidden;
    color: var(--text-primary);
    /* La surface d'une carte, pas celle de la page : le panneau EST la carte
       (celle qu'il contient perd son cadre, voir plus bas), et une seule boite
       vaut mieux que deux boites emboitees a 8 px l'une de l'autre. Les bulles
       de question gardent ainsi leur aplat plus sombre que le fond. */
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 8px 30px rgb(0 0 0 / 0.18);
}

.assistant-panel:popover-open {
    display: flex;
    flex-direction: column;
}

.assistant-panel::backdrop {
    background: rgb(0 0 0 / 0.35);
}

/* La croix vit sur la ligne du titre, dans l'en-tete de la carte (le partial la
   pose quand on lui donne `closeTarget`). Elle avait sa bande a elle au-dessus :
   56 px de haut pour un seul bouton, quand la ligne « Téo ? » en occupait 28
   autres juste dessous (mesure au navigateur le 27/08/2026). Une seule ligne
   d'en-tete rend ces 56 px a la conversation, et la croix reste hors du fil qui
   defile, sous le pouce meme quand la conversation s'allonge. */
.assistant-panel-close {
    flex: none;
    width: var(--tap);
    height: var(--tap);
    /* La cible de 56 px deborde de part et d'autre de la ligne de titre plutot
       que de la gonfler : la croix reste large au doigt, l'en-tete garde la
       hauteur du texte. Le debordement ne recouvre que la marge de la carte,
       jamais une bulle : le fil commence plus bas. */
    margin: -0.5rem 0;
    padding: 0;
    font-size: 1.6rem;
    line-height: 1;
    color: var(--text-secondary);
    background: none;
    border: none;
    cursor: pointer;
}

.assistant-panel-close:hover,
.assistant-panel-close:focus-visible {
    color: var(--text-primary);
}

/* L'en-tete de la carte du panneau : le titre et sa bulle a gauche, la croix a
   droite, centres l'un sur l'autre (les en-tetes de carte s'alignent sinon sur
   la ligne de base du texte, ce qui ferait flotter la croix). */
.assistant-panel-body .card > h2 {
    align-items: center;
    flex-wrap: nowrap;
}

.assistant-panel-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    /* Soupape de secours : si le clavier du telephone rabote la hauteur au
       point que meme le champ ne tienne plus, le panneau defile plutot que de
       couper. En temps normal c'est le fil qui absorbe, et rien ne defile ici. */
    overflow-y: auto;
    padding: 0;
}

/* Dans le panneau, la carte n'est plus qu'une mise en page : le cadre, l'aplat
   et les coins arrondis reviennent au panneau lui-meme, qui les portait deja a
   8 px de la. Le retrait tombe de 16 a 12 px, et la colonne de texte passe de
   314 a 348 px de large sur un iPhone 13 (mesure au navigateur le
   27/08/2026) : 34 px rendus a la conversation, sur 390 px d'ecran. */
.assistant-panel-body .card {
    flex: 1;
    padding: 0.75rem;
    background: none;
    border: 0;
    border-radius: 0;
    /* Sans ce zero, la carte garde pour hauteur minimale celle de son contenu
       (regle `min-height: auto` des elements flex) : une conversation de
       quelques echanges la faisait grandir et poussait « Demander » hors du
       panneau. Mesure au navigateur le 27/08/2026, deux bulles suffisaient. */
    min-height: 0;
    display: flex;
    flex-direction: column;
    margin-bottom: 0;
}

/* Le fil est la seule partie extensible : il absorbe la hauteur libre et
   defile pour lui-meme quand la conversation s'allonge, sans jamais pousser le
   champ hors de l'ecran. Tant qu'il est vide, il ne prend rien : `:not(:empty)`
   plutot qu'une regle a annuler ailleurs, les deux se valant en specificite (la
   derniere ecrite l'emporterait, ce que personne ne devine en lisant). Le
   formulaire se pose alors juste sous le titre, et la place libre reste au bas
   du panneau. */
.assistant-panel-body .assistant-thread:not(:empty) {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

