/* ============================================================================
   ASTROPOST — Ce que l'application ajoute au système de design
   ----------------------------------------------------------------------------
   LA RÈGLE DE COHABITATION, et il faut la lire avant de toucher à ce fichier.

   Le système (`design/css/`) est enveloppé dans `@layer astropost`. Une règle
   NON couchée l'emporte donc sur lui, quelle que soit sa spécificité — c'est
   ainsi qu'une barre latérale masquée par le système restait visible, et que
   l'écran affichait à la fois la barre latérale et la barre d'onglets.

   Ce fichier déclare donc l'ordre des couches, puis se range dans la sienne :

       @layer astropost, app;

   `app` passe après `astropost` : ce qu'on écrit ici l'emporte, mais **on le
   sait**, et le système reprend la main dès qu'on retire une règle. Plus
   d'écrasement silencieux.

   On ne trouve donc ici que ce que le système ne porte pas :
     · la carte du ciel et son plan de situation ;
     · l'oculaire de mise au point ;
     · les messages, les listes, les titres d'écran ;
     · quelques réglages d'aération.

   RÈGLE HÉRITÉE DES JETONS : aucune couleur, aucun espacement, aucune durée
   écrits en dur. Tout passe par les variables de `01-jetons.css`.
   ========================================================================= */

@layer astropost, app;

@layer app {

  /* ══ Aération ═════════════════════════════════════════════════════════════
     Le ton du jeu est contemplatif : les écrans respirent plutôt qu'ils ne se
     remplissent. Une seule règle d'espacement vertical, et rien d'autre. */

  /* L'espacement d'un écran se fait en FLUX DE BLOC, et non en flex.
     Les grilles du système portent `margin-inline: auto` pour se centrer
     quand elles sont étroites ; dans un conteneur flex, cette marge
     automatique l'emporte sur l'étirement et la grille se réduit à la
     largeur d'une seule carte. En flux de bloc, elle se résout à zéro et la
     grille occupe toute la largeur — ce qu'on attend d'elle. */
  .ap-screen > * + * { margin-block-start: var(--space-lg); }
  .ap-screen-title { display: flex; flex-direction: column; gap: var(--space-xs); }

  /* ══ Une pile de cartes qui ne laisse pas de trou ═══════════════════════
     Une grille aligne ses lignes : dès que deux cartes voisines n'ont pas la
     même hauteur, la plus courte laisse un vide sous elle jusqu'à la ligne
     suivante. Sur un écran large, cela faisait des trous d'un demi-écran.

     Le flux en colonnes, lui, tasse les cartes verticalement : chacune
     commence où la précédente s'arrête. La lecture se fait colonne par
     colonne — ce qui convient à des cartes indépendantes, et seulement à
     celles-là. Pour une suite qui se lit dans l'ordre, la grille reste le
     bon outil.

     `break-inside: avoid` est ce qui empêche une carte d'être coupée en deux
     par un saut de colonne. Sans lui, le titre resterait en bas d'une colonne
     et le contenu passerait dans la suivante. */

  .ap-masonry {
    column-width: 330px;
    column-gap: var(--space-md);
  }
  .ap-masonry > * {
    display: block;
    width: 100%;
    break-inside: avoid;
    -webkit-column-break-inside: avoid;
    margin-bottom: var(--space-md);
  }

  /* ── Et le trou que le flux en colonnes fait quand même ──────────────────
     Le flux en colonnes ÉQUILIBRE PAR LA HAUTEUR : il remplit chaque colonne
     jusqu'à une hauteur cible, puis passe à la suivante. Avec peu de cartes
     de hauteurs très inégales, il n'a plus rien à mettre dans les dernières —
     et les laisse vides.

     Mesuré sur l'écran Communauté, à 1 700 px : trois cartes de 564, 197 et
     246 px, un conteneur de 1 368 px, donc quatre colonnes possibles. Le
     flux en met une dans la première, deux dans la deuxième, et abandonne
     686 px — la moitié droite de l'écran. Forcer `column-count: 3` n'y
     change rien : l'équilibrage recommence et laisse la troisième vide.

     La grille, elle, ne laisse de trou qu'à partir de DEUX lignes — et avec
     trois cartes ou moins sur un écran large, il n'y en a qu'une. C'est donc
     exactement le cas où la grille reprend l'avantage.

     `:nth-child(-n+3):last-child` se lit : « le dernier enfant est aussi l'un
     des trois premiers », c'est-à-dire trois cartes au plus. `auto-fit`
     replie les colonnes vides, et `minmax(330px, 1fr)` ramène à une seule
     colonne sur un téléphone — la même borne que le flux. */

  .ap-masonry:has(> :nth-child(-n+3):last-child) {
    columns: auto;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
    gap: var(--space-md);
    align-items: start;
  }
  .ap-masonry:has(> :nth-child(-n+3):last-child) > * {
    margin-bottom: 0;
  }

  /* Et le cas d'une SEULE carte, que la grille étirerait sur toute la
     largeur : `auto-fit` replie les colonnes vides, et l'unique carte prend
     alors les 1 368 px. Une ligne de texte aussi longue ne se lit pas. Elle
     revient donc à la largeur de lecture, centrée — la même borne que partout
     ailleurs dans le jeu. */
  .ap-masonry:has(> :first-child:last-child) {
    /* `width: 100%` avant la borne, pour la même raison que `.ap-card-flow`
       plus haut : dans une pile en colonne, une marge automatique absorbe la
       place libre et fait tomber l'étirement — la carte se réduit alors à sa
       largeur intrinsèque, 373 px au lieu de 660. */
    width: 100%;
    max-width: var(--reading-width);
    margin-inline: auto;
  }

  /* Même raison, pour les grilles imbriquées dans une pile : une largeur
     explicite fait tomber les marges automatiques à zéro, et la grille
     s'étale. Sa borne de lecture continue de jouer quand la place manque. */
  .ap-card-flow, .ap-tile-grid { width: 100%; }
  .ap-screen-padding { padding-block: var(--space-lg) var(--space-xl); }

  .ap-section-title {
    font-size: var(--text-footnote);
    font-weight: var(--weight-semibold);
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-secondary);
  }

  /* La zone défilante doit être LE défilement, et non un bloc qui pousse la
     page : sans cela, `overscroll-behavior: contain` bloque la molette sans
     rien faire défiler. */
  .ap-shell { height: 100vh; height: 100dvh; }
  .ap-shell[data-layout="compact"] { grid-template-columns: minmax(0, 1fr); }
  .ap-content { display: flex; flex-direction: column; min-height: 0; }
  .ap-scroll { flex: 1 1 auto; min-height: 0; }

  /* ══ La tête du profil ════════════════════════════════════════════════════
     Le score est le résumé de la partie : il se lit d'un coup d'œil, à côté
     du nom de l'observatoire, et non serré au fond d'une colonne. */

  .ap-profile-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-lg);
  }
  .ap-profile-head__identity {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    min-width: min(100%, 280px);
    flex: 1 1 320px;
  }
  /* L'avatar et le nom sur la même ligne : l'avatar est une identité, pas
     une vignette posée au-dessus du titre. `min-width: 0` sur le titre pour
     qu'un nom long se replie au lieu de pousser l'avatar hors de la carte. */
  .ap-profile-head__title-row {
    display: flex;
    align-items: center;
    gap: var(--space-md);
  }
  .ap-profile-head__title-row > h1 {
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .ap-profile-head__score {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    min-width: 220px;
    flex: 0 1 260px;
  }
  .ap-profile-head__total {
    font-size: var(--text-large-title);
    font-weight: var(--weight-semibold);
    line-height: 1;
  }

  /* Un code se dicte au téléphone : il se lit en grand, en chasse fixe, et
     les caractères sont écartés pour qu'on ne confonde rien. */
  .ap-profile-code {
    /* Un code se DICTE : il doit tenir sur une ligne, faute de quoi on le
       lit en deux morceaux et on se trompe. Depuis que la carte vit dans
       une colonne d'appoint de 287 px, la taille fixe le coupait en deux.
       `clamp` le laisse rétrécir jusqu'à ce qu'il entre, sans jamais
       descendre sous une taille lisible. */
    font-size: clamp(var(--text-headline), 7cqi, var(--text-large-title));
    font-weight: var(--weight-semibold);
    letter-spacing: .12em;
    white-space: nowrap;
  }

  /* ══ Un graphique à barres, servi par le serveur ══════════════════════════
     Aucune bibliothèque : le squelette est calculé côté serveur et le dessin
     tient en trois éléments. C'est le même choix que la carte du ciel et la
     courbe solaire.

     LA COULEUR NE PORTE PAS L'IDENTITÉ. Les teintes par type d'astre ont été
     passées au contrôle de lisibilité : « galaxie » et « nébuleuse » sont à
     ΔE 2,4 en deutéranopie — indiscernables. C'est donc le NOM écrit à côté
     qui identifie, et toutes les barres portent le même ton. La pastille de
     couleur reste accolée au texte, pour le lien avec la carte du ciel.

     La barre est FINE et son extrémité arrondie de 4 px : une barre épaisse
     donne du poids à ce qui n'en a pas. */

  .ap-chart {
    display: grid;
    gap: var(--space-xs);
  }

  .ap-chart__row {
    display: grid;
    grid-template-columns: minmax(0, 8.5rem) minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-sm);
  }

  .ap-chart__label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-caption);
    color: var(--text-secondary);
  }

  .ap-chart__dot {
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
    color: var(--text-secondary);
  }
  .ap-chart__dot--warning   { color: var(--type-star); }
  .ap-chart__dot--success   { color: var(--type-planet); }
  .ap-chart__dot--secondary { color: var(--type-nebula); }
  .ap-chart__dot--primary   { color: var(--type-galaxy); }
  .ap-chart__dot--glow      { color: var(--type-cluster); }
  .ap-chart__dot--exoplanet { color: var(--type-exoplanet); }

  /* La piste est un repère discret, pas un cadre : elle dit où finit
     l'échelle sans se disputer la lecture avec la barre. */
  .ap-chart__bar {
    position: relative;
    display: block;
    height: 8px;
    border-radius: var(--pill-radius);
    background: color-mix(in srgb, var(--text-secondary) 14%, transparent);
    overflow: hidden;
  }

  .ap-chart__fill {
    display: block;
    height: 100%;
    border-radius: var(--pill-radius);
    background: var(--brand-primary);
    /* Une barre à zéro reste visible : un trait fin vaut mieux qu'un vide
       qu'on prend pour une donnée manquante. */
    min-width: 2px;
  }

  .ap-chart__value {
    font-size: var(--text-caption);
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
  }

  /* ══ Messages ═════════════════════════════════════════════════════════════
     Le retour d'une action : réussite, ou blocage avec sa cause. */

  .ap-notices { display: flex; flex-direction: column; gap: var(--space-sm); }
  .ap-notice {
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--card-radius);
    font-size: var(--text-subheadline);
    background: var(--app-surface);
  }
  .ap-notice--success { background: var(--status-success-soft); color: var(--status-success); }
  .ap-notice--error   { background: var(--status-error-soft);   color: var(--status-error); }
  .ap-notice--info    { background: var(--brand-primary-soft);  color: var(--brand-primary); }

  /* ══ Liste ════════════════════════════════════════════════════════════════
     Des lignes séparées d'un filet, sans carte autour de chacune : une pile de
     cartes pour une liste de dix éléments fait un escalier. */

  .ap-list { display: flex; flex-direction: column; }
  .ap-list__row {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding-block: var(--space-md);
    color: inherit;
    text-decoration: none;
  }
  .ap-list__row + .ap-list__row { border-top: 1px solid var(--hairline); }
  a.ap-list__row:hover { text-decoration: none; color: inherit; }

  .ap-field__error { font-size: var(--text-footnote); color: var(--status-error); }

  /* Un formulaire de coordonnées se lit en colonnes : la latitude, la
     longitude et l'altitude tiennent sur une ligne, et le nom du lieu prend
     toute la largeur. Sur un téléphone, tout se remet à la queue leu leu. */
  .ap-field-grid {
    display: grid;
    gap: var(--space-md);
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  }
  .ap-field-grid .ap-field--wide { grid-column: 1 / -1; }

  /* Un panneau qu'on déplie. Le réglage du lieu vit sur la carte du site,
     mais replié : on regarde son lieu plus souvent qu'on ne le corrige. */
  .ap-details__summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 32px;
    font-size: var(--text-subheadline);
    color: var(--brand-primary);
  }
  .ap-details__summary::-webkit-details-marker { display: none; }
  .ap-details__summary::before {
    content: "";
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-block: 4px solid transparent;
    transition: transform var(--motion-soft);
  }
  .ap-details[open] > .ap-details__summary::before { transform: rotate(90deg); }

  /* ══ La carte du ciel ═════════════════════════════════════════════════════
     Le seul écran qui gagne à occuper tout le cadre. La toile fait 2,2 fois le
     cadre dans les deux sens : le ciel a une étendue à explorer, et les
     repères ne se chevauchent pas. */

  .ap-sky-map {
    position: absolute;
    top: 0;
    left: 0;
    will-change: transform;
  }

  /* La coupole : un carré centré dans la toile, qui porte l'horizon, les
     cercles de hauteur, les points cardinaux et les repères. Tout y est
     positionné en pourcentage, donc la coupole peut changer de taille sans
     qu'aucune coordonnée bouge. */
  .ap-sky-dome { position: absolute; }

  .ap-sky-horizon { position: absolute; inset: 0; width: 100%; height: 100%; }
  .ap-sky-horizon__ring {
    fill: none;
    stroke: var(--hairline);
    stroke-width: 1;
  }
  /* L'horizon est le seul trait qui compte vraiment : au-delà, l'astre est
     sous terre. Il est donc plus marqué que les cercles de hauteur. */
  .ap-sky-horizon__line {
    fill: none;
    stroke: var(--hairline-strong);
    stroke-width: 1.5;
    stroke-dasharray: 6 5;
  }
  .ap-sky-horizon__zenith { stroke: var(--text-secondary); stroke-width: 1.5; opacity: .5; }
  .ap-sky-horizon__degree {
    fill: var(--text-secondary);
    font-size: 15px;
    text-anchor: middle;
    opacity: .55;
  }

  /* Les points cardinaux vivent DANS la toile, qui se réduit à l'échelle
     « Tout voir » : une taille de texte ordinaire y deviendrait illisible.
     Ils sont donc dessinés grands, pour rester lisibles à toutes les
     échelles — ce sont les repères qui tiennent la carte. */
  .ap-sky-cardinal {
    position: absolute;
    transform: translate(-50%, -50%);
    font-size: 2rem;
    font-weight: var(--weight-semibold);
    color: var(--text-secondary);
    letter-spacing: .08em;
    pointer-events: none;
  }

  /* Sous l'horizon : le repère reste, en retrait. Voir qu'un astre existe, et
     de quel côté il reviendra, fait partie du jeu. */
  .ap-marker--below { opacity: .3; }
  .ap-marker--below .ap-marker__name { font-style: italic; }



  /* ── Le menu « Plus », sur téléphone ────────────────────────────────────
     Sept onglets sur une largeur de téléphone font cinquante pixels chacun :
     « Observatoire » et « Exoplanètes » y deviennent illisibles. Quatre
     onglets restent donc visibles — ceux de la boucle de jeu — et le
     cinquième ouvre le reste.

     Il y gagne Profil et Réglages, qui n'existaient QUE dans la barre
     latérale : sur téléphone, ils étaient inaccessibles. */

  .ap-tabbar__more {
    background: none;
    border: 0;
    font: inherit;
    cursor: pointer;
  }

  .ap-more-scrim {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: color-mix(in srgb, var(--sky-deep) 62%, transparent);
    backdrop-filter: blur(2px);
    animation: ap-scrim-in var(--motion-soft) both;
  }
  @keyframes ap-scrim-in { from { opacity: 0 } to { opacity: 1 } }

  .ap-more-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 41;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--gutter)
             calc(var(--space-lg) + env(safe-area-inset-bottom));
    border-radius: var(--sheet-radius) var(--sheet-radius) 0 0;
    background: color-mix(in srgb, var(--app-surface) 96%, transparent);
    backdrop-filter: blur(24px);
    border-top: 1px solid var(--hairline);
    box-shadow: var(--shadow-sheet);
    /* Le panneau MONTE depuis le bas : c'est le geste qu'on vient de faire
       en touchant la barre, et l'animation le prolonge au lieu de le
       contredire. Sous `prefers-reduced-motion`, le socle la réduit à une
       milliseconde — on ne la désactive pas ici, il le fait déjà. */
    animation: ap-sheet-up var(--motion-soft) cubic-bezier(.22, .9, .3, 1) both;
  }
  @keyframes ap-sheet-up {
    from { transform: translateY(100%); opacity: .4 }
    to   { transform: translateY(0);    opacity: 1 }
  }

  .ap-more-sheet__grip {
    width: 38px;
    height: 4px;
    margin: 2px auto var(--space-xs);
    border-radius: 999px;
    background: var(--hairline);
  }

  .ap-more-sheet__list { display: flex; flex-direction: column; }

  .ap-more-item {
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr);
    align-items: center;
    column-gap: var(--space-md);
    padding: var(--space-sm) var(--space-xs);
    border-top: 1px solid color-mix(in srgb, var(--hairline) 55%, transparent);
    text-decoration: none;
    min-height: 56px;
  }
  .ap-more-item:first-child { border-top: 0; }
  .ap-more-item:hover { text-decoration: none; }

  .ap-more-item__icon { width: 22px; height: 22px; grid-row: span 2; }
  .ap-more-item__label { font-weight: var(--weight-semibold); }
  .ap-more-item__hint {
    grid-column: 2;
    font-size: var(--text-caption);
    color: var(--text-secondary);
  }

  .ap-more-item--current { color: var(--brand-primary); }
  .ap-more-item--current .ap-more-item__hint { color: var(--brand-primary); opacity: .75; }

  /* Au large, la barre latérale porte tout : le panneau n'a plus lieu
     d'être, et le laisser ouvrable créerait deux chemins vers le même
     écran. */
  .ap-shell[data-layout="regular"] ~ .ap-more-sheet,
  .ap-shell[data-layout="regular"] ~ .ap-more-scrim { display: none; }



  /* ── La photo d'un lieu ─────────────────────────────────────────────────
     Elle vient de Wikimedia Commons, sous licence libre. Ces licences
     EXIGENT l'attribution : le crédit n'est pas une politesse, c'est la
     condition du droit d'usage. Un lieu sans attribution n'a pas d'image.

     `referrerpolicy="no-referrer"` sur la balise : sans cela, le serveur de
     Wikimedia apprend quelle page du jeu le joueur regarde. */

  .ap-site-figure {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
  }

  /* ── Le cadrage de la photo du joueur ───────────────────────────────────
     Le bandeau est plus large que haut : `object-fit: cover` rogne. Centré
     d'office, il coupait une fois sur deux ce qu'on voulait montrer — un
     ciel se cadre en haut, une coupole au milieu, un horizon en bas.

     Le point à garder visible vient du serveur, posé en variables sur la
     figure. Le repli à 50 % n'est pas décoratif : une photo servie sans
     cadrage enregistré doit rester centrée, comme avant. */
  .ap-site-figure--cadrable .ap-site-photo {
    object-position: var(--cadrage-x, 50%) var(--cadrage-y, 50%);
  }

  /* Le cadre prend la FORME de l'image, entre deux bornes.
     Une hauteur fixe réduisait une photo en hauteur à une fente, et l'on
     cadrait un portrait dans une meurtrière. Les bornes restent : un bandeau
     reste un bandeau, et une photo de 3 000 px de haut n'a pas à pousser
     toute la page sous la ligne de flottaison.

     `aspect-ratio` l'emporte sur `max-height` parce que la hauteur devient
     une conséquence de la largeur ; le plafond reste utile pour le cas où le
     rapport manque — une photo déposée avant ce relevé. */
  .ap-site-figure--cadrable .ap-site-photo-frame {
    aspect-ratio: var(--photo-ratio, 2.4);
    max-height: 60vh;
  }
  .ap-site-figure--cadrable .ap-site-photo-frame .ap-site-photo {
    width: 100%;
    height: 100%;
    max-height: none;

    /* `contain` ET NON `cover`, et c'est tout l'enjeu du recul.

       Avec `cover`, l'image est d'abord rognée pour remplir : la réduire
       ensuite ne montre pas plus de photo, elle montre la même chose en
       plus petit. On ne pouvait donc jamais revoir l'image entière.

       Avec `contain`, l'image tient tout entière dans le cadre à 100 %, et
       l'agrandissement la fait déborder au-delà. Reculer découvre les
       bords ; avancer rogne. Le cadre prenant déjà la FORME de l'image, les
       deux coïncident pour la plupart des photos — seules les très hautes
       ou très larges, celles que les bornes ramènent, laissent paraître une
       marge, et le réglage sert précisément à la combler. */
    object-fit: contain;

    /* L'agrandissement part du POINT CADRÉ, pas du centre : on vise d'abord,
       on grossit ensuite, et ce qu'on visait reste sous les yeux. Grossir
       depuis le centre aurait chassé la cible hors du cadre à chaque cran. */
    scale: calc(var(--photo-zoom, 100%) / 100%);
    transform-origin: var(--cadrage-x, 50%) var(--cadrage-y, 50%);
  }
  /* Sans cela, l'image agrandie déborde du cadre et recouvre la page.
     Le fond se voit quand on recule : il vaut mieux qu'il soit celui d'une
     carte qu'un trou noir. */
  .ap-site-figure--cadrable .ap-site-photo-frame {
    overflow: hidden;
    background: var(--app-surface);
    border-radius: var(--card-radius);
  }

  /* On peut l'attraper. `touch-action: none` est ce qui rend le glissement
     possible au doigt : sans lui le navigateur prend le geste pour un
     défilement et coupe la série d'événements en plein milieu. */
  .ap-site-figure--cadrable .ap-site-photo { cursor: grab; touch-action: none; }
  .ap-site-figure--cadrable[data-cadrage="en-cours"] .ap-site-photo { cursor: grabbing; }

  /* Pendant le geste, une croix dit OÙ l'on vise. Sans elle on déplace une
     image sans savoir quel point on désigne, et on règle à tâtons.

     Elle se place sur le CADRE de l'image, et non sur la figure : la légende
     fait partie de la figure, et le point aurait glissé de sa hauteur. */
  .ap-site-photo-frame { position: relative; display: block; }

  /* ── Un relevé solaire, avec ce que son instrument voit ─────────────────
     La liste disait « H-α » en trois caractères. Or l'instrument ne change
     pas une teinte, il change de COUCHE du Soleil — et une couche, cela se
     montre. */
  .ap-releve {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
  }
  .ap-releve__vue {
    flex: none;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    object-fit: cover;
    /* Le disque solaire ne touche pas les bords de l'image publiée : sans ce
       léger agrandissement, la vignette ronde montrerait surtout du noir. */
    object-position: center;
    scale: 1.18;
    background: var(--sky-deep);
    border: 1px solid var(--hairline);
  }
  .ap-releve__corps { min-width: 0; }
  .ap-releve__instrument {
    font-size: var(--text-footnote);
    font-weight: var(--weight-semibold);
  }

  /* ── Une colonne qui ne s'ouvre que si elle a de quoi ───────────────────
     Le tableau du Soleil réserve une colonne de droite. Quand le carnet est
     vide, elle n'y porte qu'une carte de quatre tuiles — et laisse sous elle
     un demi-écran de vide, exactement le défaut déjà corrigé ailleurs.

     `:has()` regarde le contenu avant de décider : une seule carte, et la
     grille reste sur une colonne. Rien à dupliquer dans le gabarit, et la
     colonne revient d'elle-même au premier relevé. */
  @container screen (min-width: 1000px) {
    .ap-dashboard:has(> .ap-card-flow > :first-child:last-child) {
      grid-template-columns: minmax(0, 1fr);
    }
  }

  /* ── Changer la photo, depuis la photo ──────────────────────────────────
     Le seul moyen de la remplacer était un champ de fichier dans la colonne
     de droite, à côté d'un paragraphe. On regarde la photo, on décide
     qu'elle ne convient pas, et il fallait aller chercher ailleurs.

     C'est une `<label>` maquillée en bouton, pas un bouton : un libellé
     ouvre le sélecteur de fichier sans une ligne de script. Elle en a donc
     tous les signes — le pointeur, l'anneau de focus, l'état pressé — parce
     qu'elle n'en a aucun d'origine. */
  .ap-photo-action {
    position: absolute;
    inset-block-start: var(--space-sm);
    inset-inline-end: var(--space-sm);
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 12px;
    border-radius: var(--pill-radius);
    border: 1px solid color-mix(in srgb, var(--text-primary) 22%, transparent);
    /* Le fond est sombre et flouté plutôt qu'opaque : posé sur une photo
       dont on ne sait rien, un aplat clair disparaîtrait sur un ciel clair.
       Le flou garantit le contraste quelle que soit l'image dessous. */
    background: color-mix(in srgb, var(--sky-deep) 72%, transparent);
    backdrop-filter: blur(10px);
    color: var(--text-primary);
    font-size: var(--text-footnote);
    font-weight: var(--weight-semibold);
    cursor: pointer;
    transition: background var(--duration-fast, 120ms) ease;
  }
  .ap-photo-action:hover {
    background: color-mix(in srgb, var(--sky-deep) 88%, transparent);
  }
  .ap-photo-action:active { transform: translateY(1px); }
  /* Le champ qu'elle commande est caché, mais il reste ATTEIGNABLE AU
     CLAVIER : `display: none` l'aurait sorti de l'ordre de tabulation, et
     le libellé n'aurait plus eu de cible à activer. On le rend donc
     invisible sans le retirer, et le libellé porte l'anneau de focus à sa
     place. */
  .ap-photo-action:has(~ :focus-visible),
  .ap-photo-action:focus-visible {
    outline: 2px solid var(--brand-primary);
    outline-offset: 2px;
  }
  .ap-photo-action__icone { width: 16px; height: 16px; flex: none; }
  /* C'est un lien : il ne doit pas porter le soulignement des liens de
     texte, ni leur couleur. */
  a.ap-photo-action { text-decoration: none; }

  /* Les réglages à gauche, ce qu'il advient de l'image à droite.
     L'avertissement occupait toute la largeur SOUS les curseurs : on le
     lisait après avoir décidé, donc trop tard. À côté d'eux, il est sous les
     yeux pendant qu'on règle.

     Deux colonnes seulement quand il y a la place : en dessous, elles se
     remettent l'une sous l'autre, et la commande passe la première. */
  .ap-photo-reglages {
    display: grid;
    gap: var(--space-md);
    grid-template-columns: 1fr;
  }
  @container screen (min-width: 720px) {
    .ap-photo-reglages { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  }

  /* ── L'éditeur d'avatar ─────────────────────────────────────────────────
     L'aperçu est ROND et CARRÉ, comme l'avatar le sera partout : régler un
     cadrage sur une forme différente de celle où il s'appliquera reviendrait
     à régler autre chose. */
  .ap-avatar-editeur {
    display: grid;
    gap: var(--space-md);
    grid-template-columns: 1fr;
    align-items: start;
  }
  @container screen (min-width: 720px) {
    .ap-avatar-editeur { grid-template-columns: 260px minmax(0, 1fr); }
  }
  .ap-avatar-figure { max-width: 260px; }
  .ap-avatar-figure .ap-site-photo-frame { border-radius: 50%; }
  /* La croix de visée resterait carrée dans un cadre rond. */
  .ap-avatar-figure .ap-photo-action {
    inset-block-start: auto;
    inset-block-end: 6px;
    inset-inline-end: 6px;
  }

  /* La pastille d'appareil photo sur l'avatar du profil : une image sur
     laquelle on peut cliquer doit annoncer qu'elle est cliquable, sinon
     personne ne l'essaie. */
  .ap-avatar--modifiable { position: relative; text-decoration: none; }
  .ap-avatar__badge {
    position: absolute;
    inset-block-end: -2px;
    inset-inline-end: -2px;
    width: 28px;
    height: 28px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--app-surface);
    border: 1px solid var(--hairline-strong);
    color: var(--text-primary);
  }
  .ap-avatar__badge svg { width: 15px; height: 15px; }

  /* L'avatar quand il porte une image : le fond dégradé et les initiales
     n'ont plus lieu d'être. */
  .ap-avatar--photo {
    background: var(--app-surface) center / cover no-repeat;
    background-image: var(--avatar-image);
    color: transparent;
  }

  /* Le cadre en attente, quand aucune photo n'a encore été déposée. Un trou
     noir n'aurait rien dit ; celui-ci annonce ce qu'il attend. */
  .ap-photo-vide {
    display: grid;
    place-items: center;
    min-height: 220px;
    padding: var(--space-xl) var(--space-md);
    text-align: center;
    color: var(--text-secondary);
    font-size: var(--text-footnote);
    border: 1px dashed var(--hairline-strong);
    border-radius: var(--card-radius);
    background: color-mix(in srgb, var(--app-surface) 60%, transparent);
  }

  /* Sur un téléphone, le libellé se réduit à son icône : « Modifier la
     photo » posé sur une image de 343 px de large en mangeait le tiers. */
  .ap-shell[data-layout="compact"] .ap-photo-action span {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .ap-shell[data-layout="compact"] .ap-photo-action { padding: 9px; }
  .ap-site-photo-frame::after {
    content: "";
    position: absolute;
    left: var(--cadrage-x, 50%);
    top: var(--cadrage-y, 50%);
    width: 22px;
    height: 22px;
    margin: -11px 0 0 -11px;
    border: 2px solid var(--brand-primary);
    border-radius: 50%;
    background: color-mix(in srgb, var(--brand-primary) 18%, transparent);
    opacity: 0;
    transition: opacity var(--duration-fast, 120ms) ease;
    pointer-events: none;
  }
  .ap-site-figure--cadrable[data-cadrage="en-cours"] .ap-site-photo-frame::after {
    opacity: 1;
  }

  .ap-site-photo {
    width: 100%;
    max-height: 380px;
    object-fit: cover;
    border-radius: var(--card-radius);
    display: block;
    border: 1px solid var(--hairline);
  }

  .ap-site-figure__credit {
    font-size: var(--text-caption);
    color: var(--text-secondary);
    line-height: var(--line-relaxed);
  }

  /* ── La photo du Soleil ─────────────────────────────────────────────────
     Une vraie image, carrée, servie par notre serveur. Le disque dessiné
     garde exactement la même place : basculer de l'un à l'autre ne doit pas
     faire sauter la page. */

  .ap-solar-figure { margin: 0; display: flex; flex-direction: column; gap: var(--space-sm); }

  .ap-solar-photo {
    width: 100%;
    max-width: 420px;
    height: auto;
    aspect-ratio: 1;
    margin-inline: auto;
    display: block;
    /* Un coin arrondi, pas un cercle. Le disque dessiné du jeu est rond
       parce qu'il EST le Soleil ; cette image est une photographie, et le
       noir autour n'est pas du vide de mise en page : c'est le ciel. La
       découper en rond n'en ferait qu'un anneau noir maladroit. */
    border-radius: var(--card-radius);
    /* Le Soleil rayonne : la lueur vient du jeu, pas de la photo. */
    box-shadow:
      0 0 40px color-mix(in srgb, #FFD84A 22%, transparent),
      0 0 120px color-mix(in srgb, #FFD84A 10%, transparent);
  }

  .ap-solar-figure__credit {
    font-size: var(--text-caption);
    color: var(--text-secondary);
    line-height: var(--line-relaxed);
    text-align: center;
  }

  /* ── La couleur par type : quatre familles, pas dix teintes ─────────────
     MESURÉ, pas choisi à l'œil. `outils/verifier_palette.py` confronte
     toutes les paires en CIEDE2000, en vision normale ET en deutéranopie,
     que huit pour cent des hommes ont.

     L'ancienne palette de dix teintes y laissait SIX paires sous le seuil,
     dont quatre indiscernables : amas/comète à ΔE 1,5, nébuleuse/galaxie à
     2,3, étoile/soleil à 3,8, planète/astéroïde à 4,3. Sur la carte du ciel,
     où la couleur est le seul indice de type, deux repères voisins étaient
     donc le même point pour une partie des joueurs.

     Passé quatre couleurs, deux finissent toujours par se toucher. On s'en
     tient donc à quatre FAMILLES, et la forme sépare les types à
     l'intérieur de chacune. Les teintes ne sont pas arbitraires :

       étoiles         la couleur qu'a vraiment une photosphère vers 5 000 K
       système solaire le bleu d'un ciel diffusé
       ciel profond    la raie Hα à 656 nm, celle qui domine les nébuleuses,
                       les bras de galaxie et les nuages où naissent les amas
       exoplanète      un vert qu'on ne voit nulle part dans le ciel — et
                       c'est l'idée : une exoplanète n'est jamais vue, elle
                       se déduit d'une courbe de lumière

     Les jetons sont définis une seule fois, plus bas, hors couche. Ici on ne
     fait que les employer : les trois écrans — carte, catalogue, légende —
     lisaient auparavant cinq couleurs recopiées chacune trois fois, et trois
     copies se contredisent dès que l'une change. */

  .ap-marker.ap-tag--star         { color: var(--type-star); }
  .ap-marker.ap-tag--variableStar { color: var(--type-variable-star); }
  .ap-marker.ap-tag--sun          { color: var(--type-sun); }
  .ap-marker.ap-tag--planet       { color: var(--type-planet); }
  .ap-marker.ap-tag--asteroid     { color: var(--type-asteroid); }
  .ap-marker.ap-tag--comet        { color: var(--type-comet); }
  .ap-marker.ap-tag--cluster      { color: var(--type-cluster); }
  .ap-marker.ap-tag--nebula       { color: var(--type-nebula); }
  .ap-marker.ap-tag--galaxy       { color: var(--type-galaxy); }
  .ap-marker.ap-tag--exoplanet    { color: var(--type-exoplanet); }

  /* Le Soleil reste un disque, et non un point : c'est le seul astre qu'on
     voie comme tel à l'œil nu, et le seul qui interdise de regarder les
     autres. */
  .ap-marker.ap-tag--sun {
    --marker-size: 30px;
    box-shadow:
      0 0 18px color-mix(in srgb, currentColor 75%, transparent),
      0 0 46px color-mix(in srgb, currentColor 40%, transparent);
  }
  .ap-marker.ap-tag--sun .ap-marker__name {
    font-weight: var(--weight-semibold);
  }

  /* ── La légende ─────────────────────────────────────────────────────────
     Une couleur qu'on ne peut pas nommer reste une décoration.

     SUR UN GRAND ÉCRAN, elle est donc TOUJOURS LÀ : le ciel a de la place à
     revendre, et replier ce qui explique la carte oblige à un geste pour
     comprendre ce qu'on regarde. Le bouton disparaît avec elle — il n'aurait
     plus rien à replier.

     SUR UN TÉLÉPHONE, elle reste repliée derrière son bouton : dix pastilles
     et leurs noms mangeraient un tiers d'un écran de 375 px, et c'est
     précisément là que le ciel manque de place.

     La bascule est portée par `data-layout`, que le script pose en mesurant
     la coquille — le même mécanisme que la barre latérale et la barre
     d'onglets, plutôt qu'un second point de rupture à tenir à jour. */

  /* Seul le BOUTON se cache ici. L'état de la légende, lui, est porté par
     l'attribut `hidden` que le script pose : `[hidden]` ne se contourne pas
     par une feuille, `!important` compris — vérifié dans le navigateur. */
  .ap-shell[data-layout="regular"] #ap-sky-legend-toggle { display: none; }


  /* ── Le téléphone ───────────────────────────────────────────────────────
     Trois ajustements demandés à l'écran, et chacun rend de la place. */

  /* Les quatre compteurs se partagent la largeur au lieu de la laisser vide
     à droite : sur un téléphone, une bande morte à côté de quatre pastilles
     serrées à gauche est de la place perdue.

     `data-layout` et non `@container` : le conteneur nommé `screen` est la
     zone défilante, et la carte du ciel n'en a pas — c'est la seule page qui
     occupe tout le cadre. Une requête de conteneur y serait muette. */
  .ap-shell[data-layout="compact"] .ap-resource-bar { flex-wrap: nowrap; }
  .ap-shell[data-layout="compact"] .ap-resource {
    flex: 1;
    justify-content: center;
    min-width: 0;
    padding-inline: 6px;
  }

  /* Les corps sans position modélisée — Cérès, Vesta, la comète de Halley —
     ne paraissent plus sur un téléphone. Ils se posaient au bas du ciel,
     par-dessus les astres, pour trois cibles qu'on atteint aussi bien par le
     catalogue. Ils restent au large, où la place ne manque pas. */
  .ap-shell[data-layout="compact"] .ap-sky-unplaced { display: none; }

  /* ── L'en-tête de la carte : la barre, et la légende ────────────────────
     La légende a vécu à quatre endroits, et trois d'entre eux avaient le
     même défaut : ils la posaient quelque part SUR le ciel, à une distance
     devinée du haut. Or la barre se replie sur deux à quatre rangées selon
     la largeur, et toute position mesurée depuis son bas dérive avec elle.

     Elles sont donc dans un MÊME conteneur, et c'est lui qui décide :

       au large     côte à côte — la légende est une colonne au bord droit
       au téléphone l'une sous l'autre — la légende sous la note, pleine
                    largeur, en rangée repliée

     Plus rien à réserver ni à mesurer : une boîte flexible n'a pas besoin de
     connaître d'avance la largeur de ce qu'elle place. */
  .ap-sky-head {
    position: absolute;
    inset: var(--space-md) var(--space-md) auto;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    /* Le ciel se déplace en glissant : l'en-tête ne prend le pointeur que
       là où il y a quelque chose. */
    pointer-events: none;
  }
  .ap-sky-head > * { pointer-events: auto; }

  .ap-sky-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 6px var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--card-radius);
    background: color-mix(in srgb, var(--app-surface) 90%, transparent);
    backdrop-filter: blur(16px);
    border: 1px solid var(--hairline);
  }

  .ap-shell[data-layout="regular"] .ap-sky-head {
    flex-direction: row;
    align-items: start;
  }
  .ap-shell[data-layout="regular"] .ap-sky-toolbar { flex: 1; min-width: 0; }
  .ap-shell[data-layout="regular"] .ap-sky-legend {
    flex: none;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: start;
    gap: 6px;
    /* Elle ne descend jamais jusqu'à la minicarte, au même bord. */
    max-height: min(62vh, 420px);
    overflow-y: auto;
  }

  /* Pas de `color` ici, et c'est tout l'objet de cette règle. Posée sur
     l'élément, elle l'emportait sur `.ap-tag--star` et ses voisins : plus
     spécifique, et dans la couche qui gagne. Cinq pastilles sur dix en
     sortaient grises.

     La pastille prend donc la couleur de son type ; le libellé garde la
     sienne, portée par sa propre classe. */
  .ap-sky-legend__item {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-size: var(--text-caption);
    white-space: nowrap;
  }

  .ap-sky-legend__label { color: var(--text-secondary); }

  .ap-sky-legend__dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: currentColor;
  }

  /* ── La forme, là où la couleur ne suffit pas ───────────────────────────
     Dix pastilles rondes de la même taille ne se distinguent que par la
     teinte, et la mesure dit que la teinte n'y arrive pas : galaxie et
     nébuleuse tombent à ΔE 2,4 en deutéranopie, comète et amas à ΔE 4,1 en
     VISION NORMALE. Invisible pour tout le monde, pas seulement pour les
     daltoniens.

     La forme s'ajoute donc à la couleur, et elle dit quelque chose de
     l'astre : l'anneau d'une variable est sa pulsation, l'ovale d'une
     galaxie son disque vu de biais, le carré d'un astéroïde sa roche sans
     forme, le flou d'une nébuleuse son absence de bord, la traînée d'une
     comète sa queue.

     Les mêmes règles servent la légende du ciel et le catalogue : dans
     l'une la classe est sur le parent, dans l'autre sur la pastille. Sans
     cela les deux écrans se contrediraient. */

  /* Elle bat, et l'amas est un anneau d'étoiles. */
  .ap-tag--variableStar .ap-sky-legend__dot,
  .ap-tag--cluster .ap-sky-legend__dot,
  .ap-object__dot.ap-tag--variableStar,
  .ap-object__dot.ap-tag--cluster,
  .ap-marker.ap-tag--variableStar,
  .ap-marker.ap-tag--cluster {
    background: transparent;
    box-shadow: inset 0 0 0 2px currentColor;
  }

  /* Une roche n'a pas de forme. */
  .ap-tag--asteroid .ap-sky-legend__dot,
  .ap-object__dot.ap-tag--asteroid,
  .ap-marker.ap-tag--asteroid { border-radius: 2px; }

  /* Un disque vu de biais. */
  .ap-tag--galaxy .ap-sky-legend__dot,
  .ap-object__dot.ap-tag--galaxy { transform: scaleX(1.5) scaleY(.65); }
  /* Le repère porte déjà `translate: -50% -50%` pour se centrer : écraser sa
     `transform` le décalerait d'une demi-largeur. On passe donc par
     `scale`, qui est une propriété distincte. */
  .ap-marker.ap-tag--galaxy { scale: 1.5 .65; }

  /* Une nébuleuse n'a pas de bord. */
  .ap-tag--nebula .ap-sky-legend__dot,
  .ap-object__dot.ap-tag--nebula,
  .ap-marker.ap-tag--nebula {
    background: radial-gradient(closest-side,
      currentColor 40%,
      color-mix(in srgb, currentColor 40%, transparent) 68%,
      transparent 100%);
    transform: scale(1.45);
  }
  .ap-marker.ap-tag--nebula { scale: 1.45; }

  /* Le seul astre qu'on voie comme un disque à l'œil nu. */
  .ap-tag--sun .ap-sky-legend__dot,
  .ap-object__dot.ap-tag--sun {
    box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 32%, transparent);
  }

  /* La queue, toujours à l'opposé du Soleil. */
  .ap-tag--comet .ap-sky-legend__dot,
  .ap-object__dot.ap-tag--comet { position: relative; }
  .ap-tag--comet .ap-sky-legend__dot::after,
  .ap-object__dot.ap-tag--comet::after,
  .ap-marker.ap-tag--comet::after {
    content: "";
    position: absolute;
    inset-inline-start: 100%;
    inset-block-start: 50%;
    width: 7px;
    height: 2px;
    transform: translateY(-50%);
    background: linear-gradient(to right, currentColor, transparent);
  }

  /* Le curseur d'heure, posé sous la barre d'outils. */
  .ap-sky-hour {
    position: absolute;
    left: 50%;
    bottom: var(--space-md);
    transform: translateX(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    width: min(420px, calc(100% - var(--space-xl)));
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--pill-radius);
    background: color-mix(in srgb, var(--app-surface) 88%, transparent);
    backdrop-filter: blur(16px);
  }
  .ap-sky-hour .ap-focus-slider { flex: 1; }

  /* L'heure regardée. Quand c'est le présent, elle se teinte de l'accent :
     sans ce signe, rien ne distinguerait « maintenant » d'un moment choisi
     au hasard qui tomberait juste. */
  .ap-sky-hour__label--now { color: var(--brand-primary); }

  /* Le bouton « Maintenant » se désactive quand on y est déjà — et sa cause
     est alors écrite par la teinte de l'heure, juste à côté. */
  #ap-sky-now[disabled] { opacity: .45; pointer-events: none; }

  /* Quand le panneau d'une cible est ouvert, il prend la place du curseur.

     PIÈGE VÉRIFIÉ À L'ÉCRAN : c'était écrit avec le combinateur `~`, qui ne
     vise que les frères SUIVANTS. Or le curseur est écrit avant le panneau
     dans le gabarit : la règle ne masquait donc jamais rien, et la barre
     d'heure recouvrait le bouton « Préparer l'observation ». On ne pouvait
     pas lancer l'observation qu'on venait de choisir.

     `:has()` ne dépend pas de l'ordre du document. Et le panneau passe
     au-dessus du curseur, pour que son bouton reste atteignable même si un
     navigateur ignorait `:has()`. */
  .ap-sky-screen:has(.ap-sky-panel:not([hidden])) .ap-sky-hour { display: none; }

  .ap-sky-unplaced {
    position: absolute;
    left: var(--space-md);
    bottom: var(--space-md);
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
    max-width: min(340px, 50%);
  }

  .ap-sky-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    pointer-events: none;
  }
  .ap-sky-toolbar > * { pointer-events: auto; }

  /* La note prend sa PROPRE LIGNE dans la barre d'outils. Posée à part, à
     quarante-quatre pixels du haut, elle atterrissait entre deux rangées dès
     que la barre se repliait — quatre lignes sur un téléphone de 375 px.

     `flex-basis: 100%` la fait passer à la ligne quoi qu'il arrive, et la
     marge du bas la décolle de la rangée suivante : sans elle, le texte
     touchait les sélecteurs. */
  .ap-sky-note {
    flex-basis: 100%;
    /* `.ap-tag` ne revient pas à la ligne — c'est juste pour une étiquette
       de deux mots, faux pour une phrase : « 118 cibles à votre portée ·
       154 attendent un palier plus haut » débordait de 19 px sur un écran
       de 375. */
    white-space: normal;
    margin-block-start: calc(var(--space-xs) * -1);
    margin-block-end: var(--space-sm);
  }

  /* ── Le bas de la carte ──────────────────────────────────────────────────
     Sur un téléphone, le curseur d'heure et les astres sans position se
     disputaient le même coin : le premier centré, les seconds à gauche, l'un
     par-dessus l'autre. Rassemblés dans une colonne, ils s'empilent — le
     curseur AU-DESSUS du texte, comme on le lit.

     Sur un grand écran, `display: contents` rend au conteneur son
     inexistence : chacun retrouve sa position d'origine, et rien ne
     change. */
  .ap-sky-bottom { display: contents; }

  .ap-shell[data-layout="compact"] .ap-sky-bottom {
    position: absolute;
    left: var(--space-md);
    right: var(--space-md);
    bottom: var(--space-md);
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
  }
  .ap-shell[data-layout="compact"] .ap-sky-bottom > * {
    position: static;
    transform: none;
    width: auto;
    max-width: none;
  }
  /* Le plan de situation garde sa taille et passe en tête, à droite : il
     regarde le ciel, pas le texte. */
  .ap-shell[data-layout="compact"] .ap-sky-bottom > .ap-minimap {
    order: -1;
    align-self: flex-end;
    width: 96px;
  }

  /* Le panneau de la cible. Il flotte au-dessus du ciel plutôt que de le
     pousser : la carte reste visible pendant qu'on lit la fiche. */
  .ap-sky-panel {
    position: absolute;
    left: 50%;
    bottom: var(--space-md);
    transform: translateX(-50%);
    width: min(var(--reading-width), calc(100% - var(--space-xl)));
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    padding: var(--card-padding);
    z-index: 3;
    border-radius: var(--sheet-radius);
    background: color-mix(in srgb, var(--app-surface) 92%, transparent);
    backdrop-filter: blur(20px);
    box-shadow: var(--shadow-sheet);
    animation: ap-rise var(--motion-soft) both;
  }
  @keyframes ap-rise {
    from { opacity: 0; transform: translate(-50%, 10px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
  }

  /* Le plan de situation garde la couleur de chaque type : il se lit d'un
     coup d'œil, sans légende. */
  .ap-minimap__dot {
    position: absolute;
    width: 2px;
    height: 2px;
    border-radius: 50%;
    background: currentColor;
  }

  /* ── Le plan de situation se saisit ─────────────────────────────────────
     Il affichait la part visible sans permettre de la déplacer. Le curseur
     dit maintenant qu'on peut l'attraper — sans ce signe, rien ne distingue
     une commande d'une vignette.

     `touch-action: none` est ce qui rend le glissement possible au doigt :
     sans lui, le navigateur prend le geste pour un défilement de page et
     nous coupe la série d'événements en plein milieu.

     Les points du ciel, eux, laissent passer le pointeur : viser une étoile
     large de deux pixels pour déplacer la vue serait un piège. */
  .ap-minimap {
    cursor: grab;
    touch-action: none;
  }
  .ap-minimap--dragging { cursor: grabbing; }
  .ap-minimap__dot { pointer-events: none; }

  /* Le cadre suit le pointeur, il ne le reçoit pas : c'est le PLAN qui écoute,
     pour qu'un clic hors du cadre y amène la vue au lieu de ne rien faire. */
  .ap-minimap__window {
    pointer-events: none;
    background: color-mix(in srgb, var(--brand-primary) 22%, transparent);
  }
  .ap-minimap--dragging .ap-minimap__window {
    background: color-mix(in srgb, var(--brand-primary) 34%, transparent);
  }

  /* ══ L'oculaire ═══════════════════════════════════════════════════════════
     Le seul retour vrai est l'image : un astronome ne lit pas un chiffre, il
     regarde l'étoile. Le halo est donc l'élément principal de l'écran. */

  .ap-eyepiece {
    display: grid;
    place-items: center;
    aspect-ratio: 1;
    width: min(280px, 70vw);
    margin-inline: auto;
    border-radius: 50%;
    background: radial-gradient(circle at 50% 50%, #0E1226, var(--sky-deep) 72%);
    border: 1px solid var(--hairline);
  }
  .ap-eyepiece__star {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--star-tint);
    transition: box-shadow var(--motion-soft), filter var(--motion-soft);
  }

  /* Le curseur du porte-oculaire : un vrai geste, pas un champ de saisie. */
  .ap-focus-slider {
    width: 100%;
    accent-color: var(--brand-primary);
  }

  /* ══ Détails ══════════════════════════════════════════════════════════════ */

  /* `display` l'emporte sur l'attribut `hidden` : sans cette règle, un panneau
     marqué `hidden` reste visible, et l'analyse s'ouvrait sur un écran vide. */
  [hidden] { display: none !important; }

  /* Une valeur qui change en place se lit en chasse fixe. */
  .ap-numeric { font-variant-numeric: tabular-nums; }






  /* ── La marque ──────────────────────────────────────────────────────────
     Le logo est un badge circulaire portant déjà le mot « Astropost ». À
     28 px son texte ne se lit pas, et c'est sans importance : c'est la forme
     qu'on reconnaît. Le mot est donc répété à côté, en texte — un lecteur
     d'écran ne lit pas une image, et l'image porte `alt=""` pour ne pas
     annoncer deux fois la même chose. */

  .ap-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    text-decoration: none;
  }
  .ap-brand:hover { text-decoration: none; }

  .ap-brand__mark {
    width: 28px;
    height: 28px;
    flex: none;
    border-radius: 50%;
  }

  .ap-brand__name {
    font-size: var(--text-caption);
    font-weight: var(--weight-semibold);
    letter-spacing: .12em;
    color: var(--text-secondary);
  }

  .ap-auth-logo {
    width: 96px;
    height: 96px;
    align-self: start;
    border-radius: 50%;
    /* Le badge est sombre sur fond sombre : une lueur le décolle du ciel. */
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand-primary) 30%, transparent),
                0 12px 40px color-mix(in srgb, var(--brand-primary) 22%, transparent);
  }

  /* ── L'annuaire des observatoires ───────────────────────────────────────
     Une carte par joueur donnait trois observatoires par écran. L'annuaire
     est une LISTE : une ligne par observatoire, le rang à gauche, les
     chiffres alignés à droite, et le geste au bout. Elle tient autant de
     monde que la communauté en compte. */

  .ap-roster {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: var(--surface-1);
    overflow: hidden;
  }

  .ap-observatory {
    display: grid;
    grid-template-columns: 2.6rem minmax(0, 1fr) auto auto;
    align-items: center;
    gap: var(--space-sm) var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border-top: 1px solid color-mix(in srgb, var(--hairline) 55%, transparent);
  }
  .ap-observatory:first-child { border-top: 0; }
  .ap-observatory:hover {
    background: color-mix(in srgb, var(--brand-primary) 6%, transparent);
  }

  .ap-observatory--mine {
    background: color-mix(in srgb, var(--brand-primary) 12%, transparent);
  }

  .ap-observatory__rank {
    font-size: var(--text-footnote);
    color: var(--text-secondary);
    text-align: end;
  }

  .ap-observatory__body { min-width: 0; }

  .ap-observatory__name {
    font-weight: var(--weight-semibold);
    text-decoration: none;
  }
  .ap-observatory__name:hover { text-decoration: underline; }

  .ap-observatory__meta {
    display: block;
    font-size: var(--text-caption);
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .ap-observatory__figures {
    display: grid;
    grid-template-columns: auto auto;
    align-items: baseline;
    gap: 0 6px;
    text-align: end;
  }

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

  .ap-observatory__unit,
  .ap-observatory__aside {
    font-size: var(--text-caption);
    color: var(--text-secondary);
  }
  .ap-observatory__aside { grid-column: 1 / -1; }

  /* L'écart : vert quand on mène, orangé quand on est derrière. Ce n'est pas
     un jugement, c'est une direction. */
  .ap-observatory__gap {
    grid-column: 1 / -1;
    font-size: var(--text-caption);
    color: var(--status-success);
  }
  .ap-observatory__gap--behind { color: var(--status-warning, var(--brand-primary)); }

  .ap-observatory__action { justify-self: end; }

  /* À l'étroit, la ligne se plie en deux rangées : le NOM d'abord, sur toute
     la largeur, puis les chiffres et le geste. Quatre colonnes sur un
     téléphone coupaient le nom au milieu d'un mot, et le bouton le serrait
     encore davantage. */
  @container screen (max-width: 640px) {
    .ap-observatory {
      grid-template-columns: 2.4rem minmax(0, 1fr) auto;
      row-gap: 6px;
      padding-block: var(--space-sm);
    }
    .ap-observatory__rank { align-self: start; padding-top: 2px; }
    .ap-observatory__body { grid-column: 2 / -1; }
    .ap-observatory__figures {
      grid-column: 2;
      grid-template-columns: auto auto;
      justify-content: start;
      text-align: start;
      column-gap: 8px;
    }
    .ap-observatory__aside,
    .ap-observatory__gap { grid-column: auto; }
    .ap-observatory__score { font-size: var(--text-body); }
    .ap-observatory__action { grid-column: 3; align-self: center; }
  }

  /* ── L'écoute ───────────────────────────────────────────────────────────
     Le bandeau de tête porte trois choses de front : où l'on en est, ce que
     le palier apporte, et ce qui attend d'être tranché. Il s'empile à
     l'étroit et s'aligne au large, sans jamais laisser de bande vide. */

  .ap-listen-head {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md);
    margin-bottom: var(--space-lg);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: linear-gradient(135deg,
      color-mix(in srgb, var(--type-exoplanet, var(--brand-primary)) 16%, transparent),
      transparent 70%), var(--surface-1);
  }

  .ap-listen-head__now {
    grid-row: span 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 92px; height: 92px;
    border-radius: 50%;
    border: 2px solid var(--brand-primary);
    background: color-mix(in srgb, var(--brand-primary) 12%, transparent);
  }

  .ap-listen-head__stats { grid-column: 1 / -1; }

  @container screen (min-width: 900px) {
    .ap-listen-head { grid-template-columns: auto minmax(0, 1fr) auto; }
    .ap-listen-head__stats { grid-column: auto; min-width: 380px; }
  }

  .ap-card--muted { opacity: .55; }

  /* ── L'échelle des paliers ──────────────────────────────────────────────
     Vingt-huit paliers : ni une grille ni une liste plate. Une échelle, dont
     le fil vertical relie les barreaux et dit d'un coup d'œil où l'on en est.
     Elle se lit du haut (le sommet, encore lointain) vers le bas (ce qui est
     acquis) — comme on regarde une montagne. */

  .ap-ladder-head {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md);
    margin-bottom: var(--space-lg);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: linear-gradient(135deg,
      color-mix(in srgb, var(--brand-primary) 16%, transparent),
      transparent 70%), var(--surface-1);
  }

  .ap-ladder-head__now {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 92px; height: 92px;
    border-radius: 50%;
    border: 2px solid var(--brand-primary);
    background: color-mix(in srgb, var(--brand-primary) 12%, transparent);
  }

  .ap-ladder-head__level {
    font-size: 2.4rem;
    font-weight: var(--weight-semibold);
    line-height: 1;
    color: var(--brand-primary);
  }

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

  .ap-ladder-head__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-xs); }

  .ap-ladder {
    list-style: none;
    margin: 0;
    padding: 0;
    position: relative;
  }

  /* Le fil qui relie les barreaux. Il s'arrête avant le premier et le dernier
     repère, pour ne pas dépasser. */
  .ap-ladder::before {
    content: "";
    position: absolute;
    inset-block: 18px;
    inset-inline-start: 17px;
    width: 2px;
    background: var(--hairline);
  }

  .ap-rung {
    position: relative;
    display: flex;
    gap: var(--space-md);
    padding-bottom: var(--space-md);
  }

  .ap-rung__mark {
    flex: none;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 36px; height: 36px;
    border-radius: 50%;
    font-size: var(--text-caption);
    font-weight: var(--weight-semibold);
    color: var(--sky-deep);
    background: var(--brand-primary);
    border: 2px solid var(--app-background);
  }

  .ap-rung--ahead .ap-rung__mark {
    color: var(--text-secondary);
    background: var(--surface-2);
    border-color: var(--app-background);
  }

  .ap-rung--current .ap-rung__mark {
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand-primary) 30%, transparent);
  }

  .ap-rung__card {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: var(--surface-1);
  }

  .ap-rung--current .ap-rung__card {
    border-color: color-mix(in srgb, var(--brand-primary) 55%, transparent);
    background: color-mix(in srgb, var(--brand-primary) 8%, var(--surface-1));
  }

  /* Un palier à venir reste lisible : il est la raison de continuer. Il est
     estompé, pas effacé. */
  .ap-rung--ahead .ap-rung__card { opacity: .62; }

  .ap-rung__name { font-size: var(--text-headline); }
  .ap-rung__summary { color: var(--text-secondary); line-height: var(--line-relaxed); }

  .ap-rung__done-icon { width: 18px; height: 18px; color: var(--status-success); }

  .ap-rung__unlocks {
    list-style: none;
    margin: 6px 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }

  .ap-unlock {
    font-size: var(--text-caption);
    color: var(--text-secondary);
    padding: 2px 9px;
    border-radius: 999px;
    border: 1px solid var(--hairline);
    background: color-mix(in srgb, var(--surface-2) 70%, transparent);
  }

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

  @container screen (min-width: 1100px) {
    /* Au large, l'échelle se dédouble : deux colonnes de barreaux, et plus de
       bande vide à droite. Le fil n'a alors plus de sens et disparaît. */
    .ap-ladder { columns: 2; column-gap: var(--space-lg); }
    .ap-ladder::before { display: none; }
    .ap-rung { break-inside: avoid; }
  }

  /* ── Ce qui n'est pas encore ouvert ─────────────────────────────────────
     Un onglet verrouillé reste CLIQUABLE : il mène à un écran qui dit quel
     palier l'ouvre et ce qu'il contiendra. Le cacher priverait le joueur de
     la raison de monter d'un niveau. */

  .ap-pane-selector__item--locked { color: var(--text-secondary); }

  .ap-pane-selector__lock {
    width: 14px; height: 14px;
    margin-inline-end: 4px;
    vertical-align: -2px;
    color: var(--status-error);
  }

  .ap-gate {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    text-align: center;
    padding: var(--space-lg) var(--gutter);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: var(--surface-1);
  }

  .ap-gate__seal {
    display: grid;
    place-items: center;
    width: 64px; height: 64px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--status-error) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--status-error) 35%, transparent);
  }

  .ap-gate__icon { width: 30px; height: 30px; color: var(--status-error); }

  .ap-gate__title { font-size: var(--text-title); }

  .ap-gate__body {
    max-width: var(--reading-width);
    color: var(--text-secondary);
    line-height: var(--line-relaxed);
  }

  .ap-gate__list { width: 100%; max-width: var(--reading-width); text-align: start; }

  /* ── Catalogue ──────────────────────────────────────────────────────────
     Deux cent soixante-douze objets ne tiennent pas en cartes : une carte par
     objet donnerait quatre écrans de défilement pour une seule constellation.
     Le catalogue est donc une LISTE dense, groupée par constellation, où
     chaque ligne tient sur une seule hauteur de texte. */

  .ap-filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    align-items: end;
    margin-bottom: var(--space-md);
  }

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

  .ap-filter__label {
    font-size: var(--text-caption);
    color: var(--text-secondary);
    padding-inline-start: 2px;
  }

  .ap-catalog-note { margin-bottom: var(--space-md); display: block; }

  /* Les constellations se rangent en colonnes : une constellation courte ne
     laisse pas un demi-écran vide à côté d'une longue. */
  .ap-catalog {
    columns: 3 300px;
    column-gap: var(--space-md);
  }

  .ap-constellation {
    break-inside: avoid;
    margin-bottom: var(--space-md);
    border: 1px solid var(--hairline);
    border-radius: var(--card-radius);
    background: var(--surface-1);
    overflow: hidden;
  }

  .ap-constellation__name {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
    margin: 0;
    padding: var(--space-sm) var(--space-md);
    font-size: var(--text-subheadline);
    border-bottom: 1px solid var(--hairline);
    background: color-mix(in srgb, var(--brand-primary) 8%, transparent);
  }

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

  .ap-object {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 6px var(--space-md);
    border-top: 1px solid color-mix(in srgb, var(--hairline) 55%, transparent);
    text-decoration: none;
  }
  .ap-object:first-child { border-top: 0; }
  .ap-object:hover {
    background: color-mix(in srgb, var(--brand-primary) 10%, transparent);
    text-decoration: none;
  }

  /* La pastille prend la couleur du type : `.ap-tag--galaxy` et consorts ne
     posent qu'une `color`, dont on se sert ici comme fond. */
  .ap-object__dot {
    flex: none;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: currentColor;
  }

  .ap-object__body { flex: 1; min-width: 0; display: flex; flex-direction: column; }

  .ap-object__name {
    font-size: var(--text-footnote);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .ap-object__meta {
    font-size: var(--text-caption);
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .ap-object__quality {
    flex: none;
    font-size: var(--text-caption);
    color: var(--status-success);
  }

  /* Un objet hors de portée reste VISIBLE : il montre ce que le prochain
     palier apportera. Il est estompé, et son palier est écrit. */
  .ap-object--locked { opacity: .5; }

  .ap-object__gate {
    flex: none;
    font-size: var(--text-caption);
    color: var(--text-secondary);
    border: 1px solid var(--hairline);
    border-radius: 999px;
    padding: 1px 7px;
  }

  .ap-object--found .ap-object__name { font-weight: var(--weight-semibold); }
}

/* ============================================================================
   HORS COUCHE — et il y a une raison, écrite ici pour qu'on puisse la retirer
   ----------------------------------------------------------------------------
   `02-socle.css` pose `a { color: var(--brand-primary) }` SANS l'envelopper
   dans `@layer astropost`, contrairement aux feuilles 03 et 04. Une règle non
   couchée l'emporte sur toutes les règles couchées, quelle que soit leur
   spécificité : le texte de tout bouton qui est un lien prenait donc la
   couleur d'accent — et le bouton principal, bleu sur bleu, s'affichait vide.

   Ces règles sont donc volontairement hors couche, et ne portent que sur la
   couleur du texte des composants qui sont des liens.

   À RETIRER dès que `02-socle.css` enveloppe son sélecteur `a` dans la
   couche : la cascade suffira alors toute seule.
   ========================================================================= */

a.ap-button                       { color: var(--text-primary); }
a.ap-button--primary              { color: var(--sky-deep); }
a.ap-button--ghost,
a.ap-button--plain                { color: var(--brand-primary); }
a.ap-button--destructive,
a.ap-button--danger               { color: var(--status-error); }

a.ap-sidebar__item,
a.ap-tabbar__item,
a.ap-pane-selector__item,
a.ap-list__row,
a.ap-card--interactive,
a.ap-marker                       { color: var(--text-primary); }

a.ap-tabbar__item                 { color: var(--text-secondary); }
a.ap-tabbar__item[aria-selected="true"],
a.ap-sidebar__item[aria-selected="true"] { color: var(--brand-primary); }

a.ap-progress__label              { color: var(--text-secondary); }
a.ap-object                       { color: var(--text-primary); }
a.ap-more-item                    { color: var(--text-primary); }
a.ap-more-item--current           { color: var(--brand-primary); }


/* ============================================================================
   LES COULEURS DE TYPE — hors couche, et pour une raison précise
   ----------------------------------------------------------------------------
   `design/css/01-jetons.css` n'est PAS enveloppé dans `@layer astropost`,
   contrairement aux feuilles 03 et 04, ET il se charge APRÈS cette feuille.
   Une redéfinition posée dans `@layer app` perdrait donc deux fois : le non
   couché l'emporte sur le couché, et à égalité c'est le dernier chargé qui
   gagne.

   D'où ce bloc hors couche et le sélecteur doublé `:root:root`, qui ajoute
   un cran de spécificité. Le ranger « proprement » dans `@layer app` le
   ferait taire SANS RIEN CASSER D'APPARENT : les anciennes couleurs
   reviendraient simplement, et personne ne verrait l'erreur.

   Les quatre familles sont mesurées — voir `outils/verifier_palette.py`, et
   l'épreuve qui l'exécute. Les dix types s'y rattachent ; la FORME les
   sépare à l'intérieur d'une famille.
   ========================================================================= */

:root:root {
  /* La couleur d'une photosphère vers 5 000 K. */
  --famille-etoile:     #E49900;
  /* Le bleu d'un ciel diffusé. */
  --famille-solaire:    #00C2E9;
  /* La raie Hα, à 656 nm. */
  --famille-profond:    #F47DB9;
  /* Un vert qu'on ne voit nulle part dans le ciel : une exoplanète ne se
     voit pas, elle se déduit d'une courbe de lumière. */
  --famille-exoplanete: #51C672;

  --type-star:          var(--famille-etoile);
  --type-variable-star: var(--famille-etoile);
  --type-sun:           var(--famille-etoile);

  --type-planet:        var(--famille-solaire);
  --type-asteroid:      var(--famille-solaire);
  --type-comet:         var(--famille-solaire);

  --type-cluster:       var(--famille-profond);
  --type-nebula:        var(--famille-profond);
  --type-galaxy:        var(--famille-profond);

  --type-exoplanet:     var(--famille-exoplanete);
}

/* Les quatre vues du Soleil. Une grille qui se remplit toute seule : deux
   colonnes dès qu'il y a la place, une seule sinon. Aucun point de rupture à
   écrire — c'est la largeur disponible qui décide, pas la taille de l'écran,
   et la carte vit aussi bien dans une colonne étroite. */
.ap-solar-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-sm);
}

.ap-solar-view {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.ap-solar-view__image {
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  /* Le disque est déjà sur fond noir dans l'image : le cadre le prolonge au
     lieu de le trancher. */
  background: #000;
  display: block;
}

.ap-solar-view__caption {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--font-size-sm);
}

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

.ap-source__name {
  font-weight: 600;
}
