/* ==========================================================
FITZUNIVERSE — SYSTÈME DE CATALOGUE (2026-08-22)
==========================================================
Source unique des composants partagés par les deux pages de
catalogue, scholarships.html et universities.html : bannière,
barre de filtres, raccourcis pays, groupes par pays, grille,
cartes, squelettes et états vides.

POURQUOI CE FICHIER EXISTE
--------------------------
Avant lui, scholarships.css et universities.css contenaient le même
bloc `.catalog-*` recopié : 349 lignes chacune, identiques à DEUX
valeurs de marge près (vérifié au diff). Les bannières l'étaient
aussi, et les mêmes composants existaient en double sous deux noms
différents — `.bourses-results-count`/`.uni-results-count`,
`.scholarship-card-chip`/`.uni-card-chip`,
`.bourses-empty-state`/`.uni-empty-state`,
`.bourses-skeleton`/`.uni-card-skeleton`.

Ce sont les deux pages qui intéressent le plus les utilisateurs, et
les deux qu'un visiteur enchaîne systématiquement (une bourse mène à
ses universités partenaires). Elles doivent se ressembler au pixel,
et une correction faite sur l'une doit valoir pour l'autre. Deux
copies garantissaient exactement l'inverse.

CONVENTION DE NOMMAGE
---------------------
Les nouveaux noms sont en `.catalog-*`. Les anciens noms propres à
chaque page sont conservés en ALIAS dans les listes de sélecteurs —
ils sont encore écrits dans scholarships.js et universities-page.js,
et les renommer partout n'apporterait rien que cet alias ne donne
déjà, pour un risque de régression bien réel sur du code qui
fonctionne. Toute NOUVELLE règle s'écrit sous le nom `.catalog-*`.

RÈGLE
-----
Ne jamais redéfinir un de ces composants dans scholarships.css ou
universities.css. Ce qui est propre à une page (la photo de sa
bannière, ce que sa carte affiche) reste chez elle ; ce que les deux
partagent vit ici.
========================================================== */


/* ==========================================================
BANNIÈRE DE CATALOGUE
==========================================================
Photo + voile sombre + éclat ambré, même grammaire que l'en-tête
d'offers.html : la photo donne le lieu, le voile rend le texte
lisible, l'ambre rattache la page à la marque. */

.catalog-hero,
.bourses-hero,
.uni-hero{

position:relative;

isolation:isolate;

border-radius:var(--radius-lg);

overflow:hidden;

/* 120px en haut : le header public est fixed et flotte par-dessus
la photo (voir css/header.css). Cette réserve l'empêche de couvrir
le badge et le titre au chargement, avant qu'il ne s'efface au
premier défilement. */
padding:124px 48px 44px;

margin-bottom:26px;

min-height:300px;

display:flex;

flex-direction:column;

justify-content:flex-end;

}

.catalog-hero-bg,
.bourses-hero-bg,
.uni-hero-bg{

position:absolute;

inset:0;

z-index:-2;

width:100%;

height:100%;

object-fit:cover;

/* Léger zoom : les photos de ville et de campus sont cadrées large,
le recadrage rapproche le sujet et évite les bords vides. */
transform:scale(1.04);

filter:saturate(1.06) contrast(1.02);

}

/* Voile en trois couches, dans cet ordre :
1. l'éclat ambré, en haut à droite, là où le texte ne va jamais —
   c'est la seule touche de marque de la bannière ;
2. un dégradé oblique qui assombrit fortement le côté gauche, où
   vivent badge, titre et statistiques ;
3. un dégradé vertical qui ancre le bas et détache la barre de
   filtres posée juste en dessous.
L'ancien voile (deux couches, gris ardoise pur) noyait la photo dans
un gris uniforme : le sujet ne se lisait plus, et la bannière était
interchangeable d'une page à l'autre. */
.catalog-hero::after,
.bourses-hero::after,
.uni-hero::after{

content:"";

position:absolute;

inset:0;

z-index:-1;

background:
radial-gradient(ellipse 58% 76% at 88% 8%, rgba(233,163,25,.30) 0%, rgba(233,163,25,.09) 46%, transparent 70%),
linear-gradient(102deg, rgba(13,10,6,.94) 0%, rgba(20,16,13,.80) 42%, rgba(20,16,13,.34) 100%),
linear-gradient(180deg, rgba(13,10,6,.30) 0%, transparent 34%, rgba(13,10,6,.62) 100%);

}

.catalog-hero-badge,
.bourses-hero-badge,
.uni-hero-badge{

display:flex;

align-items:center;

gap:9px;

width:max-content;

max-width:100%;

padding:8px 16px;

margin-bottom:18px;

border-radius:var(--radius-full);

background:rgba(255,255,255,.10);

border:1px solid rgba(255,255,255,.18);

backdrop-filter:blur(12px);
-webkit-backdrop-filter:blur(12px);

/* Blanc plein (12,66:1). --accent-color a été mesuré à ~3,16:1 sur
ce fond : sous l'AA, donc écarté ici comme partout ailleurs sur les
fonds sombres du site. */
color:#fff;

font-family:var(--font-display);

font-weight:var(--fw-bold);

font-size:.82rem;

letter-spacing:-.005em;

}

/* Icône ambrée : c'est elle, et non un fond de pastille, qui porte la
couleur — règle « Icônes » de DESIGN_SYSTEM.md. --amber-500 tient
7:1 à 8:1 sur ce voile, très au-dessus du seuil des composants non
textuels. */
.catalog-hero-badge i,
.catalog-hero-badge svg,
.bourses-hero-badge i,
.bourses-hero-badge svg,
.uni-hero-badge i,
.uni-hero-badge svg{

width:15px;

height:15px;

flex-shrink:0;

color:var(--amber-500);

}

.catalog-hero h1,
.bourses-hero h1,
.uni-hero h1{

font-size:clamp(1.9rem,3.6vw,2.65rem);

line-height:1.12;

font-weight:var(--fw-extrabold);

letter-spacing:-.025em;

color:#fff;

max-width:660px;

margin-bottom:12px;

text-shadow:0 1px 3px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);

}

.catalog-hero > p,
.bourses-hero > p,
.uni-hero > p{

max-width:580px;

color:rgba(255,255,255,.84);

font-size:1rem;

line-height:1.7;

text-shadow:0 1px 3px rgba(0,0,0,.35), 0 8px 20px rgba(0,0,0,.22);

}

/* ---- Rangée de statistiques du catalogue ----
Vivait DANS la bannière jusqu'au 2026-08-31. Cumulés à la pastille,
au titre et au sous-titre, les chiffres poussaient les filtres au bas
du cadre et la première carte sous la ligne de flottaison sur un
portable 1366×768 — sur une page de catalogue, le contenu c'est le
catalogue. Sortis de la photo, posés sous les filtres et au-dessus de
la grille, ils retrouvent leur sens : « 145 bourses » lu juste
au-dessus des cartes se comprend comme un décompte de résultats, plus
comme un argument marketing. Les identifiants (statScholarships,
statTotal…) n'ont pas bougé : scholarships.js et universities-page.js
les remplissent à l'identique. */

.catalog-stats{

display:flex;

flex-wrap:wrap;

gap:0;

margin:18px 0 2px;

}

.catalog-stat{

padding:0 26px;

/* Filet de séparation plutôt qu'un simple écart : trois nombres
alignés sans séparateur se lisaient comme une seule suite de
chiffres. Retiré sur le premier (voir :first-child). */
border-left:1px solid var(--border-color);

}

.catalog-stat:first-child{

padding-left:0;

border-left:none;

}

.catalog-stat strong{

display:block;

font-family:var(--font-display);

font-size:var(--fs-stat-sm);

font-weight:var(--fw-extrabold);

font-variant-numeric:tabular-nums;

letter-spacing:-.02em;

/* Hors de la bannière, plus de voile sombre : l'encre du site
remplace l'ambre-sur-photo (et ses text-shadow) de l'ancienne
version. L'ambre en petit chiffre sur papier clair tomberait
sous l'AA — même mesure que --accent-color à l'époque. */
color:var(--text-color);

}

.catalog-stat span{

display:block;

margin-top:2px;

font-size:.8rem;

color:var(--text-muted);

}


/* Corps de page — filtres, raccourcis, compteur et grille.
Tient l'espacement vertical de tout ce qui suit la bannière. Avant,
ces blocs étaient répartis dans deux <section class="section">
successives dont le rembourrage cumulé laissait environ 200 px de
vide entre la bannière et le premier résultat, et autant entre le
compteur et la première carte. Sur une page d'accueil c'est de la
respiration ; sur un catalogue c'est un écran entier pris au
contenu que le visiteur est venu chercher. */
.catalog-body{

padding:0 0 96px;

/* INDISPENSABLE à la barre de filtres collante. css/base.css donne
`overflow:hidden` à tout élément `section` (utile aux sections
décoratives d'une page d'accueil, dont les halos débordent). Or un
ancêtre dont l'overflow n'est pas `visible` devient le conteneur de
défilement de ses descendants : position:sticky s'y accroche au lieu
de s'accrocher à la fenêtre, et ne se déclenche donc jamais.
C'est exactement ce qui se passait ici — la barre restait collante
sur universities.html (pas de <section> englobante) et glissait hors
de l'écran sur scholarships.html. Symptôme identique à un `sticky`
« qui ne marche pas », cause située trois niveaux plus haut. */
overflow:visible;

}


/* ==========================================================
BARRE DE FILTRES — COLLANTE
==========================================================
Un catalogue se parcourt sur plusieurs écrans de haut. Avant, filtrer
après avoir descendu trois pays obligeait à remonter tout en haut.
La barre reste désormais accessible pendant tout le défilement.

top:14px et non une valeur calée sur la hauteur du header : le header
public s'efface complètement dès le premier défilement vers le bas
(.is-hidden, voir js/header-scroll.js), il n'y a donc rien à éviter
au moment où la barre se colle. Le seul cas où il reste affiché est
prefers-reduced-motion — traité en fin de fichier. */

.catalog-filters,
.catalog-search-bar{

position:sticky;

top:14px;

z-index:20;

display:flex;

align-items:center;

gap:10px;

width:100%;

max-width:900px;

margin:0 auto 18px;

padding:10px;

border-radius:var(--radius-full);

background:rgba(255,255,255,.86);

backdrop-filter:blur(16px) saturate(1.4);
-webkit-backdrop-filter:blur(16px) saturate(1.4);

border:1px solid var(--border-color);

box-shadow:0 1px 2px rgba(20,16,13,.05), 0 10px 30px -12px rgba(20,16,13,.24);

transition:box-shadow var(--transition), border-color var(--transition);

}

.catalog-filters:focus-within,
.catalog-search-bar:focus-within{

border-color:var(--amber-500);

box-shadow:0 1px 2px rgba(20,16,13,.05), 0 14px 36px -12px rgba(233,163,25,.34);

}

.catalog-search-field{

position:relative;

display:flex;

align-items:center;

flex:1 1 auto;

min-width:0;

}

.catalog-search-icon{

position:absolute;

left:14px;

width:17px;

height:17px;

color:var(--text-muted);

pointer-events:none;

transition:color var(--transition);

}

.catalog-filters:focus-within .catalog-search-icon,
.catalog-search-bar:focus-within .catalog-search-icon{ color:var(--amber-700); }

.catalog-search-field input{

width:100%;

min-width:0;

padding:11px 38px 11px 40px;

border:none;

background:transparent;

font-family:inherit;

font-size:.92rem;

color:var(--text-color);

}

.catalog-search-field input:focus{ outline:none; }

.catalog-search-field input::placeholder{ color:var(--text-muted); }

.catalog-search-clear{

position:absolute;

right:6px;

display:flex;

align-items:center;

justify-content:center;

width:26px;

height:26px;

border:none;

border-radius:var(--radius-full);

background:transparent;

color:var(--text-muted);

cursor:pointer;

transition:background var(--transition), color var(--transition);

}

.catalog-search-clear i,
.catalog-search-clear svg{ width:15px; height:15px; }

.catalog-search-clear:hover{

background:var(--background-alt);

color:var(--text-color);

}

/* Les deux listes déroulantes ne rétrécissent jamais sous leur
libellé : flex-shrink:0. Sans lui, « Tous les niveaux » se faisait
tronquer dès que le champ de recherche réclamait de la place. */
.catalog-filters select,
.catalog-search-bar select{

flex:0 0 auto;

max-width:170px;

padding:10px 14px;

border:1px solid var(--border-color);

border-radius:var(--radius-full);

background:var(--background);

font-family:inherit;

font-size:.86rem;

font-weight:var(--fw-semibold);

color:var(--text-light);

cursor:pointer;

transition:border-color var(--transition), color var(--transition);

}

.catalog-filters select:hover,
.catalog-search-bar select:hover{

border-color:var(--amber-500);

color:var(--text-color);

}

.catalog-filters select:focus-visible,
.catalog-search-bar select:focus-visible{

outline:2px solid var(--amber-700);

outline-offset:2px;

}

/* ----------------------------------------------------------
Filtre « Ouvertes à ma nationalité » (2026-08-30) — rangée
compacte sous la barre-pilule (la barre est un flex SANS retour à
la ligne, y insérer deux contrôles de plus la faisait déborder,
cf. le commentaire de .catalog-search-reset). Texte seul + case,
pas de pastille : règle « Icônes » du DESIGN_SYSTEM.md.
---------------------------------------------------------- */
.catalog-natio-bar{

display:flex;

align-items:center;

justify-content:center;

flex-wrap:wrap;

gap:10px 14px;

max-width:900px;

margin:0 auto 16px;

}

.catalog-natio-toggle{

display:flex;

align-items:center;

gap:8px;

font-size:.88rem;

font-weight:var(--fw-semibold);

color:var(--text-light);

cursor:pointer;

}

.catalog-natio-toggle input{

accent-color:var(--amber-500);

width:16px;

height:16px;

cursor:pointer;

}

.catalog-natio-toggle:hover{ color:var(--text-color); }

.catalog-natio-input{

flex:0 1 200px;

min-width:150px;

padding:8px 14px;

border:1px solid var(--border-color);

border-radius:var(--radius-full);

background:var(--background);

font-family:inherit;

font-size:.86rem;

color:var(--text-color);

transition:border-color var(--transition);

}

.catalog-natio-input:hover,
.catalog-natio-input:focus{ border-color:var(--amber-500); outline:none; }

.catalog-natio-input:focus-visible{ outline:2px solid var(--amber-700); outline-offset:2px; }

/* Le champ âge n'a pas besoin des 200px du champ nationalité. */
.catalog-age-input{
flex:0 1 110px;
min-width:90px;
}

/* Mention d'inéligibilité sur une carte : une ligne de texte sous le
titre, sans fond ni icône. On informe, on n'empêche pas — la carte
reste cliquable et le lien officiel actif. --tone-* : voir base.css. */
.scholarship-card-inel{

font-size:.82rem;

font-weight:var(--fw-semibold);

/* --danger-600 : la seule valeur rouge admise pour du texte sur fond
clair (voir la mesure dans base.css ~l.850). */
color:var(--danger-600);

margin:2px 0 0;

}

/* Bouton de réinitialisation. Il passait à la ligne à l'intérieur de
la pilule sur universities.html — la barre est un flex sans wrap,
et ce bouton était le seul élément à ne pas avoir de flex-shrink:0.
Le libellé est masqué sous 900px, l'icône suffit alors. */
.catalog-search-reset{

flex:0 0 auto;

display:flex;

align-items:center;

gap:7px;

padding:10px 16px;

border:1px solid var(--border-color);

border-radius:var(--radius-full);

background:var(--background);

font-family:inherit;

font-size:.86rem;

font-weight:var(--fw-semibold);

color:var(--text-light);

white-space:nowrap;

cursor:pointer;

transition:border-color var(--transition), color var(--transition);

}

.catalog-search-reset i,
.catalog-search-reset svg{ width:15px; height:15px; }

.catalog-search-reset:hover{

border-color:var(--amber-500);

color:var(--text-color);

}


/* ==========================================================
RACCOURCIS PAYS
==========================================================
Défilement horizontal plutôt que retour à la ligne : dix pays
formaient trois rangées de pastilles qui repoussaient le catalogue
sous la ligne de flottaison. Une seule rangée qui se fait glisser,
c'est le geste attendu sur ce genre de rail. */

.catalog-country-pills{

display:flex;

gap:8px;

margin:0 auto 24px;

padding-bottom:4px;

max-width:1160px;

overflow-x:auto;

scrollbar-width:none;

-webkit-overflow-scrolling:touch;

/* Fondu au bord droit : signale qu'il reste des pays à droite sans
ajouter de flèche ni de compteur. */
mask-image:linear-gradient(90deg, #000 0, #000 calc(100% - 44px), transparent 100%);

}

.catalog-country-pills::-webkit-scrollbar{ display:none; }

.catalog-country-pill{

flex:0 0 auto;

display:flex;

align-items:center;

gap:8px;

padding:7px 14px;

border-radius:var(--radius-full);

border:1px solid var(--border-color);

background:var(--background);

font-size:.82rem;

font-weight:var(--fw-semibold);

color:var(--text-light);

text-decoration:none;

transition:border-color var(--transition), color var(--transition), background var(--transition);

}

.catalog-country-pill:hover{

border-color:var(--amber-500);

color:var(--text-color);

}

.catalog-country-pill.is-active{

background:var(--primary-900);

border-color:var(--primary-900);

color:#fff;

}

.catalog-country-pill img{

width:18px;

height:13px;

object-fit:cover;

border-radius:2px;

flex-shrink:0;

}

.catalog-country-pill-count{

font-variant-numeric:tabular-nums;

font-size:.76rem;

color:var(--text-muted);

}

.catalog-country-pill.is-active .catalog-country-pill-count{ color:rgba(255,255,255,.72); }


/* ==========================================================
GROUPES PAR PAYS
==========================================================
LE BOGUE QUE CE BLOC CORRIGE (2026-08-22)

Les deux pages posaient la classe de grille (.bourses-grid /
.uni-grid, `display:grid` sur 3 colonnes) sur le conteneur de
résultats. Or le JS n'y insère pas des cartes : il y insère des
<section> « pays », chacune contenant SA propre grille.

Chaque groupe pays devenait donc une CASE de la grille à 3 colonnes.
Conséquences visibles sur les deux pages :

  - universities.html : « Chine », « Hongrie » et « Italie »
    s'affichaient côte à côte comme trois colonnes de journal. En
    lisant une rangée de gauche à droite, on passait d'un pays à
    l'autre — l'inverse exact de ce qu'un regroupement par pays est
    censé donner.
  - scholarships.html : la hauteur de chaque rangée s'alignait sur le
    groupe le plus fourni, laissant des bandes vides de plusieurs
    centaines de pixels entre les pays.

Le conteneur redevient un simple bloc dès qu'il contient des
groupes ; la grille reste à l'intérieur de chaque groupe. `:has()`
plutôt qu'une classe posée par le JS : la règle vaut alors quel que
soit le chemin de rendu (filtre, recherche, retour arrière), sans
qu'aucun script n'ait à penser à la maintenir. Le sélecteur est déjà
employé ailleurs dans ces feuilles. */

.bourses-grid:has(> .catalog-country-group),
.uni-grid:has(> .catalog-country-group),
.catalog-grid:has(> .catalog-country-group){

display:block;

}

/* Les groupes sont des <section> créées par le JS. css/base.css donne
à TOUT élément `section` un rembourrage vertical de 110px — pensé
pour les grandes sections d'une page d'accueil, absurde pour dix
groupes empilés : 220px de vide par pays, soit près de deux écrans
de blanc sur un catalogue de dix pays. On le remet à zéro et on
tient l'écart avec la seule marge basse, qui, elle, ne se cumule
pas entre voisins. */
.catalog-country-group{

padding:0;

margin-bottom:52px;

/* Le rail de raccourcis pays amène ici par ancre (#country-xxx) :
sans cette réserve, l'en-tête du pays se retrouvait collé au bord
haut de la fenêtre, sous la barre de filtres collante. */
scroll-margin-top:96px;

}

.catalog-country-group:last-child{ margin-bottom:0; }

.catalog-country-header{

display:flex;

align-items:center;

gap:12px;

margin-bottom:20px;

padding-bottom:12px;

border-bottom:1px solid var(--border-color);

}

.catalog-country-header img{

width:26px;

height:19px;

object-fit:cover;

border-radius:3px;

box-shadow:0 1px 3px rgba(20,16,13,.18);

flex-shrink:0;

}

.catalog-country-header h2{

font-family:var(--font-display);

font-size:1.3rem;

font-weight:var(--fw-extrabold);

letter-spacing:-.02em;

color:var(--text-color);

}

/* Compteur repoussé à droite, en pastille discrète : accolé au nom du
pays, il se lisait comme une partie du titre. */
.catalog-country-header span{

margin-left:auto;

padding:4px 11px;

border-radius:var(--radius-full);

background:var(--background-alt);

font-size:.76rem;

font-weight:var(--fw-semibold);

font-variant-numeric:tabular-nums;

color:var(--text-muted);

white-space:nowrap;

}


/* ==========================================================
BARRE DE RÉSULTATS
========================================================== */

.catalog-results-bar,
.bourses-results-bar{

display:flex;

align-items:center;

justify-content:space-between;

gap:16px;

flex-wrap:wrap;

margin-bottom:22px;

}

/* Dans la barre, la marge du compteur ferait doublon avec celle de la
barre elle-même. */
.catalog-results-bar .catalog-results-count,
.bourses-results-bar .bourses-results-count{ margin-bottom:0; }

/* universities.html pose le compteur en <p> nu, hors barre de
résultats : sans marge propre il se collait à l'en-tête du premier
pays. La marge est portée par le compteur lui-même plutôt que par
la barre, pour valoir dans les deux mises en page. */
.catalog-results-count,
.bourses-results-count,
.uni-results-count{

margin-bottom:22px;

font-size:.88rem;

font-weight:var(--fw-semibold);

color:var(--text-light);

font-variant-numeric:tabular-nums;

}

.catalog-clear-link,
.bourses-clear-link{

display:flex;

align-items:center;

gap:6px;

font-size:.84rem;

font-weight:var(--fw-semibold);

color:var(--amber-700);

text-decoration:none;

}

.catalog-clear-link i,
.catalog-clear-link svg,
.bourses-clear-link i,
.bourses-clear-link svg{ width:14px; height:14px; }

.catalog-clear-link:hover,
.bourses-clear-link:hover{ text-decoration:underline; }


/* ==========================================================
GRILLE
==========================================================
auto-fill + minmax plutôt qu'un nombre de colonnes figé par palier :
la grille se réorganise à la largeur réelle disponible, sans qu'on
ait à maintenir une liste de points de rupture. */

.catalog-grid,
.bourses-grid,
.uni-grid{

display:grid;

grid-template-columns:repeat(auto-fill, minmax(300px, 1fr));

gap:24px;

}


/* ==========================================================
PUCES DE CARTE
========================================================== */

.catalog-chip,
.scholarship-card-chip,
.uni-card-chip{

display:inline-flex;

align-items:center;

gap:6px;

padding:5px 11px;

border-radius:var(--radius-full);

background:var(--background-alt);

font-size:.75rem;

font-weight:var(--fw-semibold);

color:var(--text-light);

white-space:nowrap;

}

.catalog-chip i,
.catalog-chip svg,
.scholarship-card-chip i,
.scholarship-card-chip svg,
.uni-card-chip i,
.uni-card-chip svg{

width:13px;

height:13px;

flex-shrink:0;

color:var(--text-muted);

}

/* Coût réel par université (lot D1, 2026-09-07). Un prix vérifié se
lit comme les autres puces. « Frais non vérifiés » est ATTÉNUÉ, jamais
rouge : ce n'est pas une alerte, c'est une information manquante — et
aujourd'hui c'est le cas de tout le catalogue, la donnée arrive au lot
D2. */
.uni-card-chip.is-cost-unknown{
color:var(--text-muted);
background:transparent;
border:1px dashed var(--border-color);
font-weight:var(--fw-medium, 500);
}

.uni-card-chip.is-cost-unknown i,
.uni-card-chip.is-cost-unknown svg{ opacity:.7; }

/* Puce « financement » : la seule information de la carte qui décide
vraiment si l'étudiant clique. Elle est donc la seule colorée. */
.catalog-chip.is-funding,
.scholarship-card-chip.is-funding{

background:var(--amber-50, rgba(233,163,25,.13));

color:var(--amber-700);

}

.catalog-chip.is-funding i,
.catalog-chip.is-funding svg,
.scholarship-card-chip.is-funding i,
.scholarship-card-chip.is-funding svg{ color:var(--amber-700); }

/* La puce financement porte du texte LIBRE (montants à plusieurs
paliers, mentions entre parenthèses) qui peut dépasser de loin la
largeur de la carte — mesuré 1 495px dans une carte de 308px sur
téléphone (passe mobile du 2026-09-01) : en nowrap hérité de la règle
commune, le texte était simplement coupé au bord. Elle passe donc en
multi-lignes, BORNÉES à 3 (line-clamp) : certains montants italiens
font un paragraphe entier (paliers fuori sede/pendolare/in sede +
revalorisation ISTAT + majorations), et 6 lignes d'ambre avalaient la
carte — l'intégralité reste lisible sur la fiche de la bourse. Les
autres puces (niveau, pays) restent courtes et en nowrap.
-webkit-box remplace inline-flex (nécessaire au clamp) : le gap ne
s'applique plus, l'écart icône-texte passe par un margin-right. */
.catalog-chip.is-funding,
.scholarship-card-chip.is-funding{

display:-webkit-box;

-webkit-line-clamp:3;

-webkit-box-orient:vertical;

overflow:hidden;

white-space:normal;

line-height:1.45;

}

.catalog-chip.is-funding i,
.catalog-chip.is-funding svg,
.scholarship-card-chip.is-funding i,
.scholarship-card-chip.is-funding svg{

margin-right:6px;

vertical-align:-2px;

}


/* ==========================================================
SQUELETTES DE CHARGEMENT
========================================================== */

.catalog-skeleton,
.bourses-skeleton,
.uni-card-skeleton{

border-radius:var(--radius-md);

border:1px solid var(--border-color);

background:var(--background);

padding:0 0 20px;

overflow:hidden;

}

.catalog-skeleton-line,
.bourses-skeleton-line,
.uni-card-skeleton .uni-skel-line,
.uni-card-skeleton .uni-skel-media{

background:linear-gradient(90deg, var(--background-alt) 25%, var(--background-soft) 50%, var(--background-alt) 75%);

background-size:200% 100%;

animation:shimmer-sweep 1.4s linear infinite;

border-radius:6px;

height:13px;

margin:10px 18px 0;

}

.uni-card-skeleton .uni-skel-media,
.bourses-skeleton .bourses-skeleton-line:first-child{

height:170px;

margin:0;

border-radius:0;

}


/* ==========================================================
ÉTAT VIDE / ERREUR
==========================================================
Il occupe toute la largeur de la grille (grid-column:1/-1) : posé
dans une case de 300px, le message se retrouvait tassé dans une
colonne avec deux tiers de la page vides à côté. */

.catalog-empty,
.bourses-empty-state,
.uni-empty-state{

grid-column:1 / -1;

display:flex;

flex-direction:column;

align-items:center;

gap:12px;

padding:64px 24px;

text-align:center;

border:1px dashed var(--border-color);

border-radius:var(--radius-lg);

background:var(--background-soft);

}

/* Icône nue et ambrée — ces états signalent tous une interruption ou
une absence de résultat. Pas de pastille : règle « Icônes ». */
.catalog-empty-icon,
.bourses-empty-state-icon,
.uni-empty-state-icon{

display:flex;

align-items:center;

justify-content:center;

color:var(--amber-700);

}

.catalog-empty-icon i,
.catalog-empty-icon svg,
.bourses-empty-state-icon i,
.bourses-empty-state-icon svg,
.uni-empty-state-icon i,
.uni-empty-state-icon svg{ width:34px; height:34px; }

.catalog-empty h3,
.bourses-empty-state h3,
.uni-empty-state h3{

font-family:var(--font-display);

font-size:1.15rem;

font-weight:var(--fw-extrabold);

color:var(--text-color);

}

.catalog-empty p,
.bourses-empty-state p,
.uni-empty-state p{

max-width:420px;

font-size:.92rem;

line-height:1.7;

color:var(--text-light);

}


/* ==========================================================
RESPONSIVE
========================================================== */

@media(max-width:900px){

/* Le libellé du bouton de réinitialisation cède la place à l'icône
seule : c'est lui qui faisait passer la barre à la ligne. */
.catalog-search-reset span{ display:none; }

.catalog-search-reset{ padding:10px 12px; }

.catalog-filters select,
.catalog-search-bar select{ max-width:130px; font-size:.8rem; padding:10px; }

}

@media(max-width:720px){

.catalog-hero,
.bourses-hero,
.uni-hero{

padding:104px 22px 30px;

min-height:0;

border-radius:var(--radius-md);

}

/* Les statistiques passent en grille sur mobile. En flex-wrap, la
troisième passait à la ligne en gardant son filet de séparation à
gauche : elle apparaissait décalée vers la droite, comme un élément
mal aligné plutôt que comme une deuxième rangée. La grille les
aligne en colonnes ; les filets disparaissent, l'écart suffit à
les séparer. */
.catalog-stats{

display:grid;

grid-template-columns:repeat(3, 1fr);

gap:14px;

margin-top:14px;

}

.catalog-stat{

padding:0;

border-left:none;

}

.catalog-stat span{ font-size:.74rem; line-height:1.35; }

/* Sous 720px la barre cesse d'être collante : entre le clavier
virtuel et une pilule de 3 rangées, elle mangeait la moitié de
l'écran au moment précis où l'on lit les résultats. */
.catalog-filters,
.catalog-search-bar{

position:static;

flex-wrap:wrap;

border-radius:var(--radius-lg);

}

.catalog-search-field{ flex:1 0 100%; }

/* `min-width:0` n'est pas décoratif : sans lui, scholarships.html
débordait de 53px à 375px (mesuré le 2026-09-01 — page large de
428px, barre de défilement horizontale sur toute la page). Un
<select> a une largeur MIN-CONTENT égale à sa plus longue option, et
`min-width:auto` (défaut des éléments flex) interdit de descendre
en dessous : #funderFilter, dont l'option la plus longue est
« Organisation (fondation, ONG, UE, banque de développement) »,
réclamait 401px et refusait de rétrécir malgré `flex:1 1 0`.
universities.html y échappait par chance — ses deux seuls selects
ont des options courtes. Même correctif que .catalog-search-field
plus haut, et même raison. */
/* `flex:1 1 140px` et non `1 1 0` : à trois selects sur une rangée de
375px, une base nulle les réduisait à ~63px chacun et « Tous les
types » se lisait « Tous le ». Avec une base de 140px, la barre
(flex-wrap:wrap ci-dessus) en pose deux par rangée et renvoie le
troisième à la ligne, lisible en entier. */
.catalog-filters select,
.catalog-search-bar select{ flex:1 1 140px; max-width:none; min-width:0; }

.catalog-grid,
.bourses-grid,
.uni-grid{ grid-template-columns:1fr; gap:18px; }

.catalog-country-group{ margin-bottom:38px; }

}

/* Seul cas où le header reste affiché pendant le défilement (voir
js/header-scroll.js et le bloc prefers-reduced-motion de
css/header.css) : la barre collante doit alors se caler SOUS lui,
sinon elle glisse derrière. 82px de header + 12px de respiration. */
@media (prefers-reduced-motion: reduce){

.catalog-filters,
.catalog-search-bar{ top:94px; }

.catalog-hero-bg,
.bourses-hero-bg,
.uni-hero-bg{ transform:none; }

}


/* ==========================================================
CATALOGUES SUR FOND SOMBRE — 2026-09-01
==========================================================
Les deux pages de catalogue passent du papier chaud à l'encre chaude,
la même que le bandeau « Ouvert en ce moment » de l'accueil
(css/homepage-redesign.css:194). PAS le graphite --gradient-deep des
pages de diagnostic : celui-ci est neutre-froid, l'encre du bandeau est
chaude et s'accorde au papier du reste du site.

MÉTHODE — deux calques de tokens, aucun correctif élément par élément.
Ces feuilles sont écrites au token (185 var() contre 15 couleurs en
dur) : il suffit donc de redéfinir les neutres dans le scope de la
page, puis de RÉTABLIR les neutres clairs dans les îlots qui restent
clairs — les cartes. C'est la règle déjà posée dans css/base.css :
« toute page à fond sombre doit surcharger les tokens ci-dessus […]
jamais avec de nouvelles valeurs inventées localement ».

POURQUOI #pageMain ET NON body — .header.is-scrolled a un fond blanc
écrit en dur (css/header.css:55) et son texte suit --text-light. Des
tokens sombres posés sur body donneraient une barre blanche à texte
taupe clair, illisible. #pageMain exclut les deux en-têtes, publique
et connectée, et il est déplacé d'un shell à l'autre par
js/dual-shell.js sans jamais cesser d'être ce même élément.

CONTRASTES MESURÉS sur les trois arrêts du dégradé (#241E15, #2C2419,
#211D18), au pire cas :
  #FDFCFA titres .................. 14,9:1
  #D8CBB4 texte courant ........... 9,6:1
  #A2957F texte sourd, placeholder  5,2:1
  #D9805A ambre de page ........... 5,2:1
--amber-700 (#8F3E22 sur ces pages) tombe à 2,1:1 : il est remappé sur
--amber-500. C'est la seule valeur qui devenait illisible.
========================================================== */

body.catalog-dark{

/* Aplat d'encre + dégradé sur le premier écran seulement. Ni
`background-attachment:fixed` (saccades connues sur Safari mobile) ni
pseudo-élément (aucun n'est posé sur body par ces feuilles, autant ne
pas en introduire un) : le dégradé accueille en haut de page, la page
se pose ensuite sur l'encre pleine. */
background-color:var(--ink-surface);
background-image:
radial-gradient(120% 90% at 12% 0%, #2C2419 0%, rgba(44,36,25,0) 58%),
linear-gradient(180deg, #241E15 0%, var(--ink-surface) 100%);
background-repeat:no-repeat;
background-size:100% 100vh;

}

/* --- Calque 1 : le chrome de page passe en sombre --- */
body.catalog-dark #pageMain{

--text-color:#FDFCFA;
--text-light:#D8CBB4;
--text-muted:#A2957F;

--border-color:rgba(255,255,255,.14);

--background:rgba(255,255,255,.055);
--background-soft:rgba(255,255,255,.045);
--background-alt:rgba(255,255,255,.10);
--background-light:rgba(255,255,255,.07);

/* Voir l'en-tête : #8F3E22 tombe à 2,1:1 sur cette encre. */
--amber-700:var(--amber-500);

/* INDIRECTIONS FIGÉES — à ne pas oublier. Ces trois tokens valent
`var(--amber-700)` ou `var(--amber-600)` et sont déclarés à :root
(css/base.css:287, css/catalog-redesign.css:50, css/journey-shell.css
:37 et :44). Un var() écrit DANS la valeur d'une propriété
personnalisée est résolu sur l'élément qui la déclare : la valeur
héritée ici est donc déjà #8F3E22, et le remappage ci-dessus ne
l'atteint pas. Il faut les réécrire explicitement. */
--primary-color:var(--amber-500);
--fitz-color:var(--amber-500);
--accent-color:var(--amber-500);

}

/* --- Calque 2 : les cartes, surfaces ÉLEVÉES et non îlots clairs ---
Première version : les cartes restaient blanches, comme celles du
bandeau de l'accueil. À l'écran, sur une grille entière, l'effet était
tout autre — le bandeau montre 4 cartes qui défilent, le catalogue en
aligne 16 : des pavés blancs sur l'encre, sans cohérence avec le reste
de la page. Corrigé le 2026-09-01 après lecture à l'écran.

Une carte sur fond sombre ne s'éclaircit pas jusqu'au papier : elle
s'ÉLÈVE d'un cran au-dessus du fond, et c'est la bordure qui la
découpe. Elles héritent donc des tokens sombres du calque 1, et ne
gardent en propre que leur surface.

CONTRASTES MESURÉS sur la surface de carte résultante (#312D28) :
  #FDFCFA titre .............. 13,3:1
  #D8CBB4 texte courant ...... 8,5:1
  #A2957F texte sourd ........ 4,6:1
  #D9805A nom de bourse ...... 4,7:1
et sur la puce de niveau (#46423E), dont le texte est --text-light :
  #D8CBB4 ..................... 6,2:1 */
body.catalog-dark .scholarship-card,
body.catalog-dark .uni-card{

background:rgba(255,255,255,.07);

}

/* Le panneau d'accompagnement est un .glass-panel : --glass vaut
rgba(255,255,255,.72), pensé pour du papier. Laissé tel quel, il
poserait une vitre presque blanche au milieu de l'encre. */
body.catalog-dark .uni-accompaniment-banner{

--glass:rgba(255,255,255,.06);
--glass-border:rgba(255,255,255,.14);

}

/* --- La seule couleur écrite en dur du chrome ---
.catalog-filters / .catalog-search-bar portent rgba(255,255,255,.86)
(l.378) : un token ne peut pas l'atteindre. L'ombre portée d'origine
est inutile sur fond sombre — c'est la bordure et le voile translucide
qui donnent le relief. */
body.catalog-dark .catalog-filters,
body.catalog-dark .catalog-search-bar{

background:rgba(255,255,255,.06);
box-shadow:none;

}

body.catalog-dark .catalog-filters:focus-within,
body.catalog-dark .catalog-search-bar:focus-within{

background:rgba(255,255,255,.10);
box-shadow:0 14px 36px -12px rgba(0,0,0,.5);

}

/* Les listes déroulantes suivent --background, mais leur menu natif
est rendu par le système : on force la couleur du texte des options,
sinon Windows peut servir du texte clair sur fond clair. */
body.catalog-dark #pageMain select option{

color:#1A1714;
background:#FDFCFA;

}

/* --- Calque 3 : le voile de la bannière quitte le marine ---
LE DÉFAUT (signalé à l'écran le 2026-09-01, à la jonction bannière /
page) : le voile de la bannière est construit sur rgba(7,10,22) et
rgba(18,22,46) — ce dernier est exactement --primary-900, l'ancien
marine de la marque. Tant que la page était en papier chaud, le
contraste masquait la différence de teinte. Sur l'encre chaude, la
bannière tire au bleu-gris et la page au brun : deux noirs côte à côte
qui ne sont pas de la même famille, et la jonction se voit.

Les deux arrêts sont remplacés par leurs équivalents CHAUDS À
LUMINANCE ÉGALE — le voile garde exactement son pouvoir d'assombrissement,
seule la teinte change :
  #070A16 (L 0,0032) -> #0D0A06 (L 0,0032)
  #12162E (L 0,0090) -> #1C1710 (L 0,0090)
Titre blanc : 19,3:1 et 17,4:1 sur ces deux fonds.

Le dernier arrêt du dégradé vertical descend désormais jusqu'à
--ink-surface, l'encre de la page elle-même : le bas de la bannière se
dissout dans la page au lieu de s'y arrêter net.

L'éclat ambré reste écrit en littéral (#E9A319) : c'est la seule
touche de marque de la bannière, et --amber-500 vaut terracotta sur
ces deux pages. */
body.catalog-dark .catalog-hero::after,
body.catalog-dark .bourses-hero::after,
body.catalog-dark .uni-hero::after{

background:
radial-gradient(ellipse 58% 76% at 88% 8%, rgba(233,163,25,.30) 0%, rgba(233,163,25,.09) 46%, transparent 70%),
linear-gradient(102deg, rgba(13,10,6,.94) 0%, rgba(28,23,16,.80) 42%, rgba(28,23,16,.34) 100%),
linear-gradient(180deg, rgba(13,10,6,.30) 0%, transparent 34%, rgba(13,10,6,.62) 86%, var(--ink-surface) 100%);

}

/* La bannière n'a plus de bord à défendre : elle se fond dans la page
par le bas. Les coins arrondis ne servaient qu'à la détacher du papier
et, à pleine largeur, ne se voient plus que tout en haut de l'écran.
On les garde en haut, on les supprime en bas — là où la dissolution
doit être franche. */
body.catalog-dark .catalog-hero,
body.catalog-dark .bourses-hero,
body.catalog-dark .uni-hero{

border-bottom-left-radius:0;
border-bottom-right-radius:0;

/* margin-bottom:26px est CONSERVÉ. L'avoir supprimé collait la barre
de filtres au bas de la bannière : .catalog-body n'a aucun rembourrage
haut (l.323), et universities.html n'a même pas ce conteneur. Le fond
de la bannière et celui de la page étant désormais la même encre, ces
26px ne se voient pas — ils ne font que respirer. */

}

/* ==========================================================
LIEN « ACCUEIL » DU HEADER (2026-09-01)
==========================================================
Le header des deux pages catalogue ne portait plus aucune navigation
sur mobile (liens croisés masqués sous 640px par css/header.css,
Connexion et CTA retirés à la demande de l'utilisateur) : un visiteur
arrivé sur le catalogue n'avait aucun retour visible vers l'accueil.
Même composant .back-link que le header de scholarship-details, aux
couleurs des deux états du header : clair sur la photo (header
transparent), encre une fois défilé (.is-scrolled, fond papier). Le
margin-bottom:32px du .back-link générique (css/base.css, pensé pour
un lien posé au-dessus d'un titre) est neutralisé — ici il vit DANS la
ligne du header. */
.header .back-link{

display:inline-flex;

align-items:center;

gap:8px;

margin:0;

font-weight:var(--fw-semibold);

font-size:var(--fs-small);

}

.header .back-link i,
.header .back-link svg{

width:16px;

height:16px;

flex-shrink:0;

}

.header:not(.is-scrolled) .back-link{

color:rgba(255,255,255,.94);

}

.header.is-scrolled .back-link{

color:var(--text-light);

}

.header.is-scrolled .back-link:hover{

color:var(--text-color);

}

/* ==========================================================
   LA VUE PAR PAYS — donner une fin au catalogue (2026-09-06)
   ==========================================================
   Mesuré avant : scholarships.html faisait 103 077 px de haut, soit
   environ 113 écrans, pour 156 cartes. La page ne se lisait pas, elle
   se subissait. La logique de découpe vit dans js/catalogue-vue.js ;
   ici, seulement de quoi rendre lisibles les deux éléments qu'elle
   ajoute : le lien d'un pays écourté, et la table des matières.
   ========================================================== */

/* « Voir les 29 bourses de l'Italie » — la sortie d'un groupe écourté.
   Pleine largeur et aligné à droite : c'est la fin d'une section, pas
   un bouton perdu au milieu des cartes. */
.catalog-country-more{

display:inline-flex;

align-items:center;

gap:8px;

margin-top:18px;

/* 44px de haut : cible tactile réglementaire, sur mobile comme
   ailleurs (la règle vaut pour tous les liens ajoutés ici). */
min-height:44px;

padding:10px 18px;

border-radius:var(--radius-full);

border:1px solid var(--border-color);

background:var(--background);

font-size:.86rem;

font-weight:var(--fw-semibold);

color:var(--text-color);

text-decoration:none;

transition:border-color var(--transition), background var(--transition);

}

.catalog-country-more:hover,
.catalog-country-more:focus-visible{

border-color:var(--amber-500);

background:var(--background-soft);

}

.catalog-country-more i,
.catalog-country-more svg{ width:16px; height:16px; flex-shrink:0; }

/* La table des matières : c'est elle qui rend la vue courte honnête.
   On montre moins de pays, mais on les NOMME tous. */
.catalog-country-index{

margin:48px auto 0;

max-width:1160px;

padding:26px 0 0;

border-top:1px solid var(--border-color);

}

.catalog-country-index-lead{

margin:0 0 16px;

font-size:.92rem;

color:var(--text-light);

}

/* GRILLE qui retourne à la ligne — surtout PAS le défilement
   horizontal de la barre du haut (.catalog-country-pills) : une table
   des matières qui sort de l'écran par la droite ne se lit pas, et sur
   téléphone on ne devine pas qu'il faut la faire glisser. */
.catalog-country-index-pills{

display:flex;

flex-wrap:wrap;

gap:8px;

}

/* Les pastilles de l'index sont les mêmes qu'en haut, mais elles ne
   doivent pas rester figées à leur largeur naturelle dans une grille
   qui enveloppe. */
.catalog-country-index-pills .catalog-country-pill{

flex:0 1 auto;

min-height:44px;

}

/* « Tout afficher » : une porte de sortie, jamais le chemin normal —
   d'où le ton discret et la place, tout en bas. */
.catalog-tout-afficher{

display:inline-flex;

align-items:center;

min-height:44px;

margin-top:20px;

font-size:.84rem;

font-weight:var(--fw-semibold);

color:var(--text-muted);

text-decoration:underline;

text-decoration-color:rgba(233,163,25,.45);

text-underline-offset:4px;

}

.catalog-tout-afficher:hover,
.catalog-tout-afficher:focus-visible{ color:var(--text-color); }

@media (max-width:640px){

.catalog-country-index{ margin-top:36px; padding-top:22px; }

/* Pleine largeur du lien de sortie de groupe : à 390px, un lien
   « Voir les 29 bourses de l'Italie » sur une seule ligne déborderait. */
.catalog-country-more{ display:flex; width:100%; justify-content:space-between; }

}
