/* bluetti-web — style responsive inspiré de l'app Bluetti officielle */
:root {
  --bg:          #F5F7FA;
  --card:        #FFFFFF;
  --border:      #EAEEF2;
  --border-2:    #DDE3EA;
  --text:        #1B2432;
  --text-2:      #6B7A85;
  --muted:       #A3AEB8;

  --pv:          #F5A623;
  --pv-bg:       #FFF4E0;
  --grid:        #7C8894;
  --grid-bg:     #F1F3F5;
  --house:       #3D8CE8;
  --house-bg:    #E7F1FB;
  --bat:         #34C759;
  --bat-bg:      #E4F7EA;
  --danger:      #FF3B30;

  --radius:      14px;
  --radius-lg:   20px;
  --shadow:      0 2px 8px rgba(20,25,35,.05);
  --shadow-lg:   0 4px 16px rgba(20,25,35,.06);
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0E1116; --card: #1A1F27; --border: #262C36; --border-2: #303842;
    --text: #E8ECEF; --text-2: #A8B2BA; --muted: #7F8A92;
    --pv-bg: #3B2809; --grid-bg: #23282F; --house-bg: #0F2A4A; --bat-bg: #0C3327;
  }
}

* { box-sizing: border-box; }
/* L'attribut `hidden` du HTML ne masque que via la feuille du navigateur, qui
   perd contre n'importe quelle règle d'auteur : `.btn { display:inline-block }`
   suffisait à réafficher un bouton pourtant marqué hidden. D'où le !important —
   c'est le seul endroit du fichier qui en justifie un. */
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  padding-bottom: 92px;
}
h1, h2, h3, h4 { margin: 0; font-weight: 600; }
a { color: inherit; text-decoration: none; }
.muted { color: var(--text-2); font-size: .85rem; }
small { color: var(--muted); font-weight: 400; }

/* ---------- Topbar ---------- */
.topbar {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 18px 12px;
  background: var(--bg);
  position: sticky; top: 0; z-index: 10;
}
.brand__link { display: inline-flex; align-items: center; gap: 8px; }
.brand__name { font-weight: 600; font-size: 1rem; }
.brand__dot { width: 20px; height: 20px; border-radius: 6px; background: var(--pv); }
.topbar__actions { display: flex; gap: 6px; }
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 50%;
  color: var(--text); background: var(--card);
  border: 1px solid var(--border); cursor: pointer;
}
.icon-btn:hover { background: var(--border); }
.icon-btn.is-spinning svg { animation: iconspin 1s linear infinite; }
@keyframes iconspin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 12px; font-size: .78rem; font-weight: 500;
  background: var(--card); color: var(--text-2);
  border: 1px solid var(--border); border-radius: 999px;
}
.chip--live { color: var(--bat); border-color: rgba(52,199,89,.3); }
.chip__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--bat); }
.chip--warn { color: var(--danger); border-color: rgba(255,59,48,.3); }

/* ---------- Main ---------- */
.main { max-width: 480px; margin: 0 auto; padding: 4px 16px 20px; }
.section { margin-top: 20px; }
.section h2 { font-size: 1rem; color: var(--text); margin-bottom: 10px; padding-left: 4px; }

/* ---------- Home header ---------- */
.home-head { padding: 4px 4px 12px; }
.home-head__title { display: flex; align-items: center; gap: 4px; font-size: 1.35rem; font-weight: 600; }
.home-head__title .chev { font-size: .7em; color: var(--text-2); }
.home-head__meta { display: flex; align-items: center; gap: 14px; margin-top: 6px; color: var(--text-2); font-size: .82rem; }
.home-head__meta .status { display: inline-flex; align-items: center; gap: 5px; }
.home-head__meta .status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--bat); }

/* ---------- Scène maison ---------- */
/* La photo fait 1536 × 1365 et le format est porté ici, avec le viewBox du SVG de
   flux (100 × 88,867) : les deux vont ensemble, et changer l'un sans l'autre étire
   les pointillés. Le rapport est écrit en pixels de la photo plutôt qu'en 9/8
   arrondi — la boîte a exactement le cadrage de l'image, donc `object-fit: cover`
   ne recadre rien et les repères en % tombent au pixel.

   Ces 1365 px sont 1024 px de photo plus 341 px de ciel ajoutés en haut (0.12.1).
   La scène gagne un tiers de hauteur sans qu'on recadre les côtés — ce qui aurait
   fait sortir du cadre le poteau ou le panneau du carport, c'est-à-dire les deux
   organes que les flux désignent. */
.scene {
  position: relative;
  margin: 0 -4px 12px;
  background: #F5F7FA;
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 1536 / 1024;
}
.scene__img {
  width: 100%; height: 100%;
  display: block;
  object-fit: cover;
}
/* SVG overlay pour les flèches de flux */
.scene__flow {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 1;
}
/* Les flèches suivent les gaines de la photo : des coudes, donc
   `stroke-linejoin`. Le décalage d'animation est un MULTIPLE du motif (5 × 3),
   sinon le pointillé saute à chaque tour.

   ⚠️ **Traits fins et couleurs douces demandent un HALO, pas une ombre portée.**
   Le trait ne fait que 0,4 unité, soit 1,5 px sur une scène de 366, et les
   couleurs sont adoucies. Mesuré sur la photo de jour, sur la façade blanche :
   l'orange vif donnait 1,67:1, le premier pastel 1,24:1 — le trait disparaissait,
   et c'est ce qui a été jugé « trop pâle » ; la teinte soutenue actuelle remonte
   à 1,44:1. Aucune de ces trois valeurs ne suffit seule : c'est le HALO qui rend
   le trait visible. Une ombre PORTÉE (décalée) n'assombrit qu'un côté ; un halo
   centré (`drop-shadow` sans décalage) cerne le trait sur toute sa longueur et le
   détache de n'importe quel fond. Ne pas le retirer en croyant alléger.

   Sur la photo de NUIT le rapport s'inverse : la teinte douce y est meilleure que
   le vif (5,7:1 contre 4,9:1 pour l'orange), c'est du clair sur du sombre. */
.flow-edge {
  fill: none;
  stroke-width: 0.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1.6 1.4;
  opacity: 0;
  transition: opacity .3s;
  filter: drop-shadow(0 0 1px rgba(8,12,20,.95)) drop-shadow(0 0 2.5px rgba(8,12,20,.55));
}
.flow-edge.is-active {
  opacity: 1;
  animation: flowmove 1.4s linear infinite;
}
@keyframes flowmove { to { stroke-dashoffset: -15; } }

/* La liaison vers l'armoire fait 6 unités de long : au pas normal, la pointe de
   flèche en mange le tiers et il reste deux tirets. Motif plus fin, décalage
   ajusté (5 × 1,8) — sans quoi le flux batterie se réduit à une flèche isolée
   dont on ne voit pas le sens. */
.flow-edge--short { stroke-dasharray: 1 .8; }
.flow-edge--short.is-active { animation-name: flowmove-short; }
@keyframes flowmove-short { to { stroke-dashoffset: -9; } }

/* Labels de puissance sur les flèches actives */
.flow-label {
  position: absolute;
  transform: translate(-50%, -50%);
  background: rgba(255, 255, 255, 0.95);
  padding: 2px 8px;
  border-radius: 999px;
  /* Un cran plus grandes depuis que les pastilles ont quitté la scène (0.12.3) :
     ce sont désormais les seuls chiffres posés sur la photo, et il n'y a plus
     rien à côté de quoi elles doivent s'effacer. */
  font-size: .7rem;
  font-weight: 600;
  box-shadow: 0 1px 3px rgba(20,25,35,.15);
  z-index: 2;
  pointer-events: none;
  white-space: nowrap;
  opacity: 0;
  transition: opacity .3s;
}
.flow-label.is-active { opacity: 1; }
.flow-label span::after { content: ' kW'; font-weight: 400; font-size: .85em; color: var(--text-2); margin-left: 2px; }

/* ---------- Niveau de charge, peint dans la bande de la batterie ---------- */
/* Le SOC est peint là où il se lit sans légende : dans le PANNEAU SOMBRE du module
   du bas, que le rendu de la 0.13.1 a ajouté exprès pour lui. Les quatre bornes
   sont des mesures faites sur `maison-day.png` : la zone sombre va de x 33,20 à
   36,39 % et de y 68,16 à 75,98 %, relevée en balayant la luminance depuis son
   centre — pas à l'œil, et surtout pas arrondie « à ce qui va bien ». La barre
   remplit cette zone ENTIÈRE (un cheveu de marge, 0,1 %, pour rester dans le
   congé des angles) : elle est dédiée à ça, une barre plus étroite laisserait
   croire à un cadre qu'on n'a pas rempli. Avant, faute de bande, la barre était peinte sur la façade entière
   des modules — ce qui se lisait mal, un module vert ne ressemblant pas à une
   jauge.

   `--soc` est une FRACTION (0 → 1) et non un pourcentage : la hauteur est un
   pourcentage de la scène, et `calc(7.7% * 0.73)` se calcule, alors qu'un
   pourcentage multiplié par un pourcentage ne veut rien dire. À 0 %, la barre
   disparaît — c'est voulu : une batterie vide ne doit pas laisser un liseré vert
   qui se lirait comme un fond de charge.

   ⚠️ **La couleur est calculée à partir de `--soc`, elle n'est pas pilotée par le
   JS.** Une teinte posée en JavaScript demanderait de la recalculer partout où le
   SOC est écrit (premier rendu, rafraîchissement, tirer-pour-actualiser), et le
   jour où l'un des trois chemins serait oublié, la barre afficherait une hauteur
   juste dans une couleur fausse. Ici il n'y a qu'une source : la fraction.

   La rampe va du ROUGE sous 5 % au VERT FONCÉ au-delà de 90 %, et passe par
   l'orange et le jaune — la teinte suit le niveau linéairement entre les deux
   bornes, et la clarté descend avec elle (54 % → 32 %) pour que le vert de fin
   soit un vert foncé et non un vert vif. Les deux `clamp()` tiennent les bornes :
   au-dessous de 5 % on ne descend pas « sous le rouge » (une teinte négative
   repartirait vers le violet), au-dessus de 90 % on ne dépasse pas le vert (on
   virerait au cyan). La déclaration plate qui précède le dégradé est le repli des
   navigateurs sans `hsl()` moderne : sans elle, la barre serait invisible plutôt
   que d'une couleur approximative. */
.scene__bat {
  position: absolute;
  left: 33.30%; width: 3.05%;
  bottom: 24.10%;
  /* ⚠️ Un PLANCHER de visibilité, et il est assumé. La bande fait 17 px de haut
     sur un téléphone : 3 % de charge y feraient un demi-pixel, donc le rouge des
     batteries presque vides — précisément ce que cette bande doit crier — ne se
     verrait jamais. En dessous de 15 % la barre garde donc 15 % de la bande. Le
     zéro, lui, reste un vrai zéro : `min(1, --soc × 50)` retombe à 0 pour 0, et
     la barre disparaît complètement. Le pourcentage exact, lui, est écrit en
     toutes lettres dans la carte KPI juste dessous — la bande est un signal, pas
     un cadran. */
  --soc-vis: min(1, calc(var(--soc, 0) * 50));
  height: calc(7.70% * max(var(--soc, 0), calc(var(--soc-vis) * 0.15)));
  z-index: 1;
  border-radius: 2px;
  --soc-hue: clamp(0, calc((var(--soc, 0) - 0.05) * 152), 128);
  --soc-lit: calc(54% - var(--soc-hue) * 0.17%);
  background: #34C759;
  background: linear-gradient(180deg,
      hsl(var(--soc-hue) 84% calc(var(--soc-lit) + 9%)) 0%,
      hsl(var(--soc-hue) 80% var(--soc-lit)) 55%,
      hsl(var(--soc-hue) 86% calc(var(--soc-lit) - 8%)) 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.5),
              0 0 5px hsl(var(--soc-hue) 82% 46% / .55);
  transition: height .6s ease, background .6s ease, box-shadow .6s ease;
}

/* ---------- Heure du relevé, posée dans le ciel ---------- */
/* Seul repère de la scène depuis la 0.12.3, et il vient de l'en-tête : déplacé,
   pas dupliqué.

   Le voile n'est pas là pour rendre le texte lisible — mesuré sur les deux
   photos, à l'endroit exact où il se pose (y ≈ 5,5 %), le ciel y est un bleu
   PROFOND, pas le bleu clair de l'horizon : rgb(1, 94, 208) de jour et
   rgb(0, 35, 124) de nuit, soit du blanc à 5,95:1 et 13,7:1 sans rien. Il est là
   pour deux autres raisons : ancrer le repère comme élément d'interface plutôt
   que comme légende peinte dans l'image, et tenir si la photo change un jour pour
   une dont le haut serait clair. D'où un voile FAIBLE (0,32) : sur ce ciel-là il
   se voit à peine, et il monte quand même le contraste à ~9:1.

   Un fond clair, lui, était exclu : il ferait une tache lumineuse sur le ciel
   nocturne. */
.scene__stamp {
  position: absolute;
  bottom: 3.2%; left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  padding: 3px 11px;
  border-radius: 999px;
  /* ⚠️ Voile plus dense qu'au temps où le repère était dans le ciel (0,32) : il
     est descendu au bas du cadre en 0.13.0, la nouvelle photo n'ayant plus assez
     de ciel pour lui ET la rangée de chiffres. Le fond y est un dallage CLAIR —
     mesuré ~rgb(200,200,198) — où du blanc sur 0,32 ne donnait que 3,3:1. À 0,46
     le composite retombe à 4,9:1. C'est le seul endroit de la scène où le voile
     travaille vraiment pour la lisibilité. */
  background: rgba(10, 16, 30, .46);
  color: #fff;
  font-size: .66rem;
  font-weight: 500;
  white-space: nowrap;
  pointer-events: none;
}
.scene__stamp.is-syncing { opacity: .75; font-style: italic; }
/* Doublure du bandeau « Données figées » : quand la collecte s'est arrêtée,
   l'heure affichée EST l'information. */
.scene__stamp.is-stale { background: rgba(183, 28, 28, .62); }

/* ---------- Production et consommation, les chiffres en évidence ---------- */
/* Ce sont les mêmes étiquettes de flux que les autres (elles s'allument et se
   mettent à jour par `data-flow-label`), mais traitées en titre : posées dans le
   ciel, centrées sur l'axe du pignon, lisibles d'un coup d'œil. Celle de la
   consommation est passée par deux positions ratées — centrée sur sa flèche elle
   couvrait la porte d'entrée, sous la porte elle était perdue en bas de cadre.

   ⚠️ **Ici l'état inactif est `display: none`, pas `opacity: 0`.** Les deux cartes
   sont dans une rangée centrée : une carte transparente garderait sa place, et la
   nuit — PV à zéro — la consommation resterait décalée du pignon au lieu de se
   recentrer. C'est le seul endroit de la scène où un flux inactif quitte le flux
   de mise en page ; ailleurs l'opacité suffit et permet le fondu.

   `column-reverse` met l'intitulé AU-DESSUS de la valeur sans changer l'ordre du
   HTML : le premier enfant doit rester le `<span>` de la valeur, c'est lui que le
   JS remplit (`lbl.querySelector('span')`).

   Le fond sombre suit celui du repère d'heure, seul autre élément du ciel. */
/* ⚠️ **Une GRILLE à colonnes égales, pas une rangée flex.** Les cartes sont trois
   depuis la 0.12.9 (production, consommation, batterie) et leurs intitulés n'ont
   pas la même longueur : en flex, chacune prenait la largeur de son texte, et
   « Charge » finissait deux fois plus étroite que « Consommation ». Trois blocs
   d'aplomb inégal, posés côte à côte, se lisent comme trois choses de nature
   différente.

   `grid-auto-columns: 1fr` sur un conteneur de largeur automatique donne à
   TOUTES les colonnes la largeur de la plus exigeante : les cartes sont égales
   sans qu'on ait à leur inventer une largeur en dur, qui serait à reprendre au
   premier intitulé changé. Une carte inactive est en `display: none`, donc elle
   ne crée pas de colonne : à deux cartes, les deux se repartagent la place. */
.scene__headline {
  position: absolute;
  /* ⚠️ Ancrée par le HAUT, pas centrée sur une ligne. La hauteur d'une carte est
     fixée par sa police, donc sa part de la scène grandit quand l'écran rétrécit :
     centrée à 9,6 %, la rangée sortait du cadre par le haut sur un écran de
     320 px (bord supérieur à -0,2 %). Ancrée en haut, elle descend au lieu de se
     faire couper. */
  top: 1.8%; left: 51%;
  transform: translateX(-50%);
  z-index: 2;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  justify-items: stretch; align-items: stretch;
  gap: 6px;
  /* ⚠️ `width: max-content` n'est pas décoratif. La boîte est absolue et posée à
     `left: 55%` : sans largeur explicite, la place disponible s'arrête au bord
     droit du cadre, soit 45 % de la scène — les trois colonnes s'y écrasaient
     à 40 px et « Consommation » se retrouvait rogné. La largeur vient donc du
     contenu, et le plafond garde le bloc dans la photo : centré sur 55 %, 88 %
     de large met le bord droit à 99 %. */
  width: max-content;
  max-width: 88%;
  pointer-events: none;
}
.flow-label--big {
  position: static;
  transform: none;
  display: none;
  flex-direction: column-reverse;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 6px 10px;
  border-radius: 14px;
  background: rgba(10, 16, 30, .44);
  box-shadow: none;
  color: #fff;
  /* Le ciel de cette photo est mince (il descend à 11,5 % au faîtage) : la
     police suit un peu la largeur pour que la rangée n'aille pas mordre sur le
     toit des petits écrans. */
  font-size: clamp(.92rem, 4.3vw, 1.05rem);
  line-height: 1.05;
  min-width: 0;
}
.flow-label--big.is-active { display: flex; }
.flow-label--big b {
  font-size: .46em; font-weight: 600; letter-spacing: .01em;
}
.flow-label--pv b    { color: #F9D49B; }
.flow-label--house b { color: #A8CDF5; }
/* Le vert du câble de l'armoire, éclairci pour tenir sur le voile sombre — même
   traitement que les deux autres intitulés. */
.flow-label--bat b   { color: #A9E7BE; }
/* L'unité vient du `::after` commun ; sur fond sombre, sa couleur de texte
   secondaire serait illisible. */
.flow-label--big span::after { color: rgba(255,255,255,.72); font-size: .58em; }

/* ---------- Pas de pastilles sur la scène ---------- */
/* Il y en avait quatre, une par coin : PV, Réseau, Batterie, Conso. Retirées en
   0.12.3, et il faut savoir pourquoi avant d'en remettre.

   Elles affichaient les quatre chiffres que les cartes KPI donnent en gros juste
   dessous — et les mêmes que les étiquettes de flux, qui sont sur la même image :
   le relevé de 22 h 50 montrait « 94 W Réseau » en pastille et « 0,09 kW » sur la
   flèche du poteau, « 6,57 kW Conso » et « 6,57 kW » sur celle de la maison. Deux
   fois la même valeur, et jusqu'à 130 px de large pris sur une scène de 366 —
   de quoi recouvrir la ligne du réseau et le bas de la barre de charge.

   La scène ne garde donc que ce qu'elle seule sait montrer : le sens et la
   répartition des flux (dont la puissance batterie, qui n'est nulle part
   ailleurs) et le niveau de charge peint sur l'armoire. La photo tient la
   légende — les panneaux, le poteau, l'armoire et la maison sont dessus. */

/* ---------- KPI grid ---------- */
.kpi-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 16px;
}
.kpi {
  background: var(--card); border-radius: var(--radius); padding: 12px 8px;
  box-shadow: var(--shadow); text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
}
.kpi__icon {
  width: 32px; height: 32px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-size: 18px;
}
.kpi--pv       .kpi__icon { background: var(--pv-bg); color: var(--pv); }
.kpi--grid     .kpi__icon { background: var(--grid-bg); color: var(--grid); }
.kpi--house    .kpi__icon { background: var(--house-bg); color: var(--house); }
.kpi--battery  .kpi__icon { background: var(--bat-bg); color: var(--bat); }
.kpi__label { font-size: .72rem; color: var(--text-2); font-weight: 500; }
.kpi__value { font-size: .95rem; font-weight: 600; color: var(--text); line-height: 1.2; }
.kpi__value small { font-size: .7em; color: var(--text-2); font-weight: 400; }
.kpi__sub { font-size: .65rem; color: var(--muted); }

/* ---------- Today ---------- */
.today-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.today-grid--4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 480px) { .today-grid--4 { grid-template-columns: repeat(2, 1fr); } }

.last3-list {
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); overflow: hidden;
}
/* 5 colonnes doivent tenir sur un écran de téléphone : la colonne date est
   resserrée, les unités sont sorties des cellules vers le titre de section, et
   la charge/décharge batterie est empilée dans une seule colonne. */
/* minmax(0, 1fr) et non 1fr : `1fr` vaut `minmax(auto, 1fr)`, dont le minimum
   automatique est la largeur min-content de la cellule — ce qui empêche les
   colonnes de se comprimer et faisait déborder la 5e hors de l'écran. */
.last3-row {
  display: grid;
  grid-template-columns: 46px repeat(4, minmax(0, 1fr));
  gap: 3px;
  align-items: center;
  padding: 9px 8px;
  border-top: 1px solid var(--border);
  font-size: .8rem;
}
.last3-row > * { min-width: 0; overflow: hidden; }
.last3-row:first-child { border-top: none; }
.last3-row.is-header { background: var(--bg); font-size: .66rem; color: var(--text-2); font-weight: 500; text-transform: uppercase; padding: 7px 8px; letter-spacing: -.2px; }
.last3-row .l3-date { font-weight: 600; }
.last3-row .l3-val  { text-align: right; font-weight: 500; }
.last3-row .l3-val small { color: var(--text-2); font-weight: 400; margin-left: 2px; }
.last3-row.today-row { background: rgba(61,140,232,.06); }
.last3-row.is-header { padding: 7px 10px; }
.last3-row .l3-date { font-size: .78rem; }

/* Charge et décharge empilées dans une seule cellule. */
.l3-bat { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.25; }
.l3-bat b { font-weight: 600; font-size: .74rem; }
.l3-bat .up { color: var(--bat); }
.l3-bat .dn { color: #B87500; }
.l3-bat--none { color: var(--muted); }

.section-unit { font-size: .72rem; color: var(--muted); font-weight: 400; }

.last3-more {
  display: block; width: 100%; margin-top: 10px; padding: 10px;
  background: var(--card); color: var(--text-2);
  border: 1px solid var(--border-2); border-radius: var(--radius);
  font-size: .82rem; font-weight: 500; cursor: pointer;
}
.last3-more:disabled { opacity: .6; cursor: default; }
/* Tirer vers le bas pour actualiser.
   L'indicateur est posé juste au-dessus du viewport et descend avec le doigt. */
/* L'indicateur est posé DANS le conteneur qui se déplace, juste au-dessus :
   la page descend et le découvre, au lieu qu'un bandeau vienne recouvrir le
   contenu. C'est ce qui fait la différence avec une app native. */
.main { position: relative; }
/* Pendant le glissement, aucun délai : le contenu colle au doigt. La transition
   ne sert qu'au retour et au calage sous l'indicateur. */
.main.is-pull-animating { transition: transform .28s cubic-bezier(.2, .8, .3, 1); }

.ptr {
  position: absolute; top: -52px; left: 0; right: 0; height: 52px;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-size: .8rem; color: var(--text-2);
  pointer-events: none; opacity: 0;
  transition: opacity .18s ease;
}
.ptr.is-visible { opacity: 1; }
.ptr.is-ready .ptr__txt { color: var(--house); font-weight: 500; }
.ptr__spin {
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid var(--border-2); border-top-color: var(--house);
}
.ptr.is-loading .ptr__spin { animation: ptr-spin .8s linear infinite; }
@keyframes ptr-spin { to { transform: rotate(360deg); } }

/* Évite que le rebond natif du navigateur ne parasite le geste. */
body.app { overscroll-behavior-y: contain; }

/* Horodatage du dernier relevé, sous l'en-tête de l'Accueil. */
.home-head__sync {
  font-size: .75rem; color: var(--muted); margin-top: 2px;
}
.home-head__sync.is-syncing { opacity: .7; font-style: italic; }

.today {
  background: var(--card); border-radius: var(--radius); padding: 14px 8px;
  box-shadow: var(--shadow); text-align: center;
}
.today__label { font-size: .72rem; color: var(--text-2); }
.today__value { font-size: 1.05rem; font-weight: 600; margin-top: 4px; color: var(--text); }
.today__value small { font-size: .65em; color: var(--text-2); font-weight: 400; }

/* ---------- Flow page ---------- */
.flow-head { display: flex; justify-content: center; padding: 8px 0 16px; }
.tabs-inline {
  display: inline-flex; background: var(--card); border: 1px solid var(--border);
  border-radius: 999px; padding: 4px; gap: 2px;
}
.tabs-inline .tab {
  padding: 6px 14px; font-size: .8rem; border-radius: 999px;
  color: var(--text-2); font-weight: 500;
}
.tabs-inline .tab.is-active { background: var(--house); color: white; }

.flow-diagram-page {
  background: var(--card); border-radius: var(--radius-lg); padding: 16px;
  box-shadow: var(--shadow); position: relative;
}
.flow-diagram-page svg { width: 100%; max-width: 320px; height: auto; margin: 0 auto; display: block; }

/* Diagramme en losange (nouveau /flow) */
.flow-diamond {
  position: relative;
  background: var(--card);
  border-radius: var(--radius-lg);
  padding: 12px;
  box-shadow: var(--shadow);
  max-width: 380px;
  margin: 0 auto 12px;
}
.flow-diamond svg { width: 100%; height: auto; display: block; }
.fd-edges .edge {
  stroke-width: 1.4;
  stroke-dasharray: 4 3;
  stroke-linecap: round;
  opacity: 0;               /* invisible tant que non actif */
  transition: opacity .3s;
}
.fd-edges .edge.is-active {
  opacity: 1;
  animation: dashmove 1.4s linear infinite;
}
@keyframes dashmove { to { stroke-dashoffset: -14; } }

/* Masquer les items inactifs de la liste flux instantané */
.flow-list li.is-inactive { display: none; }

.fd-label {
  position: absolute;
  transform: translate(-50%, -50%);
  background: var(--card);
  border: 1px solid var(--border);
  padding: 2px 8px;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 600;
  box-shadow: 0 1px 3px rgba(20,25,35,.12);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s;
  white-space: nowrap;
  z-index: 2;
}
.fd-label.is-active { opacity: 1; }

.flow-list-card {
  background: var(--card); border-radius: var(--radius); margin-top: 12px;
  box-shadow: var(--shadow); overflow: hidden;
}
.flow-list-card__head { padding: 14px 16px 6px; font-weight: 600; font-size: .95rem; }
.flow-list { list-style: none; padding: 0; margin: 0; }
.flow-list li {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px; font-size: .88rem;
  border-top: 1px solid var(--border);
}
.flow-list li:first-child { border-top: none; }
.flow-list .lbl { display: flex; align-items: center; gap: 8px; color: var(--text-2); }
.flow-list .val { font-weight: 600; color: var(--text); }
.dot-o { width: 12px; height: 12px; border-radius: 50%; border: 2px solid; background: transparent; flex-shrink: 0; }
.dot-o--pv    { border-color: var(--pv); }
.dot-o--grid  { border-color: var(--grid); }
.dot-o--house { border-color: var(--house); }
.dot-o--bat   { border-color: var(--bat); }

/* ---------- History ---------- */
/* Les onglets se partagent la largeur tant qu'ils tiennent, et la barre défile
   au-delà : ajouter un onglet ne comprime plus les libellés des autres. */
/* Les onglets PASSENT À LA LIGNE, ils ne défilent pas.
   Sept onglets ne tiennent pas sur la largeur d'un téléphone, et la version
   défilante rendait les derniers (Contrat, Enedis, Base) inatteignables : rien
   n'indique qu'une barre se balaie, et le geste horizontal entrait en conflit
   avec le « tirer pour actualiser ». Une barre qui se replie sur deux lignes
   montre tout, sans geste à deviner. */
.tabs-line { display: flex; flex-wrap: wrap; gap: 6px; background: var(--card); padding: 4px; border-radius: 20px; border: 1px solid var(--border); margin-bottom: 14px; box-shadow: var(--shadow); }
.tabs-line .tab { flex: 1 0 auto; white-space: nowrap; text-align: center; padding: 8px 12px; border-radius: 999px; font-size: .82rem; font-weight: 500; color: var(--text-2); }
.tabs-line .tab.is-active { background: var(--text); color: white; }

.day-nav {
  display: flex; align-items: center; justify-content: space-between;
  background: var(--card); border-radius: var(--radius);
  padding: 10px 14px; margin-bottom: 12px; box-shadow: var(--shadow);
  font-size: .9rem; font-weight: 500;
}
.day-nav__arrow { color: var(--text-2); padding: 4px 12px; font-size: 1.2rem; text-decoration: none; border-radius: 6px; }
a.day-nav__arrow:hover { background: var(--border); color: var(--text); }

.chart-card {
  background: var(--card); border-radius: var(--radius);
  padding: 16px; margin-bottom: 12px; box-shadow: var(--shadow);
}
.chart-card__head {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 12px;
}
.chart-card__head h3 { font-size: .95rem; font-weight: 500; color: var(--text); }
.chart-card__value { font-size: 1.15rem; font-weight: 600; }
.chart-card__value small { font-size: .65em; color: var(--text-2); font-weight: 400; margin-left: 2px; }
.chart-card__canvas { position: relative; height: 140px; }
/* Sous la courbe : d'où vient le chiffre, ou pourquoi il manque. */
.chart-card__note { margin-top: 10px; font-size: .72rem; line-height: 1.35; color: var(--text-2); }
/* Deux valeurs (charge / décharge) dans le même en-tête : ni coupure, ni débord. */
#bat-total { font-size: 1rem; }
#bat-total span { white-space: nowrap; }

.exchange-card {
  background: var(--card); border-radius: var(--radius);
  padding: 16px; box-shadow: var(--shadow);
}
.exchange-card__head { font-size: .95rem; font-weight: 500; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: baseline; }
.exchange-card__head .val { font-weight: 600; font-size: 1rem; }
.exchange-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.exchange-item { display: flex; align-items: center; gap: 10px; }
.exchange-item__ico {
  width: 32px; height: 32px; border-radius: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--house); background: var(--house-bg); font-size: 18px;
}
.exchange-item__ico.up { color: var(--bat); background: var(--bat-bg); }
.exchange-item__lbl { font-size: .72rem; color: var(--text-2); }
.exchange-item__val { font-size: .95rem; font-weight: 600; }
.exchange-item__val small { color: var(--text-2); font-weight: 400; }

/* ---------- Battery page ---------- */
.battery-hero {
  display: flex; align-items: center; gap: 24px;
  background: var(--card); border-radius: var(--radius-lg);
  padding: 24px; box-shadow: var(--shadow); margin-bottom: 12px;
}
.battery-vis { flex-shrink: 0; }
.battery-vis__cap { width: 30px; height: 8px; background: #C4CBD3; border-radius: 3px 3px 0 0; margin: 0 auto; }
.battery-vis__body {
  width: 72px; height: 128px; border: 3px solid #C4CBD3;
  border-radius: 10px; overflow: hidden; position: relative;
  background: #F5F7FA;
}
.battery-vis__fill {
  position: absolute; bottom: 0; left: 0; right: 0;
  background: linear-gradient(180deg, #7DDE96 0%, #34C759 100%);
  transition: height .5s;
}
.battery-hero__stats { flex: 1; }
.battery-hero__soc { font-size: 2.6rem; font-weight: 600; color: var(--bat); line-height: 1; }
.battery-hero__soc span { font-size: 1.2rem; color: var(--muted); margin-left: 2px; }
.battery-hero__label { color: var(--text-2); font-size: .82rem; margin-top: 6px; }
.battery-hero__flow { font-size: 1.05rem; font-weight: 600; margin-top: 2px; }
.battery-hero__eta { color: var(--text-2); font-size: .82rem; margin-top: 10px; }
.battery-hero__eta b { color: var(--text); font-weight: 600; }

.stat-list {
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); overflow: hidden; margin-bottom: 12px;
}
.stat-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 16px; font-size: .88rem;
  border-top: 1px solid var(--border);
}
.stat-row:first-child { border-top: none; }
.stat-row .lbl { color: var(--text-2); }
.stat-row .val { font-weight: 500; }
.stat-row .val small { color: var(--text-2); font-weight: 400; }
.stat-row .chev { color: var(--muted); margin-left: 8px; }

.flux-list {
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); overflow: hidden; margin-bottom: 12px;
}
.flux-row {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 16px;
  border-top: 1px solid var(--border);
  font-size: .9rem;
}
.flux-row:first-child { border-top: none; }
.flux-row .arr { color: var(--text-2); font-size: 1.1rem; width: 20px; text-align: center; }
.flux-row .arr.down { color: var(--muted); }
.flux-row .arr.up { color: var(--bat); }
.flux-row .lbl { flex: 1; color: var(--text-2); }
.flux-row .val { font-weight: 600; }
.flux-row .val small { color: var(--text-2); font-weight: 400; }

.alarm-list { list-style: none; padding: 0; margin: 0; }
.alarm { padding: 12px 14px; background: rgba(255,59,48,.08); border-left: 3px solid var(--danger); margin-bottom: 6px; border-radius: 6px; }

/* Cartes d'alerte page Batterie */
.alerts-block { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.alert-card {
  display: flex; align-items: flex-start; gap: 12px;
  background: var(--card); border-radius: var(--radius);
  padding: 12px 14px; box-shadow: var(--shadow);
  border-left: 4px solid var(--muted);
}
.alert-card__ico { font-size: 22px; line-height: 1; flex-shrink: 0; }
.alert-card__body { flex: 1; }
.alert-card__title { font-size: .95rem; font-weight: 600; margin-bottom: 2px; }
.alert-card__msg { font-size: .82rem; color: var(--text-2); }
.alert-card--critical { border-left-color: var(--danger); background: rgba(255,59,48,.06); }
.alert-card--critical .alert-card__title { color: var(--danger); }
.alert-card--warning  { border-left-color: var(--pv); background: rgba(245,166,35,.06); }
.alert-card--warning  .alert-card__title { color: #B87500; }
.alert-card--info     { border-left-color: var(--house); background: rgba(61,140,232,.06); }

/* ---------- Retour au doigt ---------- */
/*
   Sur iPhone il n'y a pas de survol : sans état d'appui, un bouton ne dit rien au
   moment où on le touche. Et plusieurs écrans de cette app interrogent le cloud
   AVANT de s'afficher (Prévision relève la météo, Historique appelle une
   statistique par jour) : entre l'appui et la page suivante il peut s'écouler
   plusieurs secondes, pendant lesquelles rien ne bougeait. On doutait alors
   d'avoir touché au bon endroit, et on tapait deux fois.

   Trois signes distincts, du plus immédiat au plus large :

     1. **l'appui** — `:active`, peint tout de suite, sans JavaScript ;
     2. **l'attente** — `.is-pending` sur l'élément touché : sablier dans un
        bouton, pulsation dans la barre de navigation ;
     3. **la requête en cours** — une barre fine en haut de l'écran, qui dit que
        quelque chose est parti même si l'élément touché a défilé hors de vue.

   `touch-action: manipulation` retire le délai de 300 ms du double-tap : sans
   lui, l'état d'appui paraît en retard sur le doigt, ce qui est pire que rien.

   Le halo natif de WebKit est gardé mais adouci : le supprimer (la mode des
   resets) est précisément ce qui laisse un appui sans écho sur les liens.
*/
.btn, .bn, .icon-btn, .tab, .day-nav__arrow, .last3-more, button, a[href], summary {
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(61, 140, 232, .12);
}
.btn, .bn, .icon-btn, .tab, .day-nav__arrow, .last3-more {
  transition: transform .08s ease, opacity .12s ease, background-color .12s ease;
}
.bn svg { transition: transform .08s ease; }

/* 1. L'appui. Des amplitudes différentes selon la taille de la cible : un gros
      bouton se contente de 3 %, une flèche de navigation a besoin de plus pour
      que le mouvement se voie. */
.btn:active, .tab:active, .last3-more:active { transform: scale(.97); opacity: .85; }
.icon-btn:active            { transform: scale(.92); background: var(--border); }
.day-nav__arrow:active      { transform: scale(.85); opacity: .55; }
.bn:active                  { opacity: 1; }
.bn:active svg              { transform: scale(.86); }
/* Un bouton désactivé doit se voir comme tel, sinon son absence de réaction se
   lit comme une panne. */
.btn:disabled { opacity: .55; cursor: default; transform: none; }

/* 2. L'attente. `pointer-events: none` empêche le second appui d'impatience,
      qui posterait deux fois le même formulaire. */
.is-pending { pointer-events: none; }
.btn.is-pending { opacity: .8; }
.btn.is-pending::after {
  content: ''; display: inline-block; vertical-align: -2px; margin-left: 8px;
  width: 13px; height: 13px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: iconspin .8s linear infinite;
}
.icon-btn.is-pending svg { animation: iconspin 1s linear infinite; }
/* Dans la barre de navigation, pas de rotation : faire tourner une maison ou un
   soleil n'évoque pas l'attente. Une pulsation, si — et elle marche pour
   n'importe quelle icône. */
.bn.is-pending { color: var(--house); opacity: 1; animation: tap-pulse .9s ease-in-out infinite; }
@keyframes tap-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Tout autre lien touché qui mène ailleurs : même pulsation. Sans cette règle,
   un onglet ou un lien de bas de page n'aurait que la barre du haut pour dire
   qu'il a été pris en compte — et elle peut être hors de vue. */
.tab.is-pending, .day-nav__arrow.is-pending,
a.is-pending:not(.bn):not(.icon-btn):not(.btn) {
  animation: tap-pulse .9s ease-in-out infinite;
  color: var(--house);
}

/* 3. La requête en cours. Posée par le JS, retirée à l'arrivée de la page
      suivante (ou par sécurité au bout de quelques secondes). */
.nav-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  z-index: 100; pointer-events: none; overflow: hidden;
}
.nav-progress::before {
  content: ''; display: block; height: 100%; width: 38%;
  background: var(--house); border-radius: 0 3px 3px 0;
  animation: nav-slide 1.1s ease-in-out infinite;
}
@keyframes nav-slide { from { margin-left: -38%; } to { margin-left: 100%; } }

/* Mouvement réduit : on garde les changements de couleur et d'opacité, qui
   portent l'information, et on retire ce qui bouge. */
@media (prefers-reduced-motion: reduce) {
  .btn, .bn, .icon-btn, .tab, .day-nav__arrow, .last3-more, .bn svg { transition: none; }
  .btn:active, .tab:active, .last3-more:active,
  .icon-btn:active, .day-nav__arrow:active, .bn:active svg { transform: none; }
  .bn.is-pending, .icon-btn.is-pending svg, .tab.is-pending,
  .day-nav__arrow.is-pending, a.is-pending:not(.bn):not(.icon-btn):not(.btn),
  .btn.is-pending::after, .nav-progress::before { animation: none; }
  .nav-progress::before { width: 100%; opacity: .5; }
}

/* ---------- Bottom nav ---------- */
.bottom-nav {
  position: fixed; bottom: 0; left: 0; right: 0;
  display: flex; justify-content: space-around;
  background: var(--card); border-top: 1px solid var(--border);
  padding: 10px 0 max(10px, env(safe-area-inset-bottom));
  z-index: 20;
}
/* Lisibilité de la barre : icônes plus grandes et plus épaisses, libellés plus
   grands et en demi-gras, et un inactif plus contrasté que --text-2 qui se
   lisait mal sur fond clair. */
.bn {
  color: var(--text); opacity: .62;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  font-size: .73rem; font-weight: 500; padding: 4px 6px;
  /* Cinq entrées depuis l'arrivée de Prévision : elles se partagent la largeur
     à parts égales plutôt que de déborder sur un écran de 320 px. Le libellé ne
     se coupe pas — « Prév... » ne veut rien dire. */
  flex: 1 1 0; min-width: 0; white-space: nowrap;
}
.bn svg { width: 25px; height: 25px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.bn.is-active { color: var(--house); opacity: 1; font-weight: 600; }

/* ---------- Forms & buttons ---------- */
.form label { display: block; margin-bottom: 12px; font-size: .82rem; color: var(--text-2); }
.form input {
  display: block; width: 100%; padding: 12px 14px; margin-top: 6px;
  border: 1px solid var(--border-2); border-radius: 10px;
  background: var(--card); color: var(--text); font-size: .95rem;
}
.form input:focus { outline: 2px solid var(--house); outline-offset: -1px; border-color: var(--house); }
/* Le sélecteur de fichier natif ne se met pas au gabarit des autres champs. */
.form input[type="file"] { padding: 9px 10px; font-size: .82rem; }
.form input[type="file"]::file-selector-button {
  margin-right: 10px; padding: 6px 10px; border: 1px solid var(--border);
  border-radius: 8px; background: var(--bg); color: var(--text);
  font: inherit; font-size: .8rem; cursor: pointer;
}
.btn {
  display: inline-block; padding: 12px 24px; font-size: .95rem; font-weight: 500;
  border-radius: 10px; border: 1px solid var(--border-2); background: var(--card); color: var(--text);
  cursor: pointer; text-align: center;
}
.btn--primary { background: var(--house); color: white; border-color: var(--house); }
.btn--ghost   { background: transparent; color: var(--text-2); }
/* Actions en ligne dans une liste (gestion des comptes) : plusieurs boutons par
   ligne, donc compacts, et sans le padding vertical d'un bouton principal. */
.btn--sm      { padding: 6px 10px; font-size: .78rem; border-radius: 8px; }
.btn--danger  { color: var(--danger); border-color: rgba(255,59,48,.35); }

/* État des notifications : une phrase qui dit ce qui se passe, y compris quand
   rien n'est possible (onglet Safari sur iPhone, autorisation refusée). */
.push-state { font-size: .82rem; color: var(--text-2); margin: 10px 0 0; line-height: 1.45; }
.push-state--ok   { color: var(--bat); }
.push-state--warn { color: var(--danger); }

/* Lien d'invitation : affiché EN ENTIER, replié. Un <input> hors d'un .form garde
   sa largeur par défaut (~20 caractères) et coupe l'URL en plein milieu — un lien
   à moitié visible ne se vérifie pas et ne se copie pas à l'œil. */
.link-box { margin-top: 10px; }
.link-box code {
  display: block; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--bg); color: var(--text); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .72rem; line-height: 1.5; word-break: break-all; user-select: all;
}
.link-box__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.link-box__actions .btn { flex: 1 1 auto; text-decoration: none; }

/* `pre-line` : les messages de diagnostic sont multi-lignes (aperçu du fichier
   refusé, colonnes retenues) et doivent le rester à l'écran. */
.alert { padding: 12px 14px; border-radius: 10px; margin-bottom: 14px; font-size: .9rem; white-space: pre-line; overflow-wrap: anywhere; }
.alert--error   { background: rgba(255,59,48,.1); color: var(--danger); }
.alert--success { background: rgba(52,199,89,.1); color: var(--bat); }
/* Ni succès ni échec : ce que l'app a supposé d'un fichier importé. */
.alert--info    { background: rgba(61,140,232,.1); color: var(--house); font-size: .82rem; line-height: 1.45; }

/* ---------- Page Coûts ---------- */
.costs-list { background: var(--card); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); }
.costs-row {
  display: grid;
  grid-template-columns: 60px 1fr 1fr 1fr;
  align-items: center;
  padding: 10px 14px;
  border-top: 1px solid var(--border);
  font-size: .88rem;
}
.costs-row:first-child { border-top: none; }
.costs-row--head { background: var(--bg); color: var(--text-2); font-size: .72rem; font-weight: 500; text-transform: uppercase; }
.costs-row .c-month { font-weight: 600; }
.costs-row .c-val { text-align: right; }
.costs-row .c-val small { color: var(--text-2); font-weight: 400; margin-left: 2px; }

/* ---------- Table Enedis (Paramètres) ---------- */
/* Grille propre : les mois portent un libellé, pas un numéro comme page Coûts. */
.enedis-row {
  display: grid;
  grid-template-columns: 1.35fr 1fr .8fr .8fr;
  align-items: center;
  padding: 10px 14px;
  border-top: 1px solid var(--border);
  font-size: .85rem;
}
.enedis-row:first-child { border-top: none; }
.enedis-row--head { background: var(--bg); color: var(--text-2); font-size: .7rem; font-weight: 500; text-transform: uppercase; }
.enedis-row .e-month { font-weight: 600; }
.enedis-row .e-month small { font-weight: 400; margin-left: 3px; }
.enedis-row .e-val { text-align: right; }
.enedis-row .e-val small { color: var(--text-2); font-weight: 400; margin-left: 2px; }

.card { background: var(--card); border-radius: var(--radius); padding: 18px; margin-bottom: 12px; box-shadow: var(--shadow); }
.card h3 { margin-bottom: 10px; font-size: 1rem; }

/* ---------- Login ---------- */
.login { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.login__card {
  width: 100%; max-width: 400px; background: var(--card);
  border-radius: var(--radius-lg); padding: 32px 28px;
  box-shadow: var(--shadow-lg);
}
.login__logo { text-align: center; margin-bottom: 24px; }
/* Le badge ne porte plus ni fond ni rayon : le SVG du logo embarque les siens,
   ce qui en fait un bloc autonome, remplaçable sans toucher au CSS. */
.login__badge {
  width: 64px; height: 64px;
  display: inline-flex; align-items: center; justify-content: center;
  margin-bottom: 12px;
}
.login__badge svg {
  display: block;
  border-radius: 18px;
  box-shadow: 0 6px 16px rgba(245, 166, 35, .25);
}
.login__logo h1 { font-size: 1.35rem; margin: 8px 0 4px; }
.login__foot { text-align: center; margin-top: 16px; font-size: .78rem; color: var(--muted); }

.notfound { text-align: center; padding: 80px 20px; }
.notfound h1 { font-size: 5rem; color: var(--muted); margin-bottom: 12px; }

/* ==========================================================================
   Tablette et desktop
   --------------------------------------------------------------------------
   Tout ce qui suit est en `min-width` UNIQUEMENT, et rien au-dessus n'a été
   touché : le rendu téléphone est celui d'avant, au pixel. C'est la contrainte
   qui gouverne ce bloc — l'app se regarde d'abord depuis un raccourci d'écran
   d'accueil, et un « responsive » qui améliore le grand écran en dégradant le
   petit serait une régression.

   Trois paliers, chacun pour une raison :

   768 px  — la colonne s'élargit et la barre de navigation devient une pilule
             centrée. Étirée sur toute la largeur, ses cinq entrées finissaient
             à 300 px l'une de l'autre : une barre d'onglets ne se lit plus
             quand elle est plus large que le regard.
   1024 px — la navigation passe en RAIL LATÉRAL. Une barre collée en bas est un
             geste de pouce ; à la souris, c'est le coin le plus loin du curseur,
             et elle mange une bande de 92 px sur toute la largeur. Le body
             devient une grille à deux colonnes (rail | contenu) — d'où la classe
             `app--nav` posée par le layout : sans navigation (connexion,
             invitation) il n'y a pas de rail, et la carte doit rester centrée
             sur l'écran entier.
   1200 px — l'Accueil se met sur deux colonnes : la scène et les KPI à gauche,
             les listes à droite. En dessous, deux colonnes rendraient chacune
             trop étroite pour la scène.

   Une chose est perdue à partir de 1024 px et c'est assumé : la barre du haut
   n'est plus collante. Elle redevient un élément de grille, seul moyen d'aligner
   exactement son contenu sur la colonne (un `sticky` dans une grille ne colle
   qu'à l'intérieur de sa propre rangée). Le rail, lui, reste visible en
   permanence — donc la navigation ne dépend plus du défilement, et il ne reste
   dans cette barre que rafraîchir, réglages et déconnexion.
   ========================================================================== */

@media (min-width: 768px) {
  body { padding-bottom: 116px; }
  .main   { max-width: 660px; padding: 6px 24px 24px; }
  .topbar { max-width: 708px; margin: 0 auto; padding: 16px 24px 12px; }

  .bottom-nav {
    left: 50%; right: auto; transform: translateX(-50%);
    bottom: 18px; width: min(620px, calc(100% - 36px));
    padding: 10px 8px;
    border: 1px solid var(--border); border-radius: 22px;
    box-shadow: var(--shadow-lg);
  }
}

@media (min-width: 1024px) {
  /* Largeur du rail, écrite une fois : la barre du haut et le contenu s'y
     réfèrent, et un rail plus large ne demande qu'une valeur à changer. */
  :root { --rail: 236px; }

  body { padding-bottom: 0; }

  body.app--nav {
    display: grid;
    grid-template-columns: var(--rail) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    /* Deux rangées, et le rail qui les traverse : la barre du haut et le
       contenu ont besoin de cellules DISTINCTES. Dans une seule (« nav main »),
       les deux se superposent et le titre de l'écran passe sous la barre. */
    grid-template-areas:
      "nav top"
      "nav main";
    min-height: 100vh;
  }

  /* Barre du haut et contenu partagent la même largeur et le même centrage :
     c'est ce qui aligne la marque sur le bord gauche des cartes. */
  .app--nav > .topbar,
  .app--nav > .main {
    width: 100%; max-width: 880px;
    margin-inline: auto;
  }
  .app--nav > .topbar { grid-area: top;  position: static; padding: 22px 32px 6px; }
  .app--nav > .main   { grid-area: main; align-self: start; padding: 6px 32px 56px; }

  /* Le rail. `sticky` + `height: 100vh` : il reste en place quand la colonne de
     droite défile, sans passer en `fixed` — donc sans avoir à réserver sa
     largeur ailleurs, la grille s'en charge. */
  .app--nav > .bottom-nav {
    grid-area: nav;
    position: sticky; top: 0; align-self: start; height: 100vh;
    left: auto; right: auto; bottom: auto; transform: none; width: auto;
    flex-direction: column; justify-content: flex-start; gap: 4px;
    padding: 22px 12px;
    border: none; border-right: 1px solid var(--border); border-radius: 0;
    box-shadow: none;
  }
  /* Les entrées passent en lignes : à cette largeur, une icône au-dessus de son
     libellé laisse un rail à moitié vide et des cibles inutilement hautes. */
  .app--nav .bn {
    flex: 0 0 auto; flex-direction: row; justify-content: flex-start;
    gap: 12px; padding: 11px 14px; border-radius: 12px;
    font-size: .9rem; opacity: .75;
  }
  .app--nav .bn svg { width: 22px; height: 22px; }
  .app--nav .bn.is-active { background: var(--house-bg); opacity: 1; }

  /* La scène grandit jusqu'à ce que la HAUTEUR de l'écran l'arrête, et garde
     son cadrage : 693 × 520 px sur un portable là où elle faisait 540 × 405,
     soit deux tiers de surface en plus.

     La photo fait 1536 × 1365 depuis la 0.12.1 (elle était en 4/3, puis en 3/2) :
     ce format-là est presque carré, donc la scène remplit bien mieux la hauteur —
     et c'est justement ce qu'on lui demandait. Le raisonnement sur la largeur ne
     change pas : remplir un 16/9 demanderait un recadrage qui perd le poteau à
     droite et le carport à gauche, c'est-à-dire les deux organes que les flux
     désignent. C'est la composition qui est gardée, et la maison est simplement
     aussi grande que la hauteur le permet.

     ⚠️ **Le plafond de hauteur est écrit en LARGEUR, et ce n'est pas une
     coquetterie.** `max-height` posé sur une boîte qui a DÉJÀ `width: 100%`
     ne rétrécit pas la largeur : le format cède, la boîte devient plus large
     que son image, et alors les deux calques divergent — la photo est recadrée
     verticalement (`object-fit: cover`) pendant que le SVG des flux est écrasé
     (`preserveAspectRatio="none"`). Les flèches quittent les gaines. C'était le
     cas en 0.12.0 au-delà de ~1600 px de large : mesuré à 1920, la boîte
     sortait en 984 × 612 pour un format qui demandait 984 × 874, soit 20 %
     d'écart entre la photo et l'overlay.

     Le plafond est donc posé sur la largeur, converti par le format
     (74vh × 1,1253 ≈ 83,3vh) : la hauteur reste bornée, le format n'est jamais
     rompu, et `margin-inline: auto` centre la scène quand elle est plus étroite
     que sa colonne. Le `vh` sert toujours à ce pour quoi il était là — sur un
     24 pouces, une maison qui suit l'écran finirait plus haute que large.

     Rien ne se déforme alors : l'overlay des flux est un SVG dont le viewBox a
     le format de la photo, les pastilles, les étiquettes et le niveau de charge
     sont en pourcentages — tout suit la boîte, qui suit l'image. */
  .scene {
    aspect-ratio: 1536 / 1024;
    /* Le plafond de HAUTEUR s'écrit en LARGEUR : sur une boîte déjà en
       `width: 100%`, un `max-height` ne rétrécit pas la largeur, c'est le FORMAT
       qui cède — la photo est alors recadrée pendant que le SVG des flux est
       écrasé, et les flèches quittent les gaines sans que rien ne le signale.
       74vh de haut, en 3:2, font 111vh de large. */
    width: min(100%, 111vh, 900px);
    margin-inline: auto;
  }

  /* Les étiquettes de flux POSÉES SUR LA SCÈNE grandissent avec elle. Elles ont
     été dessinées pour une scène de 358 px de large sur un téléphone ; sur
     1 000 px, les mêmes tailles donnent des chiffres minuscules perdus au milieu
     de la photo — alors que ce sont eux qu'on vient lire. Deux paliers plutôt
     qu'une taille relative : la scène n'est pas une fraction fixe de la fenêtre
     (c'est une colonne de grille), donc un `vw` la suivrait mal. */
  .flow-label   { font-size: .95rem; padding: 3px 11px; }
  .flow-label--big { font-size: 1.55rem; padding: 8px 14px; border-radius: 16px; }
  .scene__headline { gap: 12px; }
  .scene__stamp { font-size: .85rem; padding: 4px 14px; }

  /* De la place gagnée : les chiffres et les courbes en profitent, plutôt que
     de rester au gabarit d'un écran de 375 px. */
  .kpi { padding: 16px 12px; }
  .kpi__value { font-size: 1.15rem; }
  .kpi__label { font-size: .8rem; }
  .kpi__sub   { font-size: .72rem; }
  .today { padding: 18px 12px; }
  .today__label { font-size: .8rem; }
  .today__value { font-size: 1.25rem; }
  /* Les courbes suivent la hauteur de l'écran : 220 px, c'est le gabarit d'un
     téléphone, et une courbe de 24 barres dans 220 px de haut sur un 27 pouces
     ne se lit pas mieux, elle est juste petite. */
  .chart-card__canvas { height: clamp(240px, 32vh, 420px); }
  .flow-diamond { max-width: 460px; }
  .flow-diagram-page svg { max-width: 420px; }
  .battery-hero { padding: 32px; gap: 36px; }

  /* Tableaux : la colonne de gauche n'a plus besoin d'être serrée à 46 px, et
     les valeurs cessent de flotter dans des colonnes trois fois trop larges. */
  .last3-row { grid-template-columns: 104px repeat(4, minmax(0, 1fr)); font-size: .85rem; padding: 11px 14px; }
  .last3-row.is-header { font-size: .7rem; padding: 9px 14px; }
  .last3-row .l3-date { font-size: .84rem; }
  .costs-row  { grid-template-columns: 110px repeat(3, minmax(0, 1fr)); font-size: .9rem; padding: 12px 16px; }
  .enedis-row { padding: 12px 16px; }
  .stat-row, .flux-row, .flow-list li { padding: 15px 18px; }
}

/* Le pointeur existe : on peut enfin donner un retour au survol. Le bloc
   « Retour au doigt » plus haut ne parle que de l'appui, parce que sur iPhone
   il n'y a pas de survol — ici c'est l'inverse, et un rail sans état de survol
   paraît mort. */
@media (hover: hover) and (min-width: 1024px) {
  .app--nav .bn:hover { background: var(--border); opacity: 1; }
  .app--nav .bn.is-active:hover { background: var(--house-bg); }
  .tabs-line .tab:hover:not(.is-active),
  .tabs-inline .tab:hover:not(.is-active) { background: var(--bg); color: var(--text); }
  .stat-list a.stat-row:hover { background: var(--bg); }
  .btn:hover:not(:disabled) { border-color: var(--muted); }
  .btn--primary:hover:not(:disabled) { filter: brightness(1.06); border-color: transparent; }
  .last3-more:hover:not(:disabled) { background: var(--bg); border-color: var(--muted); }
}

@media (min-width: 1200px) {
  /* ------------------------------------------------------------------------
     « S'il y a de la place, il faut la remplir. »
     ------------------------------------------------------------------------
     Deux façons de remplir, et une seule est bonne :

     - ÉTIRER les blocs. Une liste dont le libellé est à gauche et la valeur à
       droite devient illisible passé 900 px : les deux bouts d'une même ligne
       finissent à un mètre l'un de l'autre. Remplir comme ça, c'est gâcher.
     - METTRE LES BLOCS CÔTE À CÔTE. Les lignes gardent une largeur lisible, et
       l'écran se remplit quand même — en hauteur aussi, ce qui supprime la
       grande bande vide sous les cartes.

     C'est donc la seconde partout, avec deux exceptions qui, elles, gagnent
     vraiment à s'étirer : les GRAPHIQUES (une courbe n'a pas de « bout gauche »
     et de « bout droit », juste des points, et plus de place les sépare mieux)
     et la SCÈNE de l'Accueil.
     ------------------------------------------------------------------------ */

  /* Plus de plafond à 880 px : tous les écrans prennent la largeur. */
  .app--nav > .topbar,
  .app--nav > .main { max-width: 1760px; }

  /* Les écrans de suivi rangent leurs blocs sur deux colonnes. Chaque bloc est
     autonome (une carte, une liste, un tableau) : les mettre côte à côte ne
     casse aucune lecture, et le regard descend deux colonnes au lieu d'une.

     MULTI-COLONNES et non grille CSS, et c'est le point important : une grille
     range ses éléments en rangées, si bien qu'une carte courte à droite face à
     une longue à gauche laisse un vide au bas de sa colonne — le contraire du
     but. Le multi-colonnes, lui, ÉQUILIBRE les deux colonnes par leur hauteur.
     `break-inside: avoid` empêche une carte d'être coupée en deux, et
     `column-span: all` laisse traverser ce qui doit traverser. */
  .pv-page,
  .battery-page,
  .costs-page,
  .settings-page,
  .history-page,
  .forecast-page { columns: 2; column-gap: 26px; }

  .pv-page > *,
  .battery-page > *,
  .costs-page > *,
  .settings-page > *,
  .history-page > *,
  .forecast-page > * { break-inside: avoid; }

  /* Ce qui TRAVERSE les deux colonnes : ce qui titre l'écran, ce qui le pilote,
     ce qui alerte, et les blocs qui sont eux-mêmes un résumé de la page. */
  .pv-page > .home-head,       .pv-page > .alert,
  .battery-page > .alerts-block, .battery-page > .battery-hero,
  .costs-page > .page-head,    .costs-page > .alert,
  .settings-page > .page-head, .settings-page > .tabs-line, .settings-page > .alert,
  .history-page > .tabs-line,  .history-page > .day-nav,  .history-page > .alert,
  .forecast-page > .home-head, .forecast-page > .tabs-line,
  .forecast-page > .day-nav,   .forecast-page > .alert { column-span: all; }

  /* L'Accueil garde sa grille : ses deux colonnes sont portées par les
     enveloppes de la vue, donc déjà équilibrées, et elles sont ASYMÉTRIQUES —
     ce qu'un multi-colonnes ne sait pas faire. */
  .home > .home-head,
  .home > .alert { grid-column: 1 / -1; }

  /* Les cartes qui portent une COURBE traversent aussi : c'est le seul contenu
     qui profite vraiment de 1 600 px de large. `:has()` évite d'avoir à poser
     une classe dans les vues ; un navigateur qui ne le connaît pas laisse
     simplement la carte dans sa colonne — dégradation sans dégât. */
  .history-page  > .chart-card:has(canvas),
  .forecast-page > .chart-card:has(canvas) { column-span: all; }

  /* Les commandes ne gagnent rien à s'étirer : quatre onglets répartis sur
     1 620 px, c'est un balayage du regard pour changer de vue. */
  .history-page  > .tabs-line,
  .history-page  > .day-nav,
  .forecast-page > .tabs-line,
  .forecast-page > .day-nav {
    /* Largeur EXPLICITE et non un `max-width` + marges auto : sur un élément de
       grille, une marge auto mange l'espace libre et l'empêche de s'étirer — la
       barre se recroquevillait alors sur ses quatre mots. */
    width: min(780px, 100%);
    /* Marges auto et non `justify-self` : on n'est plus dans une grille ici. */
    margin-inline: auto;
  }

  /* ---- Accueil : la scène à gauche, les listes à droite ----
     Colonnes asymétriques et bornées : la colonne de droite ne descend pas sous
     380 px (en dessous, les listes se serrent), tout le reste va à la maison.
     Ce sont les deux enveloppes de la vue qui portent les colonnes — une grille
     dont chaque bloc est un élément partage ses rangées, et la première liste
     s'alignerait sur la hauteur de la scène en laissant un vide dessous. */
  .home {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(380px, .62fr);
    column-gap: 26px;
    align-items: start;
  }
  .home-side > .section:first-child { margin-top: 0; }
  /* Quatre tuiles dans une colonne de 420 px, ça fait 95 px chacune : les
     libellés y tiennent à peine. Deux par deux, elles respirent — la règle
     mobile équivalente ne peut pas servir ici, elle regarde la largeur de
     l'ÉCRAN, pas celle de la colonne. */
  .home-side .today-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Grands écrans : la scène dépasse les 800 px de large, ses étiquettes suivent. */
@media (min-width: 1600px) {
  .flow-label   { font-size: 1.1rem; padding: 4px 13px; }
  .flow-label--big { font-size: 1.9rem; padding: 10px 18px; border-radius: 18px; }
  .scene__headline { gap: 18px; }
  .scene__stamp { font-size: .95rem; padding: 5px 16px; }
}
