/* ============================================================================
   ASTROPOST — Composants
   ----------------------------------------------------------------------------
   Chaque composant du jeu, avec la raison de sa forme quand elle n'est pas
   évidente. Les noms reprennent ceux du code iOS (DesignSystem/Components.swift)
   pour qu'on puisse passer de l'un à l'autre.
   ========================================================================= */

/* ┌───────────────────────────────────────────────────────────────────────────
   │ COUCHE DE CASCADE — à lire avant de s'étonner d'une priorité
   │
   │ Ce fichier est enveloppé dans `@layer astropost`. Conséquence : toute
   │ règle NON couchée l'emporte sur celles d'ici, quelle que soit sa
   │ spécificité et quel que soit l'ordre de chargement.
   │
   │ C'est voulu. Une application qui définit déjà `.ap-button` dans sa propre
   │ feuille garde SA version, sans avoir à surenchérir en spécificité ni à
   │ surveiller l'ordre des <link>. Le système de design fournit le socle ;
   │ l'application tranche.
   │
   │ Sans cette couche, ce fichier écrasait silencieusement 32 classes de
   │ l'application web — dont son bouton principal, qui perdait son fond.
   │ Le défaut a été vu à l'écran, pas deviné.
   └────────────────────────────────────────────────────────────────────────── */

@layer astropost {

  /* ══ Carte ═════════════════════════════════════════════════════════════════
     Le composant de base. Pas de bordure, pas d'ombre : le contraste entre
     `app-surface` et `app-background` suffit. Un filet très léger est ajouté
     parce qu'un écran de PC a un contraste plus faible qu'une dalle OLED, et
     que les cartes s'y confondaient avec le fond. */

  .ap-card {
    padding: var(--card-padding);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: var(--app-surface);
    width: 100%;
  }

  .ap-card__title {
    font-size: var(--text-headline);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }

  /* Une carte cliquable — une ligne de courrier, un site, un objet du
     catalogue. Elle doit être un <button> ou un <a>, pas une <div> cliquable. */
  .ap-card--interactive {
    display: block;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
    transition: border-color var(--motion-soft), background-color var(--motion-soft);
  }
  .ap-card--interactive:hover {
    border-color: var(--hairline-strong);
    background: color-mix(in srgb, var(--app-surface) 92%, var(--brand-primary));
  }
  .ap-card--interactive:active { background: var(--pressed-fill); }

  /* Le passage mis en avant : un bandeau, pour ce qui vient d'arriver ou ce qui
     attend le joueur. */
  .ap-card--highlight {
    background: linear-gradient(160deg,
                rgba(89, 140, 242, .14), rgba(179, 153, 242, .08));
    border-color: rgba(115, 184, 255, .22);
    border-radius: var(--sheet-radius);
  }

  /* ══ Boutons ══════════════════════════════════════════════════════════════
     Quatre variantes, et elles ont un sens :
       — plein      : l'action principale de l'écran, une seule par écran
       — contour    : une action secondaire assumée
       — discret    : une sortie, un « plus tard », un lien
       — destructif : ce qui efface
     `.ap-button` est une classe ; l'élément doit rester <button> ou <a>. */

  .ap-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    padding: 10px var(--space-md);
    border: 1px solid transparent;
    border-radius: var(--pill-radius);
    background: none;
    font-family: inherit;
    font-size: var(--text-subheadline);
    font-weight: var(--weight-medium);
    color: var(--text-primary);
    cursor: pointer;
    text-decoration: none;
    transition: background-color var(--motion-soft),
                border-color var(--motion-soft),
                transform 200ms ease;
  }

  .ap-button--large { padding: 14px var(--space-lg); font-size: var(--text-body); }
  .ap-button--small { padding: 6px var(--space-sm); font-size: var(--text-footnote);
                      min-height: 32px; }
  .ap-button--block { width: 100%; }

  .ap-button--filled {
    background: var(--brand-primary);
    /* Un bleu clair avec du texte clair ne passerait aucun contrôle de
       contraste : le texte est donc très sombre, comme sur iOS. */
    color: #08131F;
  }
  .ap-button--filled:hover  { background: #8FC6FF; transform: translateY(-1px); }
  .ap-button--filled:active { transform: none; background: #5FA9F5; }

  .ap-button--bordered { border-color: var(--hairline-strong); }
  .ap-button--bordered:hover { border-color: var(--brand-primary); color: var(--brand-primary); }

  .ap-button--plain { color: var(--brand-primary); padding-inline: var(--space-sm); }
  .ap-button--plain:hover { background: var(--brand-primary-soft); }

  .ap-button--destructive { color: var(--status-error); }
  .ap-button--destructive:hover { background: var(--status-error-soft); }

  /* Les mêmes variantes, sous les noms que l'application web emploie déjà.
     Définies ici pour que les deux vocabulaires marchent, plutôt que
     d'imposer une réécriture des gabarits. */
  .ap-button--primary { background: var(--brand-primary); color: #08131F; }
  .ap-button--primary:hover { background: #8FC6FF; }
  .ap-button--ghost { background: none; color: var(--brand-primary); }
  .ap-button--ghost:hover { background: var(--brand-primary-soft); }
  .ap-button--danger { background: var(--status-error-soft); color: var(--status-error); }
  .ap-button--danger:hover { background: var(--status-error); color: var(--sky-deep); }

  /* Un bouton désactivé reste LISIBLE : il n'est pas à demi effacé. Et il est
     TOUJOURS accompagné de son blocage écrit (voir .ap-blocker) — un bouton
     grisé sans explication se lit comme une panne. */
  .ap-button:disabled,
  .ap-button[aria-disabled="true"] {
    cursor: not-allowed;
    opacity: .45;
    transform: none;
  }

  @media (prefers-reduced-motion: reduce) {
    .ap-button:hover { transform: none; }
  }

  /* ══ Le blocage écrit ═════════════════════════════════════════════════════
     Règle transversale du jeu : TOUT BLOCAGE ANNONCE SA CAUSE, CHIFFRÉE. Le
     message porte ce qui MANQUE, pas ce que coûte l'action — savoir qu'il reste
     135 crédits à gagner dit au joueur s'il en a pour une observation ou pour
     la soirée. Le prix, lui, est déjà affiché juste à côté. */

  .ap-blocker {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    font-size: var(--text-footnote);
    color: var(--status-warning);
  }
  .ap-blocker__icon { flex: none; width: 16px; height: 16px; margin-top: 2px; }

  /* Un indice donné AVANT l'envoi, sur un formulaire : plus doux qu'un refus. */
  .ap-hint { font-size: var(--text-footnote); color: var(--text-secondary); }

  /* ══ Libellé à icône ══════════════════════════════════════════════════════
     L'équivalent du `Label` de SwiftUI : une icône et un texte, alignés. */

  .ap-label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
  }
  .ap-label__icon { flex: none; width: 1.1em; height: 1.1em; }
  .ap-label--top { align-items: flex-start; }
  .ap-label--top .ap-label__icon { margin-top: .15em; }

  /* ══ Pastille de ressource ════════════════════════════════════════════════
     La barre du haut : crédits, données, réputation, énergie. Quatre pastilles
     au fond teinté de leur couleur. */

  .ap-resource {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 10px;
    border-radius: var(--pill-radius);
    font-size: var(--text-subheadline);
    font-weight: var(--weight-medium);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }
  .ap-resource__icon { width: 1.05em; height: 1.05em; flex: none; }

  .ap-resource--credits    { background: var(--status-warning-soft);  color: var(--status-warning); }
  .ap-resource--data       { background: var(--brand-primary-soft);   color: var(--brand-primary); }
  .ap-resource--reputation { background: var(--brand-secondary-soft); color: var(--brand-secondary); }
  .ap-resource--energy     { background: var(--status-success-soft);  color: var(--status-success); }

  .ap-resource-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    padding-block: var(--space-sm);
  }

  /* ══ La barre du haut ═════════════════════════════════════════════════════
     Le nom de l'observatoire, son palier et son lieu ne vivaient que dans la
     barre latérale — donc NULLE PART sur un téléphone, où ce sont pourtant
     les trois premières choses qu'on veut voir. La barre du haut les porte
     sur les deux dispositions, et la latérale cesse de les redire.

     Elle traverse toute la largeur, au-dessus de la latérale comme du
     contenu : c'est l'identité de la partie, pas un en-tête d'écran. */

  .ap-topbar {
    /* Son PROPRE conteneur : la barre vit hors de `.ap-scroll`, donc le
       conteneur `screen` ne la mesure pas. Une requête écrite dessus ne
       s'appliquait jamais, et le nom de l'observatoire restait tronqué au
       profit du mot « ASTROPOST » — exactement l'inverse de ce qu'on veut. */
    container-type: inline-size;
    container-name: topbar;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 52px;
    padding-inline: var(--gutter);
    border-bottom: 1px solid var(--hairline);
    background: color-mix(in srgb, var(--app-surface) 82%, transparent);
    backdrop-filter: blur(12px);
  }

  /* `min-width: 0` est ce qui autorise la troncature : sans lui, un élément
     de flex ne descend pas sous sa largeur de contenu et pousse la barre. */
  .ap-topbar__identity {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-width: 0;
  }

  .ap-topbar__name,
  .ap-topbar__detail {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .ap-topbar__name {
    font-size: var(--text-subheadline);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }

  .ap-topbar__detail {
    font-size: var(--text-caption);
    color: var(--text-secondary);
  }

  .ap-topbar__actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-inline-start: auto;
  }

  .ap-topbar__action {
    display: inline-grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: var(--pill-radius);
    color: var(--text-secondary);
  }
  .ap-topbar__action:hover { background: var(--selected-fill); color: var(--text-primary); }
  .ap-topbar__action svg { width: 20px; height: 20px; }

  /* Le trait vertical qui sépare la marque de l'identité. Décoratif, donc
     `aria-hidden` dans le balisage. */
  .ap-topbar__rule {
    align-self: stretch;
    width: 1px;
    margin-block: 12px;
    background: var(--hairline);
  }

  /* Sur un téléphone, le mot « ASTROPOST » prend la place du nom de
     l'observatoire, qui est le renseignement utile. La marque reste dite par
     son logo, qui ne bouge pas. */
  @container topbar (max-width: 560px) {
    .ap-topbar__wordmark,
    .ap-topbar__rule { display: none; }
  }

  /* ══ L'avatar ═════════════════════════════════════════════════════════════
     Rien à téléverser : il est DÉDUIT du code d'observatoire, comme le reste
     du jeu se déduit au lieu de se retenir. Le code est tiré au hasard et ne
     dérive d'aucun nom (§ 5.22) — la couleur n'apprend donc rien du joueur.

     Les deux teintes viennent du style en ligne, posé par `engine/avatar.py`.
     Un aplat parfaitement plat se lit comme un trou : d'où le dégradé. */

  .ap-avatar {
    display: inline-grid;
    place-items: center;
    flex: none;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: linear-gradient(145deg,
                hsl(var(--avatar-hue, 220) 62% 58%),
                hsl(var(--avatar-hue-2, 260) 58% 44%));
    /* Texte SOMBRE sur une couleur claire, pour la même raison que la
       pastille : du blanc sur ces teintes ne passe aucun contrôle. */
    color: var(--sky-deep);
    font-size: var(--text-caption);
    font-weight: var(--weight-semibold);
    letter-spacing: .02em;
    line-height: 1;
    user-select: none;
  }

  .ap-avatar--large {
    width: 72px;
    height: 72px;
    font-size: var(--text-title);
  }

  /* ══ Pastille de compte ═══════════════════════════════════════════════════
     ROUGE, et non bleue : une pastille discrète est une pastille qu'on n'a pas
     vue. Elle ne compte que ce qui est réclamable SUR-LE-CHAMP — une pastille
     qui s'allume pour rien cesse d'être regardée.

     Le nombre RÉTRÉCIT plutôt que de passer à la ligne (UI_UX.md). */

  .ap-badge {
    display: inline-grid;
    place-items: center;
    min-width: 18px;
    height: 18px;
    padding-inline: 5px;
    border-radius: var(--pill-radius);
    background: var(--status-error);
    /* Texte SOMBRE sur le rouge, et non blanc : #FA736B avec du blanc donne
       un contraste de 2,4:1, qui ne passe aucun contrôle. Avec le fond du
       ciel, il monte à 8:1. */
    color: var(--sky-deep);
    font-size: var(--text-caption-2);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
  }

  /* Un point sans chiffre : « il y a quelque chose », sans dire combien.
     Employé pour « non lu » sur une ligne de courrier. */
  .ap-dot {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--brand-primary);
    flex: none;
  }

  /* Le même point, en ROUGE : « celui-ci vous attend ».

     La pastille de l'onglet dit qu'il y a trois choses à récupérer ; elle ne
     dit pas LESQUELLES. Dans une liste de vingt cartes, le joueur ouvre
     l'onglet puis cherche — et le tri seul ne suffit pas, parce qu'une carte
     en tête de liste ne se distingue d'une autre par rien.

     Rouge comme `.ap-badge`, et pour la même raison : ce point ne s'allume
     que sur ce qui est réclamable SUR-LE-CHAMP. Bleu, il se confondrait avec
     « non lu », qui ne demande rien. */
  .ap-dot--attention { background: var(--status-error); }

  /* ══ Sélecteur de volet ═══════════════════════════════════════════════════
     Le sélecteur segmenté du système ne sait pas porter de pastille. Or la
     pastille de l'onglet dit qu'il y a quelque chose à récupérer SANS DIRE DE
     QUEL CÔTÉ : le joueur ouvre l'onglet, puis cherche. Celui-ci répond à la
     question au moment où elle se pose.

     Il doit être un vrai groupe d'onglets : role="tablist" et des boutons
     role="tab", pour que le clavier et les lecteurs d'écran s'y retrouvent. */

  .ap-pane-selector {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 3px;
    border-radius: 11px;
    background: color-mix(in srgb, var(--app-surface) 55%, transparent);
  }

  /* QUATRE volets sur une largeur de téléphone ne tiennent pas : les items
     portent `white-space: nowrap` et `flex: 1 1 0`, donc ils ne rétrécissent
     pas — ils débordent, et le dernier est coupé net. C'est arrivé le jour
     où le laboratoire a pris son volet à côté de Missions, Contrats et
     Recherche.

     Deux par ligne plutôt qu'un libellé tronqué : un volet dont on ne lit
     pas le nom n'est pas un volet. La règle ne s'applique qu'à partir de
     QUATRE — à trois, tout tient sur une ligne et rien ne bouge. */
  @container screen (max-width: 560px) {
    .ap-pane-selector:has(> :nth-child(4)) > .ap-pane-selector__item {
      flex-basis: calc(50% - 4px);
    }
  }

  .ap-pane-selector__item {
    flex: 1 1 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    min-height: 34px;
    padding: 7px var(--space-sm);
    border: 0;
    border-radius: 8px;
    background: none;
    font-family: inherit;
    font-size: var(--text-subheadline);
    font-weight: var(--weight-regular);
    color: var(--text-primary);
    cursor: pointer;
    white-space: nowrap;
    transition: background-color var(--motion-soft);
  }
  .ap-pane-selector__item:hover { background: var(--pressed-fill); }

  .ap-pane-selector__item[aria-selected="true"] {
    background: var(--selected-fill);
    font-weight: var(--weight-semibold);
  }

  /* ══ Barre d'onglets — mobile ═════════════════════════════════════════════
     Cinq onglets, en bas. `env(safe-area-inset-bottom)` : sur un iPhone en
     navigateur, la barre système mange le bas de l'écran. */

  .ap-tabbar {
    z-index: var(--z-tabbar);
    display: flex;
    justify-content: space-around;
    gap: 2px;
    padding: 6px var(--space-sm) calc(6px + env(safe-area-inset-bottom, 0px));
    background: color-mix(in srgb, var(--app-background) 86%, transparent);
    backdrop-filter: blur(20px);
    border-top: 1px solid var(--hairline);
  }

  .ap-tabbar__item {
    position: relative;
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 6px 2px;
    border: 0;
    border-radius: 10px;
    background: none;
    font-family: inherit;
    font-size: var(--text-caption-2);
    color: var(--text-secondary);
    cursor: pointer;
  }
  .ap-tabbar__item[aria-selected="true"] { color: var(--brand-primary); }
  .ap-tabbar__icon { width: 24px; height: 24px; }

  /* La pastille se pose sur l'icône, décalée en haut à droite. */
  .ap-tabbar__badge {
    position: absolute;
    top: 2px;
    left: 50%;
    transform: translateX(6px);
  }

  /* ══ Barre latérale — tablette et desktop ═════════════════════════════════
     Elle reste VISIBLE, porte l'identité de l'observatoire, dit ce que contient
     chaque section, et affiche les rappels. Une barre d'onglets en bas d'un
     écran de treize pouces gaspille la hauteur et oblige à viser loin de ce
     qu'on regarde. */

  .ap-sidebar {
    z-index: var(--z-sidebar);
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    padding: var(--space-lg) var(--space-md);
    border-right: 1px solid var(--hairline);
    overflow-y: auto;
  }

  .ap-sidebar__identity { display: flex; flex-direction: column; gap: var(--space-xs); }
  .ap-sidebar__name  { font-size: var(--text-title); font-weight: var(--weight-semibold); }
  .ap-sidebar__title { font-size: var(--text-footnote); color: var(--brand-secondary); }

  .ap-sidebar__nav { display: flex; flex-direction: column; gap: 2px; }

  .ap-sidebar__item {
    display: grid;
    grid-template-columns: 24px 1fr auto;
    align-items: center;
    gap: var(--space-sm) var(--space-md);
    padding: 10px var(--space-sm);
    border: 0;
    border-radius: 10px;
    background: none;
    font-family: inherit;
    text-align: left;
    color: var(--text-primary);
    cursor: pointer;
    transition: background-color var(--motion-soft);
  }
  .ap-sidebar__item:hover { background: var(--pressed-fill); }
  .ap-sidebar__item[aria-selected="true"] { background: var(--selected-fill); }

  .ap-sidebar__icon { width: 22px; height: 22px; color: var(--brand-primary); }
  .ap-sidebar__label { font-size: var(--text-subheadline); font-weight: var(--weight-medium); }

  /* Le résumé de section : « Ressources, équipement et site ». Le téléphone n'a
     pas la place de l'afficher ; la barre latérale, si. */
  .ap-sidebar__summary {
    grid-column: 2 / -1;
    font-size: var(--text-caption);
    color: var(--text-secondary);
    line-height: var(--line-tight);
  }

  /* ══ Tuile de chiffre ═════════════════════════════════════════════════════ */

  .ap-stat-tile {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding: var(--space-md);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: var(--app-surface);
  }
  .ap-stat-tile__value {
    font-size: var(--text-title);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
  }
  .ap-stat-tile__label { font-size: var(--text-caption); color: var(--text-secondary); }

  /* ══ Badges de rareté et de type ══════════════════════════════════════════ */

  .ap-tag {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 9px;
    border-radius: var(--pill-radius);
    font-size: var(--text-caption);
    font-weight: var(--weight-semibold);
    letter-spacing: .03em;
    white-space: nowrap;
  }

  .ap-tag--common    { background: rgba(166, 176, 194, .16); color: var(--rarity-common); }
  .ap-tag--uncommon  { background: rgba( 92, 204, 168, .16); color: var(--rarity-uncommon); }
  .ap-tag--rare      { background: rgba(122, 179, 255, .16); color: var(--rarity-rare); }
  .ap-tag--very-rare { background: rgba(204, 148, 250, .16); color: var(--rarity-very-rare); }

  /* Par type d'objet. `currentColor` évite d'écrire dix fois la même règle :
     le fond se déduit de la couleur du texte. */
  .ap-tag--type {
    background: color-mix(in srgb, currentColor 16%, transparent);
  }
  .ap-tag--star, .ap-tag--variableStar, .ap-tag--sun { color: var(--type-star); }
  .ap-tag--planet    { color: var(--type-planet); }
  .ap-tag--nebula    { color: var(--type-nebula); }
  .ap-tag--galaxy    { color: var(--type-galaxy); }
  .ap-tag--cluster   { color: var(--type-cluster); }
  .ap-tag--comet, .ap-tag--asteroid { color: var(--type-comet); }
  .ap-tag--exoplanet { color: var(--type-exoplanet); }

  /* ══ Barre de progression ═════════════════════════════════════════════════ */

  .ap-progress { display: flex; flex-direction: column; gap: 6px; }

  .ap-progress__head {
    display: flex;
    justify-content: space-between;
    gap: var(--space-sm);
    font-size: var(--text-footnote);
  }
  .ap-progress__label { color: var(--text-secondary); }
  .ap-progress__value { color: var(--text-primary); font-variant-numeric: tabular-nums; }

  .ap-progress__track {
    height: 6px;
    border-radius: var(--pill-radius);
    background: var(--hairline);
    overflow: hidden;
  }
  .ap-progress__fill {
    height: 100%;
    border-radius: inherit;
    background: var(--brand-primary);
    transition: width var(--motion-soft);
  }
  .ap-progress--success .ap-progress__fill { background: var(--status-success); }
  .ap-progress--warning .ap-progress__fill { background: var(--status-warning); }

  /* ══ Ligne de mesure ══════════════════════════════════════════════════════
     Icône, libellé, valeur alignée à droite. Employée partout : mesures d'une
     fiche, récompenses d'une observation, compte rendu d'une intégration. */

  .ap-measurement {
    display: grid;
    grid-template-columns: 24px 1fr auto;
    align-items: center;
    gap: var(--space-md);
  }
  .ap-measurement__icon  { width: 20px; height: 20px; color: var(--brand-primary); }
  .ap-measurement__label { color: var(--text-secondary); font-size: var(--text-subheadline); }
  .ap-measurement__value {
    font-size: var(--text-subheadline);
    font-weight: var(--weight-medium);
    font-variant-numeric: tabular-nums;
  }

  /* Une mesure NON OBTENUE : son nom est affiché, sa valeur non. Savoir ce
     qu'il reste à relever fait partie du jeu ; connaître la réponse d'avance,
     non. */
  .ap-measurement--missing .ap-measurement__icon  { color: var(--text-secondary); opacity: .5; }
  .ap-measurement--missing .ap-measurement__value { color: var(--text-secondary); }

  /* Une mesure REÇUE d'un autre observatoire. En astronomie, une mesure se cite
     toujours avec son auteur. */
  .ap-measurement--shared .ap-measurement__icon { color: var(--brand-secondary); }
  .ap-measurement__credit {
    grid-column: 2 / -1;
    font-size: var(--text-caption);
    color: var(--brand-secondary);
  }

  /* ══ État vide ════════════════════════════════════════════════════════════
     Il dit TOUJOURS quoi faire ensuite. Un écran vide sans explication ne se
     distingue pas d'une panne. */

  .ap-empty {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--card-padding);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: var(--app-surface);
  }
  .ap-empty__icon  { width: 28px; height: 28px; color: var(--text-secondary); }
  .ap-empty__title { font-size: var(--text-headline); font-weight: var(--weight-semibold); }
  .ap-empty__body  { font-size: var(--text-subheadline); color: var(--text-secondary); }

  /* ══ Champs ═══════════════════════════════════════════════════════════════
     Libellé AU-DESSUS, et un exemple de saisie en gris. L'exemple ne doit
     jamais prendre la couleur du texte saisi : il se lirait comme une valeur
     déjà remplie — défaut vu et corrigé sur l'application. */

  .ap-field { display: flex; flex-direction: column; gap: var(--space-xs); }

  .ap-field__label { font-size: var(--text-caption); color: var(--text-secondary); }

  .ap-field__input {
    width: 100%;
    padding: 10px var(--space-sm);
    border: 1px solid transparent;
    border-radius: var(--field-radius);
    background: var(--app-background);
    font-family: inherit;
    font-size: var(--text-body);
    color: var(--text-primary);
  }
  .ap-field__input::placeholder { color: var(--text-secondary); opacity: 1; }
  .ap-field__input:focus-visible { border-color: var(--brand-primary); outline: none; }
  .ap-field__input[aria-invalid="true"] { border-color: var(--status-error); }

  .ap-field__input--code {
    font-size: var(--text-title);
    font-variant-numeric: tabular-nums;
    letter-spacing: .25em;
  }

  /* ══ Interrupteur ═════════════════════════════════════════════════════════ */

  .ap-toggle {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-md);
  }
  .ap-toggle__text  { display: flex; flex-direction: column; gap: var(--space-xs); }
  .ap-toggle__label { font-size: var(--text-subheadline); }
  .ap-toggle__hint  { font-size: var(--text-caption); color: var(--text-secondary); }

  .ap-toggle__switch {
    position: relative;
    flex: none;
    width: 51px;
    height: 31px;
    border: 0;
    border-radius: var(--pill-radius);
    background: var(--hairline-strong);
    cursor: pointer;
    transition: background-color var(--motion-soft);
  }
  .ap-toggle__switch::after {
    content: "";
    position: absolute;
    top: 2px; left: 2px;
    width: 27px; height: 27px;
    border-radius: 50%;
    background: #fff;
    transition: transform var(--motion-soft);
  }
  .ap-toggle__switch[aria-checked="true"] { background: var(--status-success); }
  .ap-toggle__switch[aria-checked="true"]::after { transform: translateX(20px); }

  /* La PLACE de chacun, posée explicitement.

     La grille est « 1fr auto » : sans cette règle, c'est l'ordre du balisage
     qui décide, et un interrupteur écrit AVANT son texte atterrit dans la
     colonne large — le libellé se retrouvait poussé au bord droit de la
     carte. Or une case à cocher se place volontiers en premier, parce
     qu'elle est ce que le `<label>` enveloppe. */
  .ap-toggle__text   { grid-column: 1; grid-row: 1; }
  .ap-toggle__switch { grid-column: 2; grid-row: 1; }

  /* ── Le même interrupteur, porté par une CASE À COCHER ───────────────────
     Le système le dessine comme un `<button role="switch">` : correct, mais
     il demande du script pour changer d'état. Dans un FORMULAIRE, c'est une
     case qu'il faut — elle part avec le reste, et fonctionne sans une ligne
     de JavaScript.

     Les deux doivent donc se ressembler, et ne le faisaient pas :
     `[aria-checked="true"]` ne s'applique JAMAIS à une case, si bien que
     l'état allumé n'existait pas ; et `::after` ne se dessine pas sur un
     élément remplacé, donc le bouton rond n'apparaissait pas. On obtenait un
     rectangle blanc, identique allumé ou éteint. */

  input.ap-toggle__switch {
    appearance: none;
    -webkit-appearance: none;
    margin: 0;
  }
  input.ap-toggle__switch:checked { background: var(--status-success); }
  input.ap-toggle__switch:checked::after { transform: translateX(20px); }
  input.ap-toggle__switch:disabled { opacity: .45; cursor: not-allowed; }

  /* Le clavier doit voir où il est : une case sans `appearance` perd son
     anneau par défaut. */
  .ap-toggle__switch:focus-visible {
    outline: 2px solid var(--brand-primary);
    outline-offset: 2px;
  }

  /* ══ Jauge de paramètre planétaire ════════════════════════════════════════
     Les cinq jauges de la colonie. Elles doivent montrer trois choses en même
     temps : la valeur, la CIBLE terrestre, et la TOLÉRANCE autour d'elle.
     Une barre nue ne dirait pas si l'on est « presque bon » ou « très loin ». */

  .ap-gauge { display: flex; flex-direction: column; gap: 6px; }

  .ap-gauge__head {
    display: grid;
    grid-template-columns: 22px 1fr auto;
    align-items: center;
    gap: var(--space-sm);
    font-size: var(--text-subheadline);
  }
  .ap-gauge__icon  { width: 18px; height: 18px; color: var(--brand-primary); }
  .ap-gauge__value { font-weight: var(--weight-medium); font-variant-numeric: tabular-nums; }

  .ap-gauge__track {
    position: relative;
    height: 10px;
    border-radius: var(--pill-radius);
    background: var(--hairline);
    overflow: hidden;
  }

  /* La bande de tolérance : l'endroit où le paramètre « compte comme atteint ».
     Positionnée par le code via --tolerance-start et --tolerance-width, en %. */
  .ap-gauge__tolerance {
    position: absolute;
    inset-block: 0;
    left: var(--tolerance-start, 40%);
    width: var(--tolerance-width, 20%);
    background: var(--status-success-soft);
  }

  .ap-gauge__fill {
    position: absolute;
    inset-block: 0;
    left: 0;
    width: var(--value, 0%);
    border-radius: inherit;
    background: var(--brand-primary);
    transition: width var(--motion-soft);
  }
  .ap-gauge--reached .ap-gauge__fill { background: var(--status-success); }

  /* Le repère de la valeur terrestre. Un trait, pas une flèche : il doit se
     lire sans légende. */
  .ap-gauge__target {
    position: absolute;
    inset-block: -3px;
    left: var(--target, 50%);
    width: 2px;
    background: var(--text-primary);
    border-radius: 1px;
  }

  .ap-gauge__footer {
    display: flex;
    justify-content: space-between;
    font-size: var(--text-caption);
    color: var(--text-secondary);
  }

  /* ══ Cadran d'observation ═════════════════════════════════════════════════
     Le temps restant, en anneau. `--progress` va de 0 à 1.
     `conic-gradient` évite un SVG et se met à jour sans redessiner. */

  .ap-dial {
    position: relative;
    display: grid;
    place-items: center;
    width: var(--dial-size, 132px);
    aspect-ratio: 1;
    border-radius: 50%;
    background:
      conic-gradient(var(--brand-primary) calc(var(--progress, 0) * 360deg),
                     var(--hairline) 0);
    transition: background var(--motion-soft);
  }
  /* Le trou du milieu. Un masque serait plus propre, mais un disque posé
     dessus marche partout. */
  .ap-dial::before {
    content: "";
    position: absolute;
    inset: 9px;
    border-radius: 50%;
    background: var(--app-surface);
  }
  .ap-dial__content {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
  }
  .ap-dial__time {
    font-size: var(--text-title);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
  }
  .ap-dial__label { font-size: var(--text-caption); color: var(--text-secondary); }

  /* ══ Créneau météo ════════════════════════════════════════════════════════
     La prévision : 1, 3 ou 6 créneaux selon la station météo possédée. */

  .ap-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-xs);
    min-width: 64px;
    padding: var(--space-sm);
    border: 1px solid var(--hairline);
    border-radius: 10px;
    font-size: var(--text-caption);
  }
  .ap-slot__icon { width: 22px; height: 22px; }
  .ap-slot__hour { color: var(--text-secondary); font-variant-numeric: tabular-nums; }

  .ap-slot--clear    { color: var(--brand-primary); }
  .ap-slot--hazy     { color: var(--brand-secondary); }
  .ap-slot--cloudy   { color: var(--text-secondary); }
  /* Un créneau couvert REFUSE l'observation : il doit se voir comme un refus,
     pas comme une nuance. */
  .ap-slot--overcast { color: var(--status-error); border-color: var(--status-error-soft); }

  .ap-forecast { display: flex; gap: var(--space-sm); flex-wrap: wrap; }

  /* ══ Repère de la carte du ciel ═══════════════════════════════════════════
     Positionné par le code via --sky-x et --sky-y (0 à 1). La taille suit la
     magnitude : un objet brillant est un point plus gros. */

  .ap-marker {
    position: absolute;
    left: calc(var(--sky-x) * 100%);
    top: calc(var(--sky-y) * 100%);
    translate: -50% -50%;
    display: grid;
    place-items: center;
    width: var(--marker-size, 14px);
    aspect-ratio: 1;
    border: 0;
    border-radius: 50%;
    background: currentColor;
    color: var(--type-star);
    cursor: pointer;
    box-shadow: 0 0 calc(var(--marker-size, 14px) * .9) color-mix(in srgb, currentColor 55%, transparent);
    transition: scale var(--motion-soft);
  }
  .ap-marker:hover { scale: 1.25; }

  /* La cible tactile, invisible et généreuse. Un point de 14 px ne se touche
     pas au doigt ; l'agrandir le rendrait illisible sur une carte chargée. La
     zone est donc séparée du dessin — c'est ce que `contentShape` fait sur
     iOS. */
  .ap-marker::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    translate: -50% -50%;
    width: 44px;
    height: 44px;
    border-radius: 50%;
  }

  /* Un objet hors de portée reste AFFICHÉ, mais grisé. Voir ce qui manque
     motive l'amélioration suivante ; le masquer ne dit rien. */
  .ap-marker--unavailable {
    color: var(--text-secondary);
    opacity: .38;
    box-shadow: none;
  }

  .ap-marker--selected {
    outline: 2px solid var(--text-primary);
    outline-offset: 3px;
  }

  /* L'étiquette du repère, montrée au survol ou à la sélection. */
  .ap-marker__name {
    position: absolute;
    top: calc(100% + 6px);
    left: 50%;
    translate: -50% 0;
    padding: 2px 7px;
    border-radius: var(--pill-radius);
    background: color-mix(in srgb, var(--app-background) 88%, transparent);
    color: var(--text-primary);
    font-size: var(--text-caption);
    white-space: nowrap;
    pointer-events: none;
  }

  /* Le plan de situation, en bas à droite : il montre le ciel entier et encadre
     la part visible. OBLIGATOIRE — une carte plus grande que l'écran rend le
     fait de se perdre possible. */
  .ap-minimap {
    position: absolute;
    right: var(--space-md);
    bottom: var(--space-md);
    width: 116px;
    aspect-ratio: 1;
    border: 1px solid var(--hairline-strong);
    border-radius: 10px;
    background: color-mix(in srgb, var(--sky-deep) 78%, transparent);
    backdrop-filter: blur(8px);
  }
  .ap-minimap__window {
    position: absolute;
    left: calc(var(--win-x, 0) * 100%);
    top: calc(var(--win-y, 0) * 100%);
    width: calc(var(--win-w, 1) * 100%);
    height: calc(var(--win-h, 1) * 100%);
    border: 1px solid var(--brand-primary);
    border-radius: 3px;
    background: var(--brand-primary-soft);
  }

  /* ══ Disque solaire ═══════════════════════════════════════════════════════
     Les régions actives sont posées par --disc-x et --disc-y, de -1 à 1, DÉJÀ
     raccourcies par la perspective (voir la spécification § 5.9). */

  .ap-sun {
    position: relative;
    width: var(--sun-size, 280px);
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(circle at 42% 38%, #FFE9B8, #FAC24A 62%, #E09A18);
    box-shadow: 0 0 60px rgba(250, 194, 74, .35);
  }
  .ap-sun__region {
    position: absolute;
    left: calc(50% + var(--disc-x) * 50%);
    top:  calc(50% + var(--disc-y) * 50%);
    translate: -50% -50%;
    width: var(--region-size, 12px);
    aspect-ratio: 1;
    border-radius: 50%;
    background: #4A2C06;
    opacity: .82;
    cursor: pointer;
  }
  .ap-sun__region--tracked { outline: 2px solid var(--app-background); outline-offset: 1px; }

  /* ══ Panneau ══════════════════════════════════════════════════════════════
     Pour l'analyse, la mise au point, une fiche. RÈGLE DU JEU : deux panneaux
     ne peuvent PAS s'ouvrir l'un sur l'autre. S'il y en a plusieurs à montrer,
     ils défilent un par un. */

  .ap-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    background: var(--overlay);
    backdrop-filter: blur(4px);
  }

  .ap-sheet {
    position: fixed;
    z-index: var(--z-sheet);
    inset-inline: 0;
    bottom: 0;
    max-height: 92dvh;
    display: flex;
    flex-direction: column;
    border-radius: var(--sheet-radius) var(--sheet-radius) 0 0;
    background: var(--app-background);
    box-shadow: var(--shadow-sheet);
  }

  /* Sur écran large, le panneau devient une fenêtre centrée : un bandeau
     collé en bas d'un écran de 27 pouces n'a plus de sens. */
  @media (min-width: 1000px) {
    .ap-sheet {
      inset: 50% auto auto 50%;
      translate: -50% -50%;
      width: min(680px, 92vw);
      max-height: 86dvh;
      border-radius: var(--sheet-radius);
    }
  }

  .ap-sheet__head {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-md) var(--gutter);
    border-bottom: 1px solid var(--hairline);
  }
  .ap-sheet__title { flex: 1; text-align: center; font-size: var(--text-headline);
                     font-weight: var(--weight-semibold); }
  .ap-sheet__body  { padding: var(--space-md) var(--gutter) var(--space-xl);
                     overflow-y: auto; }

  /* ══ Boîte de confirmation ════════════════════════════════════════════════
     Elle dit TOUJOURS ce qui est perdu. « Annuler cette observation » sans
     préciser que l'énergie dépensée ne revient pas serait un piège. */

  .ap-dialog {
    position: fixed;
    z-index: var(--z-sheet);
    inset: 50% auto auto 50%;
    translate: -50% -50%;
    width: min(420px, 92vw);
    padding: var(--space-lg);
    border: 1px solid var(--hairline-strong);
    border-radius: var(--sheet-radius);
    background: var(--app-surface);
    box-shadow: var(--shadow-popover);
  }
  .ap-dialog__title   { font-size: var(--text-headline); font-weight: var(--weight-semibold); }
  .ap-dialog__message { margin-top: var(--space-sm); font-size: var(--text-subheadline);
                        color: var(--text-secondary); }
  .ap-dialog__actions { margin-top: var(--space-lg); display: flex; flex-direction: column;
                        gap: var(--space-sm); }

  /* ══ Séparateur ═══════════════════════════════════════════════════════════ */

  .ap-divider { height: 1px; border: 0; margin: 0; background: var(--hairline); }

}
