/* Interface Kepler - reprend la disposition et la charte sombre de l'outil de référence. */

:root {
  --bg: #1c1c1e;
  --panel: #2b2b2e;
  --panel-head: #3a3a3e;
  --border: #3f3f45;
  --text: #dcdcdc;
  --text-strong: #f2f2f2;
  --muted: #8b8b92;
  --topbar: #232326;

  --accent: #3d6da8;
  --accent-hover: #477cbd;
  --green: #23b07a;
  --red: #d9534f;
  --orange: #e8913c;

  /* Fonds des groupes de lignes de la vue d'empire. */
  --row-building: #2f3346;
  --row-defense: #2b3a30;
  --row-ship: #3a2f2f;
  --row-setting: #303034;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 13px/1.45 -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

a { color: #6ea8e0; text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- barre supérieure ---------- */

.topbar { background: var(--topbar); border-bottom: 1px solid #000; }

.topbar-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 0 20px;
  height: 46px;
  max-width: 1700px;
  margin: 0 auto;
}

.nav-link { color: var(--text); font-size: 14px; padding: 4px 2px; }
.nav-link:hover { color: #fff; text-decoration: none; }
.nav-link.active { color: #fff; font-weight: 600; }

.spacer { flex: 1; }

/* Les pages du robot, dans la barre du haut. Elles n'apparaissent qu'une fois
   un robot ouvert : hors de là elles ne désigneraient rien. */
.nav-bot { display: flex; align-items: center; gap: 18px; }
.nav-bot:not(:empty) { margin-left: 8px; }

/* Une entrée annoncée mais sans page derrière. Grisée et non cliquable :
   promettre un écran qui n'existe pas se lit comme une panne. */
.nav-link.inerte, .nav-bot .nav-link.inerte {
  color: var(--muted); cursor: default; pointer-events: none;
}

/* ---------- onglets ---------- */

.onglets { display: flex; gap: 4px; margin-bottom: 14px; border-bottom: 1px solid var(--border); }
.onglet {
  padding: 8px 16px; color: var(--text); font-size: 14px;
  border: 1px solid transparent; border-bottom: 0; border-radius: 3px 3px 0 0;
  margin-bottom: -1px;
}
.onglet:hover { background: #34343a; text-decoration: none; }
.onglet.actif {
  background: var(--panel); border-color: var(--border);
  color: var(--text-strong); font-weight: 600;
}

/* Le « + » qui ouvre une composition de plus.
   Rendu comme un onglet ordinaire, il passait pour un signe de ponctuation
   perdu au bout de la rangée : on ne voyait pas qu'il était le seul moyen d'en
   créer une. Un carré bordé, plus gros et plus clair, le sort du lot sans en
   faire un bouton d'action - il reste dans la rangée, à sa place. */
.onglet-plus {
  padding: 4px 12px; font-size: 20px; line-height: 22px; font-weight: 600;
  color: var(--text-strong); border: 1px dashed var(--border);
  border-bottom: 1px dashed var(--border); border-radius: 3px;
  align-self: center; margin-bottom: 0;
}
.onglet-plus:hover {
  background: var(--accent); border-color: var(--accent);
  border-style: solid; color: #fff;
}
.account { color: var(--text); font-size: 14px; }

.status-dot {
  width: 12px; height: 12px; border-radius: 50%;
  background: #6b6b72; flex: none;
}
.status-dot.running { background: var(--green); }
.status-dot.error   { background: var(--red); }
.status-dot.stopped { background: var(--orange); }
.status-dot.connecting { background: var(--orange); animation: pulse 1.2s ease-in-out infinite; }
/* La maintenance n'est pas une panne du compte : elle a sa couleur, calme,
   et non le rouge qui fait chercher ce qu'on a cassé. */
.status-dot.maintenance { background: #4a90d9; }
/* La veille non plus n'est pas une panne : sans sa propre regle, la pastille
   retombait au gris eteint et la carte au rouge, toutes les nuits. */
.status-dot.sleeping { background: #8a80c8; }
@keyframes pulse { 50% { opacity: .35; } }

/* ---------- disposition ---------- */

main { max-width: 1700px; margin: 0 auto; padding: 18px 20px 48px; }

/* ---------- pleine largeur ---------- */

/* LA BORNE DE 1700 PX EST DU CONFORT DE LECTURE, PAS UNE LIMITE TECHNIQUE.
   Une ligne de texte qui court sur 3400 px ne se lit plus : l'œil perd le
   début de la ligne suivante. Mais les tableaux - empire, vols, galaxie,
   journal - gagnent tout à s'étaler, et sur un grand écran on les regarde
   bien plus qu'on ne lit. D'où un choix, et non un défaut changé.

   LA BARRE DU HAUT SUIT, et ce n'est pas cosmétique : elle porte un
   « .spacer » qui pousse horloges, langue et compte contre son bord droit.
   Élargir le contenu sans elle laisserait le menu du compte planté au milieu
   de l'écran, à mille pixels du bord que le contenu vient d'atteindre.

   Demandé par MO le 15/09/2026. */
body.pleine-largeur main,
body.pleine-largeur .topbar-inner { max-width: none; }

/* La coche est posée par le CSS et non dans le libellé : traduireLaCoquille
   réécrit le texte de ce lien à chaque changement de langue, et un « ✓ »
   collé devant disparaîtrait à ce moment-là. */
body.pleine-largeur #pleine-largeur::after { content: " ✓"; }

.layout { display: flex; gap: 18px; align-items: flex-start; }
.sidebar { width: 235px; flex: none; }
.content { flex: 1; min-width: 0; }

/* ---------- menu latéral ---------- */

.menu {
  list-style: none; margin: 0 0 14px; padding: 0;
  background: var(--panel); border: 1px solid var(--border); border-radius: 3px; overflow: hidden;
}
.menu li + li { border-top: 1px solid var(--border); }

.menu-item {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px; color: var(--text); font-size: 14px;
}
.menu-item:hover { background: #34343a; text-decoration: none; }
.menu-item.active { background: var(--accent); color: #fff; }

/* Le tiroir « Outils » : un chevron, pas un interrupteur. Il pivote d'un
   quart de tour à l'ouverture, ce qui dit d'un coup d'œil si le tiroir est
   fermé ou déplié. */
.menu-item.tiroir { cursor: pointer; user-select: none; }
.menu-item.tiroir.dedans { color: var(--accent); }
.chevron { flex: none; opacity: .65; transition: transform .15s; display: inline-block; }
.chevron.ouvert { transform: rotate(90deg); }
/* Celui du menu porte tout le sens du tiroir : à la taille du texte, le
   « › » se voyait à peine. Interligne à 1 pour qu'il grossisse sans
   épaissir la ligne du menu. */
.menu-item .chevron { font-size: 26px; line-height: 1; opacity: .85; }
.menu li.sous + li.sous { border-top: none; }
.menu-item.sous { padding-left: 30px; font-size: 13px; opacity: .92; }
.menu-item.sous.active { opacity: 1; }

.toggle { width: 30px; height: 15px; border-radius: 8px; background: #55555c; position: relative; flex: none; }
.toggle.on { background: var(--green); }
.toggle::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 11px; height: 11px; border-radius: 50%; background: #ddd; transition: left .15s;
}
.toggle.on::after { left: 17px; }

/* Décoché, mais il tourne encore.
   Arrêter un worker attend la fin de sa passe en cours, et une passe de
   scanner balaie des milliers de systèmes : entre le clic et le silence, il
   peut se passer des minutes pendant lesquelles le robot agit toujours.
   L'écran ne montrait alors que l'intention, jamais le fait - un client a
   décoché six workers le 05/09/2026 et les a crus éteints pendant une demi-
   heure. */
.toggle.arret-en-cours { background: var(--orange); }

/* Allumé, mais il attend : le ravitaillement coché pendant que la
   Construction est éteinte. Rien ne part (25/09/2026). La teinte du témoin
   « Modifications non enregistrées », pas le vert : un vert affirmerait que
   des livraisons partent. */
.toggle.on.en-attente { background: #e0b96c; }

/* Le témoin du tiroir « Outils », replié.
   Un point et non un interrupteur : cette ligne ouvre un tiroir, elle ne
   commande rien, et un interrupteur y promettrait une bascule qui n'existe
   pas. */
.point-actif {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--green); margin-left: auto; margin-right: 6px;
}

/* ---------- panneaux ---------- */

h1 { font-size: 26px; font-weight: 400; margin: 0 0 16px; color: var(--text-strong); }
h2 { font-size: 15px; font-weight: 600; margin: 0 0 10px; color: var(--text-strong); }

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 3px;
  margin-bottom: 16px;
}
.panel-head {
  background: var(--panel-head);
  padding: 10px 14px;
  font-size: 13px;
  color: var(--text-strong);
  border-bottom: 1px solid var(--border);
}
.panel-body { padding: 12px 14px; }

.muted { color: var(--muted); }

/* Le titre d'un champ se lit en clair.
 *
 * C'est ce qu'on cherche des yeux en parcourant un formulaire : le gris le
 * mettait au meme rang que les explications qui le suivent, et l'oeil devait
 * s'arreter sur chaque ligne pour trouver ou saisir. Le gris reste a ce qui
 * commente - l'aide sous le champ, les valeurs secondaires. */
.champ-titre { color: var(--text); font-size: 12px; margin-bottom: 6px; }
.empty { color: var(--muted); padding: 22px; text-align: center; }

.panel.warn { border-color: #6b5a2a; background: #38311c; }

/* Le rouge vif est reserve a une seule chose : une echeance apres laquelle
 * les robots s'arretent.
 *
 * Il n'est pas la pour signaler une erreur - rien n'est casse - mais pour
 * etre impossible a manquer par quelqu'un qui traverse l'interface sans
 * lire. C'est le seul bandeau de ce ton dans tout le bot, et il ne doit le
 * rester : deux rouges dans une page et ni l'un ni l'autre ne se voit. */
.panel.urgent {
  border: 2px solid var(--red);
  background: #4a1b1a;
}
.panel.urgent strong { color: #ffdedc; }

/* ---------- boutons ---------- */

.btn-start, .btn-stop, .btn-neutral, .btn-warn {
  border: 0; border-radius: 3px; color: #fff;
  padding: 8px 15px; font-size: 13px; cursor: pointer;
}
.btn-start { background: var(--green); }
.btn-stop  { background: var(--red); }
.btn-neutral { background: #4a4a51; }
.btn-warn { background: var(--orange); }
.btn-start:hover, .btn-stop:hover, .btn-neutral:hover, .btn-warn:hover { filter: brightness(1.12); }

/* ---------- contrôles de formulaire ---------- */

/* Les champs héritaient du thème clair du navigateur, blancs au milieu d'une
   page sombre. La règle est globale plutôt que par formulaire : chaque
   nouveau réglage aurait sinon eu à la redemander.
   Les mots de passe y manquaient, et restaient blancs sur tous les écrans -
   connexion, Mon compte, double authentification (01/10/2026). */
input[type=text], input[type=number], input[type=password], select, textarea {
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: 3px;
  font-size: 13px;
  font-family: inherit;
  background: #232326;
  color: var(--text);
}
input[type=text]:focus, input[type=number]:focus, input[type=password]:focus, select:focus {
  outline: 1px solid var(--accent);
}

/* ---------- tableaux ---------- */

table { border-collapse: collapse; width: 100%; }
th, td { padding: 5px 9px; text-align: left; white-space: nowrap; }
thead th { font-weight: 600; border-bottom: 1px solid var(--border); color: var(--text-strong); }
tbody tr:nth-child(even) { background: rgba(255,255,255,.02); }

.scroll-x { overflow-x: auto; }
.coord { font-family: Consolas, "Liberation Mono", monospace; font-size: 12px; }
.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Étapes numérotées d'une marche à suivre.
   Le numéro et son titre sur la même ligne : sans flex, le div du texte étant
   de niveau bloc, il passait à la ligne et le numéro se retrouvait seul
   au-dessus. La puce ne rétrécit pas et le texte prend le reste. */
.etape { display: flex; gap: 10px; align-items: baseline; margin-bottom: 12px; }
.etape:last-of-type { margin-bottom: 0; }
.etape > .etape-num {
  flex: 0 0 22px;
  height: 22px;
  line-height: 22px;
  text-align: center;
  border-radius: 50%;
  background: var(--border);
  color: var(--text-strong);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.etape > div { flex: 1; min-width: 0; }

/* ---------- vue d'empire ---------- */

.empire { display: flex; gap: 16px; align-items: flex-start; }
.empire-side { width: 250px; flex: none; }
.empire-side .player { font-weight: 600; margin-bottom: 8px; color: var(--text-strong); }
.empire-side td { padding: 2px 8px; font-size: 12px; }
.empire-side td:last-child { text-align: right; font-variant-numeric: tabular-nums; }

.empire-tabs { display: flex; gap: 8px; margin-bottom: 10px; }
.empire-tabs a.active { color: var(--text-strong); font-weight: 600; }

.empire-table th { border-bottom: 1px solid var(--border); vertical-align: bottom; }
.empire-table th.celestial { text-align: center; min-width: 96px; }
.empire-table th.celestial .name { display: block; color: #6ea8e0; font-weight: 600; }
.empire-table th.celestial .coord { display: block; color: var(--muted); font-weight: 400; }
.empire-table td.label { font-size: 12px; }
.empire-table td.num { min-width: 84px; }

/* La colonne des totaux est en tête, comme sur l'outil de référence : c'est la valeur
   qu'on lit en premier, avant de comparer céleste par céleste. */
.empire-table .total { font-weight: 600; border-right: 1px solid var(--border); }

.empire-table tbody tr { background: none; }
.empire-table tr.g-resource td { background: #2b2b2e; }
.empire-table tr.g-building td { background: var(--row-building); }
.empire-table tr.g-defense  td { background: var(--row-defense); }
.empire-table tr.g-ship     td { background: var(--row-ship); }
.empire-table tr.g-setting  td { background: var(--row-setting); }

.group-head td {
  background: var(--panel-head) !important;
  cursor: pointer;
  font-weight: 600;
  color: var(--text-strong);
}
.group-head td:hover { filter: brightness(1.15); }

/* ---------- journaux ---------- */

.logs { font-family: Consolas, "Liberation Mono", monospace; font-size: 12px; }
.logs .ts { color: var(--muted); }
.logs thead th { font-weight: 400; color: var(--muted); padding-top: 0; }
.logs .lvl { font-weight: 600; }
.logs .INFO { color: #5b9bd5; }
.logs .WARN { color: #d8a13a; }
.logs .ERROR { color: #e06c6c; }
.logs .DEBUG { color: var(--muted); }

.filters { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.filters select, .filters input[type=text] {
  padding: 5px 8px; border: 1px solid var(--border); border-radius: 3px;
  font-size: 13px; background: #232326; color: var(--text);
}
.filters label { display: flex; align-items: center; gap: 5px; }

/* ---------- cartes de robots ---------- */

.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 16px; }

/* La teinte de la carte donne l'état d'un coup d'œil, comme sur l'outil de référence :
   rouge à l'arrêt, vert en marche. */
.card {
  border-radius: 3px; padding: 16px 18px;
  background: #6b3634; border: 1px solid rgba(0, 0, 0, .25);
}
.card.running { background: #2c5c46; }
.card.connecting { background: #6b5730; }
.card.maintenance { background: #2f4a63; }
.card.sleeping { background: #3b3652; }
.card .attente { color: #d6e6f5; font-size: 12px; margin-bottom: 10px; word-break: break-word; }
.card .title { display: flex; align-items: center; gap: 9px; font-size: 17px; margin-bottom: 12px; color: #fff; }
.card .avatar {
  width: 26px; height: 26px; border-radius: 50%; background: #2f4f7a;
  display: flex; align-items: center; justify-content: center; font-size: 12px; color: #fff;
}
.card dl { display: grid; grid-template-columns: 90px 1fr; gap: 4px 8px; margin: 0 0 14px; font-size: 13px; }
.card dt { color: rgba(255,255,255,.75); }
.card dd { margin: 0; color: #fff; }
.card .foot { display: flex; align-items: center; gap: 10px; }
/* La pastille pousse le bouton à droite : une seule chose à gauche, une
   seule à droite. */
.card .foot .status-dot { flex: 1 0 12px; max-width: 12px; }
.card .err { color: #ffd7d7; font-size: 12px; margin-bottom: 10px; word-break: break-word; }

/* ---------- éditeur de scripts ---------- */

/* La gouttiere des numeros de ligne, et le champ, cote a cote.
 *
 * LES DEUX PARTAGENT POLICE, TAILLE, INTERLIGNE ET MARGE HAUTE, au pixel : le
 * moindre ecart et le numero 40 ne designe plus la ligne 40, ce qui est pire
 * que pas de numeros du tout.
 *
 * Et le champ ne renvoie plus a la ligne tout seul (white-space: pre). Sans
 * ca une ligne longue en occuperait deux a l'ecran pour un seul numero, et
 * tout ce qui suit serait decale. C'est aussi ce que fait n'importe quel
 * editeur de code : une ligne trop longue se fait defiler, elle ne se plie
 * pas. */
.code-editeur { display: flex; align-items: stretch; background: #202024; }
.code-lignes {
  flex: 0 0 auto; padding: 12px 8px 12px 12px; text-align: right;
  font-family: Consolas, "Liberation Mono", monospace; font-size: 12.5px; line-height: 1.5;
  color: #6b6b73; background: #1b1b1f; white-space: pre; overflow: hidden;
  user-select: none; -webkit-user-select: none;
}
textarea.code {
  flex: 1 1 auto; min-width: 0;
  min-height: 470px; border: 0; padding: 12px 14px; resize: vertical;
  font-family: Consolas, "Liberation Mono", monospace; font-size: 12.5px; line-height: 1.5;
  tab-size: 4; color: #d8d8d8; background: #202024;
  white-space: pre; overflow-x: auto;
}
textarea.code:focus { outline: 2px solid var(--accent); outline-offset: -2px; }

.script-console {
  max-height: 230px; overflow-y: auto; padding: 10px 12px;
  font-family: Consolas, "Liberation Mono", monospace; font-size: 12px;
  white-space: pre-wrap; background: #202024;
}

/* L'atelier : le code a gauche, sa sortie a droite.
 *
 * Les deux colonnes s'etirent a la meme hauteur, celle du champ de code, et
 * la console prend ce qui lui reste apres son titre - elle defile a
 * l'interieur plutot que d'allonger la page.
 *
 * En dessous de 900 px elles s'empilent : deux colonnes de code sur un
 * telephone ne se lisent ni l'une ni l'autre. */
.script-atelier { display: flex; gap: 14px; align-items: stretch; margin-bottom: 16px; }
.script-atelier > .script-code { flex: 3 1 0; min-width: 0; display: flex; }
/* Le champ de code remplit sa colonne : quand c'est la console qui donne sa
 * hauteur a la rangee, sans ca le code s'arrete plus haut qu'elle. */
.script-atelier > .script-code > .panel { flex: 1 1 auto; min-height: 0; }
.script-atelier > .script-sortie { flex: 2 1 0; min-width: 0; display: flex; flex-direction: column; }
/* Les marges basses des deux panneaux passent sur la rangee : sans ca l'une
 * des colonnes descend plus bas que l'autre, ce qui se voit tout de suite. */
.script-atelier .panel { margin-bottom: 0; }
.script-atelier > .script-sortie > .script-console { flex: 1 1 auto; max-height: none; min-height: 0; }
@media (max-width: 900px) {
  .script-atelier { flex-direction: column; }
  .script-atelier > .script-sortie > .script-console { flex: none; max-height: 230px; }
}

/* L'ATELIER PREND TOUT L'ECRAN, en largeur comme en hauteur.
 *
 * MO, le 26/09/2026 : beaucoup d'espace perdu. Sur son grand ecran la page
 * restait dans ses 1700 px, le code tenait dans un tiers de la largeur, et le
 * champ s'arretait a mi-hauteur. La borne de 1700 px sert la lecture d'un
 * texte (voir « pleine largeur ») ; du code se lit ligne a ligne, et une
 * ligne longue gagne a ne pas defiler. La barre du haut suit, pour la meme
 * raison que dans la pleine largeur.
 *
 * Le champ descend jusqu'au bas de la fenetre, jamais sous ses 470 px. Les
 * 260 px retires sont ce qui le precede - barre, titre, boutons - et la marge
 * basse de la page : la console, etiree a sa hauteur, finit au meme endroit.
 * Au-dessus de 900 px seulement : en dessous la console passe sous le code,
 * et un champ de la hauteur de l'ecran la pousserait hors de vue. */
main:has(.script-atelier),
body:has(.script-atelier) .topbar-inner { max-width: none; }
@media (min-width: 901px) {
  .script-atelier textarea.code { min-height: max(470px, calc(100vh - 260px)); }
}

/* ---------- Brain ---------- */

.brain-planet .panel-head { display: flex; align-items: center; gap: 10px; }
.brain-planet .panel-head .nom { color: #6ea8e0; font-weight: 600; }
.brain-planet .panel-head .grow { flex: 1; }

/* Une file vide se lit mieux ainsi qu'avec un tableau à zéro ligne. */
.brain-queue { list-style: none; margin: 8px 0 0; padding: 0; }
.brain-queue li {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 8px; border: 1px solid var(--border); border-radius: 3px;
  background: #232326; margin-bottom: 5px;
}
.brain-queue .rang { color: var(--muted); width: 22px; }
.brain-queue .nom { flex: 1; }
.brain-queue button { padding: 2px 8px; font-size: 12px; }

/* Le plan se lit comme un tableau de bord : les investissements - ce que le
   planificateur ajoute de son propre chef - doivent sauter aux yeux. */
.plan-table { font-size: 12px; }
.plan-table tr.invest td { background: #2f3346; }
/* La fenetre glissante des 24 heures, posee au-dessus des journees.
   Elle repond a une AUTRE question que la ligne du jour - aujourd hui
   commence a minuit, donc a 9 h il ne couvre que neuf heures - et doit
   donc se distinguer au premier regard. */
.plan-table tr.butin-fenetre td { font-weight: 600; border-bottom: 1px solid var(--border); }
.plan-table tr.butin-total td { font-weight: 600; border-top: 1px solid var(--border); }
/* Les chiffres de la période, en tête de l'onglet Butin. */
.butin-chiffres { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 14px; }
.butin-chiffre .valeur { font-size: 20px; font-weight: 600; color: var(--text-strong); margin: 2px 0; }
.plan-table th.triable { cursor: pointer; user-select: none; }
.plan-table th.triable:hover { color: var(--text-strong); }
.plan-table tr.invest td.label::before { content: "＋ "; color: var(--orange); }
.plan-table .total-row td { font-weight: 600; border-top: 1px solid var(--border); }

/* La liste des positions de départ d'une composition multi.
   Sans largeur, elle s'étalait sur toute la page : trois colonnes courtes
   séparées par des vides, et l'œil ne rattachait plus une case à sa ligne. */
.table-origines { width: auto; min-width: 340px; max-width: 520px; }
.table-origines td, .table-origines th { padding-right: 14px; }
.table-origines tr.muted td { color: var(--muted); }
.table-origines .btn-neutral { padding: 1px 8px; margin-left: 4px; }

/* ---------- Scanner ---------- */

/* L'état d'une cible se lit d'un coup d'œil : c'est ce qui décide si on
   l'attaque ou si on l'évite. */
.tag {
  display: inline-block; padding: 1px 6px; margin-right: 4px;
  border-radius: 3px; font-size: 11px; background: #45454d; color: var(--text);
}
.tag.inactive { background: #2c5c46; color: #cfe9dd; }
.tag.vacation { background: #3d4a6b; color: #ccd6ee; }
.tag.banned   { background: #6b3634; color: #f0d0d0; }
.tag.strong   { background: #6b5730; color: #f0e0c0; }

/* ---------- fenêtre modale ---------- */

/* Ajouter un robot demande une demi-douzaine de champs qu'on relit avant de
   valider. Une suite de questions ne le permet pas. */
.modal {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(0, 0, 0, .6);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 50px 16px;
}
.modal .panel { margin: 0; }
.modal input[type=text], .modal input[type=password] { width: 100%; }

/* ---------- défi Gameforge ---------- */

/* Les images arrivent telles que Gameforge les envoie, à une taille qui n'est
   pas la nôtre : la largeur est imposée, la hauteur suit. */
.captcha-img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--border); border-radius: 4px;
  margin-bottom: 8px;
}

/* La consigne est un PNG transparent, écrit en clair : Gameforge la destine à
   son lobby sombre. Posée sur du blanc, elle est invisible - texte blanc sur
   fond blanc. D'où ce fond sombre.
   Les icônes, elles, portent leur propre fond opaque : peu importe. */
.captcha-img.question { background: #2a2d33; }
.captcha-img.icons { background: #fff; }

/* Rien ne garantit que toutes les langues écrivent en clair. Un fond
   basculable coûte une ligne et évite de rester bloqué devant une consigne
   illisible, alors que le défi expire en cinq minutes. */
.captcha-img.question.fond-clair { background: #fff; }

/* Les quatre icônes sont côte à côte dans une seule image : les boutons se
   répartissent la même largeur, chacun sous la sienne. */
.captcha-choix-rangée { display: flex; gap: 6px; }
.captcha-choix { flex: 1; }

/* ---------- écran de connexion ---------- */

/* Seul écran visible sans session. Centré et nu : il n'y a rien d'autre à
   faire à ce moment-là que se connecter. */
.login-wrap {
  display: flex; justify-content: center; align-items: flex-start;
  padding-top: 60px;
}
.login-wrap input[type=text], .login-wrap input[type=password] { width: 100%; }

.user-menu { color: var(--muted); font-size: 13px; cursor: pointer; }
.user-menu:hover { color: #fff; text-decoration: none; }

/* Menu du compte d'accès. Il s'ouvre au clic et non au survol : ses entrées
   mènent aux réglages et à la déconnexion, on ne veut pas les frôler. */
.menu-compte { position: relative; }
.menu-deroulant {
  position: absolute; right: 0; top: 26px; z-index: 40; min-width: 190px;
  background: var(--panel); border: 1px solid var(--border); border-radius: 3px;
  box-shadow: 0 6px 18px rgba(0,0,0,.45); overflow: hidden;
}
.menu-deroulant a {
  display: block; padding: 9px 14px; color: var(--text); font-size: 13px;
  border-bottom: 1px solid var(--border);
}
.menu-deroulant a:last-child { border-bottom: 0; }
/* Une rangee de commandes : des boutons cote a cote, avec de l'air entre eux.
 *
 * La classe existait et etait utilisee a six endroits, sans aucune regle
 * derriere : le « gap » pose a cote ne s'appliquait pas, faute de conteneur
 * flex, et les boutons se touchaient. Un appelant avait deja contourne le
 * probleme en ecrivant display:flex a la main - le signe qu'il manquait ici. */
.rang { display: flex; align-items: center; gap: 8px; }

/* LE CHOIX SOUS LE NOM DU WORKER, et non a cote.
 *
 * Damien, le 16/09/2026 : « c'est un peu chiant que ca decale tout, le choix
 * devrait peut-etre se mettre juste en dessous du nom du worker ». Il avait
 * raison : mis DANS la rangee, le champ des campagnes poussait « Allumage »
 * de 150 px sur les seules lignes qui en ont une, et plus aucune date ne
 * tombait sous la precedente. Une colonne de dates en escalier ne se lit pas.
 *
 * UNE GRILLE, ET NON DES RANGEES INDEPENDANTES. C'est le point qui decide
 * tout : dans une grille, la premiere colonne prend la largeur de la PLUS
 * LARGE des cellules, toutes lignes confondues. Elles font donc toutes la
 * meme largeur sans qu'on ait a la fixer, et cette largeur suit la langue.
 *
 * Avec des rangees flex, chaque ligne se mesurait seule : il fallait alors
 * choisir entre laisser le bouton elargir sa ligne - et recreer le decalage
 * qu'on venait d'enlever - ou le rogner. Mesure sur place : « Aucune campagne
 * definie » depassait de 7 px la largeur du select en francais, et de bien
 * plus en allemand, ou la meme phrase se dit « Keine Zusammenstellung
 * definiert ». La grille rend les deux inutiles : la colonne s'elargit pour
 * la phrase la plus longue, et les cinq lignes la suivent ensemble.
 *
 * display:contents sur la ligne : ses deux cellules deviennent les cases de
 * la grille du dessus. Sans lui, la ligne serait UNE case et on n'aurait rien
 * aligne du tout. */
.lignes-plages {
  display: grid; grid-template-columns: max-content 1fr;
  gap: 10px 8px; align-items: start;
}
.lignes-plages > .plage { display: contents; }
.lignes-plages > .plage-vide { grid-column: 1 / -1; }

/* Un bord de plage se replie d'un seul tenant : son libellé ne part jamais
   seul en bout de ligne. Voir lignePlage dans app.js. */
.bord-plage { display: inline-flex; align-items: center; gap: 6px; }

.cellule-worker { display: flex; flex-direction: column; gap: 3px; }
.cellule-worker > select,
.cellule-worker > .sous-worker { width: 100%; }
.cellule-worker > .sous-worker { display: flex; }
.cellule-worker > .sous-worker > * { width: 100%; min-width: 0; }
.cellule-worker > .sous-worker button,
.cellule-worker > .sous-worker select { width: 100%; }

/* A l'etroit, les deux cellules s'empilent au lieu de s'ecraser : sur un
 * telephone, une colonne de 190 px prise par le worker ne laisserait pas de
 * quoi poser deux dates et deux heures a cote. */
@media (max-width: 780px) {
  .lignes-plages { display: block; }
  .lignes-plages > .plage { display: block; margin-top: 12px; }
}

.rang-robot {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 0; border-bottom: 1px solid var(--border);
}
.rang-robot:last-child { border-bottom: 0; }
.rang-robot button { padding: 3px 10px; }
.rang-robot button:disabled { opacity: .35; cursor: default; }

.menu-deroulant a:hover { background: #34343a; text-decoration: none; color: #fff; }

/* Le selecteur de robot. Meme mecanique que le menu du compte plus bas : il
   s'ouvre au CLIC et non au survol, parce qu'on ne veut pas basculer de robot
   en frolant son nom en allant vers autre chose. */
.menu-robot { position: relative; }
a.account { text-decoration: none; cursor: pointer; }
a.account:hover { color: var(--text-strong); }
/* Hors d'un robot le nom est vide : il n'y a rien a deplier, et un curseur de
   lien sur un blanc invite a cliquer sur rien. */
a.account.inerte { cursor: default; pointer-events: none; }
.menu-robots { min-width: 230px; }
/* LE DEFILEMENT N'EXISTE QUE POUR UNE LISTE VRAIMENT LONGUE.
 *
 * Pose sans condition, il sortait une barre de defilement sur QUATRE robots :
 * le menu est place a une position fractionnaire (76,140625 px), et Chrome
 * arrondit alors la hauteur du contenu au pixel superieur - scrollHeight 156
 * pour un contenu de 155 exactement. Un pixel, et une barre bien visible.
 *
 * Dix lignes font environ 390 px : bien en deca de 60vh sur tout ecran
 * ordinaire, donc la regle ne change rien au rendu d'une liste courte, elle
 * retire seulement le conteneur de defilement qui n'avait rien a y faire. */
.menu-robots:has(.ligne:nth-child(10)) { max-height: 60vh; overflow-y: auto; }
/* DEUX LIENS PAR LIGNE, d'ou la ligne qui les porte : basculer cet onglet a
   gauche, en ouvrir un de PLUS a droite. Le trait de separation appartient
   maintenant a la ligne et non aux liens, qui sont deux. */
.menu-robots .ligne {
  display: flex; align-items: stretch;
  border-bottom: 1px solid var(--border);
}
.menu-robots .ligne:last-child { border-bottom: 0; }
/* UNE HAUTEUR DE LIGNE ENTIERE, et ce n'est pas cosmetique. Sans elle les
   quatre lignes mesuraient 150,375 px : le navigateur arrondit la hauteur
   visible a 150 et celle du contenu a 151, et sort une barre de defilement
   pour UN pixel sur un menu qui tient largement. */
.menu-robots .ligne a {
  display: flex; align-items: center; gap: 8px; border-bottom: 0;
  line-height: 20px;
}
.menu-robots .bascule { flex: 1 1 auto; min-width: 0; }
/* La pastille reprend .status-dot telle quelle : les couleurs d'etat sont
   deja definies au-dessus, et en redefinir ici en ferait un second jeu a
   tenir d'accord avec le premier. Seule la taille change, 12 px etant trop
   gros au milieu d'une ligne de texte. */
.menu-robots .status-dot { width: 8px; height: 8px; }
.menu-robots .nom {
  flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.menu-robots .ligne.courant { background: #34343a; }
.menu-robots .ligne.courant .nom { color: var(--text-strong); }
/* Ouvrir un onglet DE PLUS : discret, parce que c'est le geste rare. Le geste
   courant est la bascule sur place, qui prend toute la largeur restante. */
.menu-robots .onglet { flex: 0 0 auto; opacity: .5; font-size: 13px; padding: 9px 12px; }
.menu-robots .onglet:hover { opacity: 1; color: var(--text-strong); }

/* ---------- carte de la galaxie ---------- */

.galaxy-bar button { padding: 5px 10px; }

.galaxy-table td { padding: 6px 9px; }
/* Une position libre ne doit pas retenir le regard : c'est ce qui reste
   quand on cherche une cible ou un emplacement à coloniser. */
.galaxy-table tr.g-vide td { color: #5f5f68; }
/* Ses propres célestes ressortent : c'est ce qu'on cherche en ouvrant la
   carte sur son propre système. */
.galaxy-table tr.g-own td { background: #2c5c46; }
.tag.own { background: #2f4f7a; color: #d6e4ff; }
.galaxy-table .act { font-size: 12px; }

/* ---------- Hunter ---------- */

/* L'activité se lit à la couleur avant de se lire au texte : c'est ce qui
   permet de repérer une habitude en parcourant une colonne.

   Rouge pour « à l'instant » : quelqu'un est devant son écran, et c'est un
   avertissement - pas une bonne nouvelle qu'un vert laisserait entendre.
   L'orange des quarts d'heure suivants marque la zone d'incertitude. */
.act-now    { color: #ef7b78; font-weight: 600; }
.act-recent { color: #d8a13a; }

/* Une issue qui nous arrange, dans le simulateur. Elle empruntait la couleur
   de l'activité, qui voulait dire l'inverse dès qu'elle est passée au rouge :
   deux sens pour une classe, et le premier changement de teinte trahit le
   second. */
.favorable { color: #6ce0a0; font-weight: 600; }

/* Frise des derniers relevés : une barre par passage. Les créneaux où le
   joueur est là ressortent, et le motif apparaît sans lire un chiffre. */
.frise { display: flex; gap: 2px; align-items: flex-end; }
.frise .tick {
  width: 5px; height: 14px; border-radius: 1px;
  background: #3a3a42;
}
.frise .tick.act-now    { background: #23b07a; }
.frise .tick.act-recent { background: #d8a13a; }

/* ---------- carte de robot cliquable ---------- */

/* Toute la carte mène au robot : c'est le geste qu'on tente d'instinct, et
   il n'existait aucun autre chemin que le menu du haut. */
.card.cliquable { cursor: pointer; transition: border-color .12s, transform .12s; }
.card.cliquable:hover { border-color: var(--accent); transform: translateY(-1px); }
.card.cliquable button { cursor: pointer; }

/* Une entrée de menu sans destination utile - le navigateur du jeu quand le
   robot est à l'arrêt. */
.menu-item.disabled { opacity: .45; pointer-events: none; }
.btn-neutral.disabled { opacity: .5; }
/* Grisé mais présent : dire « ceci ne s'applique pas maintenant » vaut mieux
   que faire disparaître le bouton, qu'on cherche ensuite. */
.btn-start.disabled, .btn-stop.disabled { opacity: .35; cursor: default; filter: none; }
/* L'ATTRIBUT, et pas seulement la classe. La regle ci-dessus ne repond qu'a
   « class="disabled" », alors que l'interface eteint ses boutons par la
   propriete disabled - une trentaine d'endroits le font pendant qu'une requete
   voyage. Sans cette ligne, .btn-start impose son fond vert et le bouton
   desactive reste indistinguable d'un bouton vivant : on clique, rien ne se
   passe, on reclique. C'est exactement ce qui a vide le plafond de cadence
   d'un client le 06/09/2026. */
.btn-start:disabled, .btn-stop:disabled, .btn-neutral:disabled, .btn-warn:disabled {
  opacity: .35;
  cursor: default;
  filter: none;
}

/* Identifiant de machine : lu à voix haute ou recopié dans un formulaire, il
   doit être sans ambiguïté - d'où la police à chasse fixe et l'espacement. */
.machine-id {
  font-family: Consolas, "Liberation Mono", monospace;
  font-size: 14px; letter-spacing: .5px;
  background: #232326; border: 1px solid var(--border); border-radius: 3px;
  padding: 6px 10px; color: var(--text-strong); user-select: all;
}

/* ---------- cartes de planète repliables ---------- */

.panel-head.repliable { cursor: pointer; user-select: none; }
.panel-head.repliable:hover { filter: brightness(1.12); }
.panel-head .chevron { display: inline-block; width: 14px; color: var(--muted); }

/* Une action collée à ce qu'elle vise : l'œil contre le nom de la planète,
   le recycleur contre le nombre. Une colonne d'actions séparée obligeait à
   relier des yeux à des lignes du regard. */
.cellule-action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.cellule-action .btn-neutral.petit {
  padding: 1px 6px;
  line-height: 1.4;
}

/* Le statut d'un joueur se lit à la couleur de son nom, comme dans le jeu.
   Une colonne d'étiquettes disait la même chose en trois fois plus de place
   et obligeait à lire ; la couleur se saisit d'un coup d'œil, ce qu'on veut
   en parcourant un système. */
.joueur { color: #e8e8e8; }
/* Cible honorifique : le jeu peint son nom en jaune et le note « (ph) ».
   Rien à voir avec l'icône dorée du rang, que des joueurs blancs portent
   aussi. */
.joueur.j-honorable { color: #e5c341; }
.joueur.j-debutant { color: #57c964; }
.joueur.j-vacances { color: #5aa9e6; }
.joueur.j-inactif { color: #8a8a8a; }
/* Bloqué : barré, et le trait reste visible même sur un nom déjà grisé par
   l'inactivité - les deux vont souvent ensemble. */
.joueur.j-banni { text-decoration: line-through; }

/* Un bandit s'attaque sans perdre d'honneur : le jeu le peint en rouge et
   pose un carré à côté de son nom. C'est un signal de cible, pas une
   décoration - il prime donc sur l'inactivité ou les vacances. */
.joueur.j-bandit { color: #e05a5a; }
.carre-bandit {
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 5px;
  background: #d14b4b;
  vertical-align: baseline;
}

/* Un nom de joueur mène à toutes ses positions : c'est la question qu'on se
   pose en le lisant. Le lien garde la couleur du statut - elle porte plus
   d'information que le bleu d'un lien. */
.nom-joueur {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.nom-joueur:hover { text-decoration: underline; }

/* Largeurs fixées par le colgroup : le tableau ne doit plus s'étirer au
   prorata du contenu de chaque cellule. */
.galaxy-table { table-layout: fixed; width: 100%; }
.galaxy-table td, .galaxy-table th { overflow: hidden; text-overflow: ellipsis; }

/* ---------- messages fugaces ---------- */

/* Une action réussie doit se voir sans rien demander en retour. Une fenêtre
   à fermer pour dire « c'est parti » coûte un clic de plus que l'action
   elle-même ; le message s'affiche donc et s'efface seul.

   Les échecs restent plus longtemps et en rouge : on peut se permettre de
   rater « flotte envoyée », pas « pas de vaisseau disponible ». Ils sont de
   toute façon repris dans les journaux. */
/* En haut, sous la barre de navigation : c'est là que le regard revient
   après un clic, et le bas de la fenêtre est trop loin de l'endroit où l'on
   agit. Posée par-dessus la page plutôt qu'insérée dedans - un message qui
   pousse le contenu vers le bas déplacerait le bouton suivant sous le
   curseur. */
#messages {
  position: fixed;
  top: 56px;
  left: 50%;
  transform: translateX(-50%);
  /* Au-dessus des fenêtres (.modale-fond est à 300) : un refus de saisie dans
   * une fenêtre s'affichait dessous, donc nulle part. */
  z-index: 400;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  /* La zone ne doit pas intercepter les clics destinés à la page. */
  pointer-events: none;
}

.message {
  background: var(--panel-head);
  border: 1px solid var(--border);
  border-left: 3px solid var(--green);
  border-radius: 3px;
  color: var(--text-strong);
  padding: 9px 14px;
  font-size: 13px;
  max-width: 380px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .45);
  animation: message-entre .15s ease-out;
}

.message.erreur { border-left-color: var(--red); }

.message.part {
  opacity: 0;
  transition: opacity .4s ease-out;
}

@keyframes message-entre {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- jauge de progression ---------- */

/* Le balayage de l'univers dure des dizaines de minutes. Un compteur suffit à
   savoir qu'il avance, pas à sentir où il en est : la barre le montre d'un
   coup d'œil, sans lire deux nombres et faire la division. */
.jauge {
  height: 6px;
  background: var(--panel-head);
  border: 1px solid var(--border);
  border-radius: 3px;
  overflow: hidden;
  margin-top: 8px;
}
.jauge > div {
  height: 100%;
  width: 0;
  background: var(--green);
  transition: width .4s ease-out;
}

/* ---------- grille d'activité du Hunter ---------- */

/* Une ligne par jour, découpée en créneaux de cinq minutes. Les trous se
   lisent d'un coup d'œil, ce qu'une liste de relevés ne donne jamais : ce
   qu'on cherche n'est pas un instant mais une habitude.

   Une case par heure ne suffisait pas - un passage de deux minutes teintait
   l'heure entière, et l'on perdait précisément l'information qu'on venait
   chercher. */
.grille-activite { width: 100%; }

.grille-jour { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
.grille-jour .jour {
  flex: 0 0 130px;
  text-align: right;
  color: var(--muted);
  font-size: 11px;
  white-space: nowrap;
}

/* Les vingt-quatre heures, chacune découpée en douze créneaux. Le trait de
   séparation marque l'heure ; les créneaux, eux, se touchent. */
.grille-heures { flex: 1; display: flex; min-width: 0; }
.grille-heure {
  flex: 1 1 0;
  display: flex;
  min-width: 0;
  border-left: 1px solid var(--border);
}
.grille-heure:last-child { border-right: 1px solid var(--border); }
.grille-creneau { flex: 1 1 0; height: 14px; min-width: 0; }
.grille-creneau.vert   { background: #23b07a; }
.grille-creneau.orange { background: #d8a13a; }
.grille-creneau.rouge  { background: #d9534f; }

/* L'échelle des heures, alignée sur les mêmes colonnes que les lignes. */
.grille-echelle { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.grille-echelle .jour { flex: 0 0 130px; }
.grille-echelle .heures { flex: 1; display: flex; min-width: 0; }
.grille-echelle .heures > span {
  flex: 1 1 0;
  text-align: center;
  color: var(--muted);
  font-size: 11px;
  min-width: 0;
}
/* SUR UN TÉLÉPHONE, UNE HEURE SUR TROIS. Vingt-quatre libellés dans moins de
   deux cents pixels se chevauchaient et ne se lisaient plus (vu le 30/09/2026
   à 390 px sur Mes activités). Les colonnes restent vingt-quatre : un libellé
   masqué garde sa place, et les autres restent au-dessus de leur heure. */
@media (max-width: 600px) {
  .grille-echelle .heures > span:not(:nth-child(3n+1)) { visibility: hidden; }
}

/* La pastille « maintenant » de Mes activités : ce que la galaxie montre à
   l'instant, l'étoile rouge puis les minutes du timer. Ses propres classes :
   « rouge » et « orange » seules toucheraient aussi les créneaux. */
.grille-jour .direct, .grille-echelle .direct {
  flex: 0 0 24px;
  height: 14px;
  line-height: 14px;
  text-align: center;
  font-size: 10px;
  font-weight: 700;
  border-radius: 3px;
  font-variant-numeric: tabular-nums;
}
.grille-jour .direct.direct-rouge  { background: #d9534f; color: #fff; }
.grille-jour .direct.direct-orange { background: #d8a13a; color: #1b1b1b; }

.legende-activite { display: flex; gap: 16px; align-items: center; margin: 10px 0 16px; font-size: 12px; }
.legende-activite span.case {
  display: inline-block; width: 12px; height: 12px;
  border: 1px solid var(--border); margin-right: 5px; vertical-align: -2px;
}
/* Les pastilles de la légende portent les mêmes teintes que les créneaux.
   Elles les cherchaient sous une autre règle, et restaient donc vides. */
.legende-activite span.case.vert   { background: #23b07a; }
.legende-activite span.case.orange { background: #d8a13a; }
.legende-activite span.case.rouge  { background: #d9534f; }
.legende-activite span.case.vide   { background: transparent; }

/* ---------- horloges de la barre du haut ---------- */

/* L'heure du jeu et celle de la machine, côte à côte. Toute la lecture de
   l'activité repose sur leur accord : un décalage se voit ici plutôt que de se
   découvrir en manquant une esquive. */
.horloges {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.horloges b { color: var(--text); font-weight: 600; }
.horloges .ecart { color: var(--orange); }

/* Le choix de la langue, dans la barre.
   Discret : c'est un reglage qu'on cherche une fois, a l'installation, et
   qu'on ne regarde plus jamais ensuite. Mais il doit etre la, parce que les
   parametres sont derriere un robot ouvert - et quelqu'un qui arrive sur une
   liste vide dans une langue qu'il ne lit pas n'a aucun autre moyen d'en
   sortir. */
.langue-barre {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  padding: 3px 5px;
  margin-left: 14px;
  cursor: pointer;
}
.langue-barre:hover { color: var(--text); }
/* Les options heritent du fond du systeme, pas de celui de la barre : sans
   cette regle elles sortent en texte clair sur fond clair. */
.langue-barre option { background: var(--panel); color: var(--text); }

/* ---------- fenêtre modale ---------- */

/* Une campagne de ferme se règle d'un coup : quinze champs répartis sur la
   page feraient perdre de vue ce qu'elle vise. */
.modale-fond {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .6);
  z-index: 300;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  padding: 40px 20px;
}
.modale {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 3px;
  width: 100%;
  max-width: 560px;
  box-shadow: 0 6px 30px rgba(0, 0, 0, .5);
}

/* Marqueur des lignes de file que le bot a insérées de lui-même comme
   prérequis. Discret : c'est une explication, pas une alerte - la ligne est
   parfaitement normale, seule son origine diffère. */
.etiquette-auto {
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  padding: 1px 6px; border-radius: 3px;
  background: rgba(90, 140, 220, .18); color: #8fb4e8;
  border: 1px solid rgba(90, 140, 220, .35);
}

/* Cases à cocher avec ligne d'explication.

   Le décalage de l'explication était deviné - 26 pixels - et tombait deux
   pixels à gauche du libellé, ce qui se voit. La case reçoit donc une taille
   fixe, et le décalage se calcule : 14 pixels de case plus 8 d'écart. */
.champ > label > input[type=checkbox] {
  width: 14px; height: 14px; margin: 0; flex: none;
}
.champ-aide { margin-left: 22px; }

/* ---------- petits écrans ---------- */
/*
   L'interface n'avait aucune règle de ce genre. La balise viewport mettait
   bien la page à l'échelle du téléphone, mais rien ne se réorganisait : sur
   390 px, la colonne de gauche en prenait 235 et il en restait 155 pour tout
   le reste. Les champs n'étaient pas absents, ils étaient poussés hors de
   l'écran - signalé par un client jouant depuis son téléphone.

   Le seuil est à 860 px et non à 480. Ce n'est pas « un téléphone » qu'on
   détecte, c'est le moment où deux colonnes ne tiennent plus côte à côte :
   une tablette en portrait et une fenêtre étroite sur un ordinateur sont
   dans le même cas, et méritent la même réponse.

   Rien de tout cela ne se règle, et c'est voulu. Le navigateur sait sur quoi
   il tourne ; la question n'a pas à être posée au joueur. Une case dans les
   paramètres aurait d'ailleurs été enregistrée par compte et non par
   appareil : cochée depuis le téléphone, elle aurait suivi le client jusqu'à
   son ordinateur.
*/
@media (max-width: 860px) {
  main { padding: 12px 10px 40px; }

  /* La barre du haut se replie sur deux lignes plutôt que de comprimer ses
     éléments jusqu'à l'illisible. Le ressort qui écartait la navigation des
     horloges devient la coupure de ligne. */
  /* La hauteur doit suivre le repli, sinon il ne sert à rien.
     .topbar-inner porte height: 46px, écrit pour une barre d'une seule ligne.
     Repliée sur trois, elle gardait ces 46 px et les deux lignes du bas
     passaient PAR-DESSUS le contenu - 121 px de chevauchement, mesurés. Un
     débordement vertical qu'aucune mesure de largeur ne voit, et qui saute aux
     yeux dès qu'on regarde la page. */
  .topbar-inner {
    flex-wrap: wrap; justify-content: flex-start;
    row-gap: 6px; column-gap: 12px;
    height: auto; min-height: 46px; padding: 6px 12px;
  }
  .spacer { flex: 0 0 100%; height: 0; }
  .nav-bot { flex-wrap: wrap; gap: 12px; }

  /* Les deux colonnes s'empilent. Le menu garde sa forme verticale : il porte
     un tiroir et des interrupteurs, qu'une bande horizontale rendrait
     illisibles. */
  /* align-items: flex-start aligne en haut tant qu’on est en rangée. Passé
     en colonne, il colle à gauche et chaque bloc se réduit à la largeur de son
     contenu : empilé, mais rachitique - mesuré à 131 px sur un écran de 390.
     C’est stretch qu’il faut ici, et il doit être dit. */
  .layout { flex-direction: column; align-items: stretch; gap: 12px; }
  .sidebar { width: auto; }
  .empire { flex-direction: column; align-items: stretch; }
  .empire-side { width: auto; }

  /* Une rangée d'onglets plus large que l'écran défile chez elle au lieu de
     pousser la page entière. */
  .onglets, .empire-tabs { overflow-x: auto; }
  .onglets > *, .empire-tabs > * { flex: none; }

  /* Le tableau des origines demandait 340 px au minimum : sur un écran de
     360, il débordait à lui seul. */
  .table-origines { min-width: 0; max-width: none; }

  /* Un tableau plus large que l'écran défile chez lui, au lieu de pousser la
     page entière de côté.

     C'est le plus gros défaut de l'affichage sur téléphone, et il ne vient pas
     d'une largeur mais d'une ligne : « th, td { white-space: nowrap } ». Aucune
     cellule ne se coupe, donc un tableau de galaxie à neuf colonnes mesure
     677 px sur un écran de 390 - mesuré - et toute la page glisse. Une
     vingtaine de tableaux sur trente-cinq sont bien enveloppés dans un
     conteneur à défilement ; les autres ne l'étaient pas.

     display:block, et non « laisser les cellules se couper » : celui-là a été
     mesuré aussi, et il ne suffit pas - neuf colonnes restent neuf colonnes, la
     page tenait encore 621 px. Un tableau étroit, lui, ne perd rien : 340 px
     dans un conteneur de 368, avec et sans la règle. */
  table { display: block; overflow-x: auto; }

  /* Une fenêtre modale prend l'écran. À 16 px de marge de chaque côté et un
     panneau à largeur naturelle, il ne restait presque rien pour le contenu. */
  .modal { padding: 16px 8px; }
  .modal .panel { width: 100%; }

  /* Une ligne de menu se touche avec un doigt, pas avec un curseur. */
  .menu-item { padding: 12px 14px; }

  /* Seize pixels, et pas un de moins : en dessous, iOS zoome tout seul sur le
     champ qu'on touche et laisse la page décalée, ce qui donne exactement
     l'impression d'une interface cassée. */
  input[type=text], input[type=number], input[type=password], select, textarea {
    font-size: 16px;
  }
}

/* ---------- menu latéral escamotable ---------- */
/*
   Le bouton n'existe que sur un petit écran, et c'est le CSS qui en décide
   seul : il est écrit dans la page à chaque rendu, mais reste caché tant que
   la largeur ne le réclame pas. Le faire dépendre d'une mesure en JavaScript
   laisserait un état incohérent au premier redimensionnement de fenêtre.
*/
.menu-bascule { display: none; }

@media (max-width: 860px) {
  /* Seize entrées occupaient tout le premier écran d'un téléphone : il fallait
     les faire défiler à chaque page pour atteindre le contenu. Repliées, le
     bouton porte le nom de l'entrée active - il dit donc où l'on est, et n'a
     rien à faire traduire. */
  .menu-bascule {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; margin: 0 0 14px; padding: 12px 14px;
    background: var(--panel); border: 1px solid var(--border); border-radius: 3px;
    color: var(--text); font-size: 14px; font-family: inherit; text-align: left;
    cursor: pointer;
  }
  .menu-bascule .chevron { font-size: 26px; line-height: 1; opacity: .85; }

  /* Le menu du panneau latéral seulement : la liste des scripts porte la même
     classe et vit ailleurs, elle ne doit pas disparaître. */
  .sidebar > .menu { display: none; }
  .sidebar > .menu.ouvert { display: block; }
}

/* ---------------------------------------------------------------- planning

   Le calendrier d'activation des workers. Sept colonnes, six rangées, dans
   une zone d'environ mille pixels sur écran et trois cent soixante-dix sur
   téléphone : les cases doivent donc rester lisibles très petites, d'où des
   pastilles plutôt que du texte dans chaque journée. */
.cal-grille {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
}
.cal-entete {
  color: var(--muted);
  font-size: 11px;
  text-align: center;
  padding: 2px 0;
  text-transform: capitalize;
}
.cal-jour {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 4px;
  min-height: 62px;
  padding: 4px;
  cursor: pointer;
  overflow: hidden;
}
.cal-jour:hover { border-color: var(--accent); }
.cal-jour.hors { opacity: 0.35; }
.cal-jour.aujourdhui { border-color: var(--accent); }
.cal-jour.choisi { background: var(--panel-head); border-color: var(--accent-hover); }
.cal-num { font-size: 11px; color: var(--muted); }
.cal-jour.aujourdhui .cal-num { color: var(--text-strong); font-weight: 600; }

/* La barre d'un jour couvert : elle traverse la case d'un bord à l'autre.
   C'est ce qui rend « ça tourne » lisible sans être lu, et c'est la MÊME forme
   sur tous les jours d'une même plage - une pastille le premier jour et une
   barre le second se lisaient comme deux choses différentes. */
.cal-barre {
  display: block; width: 100%; box-sizing: border-box;
  background: var(--green); color: #06210f;
  border-radius: 3px; font-size: 10px; line-height: 14px;
  padding: 0 4px; margin-top: 2px; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Une journée ENTAMÉE garde la forme d'une journée pleine - c'est ce qu'on
   veut, la même plage doit se lire pareil d'un jour à l'autre - mais son bord
   gauche dit qu'elle commence en cours de route. Un liseré, pas une autre
   forme : la distinction est là pour qui la cherche, sans crier. */
.cal-barre.entamee { border-left: 3px solid var(--bg); }
.cal-plus { font-size: 10px; color: var(--muted); }

/* ----------------------------------------------------- planning, vue semaine

   UNE VOIE PAR WORKER, SEPT COLONNES DE JOURS, LE TEMPS EN ABSCISSE.

   Du flex et rien d'autre : un <table> ne se laisse pas rétrécir sous 760 px
   sans déborder, et ces voies doivent tenir dans 325 px utiles sur téléphone.
   La largeur d'une plage vient de son flex-grow, posé en style en ligne parce
   qu'il vaut le nombre de demi-heures qu'elle couvre - une classe par durée en
   ferait quarante-huit. */
.sem-ligne { display: flex; align-items: stretch; gap: 8px; margin-bottom: 3px; }
.sem-ligne .nom {
  flex: 0 0 96px;
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  align-self: center;
}
.sem-jour {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  position: relative;
  border-left: 1px solid var(--border);
  /* Un trait toutes les six heures, sans un seul nœud de DOM. */
  background-image: repeating-linear-gradient(to right,
    var(--border) 0 1px, transparent 1px 25%);
}
.sem-entete .sem-jour, .sem-heures .sem-jour { background-image: none; }

.sem-tete {
  cursor: pointer;
  justify-content: center;
  gap: 3px;
  font-size: 11px;
  color: var(--muted);
  padding: 2px 0;
  border-radius: 3px 3px 0 0;
}
.sem-tete:hover { color: var(--text-strong); }
.sem-tete.aujourdhui { color: var(--text-strong); font-weight: 600; }
.sem-tete.choisi { background: var(--panel-head); color: var(--text-strong); }
.sem-jour.choisi { background-color: var(--panel-head); }

/* Une plage. min-width pour qu'une consigne d'une demi-heure reste visible à
   375 px, où une demi-heure vaut moins d'un pixel. */
.sem-bloc { height: 14px; align-self: center; border-radius: 2px; min-width: 2px; }
.sem-bloc.on { background: var(--green); }
.sem-bloc.off { background: transparent; }
/* Un bout CARRÉ dit que la plage continue au-delà du bord de la case ; un bout
   arrondi dit qu'un bord la ferme là. C'est ce qui distingue un allumage qui
   traverse minuit d'un allumage qui s'arrête à minuit. */
.sem-bloc.ouvert-g { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.sem-bloc.ouvert-d { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.sem-bloc.on { cursor: pointer; }
.sem-legende { flex: 0 0 18px; height: 10px; }
.sem-legende.off { border: 1px solid var(--border); }

/* Le nom et l'heure d'une barre du mois : deux morceaux pour que l'étroitesse
   puisse choisir lequel garder. Voir le bloc téléphone plus bas. */
.cal-nom + .cal-h { margin-left: 3px; }

/* « Appliquer le planning » décoché : les plages sont enregistrées mais le
   robot ne les suit pas. Elles passent au gris au lieu du vert des plages
   suivies (30/09/2026 : un client a cru les siennes actives). Gris moyen et
   texte clair, pour qu'elles restent lisibles et qu'on puisse les corriger ;
   les hachures de la semaine disent « en attente » sans texte. */
.planning-inactif .cal-barre { background: #5a5d64; color: #e6e8ec; }
.planning-inactif .sem-bloc.on {
  background: #62656c;
  background-image: repeating-linear-gradient(45deg,
    rgba(255, 255, 255, 0.14) 0 3px, transparent 3px 7px);
}

.sem-heures { margin-top: 2px; }
/* CHAQUE REPÈRE AU DÉBUT DE SA TRANCHE, et surtout pas étalés d'un bord à
   l'autre. Avec quatre repères et space-between, « 18 » atterrissait à 100 %
   de la journée : elle avait l'air de finir à dix-huit heures, et le « 00 » du
   jour suivant collé juste après achevait de brouiller la lecture. Signalé par
   Damien le 16/09/2026.

   Chacun occupe donc une part égale et s'aligne à gauche : avec quatre
   repères, 0h à 0 %, 6h à 25 %, 12h à 50 %, 18h à 75 %. Le bord droit ne porte
   plus rien, donc plus rien n'y ressemble à une fin de journée. La vue Jour en
   pose douze dans la même mécanique, sans une ligne de plus. */
.sem-heures .sem-jour { justify-content: flex-start; }
.sem-repere {
  font-size: 9px; color: var(--muted); line-height: 12px;
  flex: 1 1 0; min-width: 0; text-align: left;
}

@media (max-width: 760px) {
  .cal-jour { min-height: 48px; padding: 2px; }
  .cal-barre { font-size: 9px; line-height: 12px; padding: 0 2px; }
  /* SUR UNE CASE DE TRENTE-NEUF PIXELS, L'HEURE PASSE AVANT LE NOM. Le nom se
     répète de case en case ; l'heure est ce qui distingue une journée entamée
     d'une journée pleine, et c'est elle qu'on vient chercher. Sans ça les deux
     se lisaient pareil, « Const… ». Voir dessinerMois dans app.js. */
  .cal-barre.entamee .cal-nom { display: none; }

  /* Sur téléphone : le libellé rétrécit, le nom du jour disparaît au profit du
     seul numéro, et l'échelle des heures s'efface - quatre nombres dans
     trente-six pixels ne se lisent pas. Les traits toutes les six heures, eux,
     restent : ils suffisent à situer une plage dans la journée. */
  .sem-ligne { gap: 4px; }
  .sem-ligne .nom { flex: 0 0 62px; font-size: 10px; }
  .sem-jnom { display: none; }
  .sem-heures { display: none; }
  /* SAUF CELLE DE LA VUE JOUR. La règle du dessus vise la Semaine, où chaque
     jour tient dans trente-six pixels. Le Jour, lui, étale vingt-quatre heures
     sur toute la largeur - deux cent soixante-quatorze pixels sur un téléphone
     - et sans ses heures il ne reste que des barres vertes sans axe.

     Un repère sur deux s'efface pour leur laisser la place : 0h, 4h, 8h, 12h,
     16h, 20h. Par visibility et non par display, sinon les survivants se
     redéploieraient sur toute la largeur et n'indiqueraient plus la bonne
     heure - c'est le même piège que les quatre repères étalés en
     space-between, signé plus haut. */
  .sem-heures.sem-heures-jour { display: flex; }
  .sem-heures-jour .sem-repere:nth-child(even) { visibility: hidden; }
  /* Et la trame passe de six à douze heures : sur une colonne de trente-quatre
     pixels, un trait tous les huit pixels ne situe plus rien, il fait du bruit
     - une journée vide s'y lit comme une plage hachurée. */
  .sem-jour {
    background-image: repeating-linear-gradient(to right,
      var(--border) 0 1px, transparent 1px 50%);
  }
}

/* ---------------------------------------------- tableau des sorties de nuit

   UNE LIGNE COCHÉE DOIT SE VOIR D'UN COUP D'ŒIL. Le tableau en compte parfois
   vingt, et sur fond sombre une case cochée se distinguait à peine d'une case
   vide : « le petit vu est pas super visible » - Damien, le 14/09/2026.

   Trois gestes, du plus discret au plus net : la case elle-même grossit et
   prend la couleur d'accent, la ligne décochée s'efface, et la ligne cochée
   reçoit un liseré vert à gauche. Le troisième est celui qui se lit de loin,
   les deux autres servent quand on regarde la ligne. */
.tableau-sorties input[type=checkbox] {
  width: 16px;
  height: 16px;
  accent-color: var(--green);
  cursor: pointer;
}

/* :has regarde l'état de la case SANS aucun code : le style suit la coche à
   l'instant où l'on clique, sans qu'un rendu ait à repasser.

   LA PREMIÈRE COLONNE SEULEMENT, ET C'EST TOUT LE PIÈGE. La ligne porte DEUX
   cases : « Sortir » et « Découper ». Un sélecteur sur la ligne entière grise
   dès qu'UNE case y est décochée, et « Découper » ne l'est presque jamais :
   une ligne cochée restait donc grise. Vu par Damien une heure après la mise
   en ligne, capture à l'appui. */
.tableau-sorties tbody tr:has(td:first-child input[type=checkbox]:not(:checked)) {
  opacity: 0.45;
}
.tableau-sorties tbody tr:has(td:first-child input[type=checkbox]:not(:checked)):hover {
  opacity: 0.8;
}
.tableau-sorties tbody tr:has(td:first-child input[type=checkbox]:checked) td:first-child {
  box-shadow: inset 3px 0 0 var(--green);
}
.tableau-sorties tbody tr:has(td:first-child input[type=checkbox]:checked) {
  background: rgba(35, 176, 122, 0.07);
}

/* Un nombre illisible refusé à l'enregistrement : le champ fautif se voit.
 * Posé en fin de fichier, pour qu'aucune règle d'input plus bas ne le
 * défasse (voir la mémoire sur la cascade de ce fichier). */
input.saisie-invalide,
textarea.saisie-invalide,
/* ET AVEC LE FOCUS, que le champ refusé reçoit justement : sans ces deux
 * lignes, « input[type=text]:focus » (spécificité 0,2,1) l'emportait sur
 * « input.saisie-invalide » (0,1,1), et le contour rouge disparaissait au
 * moment exact où il devait se voir. Vu en capture le 28/09/2026. */
input.saisie-invalide:focus,
textarea.saisie-invalide:focus {
  outline: 2px solid var(--red);
  outline-offset: 1px;
  border-color: var(--red);
}
