/* ==========================================================================
   BoardGameLead — feuille de style principale

   Direction : « dos de boîte ». L'interface emprunte le langage graphique des
   boîtes de jeu contemporaines — encre profonde, aplats francs, cadres épais,
   ombres portées nettes et sans flou, autocollants de travers pour les
   signaux forts. Le thème sombre est la référence, le clair en est l'édition
   papier ; aucun des deux n'est l'inverse mécanique de l'autre.

   Le parti tenu partout : CHROME AUDACIEUX, DONNÉES CALMES. Bandeaux,
   pictogrammes, cartes, boutons et pastilles prennent le trait épais ; les
   tableaux d'offres et l'écran Santé restent compacts et sobres, parce que
   c'est là qu'on lit des prix pour décider d'un achat.

   Palette : #0E1330 encre · #FF6B35 orange · #2EC4F3 cyan · #C6E64B citron
             #FFD23F jaune · #FFF6E8 crème
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Fontes
   -------------------------------------------------------------------------- */

/*
 * Archivo et Archivo Black, hébergées dans assets/fonts (voir son README) :
 * pas de dépendance à Google au chargement de chaque page, pas de requête
 * vers un tiers à déclarer si l'application s'ouvre au public, et un rendu
 * identique hors ligne.
 *
 * Deux sous-ensembles par famille : « latin » couvre le français, « latin-ext »
 * les titres venus d'Europe centrale que BoardGameGeek rapporte tels quels
 * (Łódź, Praga Caput Regni). Le navigateur ne télécharge le second que s'il
 * rencontre un caractère qui le demande, d'où le découpage.
 *
 * `font-display: swap` : le texte s'affiche tout de suite dans la fonte
 * système, puis bascule. Une page de prix ne doit pas attendre une fonte pour
 * être lisible.
 */
@font-face {
    font-family: 'Archivo';
    font-style: normal;
    /* Fonte variable : un seul fichier couvre 400 à 700. */
    font-weight: 400 700;
    font-display: swap;
    src: url("../fonts/archivo-latin-eYWgLOH.woff2") format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                   U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                   U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Archivo';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url("../fonts/archivo-latin-ext-3uRG5nh.woff2") format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                   U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
                   U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Archivo Black';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/archivo-black-latin-ikuQ_if.woff2") format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                   U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                   U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Archivo Black';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("../fonts/archivo-black-latin-ext-jG05VBe.woff2") format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                   U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
                   U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --------------------------------------------------------------------------
   1. Jetons de design
   -------------------------------------------------------------------------- */

:root {
    /*
     * Palette brute. Les cinq aplats vifs sont communs aux deux thèmes et se
     * portent toujours avec l'encre --c-ink par-dessus : c'est la signature du
     * « dos de boîte », de l'encre sur un aplat plein plutôt que du blanc sur
     * une teinte lavée. Tous les couples ont été mesurés (WCAG AA sur petit
     * texte) plutôt qu'estimés à l'œil, le plus serré étant l'encre sur
     * l'orange à 5,99:1.
     */
    --c-ink: #0e1330;
    --c-ink-deep: #0a0e24;
    --c-ink-raised: #161c3f;
    --c-navy: #101a3d;
    --c-cream: #fff6e8;
    --c-cream-text: #f5eee0;

    --c-orange: #ff6b35;
    --c-cyan: #2ec4f3;
    --c-lime: #c6e64b;
    --c-sun: #ffd23f;
    --c-red: #ff5a4e;

    /*
     * Les mêmes teintes assombries, pour servir de TEXTE sur un fond clair —
     * un orange vif sur du blanc plafonne à 2,8:1, illisible. Une teinte ne
     * peut donc pas être à la fois l'aplat et l'encre.
     */
    --c-orange-ink: #c3350f;
    --c-cyan-ink: #0e7490;
    --c-lime-ink: #5f7a0c;
    --c-red-ink: #c1121f;

    /* Rôles — édition papier (thème clair) */
    --bg: #fff6e8;
    --bg-raised: #ffffff;
    --bg-sunken: #f6eddc;
    --bg-accent-soft: #fdeee2;

    --text: var(--c-navy);
    --text-muted: #6a6f91;
    --text-inverse: var(--c-cream);

    --border: #e6dcc7;
    /*
     * DEUX traits, et c'est la distinction qui tient toute la direction en
     * thème clair.
     *
     * --frame-ink est le trait d'encre : il va aux pièces IMPRIMÉES — le
     * bandeau, la bande de repères, l'autocollant de note, un bouton d'action.
     * Peu nombreuses, elles peuvent se permettre d'être franches.
     *
     * --border-strong est le trait des CONTENANTS — cartes, panneaux,
     * tableaux, boutons discrets. Ils sont partout, et les cerner du même
     * trait d'encre transformait la page en livre de coloriage (mesuré à
     * 15,8:1 sur le crème, là où un contour de contenant se lit très bien
     * autour de 1,8). C'est donc un trait chaud et doux, qui sépare sans
     * souligner.
     */
    --frame-ink: var(--c-navy);
    --border-strong: #c6baa4;

    --accent: var(--c-orange);
    --accent-hover: #ff8154;
    /* L'encre posée SUR l'accent, jamais du blanc : voir --c-ink ci-dessus. */
    --accent-contrast: var(--c-navy);
    /* L'accent quand il sert de texte et non d'aplat. */
    --accent-ink: var(--c-orange-ink);

    --warn: var(--c-sun);
    --danger: var(--c-red-ink);
    --highlight: var(--c-sun);

    /*
     * Rôles sémantiques, une paire par état : l'APLAT vif (sur lequel on pose
     * l'encre --accent-contrast) et l'ENCRE, sa version assombrie pour servir
     * de texte sur fond clair. Une même teinte ne peut pas faire les deux : un
     * citron vif en texte sur du blanc plafonne à 1,4:1.
     *
     * Ils remplacent l'ancien vocabulaire de couleurs brutes (--c-teal pour
     * « bon », --c-sienna pour « mauvais »), qui obligeait à savoir de quelle
     * teinte était fait un sens. Toutes les valeurs sont mesurées, pas
     * estimées : AA sur petit texte pour les encres, 3:1 pour les aplats et
     * les neutres, qui portent de l'information (segments de jauge, points de
     * couverture) autant qu'un texte.
     */
    --good: var(--c-lime);
    --good-ink: var(--c-lime-ink);
    --bad: var(--c-red);
    --bad-ink: var(--c-red-ink);
    --warn-ink: #8a5a00;
    --info: var(--c-cyan);
    --info-ink: var(--c-cyan-ink);
    --neutral: #8d8574;

    /*
     * Le bandeau : barre latérale et en-têtes de page. Sur un dos de boîte,
     * c'est la bande de titre — un navy profond dans les deux thèmes, pas une
     * surface qui suit le fond.
     */
    --band: var(--c-navy);
    --band-text: var(--c-cream-text);
    --band-muted: #9aa3cf;

    /*
     * La marque : un dé dont les points grimpent (voir _sidebar.html.twig).
     * Le pastillon reste toujours d'encre — c'est lui qui porte la forme — et
     * seul son cadre change de côté : crème sur le bandeau navy, encre sur une
     * page claire, où un cadre crème serait invisible.
     */
    --logo-tile: var(--c-ink);
    --logo-frame: var(--c-navy);
    --logo-pip: var(--c-cream-text);
    --logo-pip-lead: var(--c-orange);

    /*
     * Séries du graphe de prix, une par type de source (neuf en stock
     * permanent, ou occasion entre particuliers).
     *
     * Séparation mesurée en OKLab, vision normale et les trois dichromatismes
     * simulés, plus le contraste sur chaque fond — pas jugée à l'œil. Le
     * couple cyan/orange tient à ΔE 25,5 dans le pire cas (deutéranopie) ;
     * l'ancien couple teal/sienna tombait à 9,0 en protanopie, tout juste
     * au-dessus du plancher. Le changement de palette a donc amélioré ce
     * point plutôt que de le dégrader. Chaque thème a ses propres valeurs :
     * un trait cyan vif sur du crème plafonne à 2,0:1.
     */
    --chart-neuf: var(--c-cyan-ink);
    --chart-occasion: var(--c-orange-ink);

    /*
     * Séries du graphe de rotation des annonces (nouvelles / closes / délai
     * avant fermeture). Teintes distinctes des trois précédentes pour ne pas
     * laisser croire à un lien entre les deux graphes.
     */
    --chart-new-listings: var(--c-cyan-ink);
    --chart-closed: var(--c-orange-ink);
    /*
     * La troisième série est un TRAIT quand les deux autres sont des BARRES :
     * la forme porte déjà l'identité, un neutre suffit — et il reste sûr en
     * daltonisme, là où une troisième teinte se refermait sur l'orange
     * (mesuré : ΔE 2,0 en deutéranopie entre l'orange et un vert olive).
     */
    --chart-turnover-line: #3f4665;

    /*
     * Ombres portées NETTES, sans flou : c'est ce qui fait qu'un bloc a l'air
     * découpé et posé plutôt que dessiné. Une ombre floue évoque le logiciel,
     * une ombre franche l'imprimé — et c'est tout l'écart entre les deux
     * directions.
     *
     * Translucides, en revanche. Elles reprenaient le trait d'encre, ce qui
     * ajoutait un second contour épais à chaque bloc : le décalage seul suffit
     * à dire « posé », l'opacité n'a pas à crier avec lui.
     */
    --shadow-sm: 3px 3px 0 rgb(16 26 61 / 12%);
    --shadow-md: 5px 5px 0 rgb(16 26 61 / 14%);
    --shadow-lg: 8px 9px 0 color-mix(in srgb, var(--accent) 55%, transparent);
    /*
     * La seule ombre douce qui reste, pour ce qui flotte RÉELLEMENT au-dessus
     * de la page : barre latérale, infobulle, menu, liste de suggestions,
     * notification. Une ombre franche dit « découpé et posé sur la table »,
     * ce qui est juste pour une carte et faux pour un panneau qui plane.
     */
    --shadow-float: 0 12px 32px rgb(10 14 36 / 28%);

    /* Rythme */
    --sp-1: 0.25rem;
    --sp-2: 0.5rem;
    --sp-3: 0.75rem;
    --sp-4: 1rem;
    --sp-5: 1.5rem;
    --sp-6: 2rem;
    --sp-7: 3rem;

    --radius-sm: 6px;
    --radius: 10px;
    --radius-lg: 16px;
    --radius-pill: 999px;

    --sidebar-w: 15rem;
    --sidebar-w-collapsed: 4.5rem;
    --content-max: 84rem;

    /*
     * Hauteur commune des contrôles de formulaire pleine taille — champs,
     * sélecteurs, boutons — pour qu'ils s'alignent côte à côte. Vaut la
     * hauteur naturelle d'un champ : texte de 0.875rem à line-height 1.55,
     * plus 0.45rem de padding et 1px de bordure de chaque côté. Les boutons,
     * eux, portent line-height 1.3 (une valeur qui leur va bien sur deux
     * lignes) et finissaient donc 4 px plus courts que les champs.
     * Les variantes --sm et --icon s'en affranchissent : elles ne côtoient
     * pas de champ, et dictent leur propre gabarit.
     */
    --control-h: 2.375rem;

    --font: "Archivo", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    /*
     * Les titres, les chiffres mis en avant et les intitulés de pastille :
     * une graisse qui n'existe pas dans une fonte système, et c'est elle qui
     * fait le ton « dos de boîte ». À réserver aux blocs courts — en
     * paragraphe, elle devient illisible.
     */
    --font-display: "Archivo Black", "Archivo", system-ui, sans-serif;
    --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;

    --transition: 150ms ease;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        /* Édition nuit — la référence de la direction (voir l'en-tête). */
        --bg: var(--c-ink);
        --bg-raised: var(--c-ink-raised);
        --bg-sunken: var(--c-ink-deep);
        --bg-accent-soft: #24203f;

        --text: var(--c-cream-text);
        --text-muted: #a3abd2;

        --border: #2b3466;
        /*
         * Le cadre doit RESTER VISIBLE : c'est lui qui donne l'air découpé.
         * Mesuré à 4,7:1 sur le fond, là où une valeur plus sourde tombait à
         * 2,8:1 et faisait disparaître le trait — donc tout le style avec.
         */
        /*
         * Le trait d'encre du thème sombre est clair : c'est lui qui découpe une
         * pièce sur un fond d'encre. Mesuré à 4,7:1 sur le fond, là où une
         * valeur plus sourde tombait à 2,8:1 et faisait disparaître le trait.
         */
        --frame-ink: #8b95e8;
        --border-strong: #39447d;

        --accent: var(--c-orange);
        --accent-hover: #ff8154;
        --accent-contrast: var(--c-navy);
        /* Sur fond sombre, l'aplat vif fait aussi un bon texte (6,4:1). */
        --accent-ink: var(--c-orange);

        --danger: var(--c-red);

        --good-ink: var(--c-lime);
        --bad-ink: #ff7a70;
        --warn-ink: var(--c-sun);
        --info-ink: var(--c-cyan);
        --neutral: #6d76b0;

        --band: var(--c-ink-deep);

        /*
         * En thème sombre, le fond de page est la même encre que le pastillon de la
         * marque : sans cadre clair, elle disparaîtrait purement et simplement
         * (voir --logo-frame). C'est le cadre, ici, qui dessine la forme.
         */
        --logo-frame: var(--c-cream-text);

        --shadow-sm: 3px 3px 0 rgb(0 0 0 / 34%);
        --shadow-md: 5px 5px 0 rgb(0 0 0 / 38%);
        --shadow-lg: 8px 9px 0 color-mix(in srgb, var(--accent) 55%, transparent);
        --shadow-float: 0 14px 34px rgb(0 0 0 / 55%);

        --chart-neuf: var(--c-cyan);
        --chart-occasion: var(--c-orange);
        --chart-new-listings: var(--c-cyan);
        --chart-closed: var(--c-orange);
        --chart-turnover-line: #a3abd2;
    }
}

:root[data-theme="dark"] {
    /* Édition nuit — la référence de la direction (voir l'en-tête). */
    --bg: var(--c-ink);
    --bg-raised: var(--c-ink-raised);
    --bg-sunken: var(--c-ink-deep);
    --bg-accent-soft: #24203f;

    --text: var(--c-cream-text);
    --text-muted: #a3abd2;

    --border: #2b3466;
    /*
     * Le cadre doit RESTER VISIBLE : c'est lui qui donne l'air découpé.
     * Mesuré à 4,7:1 sur le fond, là où une valeur plus sourde tombait à
     * 2,8:1 et faisait disparaître le trait — donc tout le style avec.
     */
    /*
     * Le trait d'encre du thème sombre est clair : c'est lui qui découpe une
     * pièce sur un fond d'encre. Mesuré à 4,7:1 sur le fond, là où une
     * valeur plus sourde tombait à 2,8:1 et faisait disparaître le trait.
     */
    --frame-ink: #8b95e8;
    --border-strong: #39447d;

    --accent: var(--c-orange);
    --accent-hover: #ff8154;
    --accent-contrast: var(--c-navy);
    /* Sur fond sombre, l'aplat vif fait aussi un bon texte (6,4:1). */
    --accent-ink: var(--c-orange);

    --danger: var(--c-red);

    --good-ink: var(--c-lime);
    --bad-ink: #ff7a70;
    --warn-ink: var(--c-sun);
    --info-ink: var(--c-cyan);
    --neutral: #6d76b0;

    --band: var(--c-ink-deep);

    /*
     * En thème sombre, le fond de page est la même encre que le pastillon de la
     * marque : sans cadre clair, elle disparaîtrait purement et simplement
     * (voir --logo-frame). C'est le cadre, ici, qui dessine la forme.
     */
    --logo-frame: var(--c-cream-text);

    --shadow-sm: 3px 3px 0 rgb(0 0 0 / 34%);
    --shadow-md: 5px 5px 0 rgb(0 0 0 / 38%);
    --shadow-lg: 8px 9px 0 color-mix(in srgb, var(--accent) 55%, transparent);
    --shadow-float: 0 14px 34px rgb(0 0 0 / 55%);

    --chart-neuf: var(--c-cyan);
    --chart-occasion: var(--c-orange);
    --chart-new-listings: var(--c-cyan);
    --chart-closed: var(--c-orange);
    --chart-turnover-line: #a3abd2;
}

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 0.9375rem;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
    margin: 0 0 var(--sp-3);
    line-height: 1.2;
    /*
     * Les titres passent en Archivo Black : c'est la pièce maîtresse du
     * « dos de boîte », où le titre est massif et serré. L'interlettrage
     * négatif ne se voit qu'à cette graisse — sur du texte courant, il
     * abîmerait la lecture.
     *
     * PAS de capitales ici, contrairement à un vrai dos de boîte : la plupart
     * de ces titres sont des noms de jeux, et les capitales leur font perdre
     * leur graphie propre tout en les allongeant assez pour les faire
     * tronquer (« Le Seigneur des Anneaux: Duel… » sur une carte de Radar).
     * Les capitales sont réservées aux INTITULÉS — titres de section,
     * en-têtes de colonne, autocollants — dont le texte est court et générique.
     */
    font-family: var(--font-display);
    font-weight: 400;
    letter-spacing: -0.03em;
}

h1 {
    font-size: 1.85rem;
    line-height: 1.02;
}

h2 {
    font-size: 1.2rem;
}

h3 {
    font-size: 1rem;
}

p {
    margin: 0 0 var(--sp-4);
}

a {
    color: var(--accent);
    text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
    text-underline-offset: 2px;
}

a:hover {
    color: var(--accent-hover);
}

img {
    max-width: 100%;
    height: auto;
}

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* --------------------------------------------------------------------------
   3. Ossature
   -------------------------------------------------------------------------- */

.app {
    display: grid;
    grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
    min-height: 100vh;
    transition: grid-template-columns var(--transition);
}

@media (min-width: 60.0625rem) {
    :root[data-sidebar="collapsed"] .app {
        grid-template-columns: var(--sidebar-w-collapsed) minmax(0, 1fr);
    }
}

.sidebar {
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
    padding: var(--sp-5) var(--sp-4);
    background: var(--band);
    color: var(--band-text);
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    /* Le menu complet (toutes les sections dépliées, plus les comptes admin)
       dépasse 100vh sur un écran bas — la barre de défilement du navigateur,
       claire par défaut, tranchait durement sur ce fond sombre. */
    scrollbar-width: thin;
    scrollbar-color: rgb(255 255 255 / 25%) transparent;
}

.sidebar::-webkit-scrollbar {
    width: 8px;
}

.sidebar::-webkit-scrollbar-track {
    background: transparent;
}

.sidebar::-webkit-scrollbar-thumb {
    background: rgb(255 255 255 / 25%);
    border-radius: var(--radius-pill);
}

.sidebar::-webkit-scrollbar-thumb:hover {
    background: rgb(255 255 255 / 40%);
}

.sidebar__head {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

/*
 * Marque empilée : le dé au-dessus, le nom en dessous. À côté, le nom ne
 * disposait que d'une centaine de pixels — la largeur de la barre moins le dé,
 * le bouton de repli et les marges — et « BoardGameLead » ne tient pas
 * dedans. Il se coupait alors au milieu d'un mot (« BoardGam / eLead »).
 * Empilé, il a toute la largeur et n'a plus besoin de se couper du tout.
 */
.brand {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-2);
    flex: 1;
    min-width: 0;
    text-decoration: none;
}

.brand__die {
    width: 2.75rem;
    height: 2.75rem;
    flex: none;
}

.brand__word {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.brand__name {
    font-family: var(--font-display);
    font-size: 1.2rem;
    line-height: 1.05;
    letter-spacing: -0.035em;
    color: var(--band-text);
    /* Jamais de coupure : le nom est un seul mot, et le couper produit des
       syllabes qui ne veulent rien dire. S'il ne tenait pas, c'est la taille
       qu'il faudrait revoir, pas l'endroit de la césure. */
    white-space: nowrap;
}

.brand__name-g {
    color: var(--c-cyan);
}

.brand__name-l {
    color: var(--c-orange);
}

.brand__tagline {
    font-size: 0.64rem;
    font-weight: 500;
    line-height: 1.2;
    color: var(--band-muted);
}

/* Le dé roule d'un cran au survol : c'est un lien vers le Radar, et la marque
   d'une application de jeux a le droit de le dire. */
.brand__die {
    transform-origin: 50px 50px;
    transition: transform 220ms cubic-bezier(.2, .9, .25, 1);
}

.brand:hover .brand__die {
    transform: rotate(-8deg) scale(1.04);
}

@media (prefers-reduced-motion: reduce) {
    .brand__die,
    .brand:hover .brand__die {
        transition: none;
        transform: none;
    }
}

/* La barre latérale est un aplat navy quel que soit le thème (voir --band) :
   le cadre de la marque y passe donc au crème, indépendamment du thème. */
.sidebar {
    --logo-frame: var(--c-cream-text);
}

/* Le nom, en Archivo Black comme les titres : c'est la même voix. Posé en
   texte SVG plutôt qu'en tracé, pour qu'il reste sélectionnable, traduisible
   et lisible par un lecteur d'écran. */
.brand__name {
    font-family: var(--font-display);
    fill: var(--band-text);
}

.brand__name-g {
    fill: var(--c-cyan);
}

.brand__name-l {
    fill: var(--c-orange);
}

.brand__tagline {
    font-family: var(--font);
    font-weight: 500;
    fill: var(--band-muted);
}


.sidebar__toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    flex: none;
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1;
    cursor: pointer;
    transition: background var(--transition), color var(--transition);
}

.sidebar__toggle:hover {
    background: rgb(255 255 255 / 10%);
    color: #fff;
}

/* Jumeau du précédent, pour refermer le tiroir sur petit écran. */
.sidebar__close {
    display: none;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    flex: none;
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    background: rgb(255 255 255 / 8%);
    color: var(--band-text);
    font-size: 0.9rem;
    line-height: 1;
    cursor: pointer;
}

.sidebar__close:hover {
    background: rgb(255 255 255 / 16%);
    color: #fff;
}

/* Recherche générale, réservée au tiroir mobile (voir le point de rupture
   plus bas) : sur grand écran, le champ équivalent de la barre du haut
   (.topbar__search) suffit déjà, l'avoir aux deux endroits ferait doublon.
   Les tons clairs sur fond sombre reprennent ceux de sidebar__close plutôt
   que les jetons de formulaire habituels, pensés pour un fond clair. */
.sidebar__search {
    display: none;
    gap: var(--sp-2);
}

.sidebar__search input[type="search"] {
    flex: 1;
    min-width: 0;
    min-height: 0;
    padding: 0.5rem 0.75rem;
    border: 1px solid rgb(255 255 255 / 14%);
    border-radius: var(--radius-sm);
    background: rgb(255 255 255 / 8%);
    color: #fff;
    font: inherit;
    font-size: 0.875rem;
}

.sidebar__search input[type="search"]::placeholder {
    color: var(--text-muted);
}

.sidebar__search input[type="search"]:focus {
    outline: none;
    border-color: var(--good);
}

.sidebar__search button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    flex: none;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background: rgb(255 255 255 / 8%);
    color: var(--band-text);
    font-size: 1rem;
    cursor: pointer;
}

.sidebar__search button:hover {
    background: rgb(255 255 255 / 16%);
    color: #fff;
}

/* Voile sous le tiroir : une tape à côté le referme. */
.sidebar__scrim {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 25;
    background: rgb(9 24 30 / 55%);
}

:root[data-sidebar="collapsed"] .sidebar__head {
    justify-content: center;
}

/*
 * Barre repliée : le dé seul, centré, et le bouton de repli en dessous.
 *
 * Côte à côte ils ne tiennent pas — la barre repliée fait 4,5 rem, dont 40 px
 * de contenu utile après ses marges, quand le dé en occupait 44 à lui seul :
 * il débordait, donc se retrouvait poussé contre le bord gauche. Il se réduit
 * ici à 2,25 rem, la même largeur que les icônes de navigation en dessous,
 * avec lesquelles il doit s'aligner.
 */
:root[data-sidebar="collapsed"] .sidebar__head {
    flex-direction: column;
}

:root[data-sidebar="collapsed"] .brand {
    flex: none;
    align-items: center;
    width: 100%;
}

:root[data-sidebar="collapsed"] .brand__die {
    width: 2.25rem;
    height: 2.25rem;
}

:root[data-sidebar="collapsed"] .brand__word {
    display: none;
}

:root[data-sidebar="collapsed"] .nav__label,
:root[data-sidebar="collapsed"] .nav__section,
:root[data-sidebar="collapsed"] .sidebar__footer {
    display: none;
}

:root[data-sidebar="collapsed"] .sidebar__account {
    margin-top: auto;
}

:root[data-sidebar="collapsed"] .nav__link {
    justify-content: center;
    padding: var(--sp-3);
    position: relative;
}

/*
 * Barre repliée : le libellé a disparu, et c'est précisément là que le jeton
 * compte le plus — il est le seul signal qui reste. Il était masqué avec le
 * reste du texte, ce qui revenait à éteindre l'alerte au moment où l'on a le
 * moins de quoi la deviner. Il se cale donc dans l'angle de l'icône, un cran
 * plus petit pour tenir dans la largeur du rail.
 */
:root[data-sidebar="collapsed"] .nav__count {
    position: absolute;
    top: 3px;
    right: 3px;
    margin-left: 0;
    min-width: 1.1rem;
    height: 1.1rem;
    padding: 0 0.2rem;
    font-size: 0.62rem;
}

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

.nav__section {
    margin: var(--sp-4) 0 var(--sp-2);
    padding-left: var(--sp-3);
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}

.nav__link {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
    color: var(--band-muted);
    text-decoration: none;
    font-weight: 500;
    transition: background var(--transition), color var(--transition);
}

.nav__link:hover {
    background: rgb(255 255 255 / 7%);
    color: #fff;
}

.nav__link.is-active {
    background: var(--good);
    color: #fff;
    font-weight: 600;
}

.nav__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.1rem;
    text-align: center;
    flex: none;
    opacity: 0.9;
}

/* Les icônes tracées s'alignent sur la taille des émojis voisins. */
.nav__icon svg {
    width: 1.05em;
    height: 1.05em;
}

/* La déconnexion n'est pas une destination : elle se tient en retrait des
   liens de navigation, jusqu'à ce qu'on la vise. */
.nav__link--quiet {
    color: var(--text-muted);
}

/*
 * Le jeton du menu (voir partials/_sidebar.html.twig) : un disque plein posé
 * au bout de la ligne, avec la petite ombre franche d'une pièce qui repose
 * sur le plateau — le même parti d'encre sur aplat que les pastilles .badge--*,
 * transposé sur le navy du bandeau.
 *
 * L'ombre est portée vers le bas et non en diagonale comme --shadow-sm : sur
 * un disque de moins de vingt pixels, un décalage de trois pixels en biais se
 * lit comme un défaut d'alignement, pas comme un relief.
 *
 * Le liseré intérieur clair n'est pas décoratif : sans lui, un aplat vif posé
 * sur le navy perd son contour et le jeton se confond avec un simple aplat de
 * couleur. C'est ce qui lui donne son épaisseur de pièce.
 */
.nav__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: auto;
    flex: none;
    min-width: 1.45rem;
    height: 1.45rem;
    padding: 0 0.35rem;
    border-radius: var(--radius-pill);
    /* L'encre sur l'aplat, jamais du blanc : voir --accent-contrast. */
    color: var(--c-ink);
    font-size: 0.72rem;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    box-shadow:
        0 2px 0 rgb(0 0 0 / 35%),
        inset 0 0 0 2px rgb(255 255 255 / 35%);
}

/* Du nouveau à regarder. */
.nav__count--good {
    background: var(--good);
}

/* Une file à traiter. */
.nav__count--accent {
    background: var(--accent);
}

/*
 * Un défaut à corriger — le seul des trois à porter du blanc : l'encre navy
 * sur le rouge vif tombe sous le contraste exigible pour un texte aussi
 * petit, là où elle passe sur le citron et sur l'orange.
 */
.nav__count--bad {
    background: var(--bad);
    color: #fff;
    box-shadow:
        0 2px 0 rgb(0 0 0 / 35%),
        inset 0 0 0 2px rgb(255 255 255 / 30%);
}

/*
 * Sur un lien actif, l'aplat citron du jeton « nouveau » vient se poser sur
 * l'aplat citron du lien : il disparaîtrait. Il passe donc en crème, la seule
 * teinte qui tranche sur les deux fonds de la barre.
 */
.nav__link.is-active .nav__count--good {
    background: var(--c-cream);
}

/* Compte connecté et déconnexion, poussés en bas de la barre latérale. */
.sidebar__account {
    margin-top: auto;
    padding-top: var(--sp-3);
    border-top: 1px solid rgb(255 255 255 / 10%);
}

/* Une adresse électronique est souvent plus large que la barre : elle se
   tronque plutôt que de déborder sur le contenu de la page. */
.sidebar__account .nav__label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sidebar__footer {
    margin-top: var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 1px solid rgb(255 255 255 / 10%);
    font-size: 0.75rem;
    color: var(--text-muted);
}

.main {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/*
 * La bande de titre d'un dos de boîte : un aplat navy profond quel que soit
 * le thème — pas une surface qui suit le fond de page — fermé par un filet
 * bicolore. C'est la pièce qui fait lire la métaphore ; sans elle, la
 * direction ne se voyait qu'aux détails.
 */
.topbar {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    padding: var(--sp-4) var(--sp-5) calc(var(--sp-4) + 4px);
    background: var(--band);
    color: var(--band-text);
    border-bottom: 0;
    position: sticky;
    top: 0;
    z-index: 10;
}

/*
 * Le filet du bas, en segments alternés : la bande de repères imprimée au bord
 * d'un emballage. Un pseudo-élément plutôt qu'une bordure, aucune bordure ne
 * sachant alterner deux couleurs.
 */
.topbar::after {
    content: '';
    position: absolute;
    inset: auto 0 0;
    height: 4px;
    background: repeating-linear-gradient(90deg,
        var(--accent) 0 26px, var(--info) 26px 52px);
}

/* Ouvre le tiroir de navigation. N'existe que sur petit écran : sur grand
   écran, la barre latérale est déjà visible en permanence. */
.topbar__menu {
    display: none;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    flex: none;
    padding: 0;
    border: 2px solid rgb(255 255 255 / 24%);
    border-radius: var(--radius-sm);
    background: rgb(255 255 255 / 8%);
    color: var(--band-text);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.topbar__menu:hover {
    background: var(--bg-sunken);
}

/*
 * En capitales, contrairement aux autres titres : celui-ci nomme un écran —
 * « Radar des bonnes affaires » — et non un jeu. C'est justement la
 * distinction qui fait que les noms propres, eux, gardent leur graphie.
 */
.topbar__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.15rem;
    font-weight: 400;
    letter-spacing: -0.02em;
    text-transform: uppercase;
    line-height: 1.1;
    min-width: 0;
}

/* Champ de recherche générale, entre le titre et les actions. Caché sur
   petit écran (voir le point de rupture plus bas) au profit du champ
   équivalent dans le tiroir de navigation (voir .sidebar__search) : la
   barre du haut y est déjà occupée par le bouton de menu, le titre et,
   sur certaines fiches, plusieurs actions — un champ de texte de plus ne
   passerait pas, et surchargeait la ligne au lieu de s'y ajouter. */
.topbar__search {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    flex: 1 1 auto;
    max-width: 26rem;
    min-width: 0;
    margin-left: var(--sp-2);
}

/*
 * Champ et boutons posés SUR le bandeau : les jetons de formulaire habituels
 * sont pensés pour un fond clair et disparaissaient sur le navy. Mêmes tons
 * translucides que les contrôles de la barre latérale, qui résolvent déjà ce
 * problème au même endroit.
 */
.topbar__search input[type="search"] {
    min-height: 0;
    padding: 0.4rem 0.65rem;
    border: 2px solid rgb(255 255 255 / 20%);
    background: rgb(255 255 255 / 8%);
    color: var(--band-text);
}

.topbar__search input[type="search"]::placeholder {
    color: var(--band-muted);
}

.topbar__search input[type="search"]:focus {
    border-color: var(--accent);
    background: rgb(255 255 255 / 14%);
    outline: none;
}

/* Les boutons du bandeau : fantômes clairs, sans l'ombre portée des boutons
   de contenu — une ombre d'encre sur un aplat d'encre ne se voit pas. */
.topbar .btn--ghost {
    border-color: rgb(255 255 255 / 24%);
    background: rgb(255 255 255 / 8%);
    color: var(--band-text);
    box-shadow: none;
}

.topbar .btn--ghost:hover {
    border-color: var(--accent);
    background: rgb(255 255 255 / 16%);
    color: var(--band-text);
    transform: none;
    box-shadow: none;
}

.topbar__actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

.topbar__page-actions {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

/*
 * La rangée d'actions de la page, hors de l'en-tête : elle ne sert que sur
 * les écrans étroits, où l'en-tête n'a plus la largeur de les porter (voir
 * base.html.twig, qui rend le même bloc deux fois). Elle s'enroule plutôt
 * que de défiler : une action cachée derrière un glissement horizontal, sur
 * un écran qui défile déjà verticalement, ne se trouve pas.
 */
.page-actions {
    display: none;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
}

.content {
    flex: 1;
    width: 100%;
    max-width: var(--content-max);
    padding: var(--sp-5);
    margin: 0 auto;
}

.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--sp-4);
    margin-bottom: var(--sp-5);
}

.page-head__text {
    flex: 1 1 20rem;
    min-width: 0;
}

.page-head__lede {
    margin: 0;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   4. Composants
   -------------------------------------------------------------------------- */

/* Barre d'action groupée, visible seulement une fois au moins une ligne
   cochée (voir bulk_select_controller.js) — juste au-dessus du tableau
   qu'elle concerne. */
.bulk-bar {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
    color: var(--text-muted);
}

/* Même spécificité que .bulk-bar pour l'attribut [hidden] du navigateur :
   sans ce doublon, `display: flex` l'emportait toujours et la barre restait
   visible à « 0 sélectionnée » même avant la première coche. */
.bulk-bar[hidden] {
    display: none;
}

/* Dans « Offres closes (admin) » (voir game/show.html.twig), la barre
   d'action groupée s'intercale entre le résumé replié et le tableau : sans
   ce padding, elle rendait collée au bord gauche alors que .expansions >
   summary, elle, est indentée — même traitement que .expansions__filter,
   déjà dans cette même position ailleurs. */
.expansions .bulk-bar {
    padding: var(--sp-3) var(--sp-4);
    margin-bottom: 0;
    border-top: 1px solid var(--border);
    background: var(--bg-sunken);
}

/*
 * Le bloc découpé, motif de base de la direction : cadre d'encre de 2 px et
 * ombre portée franche. Le trait fait le style autant que la séparation, d'où
 * --border-strong et non --border, qui ne sert plus qu'aux filets internes
 * (lignes de tableau, séparateurs).
 */
.card {
    background: var(--bg-raised);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

.card__body {
    padding: var(--sp-4);
}

.card__head {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--border);
    font-weight: 620;
}

.grid {
    display: grid;
    gap: var(--sp-4);
}

.grid--cards {
    grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
}

.grid--stats {
    grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
}

/*
 * Colonnes fixes plutôt qu'auto-fit : avec un nombre de cartes qui ne
 * remplit pas exactement une largeur d'écran donnée, auto-fit laisse la
 * dernière carte seule sur sa ligne avec un grand vide à côté (le nombre de
 * colonnes ne dépend que de la largeur du conteneur, pas du nombre d'items).
 * Un compte de colonnes fixe, choisi pour le nombre de cartes de chaque
 * section, évite ce vide.
 */
.grid--2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

.grid--3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* Tuile de statistique */
.stat {
    padding: var(--sp-4);
    background: var(--bg-raised);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius);
    border-left: 8px solid var(--accent);
    box-shadow: var(--shadow-sm);
}

.stat__label {
    margin: 0;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.stat__value {
    margin: var(--sp-1) 0 0;
    font-family: var(--font-display);
    font-size: 1.9rem;
    font-weight: 400;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
}

.stat__hint {
    margin: var(--sp-1) 0 0;
    font-size: 0.78rem;
    color: var(--text-muted);
}

/*
 * Quatre variantes qui ne choisissent qu'une couleur : elles servent le
 * rythme visuel d'une rangée de compteurs, pas à qualifier ce qu'ils
 * comptent. D'où des noms de rôle de couleur plutôt que de sens — c'est bien
 * la couleur qu'on désigne ici, et le renommage a aligné chacune sur la
 * sienne : la traduction mécanique de l'ancienne palette avait laissé
 * « info » en rouge et deux variantes sur la même teinte.
 */
.stat--accent {
    border-left-color: var(--accent);
}

.stat--warn {
    border-left-color: var(--warn);
}

.stat--info {
    border-left-color: var(--info);
}

.stat--good {
    border-left-color: var(--good);
}

/* Classement à barres proportionnelles (page Statistiques) */
.rank-list {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
}

.rank-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}

.rank-row__medal {
    flex: 0 0 1.5rem;
    text-align: center;
    font-size: 0.95rem;
}

.rank-row__rank {
    flex: 0 0 1.5rem;
    text-align: center;
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.rank-row__main {
    flex: 1 1 auto;
    min-width: 0;
}

.rank-row__top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-bottom: 0.3rem;
}

.rank-row__title {
    display: inline-flex;
    align-items: baseline;
    gap: 0.35rem;
    min-width: 0;
    overflow: hidden;
}

.rank-row__title a:first-child {
    font-weight: 650;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.rank-row__secondary {
    flex: 0 0 auto;
    text-decoration: none;
    opacity: 0.7;
}

.rank-row__secondary:hover {
    opacity: 1;
}

.rank-row__value {
    flex: 0 0 auto;
    font-size: 0.82rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    white-space: nowrap;
}

.rank-row__bar-track {
    height: 7px;
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
    overflow: hidden;
}

.rank-row__bar-fill {
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--good);
    transition: background var(--transition);
}

/*
 * Chaque carte de classement porte sa propre couleur d'accent : sans elle,
 * les 7 sections de la page Statistiques étaient visuellement identiques
 * (même barre teal partout), d'où l'effet « tout en bloc ». Palette
 * restreinte à teal/sienna/safran — sable et cramoisi écartés (le premier
 * se confond avec le safran, le second porte déjà le sens « rejeté » sur
 * les goûts, voir badge--bad).
 */
/* Même principe que .stat--*, et même épaisseur de liseré : le cadre faisant
   désormais 2 px, un liseré de 3 ne se distinguait plus de lui. */
.card--good {
    border-left: 8px solid var(--good);
}

.card--info {
    border-left: 8px solid var(--info);
}

.card--accent {
    border-left: 8px solid var(--accent);
}

.card--good .rank-row__bar-fill {
    background: var(--good);
}

.card--info .rank-row__bar-fill {
    background: var(--info);
}

.card--accent .rank-row__bar-fill {
    background: var(--accent);
}

.rank-row {
    padding: var(--sp-1) var(--sp-2);
    margin: 0 calc(-1 * var(--sp-2));
    border-radius: var(--radius-sm);
    transition: background var(--transition);
}

.rank-row:hover {
    background: var(--bg-sunken);
}

/* Boutons */
/*
 * Un bouton est une pièce découpée : cadre d'encre de 2 px, ombre portée
 * franche, et il s'enfonce au clic — l'ombre disparaît tandis que le bouton
 * se déplace d'autant. C'est ce mouvement qui donne la sensation d'appuyer
 * sur quelque chose de matériel plutôt que de survoler un rectangle.
 */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: 0.45rem 0.9rem;
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--accent-contrast);
    font: inherit;
    font-weight: 700;
    font-size: 0.875rem;
    line-height: 1.3;
    min-height: var(--control-h);
    text-decoration: none;
    cursor: pointer;
    box-shadow: var(--shadow-sm);
    transition: background var(--transition), border-color var(--transition),
                color var(--transition), transform var(--transition), box-shadow var(--transition);
}

.btn:hover {
    background: var(--accent-hover);
    color: var(--accent-contrast);
    transform: translate(-1px, -1px);
    box-shadow: var(--shadow-md);
}

.btn:active {
    transform: translate(3px, 3px);
    box-shadow: none;
}

/*
 * Le bouton discret garde le trait doux et perd son ombre : il y en a quatre
 * par ligne d'un tableau d'offres, et cernés d'encre avec une ombre chacun,
 * ils faisaient à eux seuls le tapage de la page.
 */
.btn--ghost {
    background: transparent;
    border-color: var(--border-strong);
    color: var(--text);
    box-shadow: none;
}

.btn--ghost:active {
    transform: translate(1px, 1px);
}

.btn--ghost:hover {
    background: var(--bg-sunken);
    border-color: var(--accent);
    color: var(--text);
}

.btn--danger {
    background: var(--bad);
    color: var(--accent-contrast);
}

.btn--danger:hover {
    background: var(--bad);
    color: var(--accent-contrast);
}

.btn--sm {
    padding: 0.25rem 0.6rem;
    font-size: 0.8rem;
    min-height: 0;
}

.btn--icon {
    padding: 0.4rem;
    width: 2rem;
    height: 2rem;
    min-height: 0;
}

.btn[disabled],
.btn.is-disabled {
    opacity: 0.5;
    pointer-events: none;
}

/*
 * Retour de chargement. Turbo Drive ne recharge pas la page : ni onglet qui
 * tourne ni page blanche, et une page lente semblait ne rien faire. Turbo
 * pose sa barre en haut de l'écran (son bloc <style>, autorisé par le jeton
 * de style-src — voir App\Security\ContentSecurityPolicy) ; on ne lui donne
 * ici que nos couleurs, la feuille de Turbo passant avant celle-ci dans le
 * <head>. Pendant une visite ou un envoi, Turbo marque aussi le document ou
 * le formulaire d'un aria-busy, et désactive le bouton pressé (.btn[disabled]
 * ci-dessus) : le curseur suit.
 */
.turbo-progress-bar {
    height: 3px;
    background: var(--accent);
}

html[aria-busy="true"],
html[aria-busy="true"] *,
form[aria-busy="true"] {
    cursor: progress;
}

/* Badges */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.1rem 0.5rem;
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.01em;
    white-space: nowrap;
}

/* Une pastille dont l'icône tient lieu de texte (les « extras » d'une annonce
   — insert, pochettes, figurines peintes, goodies). Le tracé se cale sur la
   hauteur de texte des pastilles voisines plutôt que sur la sienne, sans quoi
   une pastille sans texte serait plus petite que toutes les autres et casserait
   l'alignement de la ligne. */
.badge svg {
    width: 1.15em;
    height: 1.15em;
    flex: none;
}

.badge--icon {
    padding-inline: 0.35rem;
}

/* Les extras d'une carte d'offre gardent leur libellé : la carte a la place que
   la ligne de tableau n'a pas, et une icône seule y perdrait son voisinage
   explicatif. */
.offer__extra {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    margin-right: 0.5rem;
}

.offer__extra svg {
    width: 1.05em;
    height: 1.05em;
    flex: none;
}

/*
 * Cinq rôles, cinq teintes, une seule proportion : 18 % de l'aplat sur la
 * surface, et l'encre du rôle par-dessus. Les proportions précédentes (16,
 * 18, 20 et 26 %) venaient de cinq couleurs différentes qu'il fallait
 * rattraper une à une ; deux d'entre elles donnaient d'ailleurs exactement le
 * même résultat depuis le changement de palette, ce qui faisait deux classes
 * pour une seule apparence.
 *
 * 18 %, parce que c'est la limite mesurée : à 26 %, trois des cinq encres
 * tombent sous le seuil AA sur le fond ainsi composité.
 */
.badge--good {
    background: color-mix(in srgb, var(--good) 18%, transparent);
    color: var(--good-ink);
}

.badge--warn {
    background: color-mix(in srgb, var(--warn) 18%, transparent);
    color: var(--warn-ink);
}

.badge--info {
    background: color-mix(in srgb, var(--info) 18%, transparent);
    color: var(--info-ink);
}

.badge--accent {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    color: var(--accent-ink);
}

.badge--bad {
    background: color-mix(in srgb, var(--bad) 18%, transparent);
    color: var(--bad-ink);
}

/* Pastille qui mène quelque part — la fiche du jeu rattaché à un lot, par
   exemple. Elle garde le gabarit d'une pastille mais prend la couleur d'un
   lien : sans quoi rien ne disait qu'on peut cliquer dessus, au milieu de
   voisines qui ne sont que des étiquettes. */
a.badge--link {
    color: var(--accent-ink);
    text-decoration: none;
}

a.badge--link:hover {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* Étiquette de catégorie/mécanisme cliquable, pour la basculer dans « Mes goûts ». */
button.badge {
    border: none;
    font: inherit;
    letter-spacing: inherit;
    cursor: pointer;
}

button.badge:hover {
    filter: brightness(0.94);
}

/*
 * Il y avait ici une surcharge qui forçait toutes les pastilles au blanc en
 * thème sombre : l'ancienne palette n'avait qu'une encre par couleur, trop
 * sombre pour un fond sombre. Chaque thème définit désormais la sienne, et
 * elle est mesurée sur le fond composité de la pastille (voir --*-ink). La
 * surcharge ne rattrapait donc plus rien : elle effaçait le codage couleur,
 * en rendant les cinq rôles identiques à la lecture.
 */

/* Formulaires */
.field {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    margin-bottom: var(--sp-4);
}

/* Même piège que .bulk-bar et .game-card plus bas : le `display: flex`
   ci-dessus l'emporte sur la règle `[hidden] { display: none }` du navigateur
   — spécificité égale, et une feuille auteur prime sur la sienne. Sans cette
   règle, masquer un champ depuis un contrôleur Stimulus ne masquait rien
   (constaté sur la composition de lot, censée n'apparaître que pour un lot). */
.field[hidden] {
    display: none;
}

.field label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
}

/* Repère chiffré sous un champ — ce que le marché a fait, sous le prix qu'on
   demande de choisir. Il ne vaut que collé au champ qu'il éclaire. */
.field__hint {
    margin: 0;
    font-size: 0.75rem;
    line-height: 1.4;
}

input[type="text"],
input[type="search"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="password"],
select,
textarea {
    width: 100%;
    padding: 0.45rem 0.65rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    font-size: 0.875rem;
}

/* Ne concerne pas textarea, dont la hauteur vient de son attribut rows.
   Sans effet visible sur les champs — c'est déjà leur hauteur naturelle —
   mais fait de --control-h la référence des deux côtés : modifier leur
   padding sans toucher au jeton ne les désalignerait plus des boutons. */
input[type="text"],
input[type="search"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="password"],
select {
    min-height: var(--control-h);
}

input:focus,
select:focus,
textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* Tableaux */
.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
}

.table th,
.table td {
    padding: 0.85rem var(--sp-3);
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

.offer-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-2);
}

.table th {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.table tbody tr:hover {
    background: var(--bg-sunken);
}

.table-wrap {
    overflow-x: auto;
    /*
     * `overflow-x: auto` ne suffit pas quand le conteneur est lui-même un
     * élément flex ou de grille : sa largeur minimale vaut `auto`, donc le
     * contenu minimal du tableau, et c'est la PAGE entière qui s'élargit au
     * lieu du seul tableau. Constaté à 320 px de large sur une fiche jeu :
     * trente-quatre pixels de débordement horizontal, sur toute la page.
     */
    min-width: 0;
}

/* Messages flash */
/*
 * En bloc et non en flex. Ces encadrés portent des phrases, parfois avec un
 * lien au milieu : en flex, chaque bout de texte et chaque lien devenaient un
 * élément flex distinct, et la phrase se retrouvait hachée et remise dans un
 * autre ordre (« Si la langue a — l'Explorateur — été mal lue »). Un appelant
 * avait déjà dû rétablir `display: block` en style en dur pour s'en sortir.
 * Le `gap` ne servait rien, aucun de ces encadrés n'ayant plusieurs enfants à
 * espacer.
 */
.flash {
    display: block;
    padding: var(--sp-3) var(--sp-4);
    margin-bottom: var(--sp-4);
    border-radius: var(--radius-sm);
    border-left: 3px solid var(--accent);
    background: var(--bg-accent-soft);
    color: var(--text);
}

.flash--warning {
    border-left-color: var(--warn);
    background: color-mix(in srgb, var(--warn) 16%, transparent);
}

.flash--danger {
    border-left-color: var(--bad);
    background: color-mix(in srgb, var(--bad) 14%, transparent);
}

/* Notifications de synchronisation */
.toast-stack {
    position: fixed;
    top: var(--sp-4);
    right: var(--sp-4);
    z-index: 1000;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    width: min(22rem, calc(100vw - 2 * var(--sp-4)));
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    display: block;
    background: var(--bg-raised);
    color: var(--text);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-float);
    padding: var(--sp-3) var(--sp-4);
    text-decoration: none;
    cursor: pointer;
    opacity: 0;
    transform: translateX(1rem);
    transition: opacity var(--transition), transform var(--transition);
}

.toast.is-visible {
    opacity: 1;
    transform: translateX(0);
}

.toast strong {
    display: block;
    color: var(--text);
}

.toast span {
    display: block;
    margin-top: 2px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.toast--failed {
    border-left-color: var(--bad);
}

/* État vide */
.empty {
    padding: var(--sp-7) var(--sp-5);
    text-align: center;
    color: var(--text-muted);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
}

.empty__icon {
    font-size: 2rem;
    margin-bottom: var(--sp-3);
}

/* --------------------------------------------------------------------------
   5. Utilitaires
   -------------------------------------------------------------------------- */

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

.nums {
    font-variant-numeric: tabular-nums;
}

/* Un montant ne se coupe pas : « 27,90 € » réparti sur deux lignes se relit
   deux fois. Le tableau défile plutôt (voir .table-wrap). */
.table td.nums,
.table th.nums {
    white-space: nowrap;
}

.row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}

.row--wrap {
    flex-wrap: wrap;
}

/* Ligne de formulaire : les champs portent leur étiquette au-dessus, les cases
   à cocher et les boutons n'en ont pas. Aligner par le bas les remet donc au
   niveau des champs plutôt qu'au milieu de la ligne.
   Reste le cas de la case à cocher, haute d'une seule ligne de texte : son
   libellé se retrouverait 8 px sous celui des champs. Lui donner la hauteur
   de contrôle suffit à recentrer son contenu (elle est déjà en inline-flex,
   align-items: center). */
.row--form {
    align-items: flex-end;
}

.row--form .field {
    margin-bottom: 0;
}

.row--form > .checkbox {
    min-height: var(--control-h);
}

.push-right {
    margin-left: auto;
}

.stack > * + * {
    margin-top: var(--sp-4);
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* --------------------------------------------------------------------------
   6. Adaptatif
   -------------------------------------------------------------------------- */

@media (max-width: 60rem) {
    .app {
        grid-template-columns: minmax(0, 1fr);
    }

    .grid--2,
    .grid--3 {
        grid-template-columns: minmax(0, 1fr);
    }

    /*
     * Le menu devient un tiroir. Il occupait auparavant une bande horizontale
     * d'icônes sans libellé : dix pictogrammes côte à côte ne se devinent pas,
     * les intitulés de section disparaissaient, et le compte n'était plus
     * joignable. Ici, la navigation complète reste lisible, et ne prend de la
     * place que le temps qu'on s'en serve.
     */
    .sidebar {
        position: fixed;
        z-index: 30;
        top: 0;
        bottom: 0;
        left: 0;
        width: min(19rem, 85vw);
        height: 100dvh;
        transform: translateX(-100%);
        transition: transform var(--transition);
        box-shadow: var(--shadow-float);
    }

    :root[data-drawer="open"] .sidebar {
        transform: none;
    }

    :root[data-drawer="open"] .sidebar__scrim {
        display: block;
    }

    /* Le repli en colonne d'icônes n'a pas cours ici : le tiroir est ouvert
       ou fermé, jamais entre les deux. */
    .sidebar__toggle {
        display: none;
    }

    .sidebar__close,
    .topbar__menu {
        display: inline-flex;
    }

    /* Cède la place au champ du tiroir (.sidebar__search ci-dessous) : voir
       le commentaire sur .topbar__search. */
    .topbar__search {
        display: none;
    }

    .sidebar__search {
        display: flex;
    }

    :root[data-sidebar="collapsed"] .nav__label,
    :root[data-sidebar="collapsed"] .nav__section,
    :root[data-sidebar="collapsed"] .sidebar__footer {
        display: revert;
    }

    /* Le tiroir étant toujours déplié, le jeton retrouve le bout de la ligne
       plutôt que l'angle de l'icône (voir la règle repliée plus haut). */
    :root[data-sidebar="collapsed"] .nav__count {
        position: static;
        margin-left: auto;
        min-width: 1.45rem;
        height: 1.45rem;
        padding: 0 0.35rem;
        font-size: 0.72rem;
    }

    :root[data-sidebar="collapsed"] .nav__link {
        justify-content: flex-start;
        padding: var(--sp-3);
    }

    /* Le tiroir mobile est toujours déplié : le nom revient même quand le
       réglage « replié » est actif sur grand écran. */
    :root[data-sidebar="collapsed"] .brand__word {
        display: flex;
    }

    /* Cibles tactiles : un doigt vise moins bien qu'un curseur. */
    .nav__link {
        padding: var(--sp-3);
    }

    /* Le pavé de marque n'a pas à occuper le quart du tiroir : la place
       gagnée met le compte et la déconnexion à portée sans faire défiler. */
    .sidebar__head {
        justify-content: space-between;
    }

    .brand__name {
        font-size: 1.1rem;
    }

    /* Barre du haut resserrée : le bouton de menu, le titre et l'action
       principale doivent tenir sur une seule ligne. */
    .topbar {
        gap: var(--sp-2);
        padding: var(--sp-2) var(--sp-3);
    }

    .topbar__title {
        font-size: 0.95rem;
        line-height: 1.25;
    }

    .topbar__actions .btn {
        padding: 0.35rem 0.6rem;
        font-size: 0.8rem;
        /* Ces boutons ne côtoient aucun champ : ils s'affranchissent de
           --control-h, sans quoi le resserrement ci-dessus ne gagnerait
           aucune hauteur. */
        min-height: 0;
    }

    /* L'en-tête ne garde que le tiroir, le titre et le thème. Les actions de
       la page passent dans le contenu, la recherche dans le tiroir — qui a
       déjà la sienne, identique (voir .sidebar__search). */
    .topbar__page-actions,
    .topbar__search {
        display: none;
    }

    .page-actions {
        display: flex;
    }

    .content {
        padding: var(--sp-4);
    }

    /* Une pile de notifications pleine largeur masquait toute la page. */
    .toast-stack {
        top: auto;
        bottom: var(--sp-4);
        left: var(--sp-3);
        right: var(--sp-3);
        width: auto;
    }
}

@media (prefers-reduced-motion: reduce) {
    .sidebar {
        transition: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
    }
}

/* --------------------------------------------------------------------------
   7. Carte d'offre
   -------------------------------------------------------------------------- */

/* Pas d'`overflow: hidden` ici, contrairement à ce qu'on attendrait d'une carte
   à coins arrondis : il servait à rogner l'image, mais rognait du même coup le
   menu d'actions qui s'ouvre par-dessus (voir .offer-menu__panel), coupé net au
   bord de la carte. Le rognage appartient à l'image, et vit donc sur elle. */
.offer {
    /* Repère de position de l'autocollant, qui déborde du cadre. */
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--bg-raised);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    transition: transform var(--transition), box-shadow var(--transition);
}

.offer:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
}

/*
 * Le `transform` ci-dessus fait de la carte un contexte d'empilement : le
 * `z-index` du menu d'actions n'a plus cours qu'à l'intérieur d'elle, et la
 * carte de la rangée suivante — plus loin dans le document — le recouvre. Le
 * panneau se retrouvait tranché en deux, mais seulement au survol, ce qui rend
 * le défaut d'autant plus déroutant.
 *
 * C'est donc la carte entière qu'il faut élever, pas son menu. Le `:has()`
 * couvre le menu resté ouvert alors que le pointeur est reparti ailleurs —
 * après un clic, ou en navigation au clavier.
 */
.offer:hover,
.offer:has(.offer-menu[open]) {
    position: relative;
    z-index: 5;
}

/*
 * Le repère de position est la carte, pas cette vignette (voir .offer) :
 * l'`overflow: hidden` ci-dessous est indispensable pour clipper l'image sur
 * les coins arrondis, et il coupait l'autocollant qui doit dépasser.
 */
.offer__visual {
    display: grid;
    place-items: center;
    aspect-ratio: 4 / 3;
    background: var(--bg-sunken);
    overflow: hidden;
    /* Les deux coins hauts de la carte, moins l'épaisseur de sa bordure. */
    border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}

.offer__visual img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.offer__placeholder {
    font-size: 2.5rem;
    opacity: 0.35;
}

/*
 * Un autocollant, pas une pastille : posé de travers et cerné d'encre, comme
 * la pastille « nouveauté » collée sur un emballage. Deux ou trois mots au
 * plus — c'est ce qui autorise l'inclinaison sans nuire à la lecture.
 */
.offer__flag {
    position: absolute;
    top: -9px;
    /* Décalé vers l'intérieur : il chevauche le cadre du haut sans jamais
       sortir de la largeur de la carte, quelle que soit la longueur du mot. */
    left: 10px;
    max-width: calc(100% - 20px);
    padding: 0.22rem 0.55rem;
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-sm);
    background: var(--bad);
    color: var(--accent-contrast);
    font-family: var(--font-display);
    font-size: 0.68rem;
    font-weight: 400;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    transform: rotate(-4deg);
    box-shadow: var(--shadow-sm);
    z-index: 2;
}

/* Un autocollant posé par une autre main : l'alternance évite l'effet de
   série quand plusieurs cartes en portent un, côte à côte. */
.offer:nth-child(even) .offer__flag {
    transform: rotate(3deg);
}

.offer__flag--warn {
    background: var(--warn);
    color: var(--accent-contrast);
}

/* Prix jamais atteint : le signal le plus fort, donc le plus visible */
.offer__flag--record {
    background: linear-gradient(135deg, var(--good), var(--warn));
    color: var(--accent-contrast);
    box-shadow: var(--shadow-md);
}

.offer--record {
    border-color: var(--good);
    box-shadow: 0 0 0 1px var(--good), var(--shadow-md);
}

.offer__body {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4) var(--sp-4);
    flex: 1;
}

.offer__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-2);
}

.offer__title {
    margin: 0;
    font-size: 0.95rem;
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.offer__head .offer__title {
    flex: 1;
    min-width: 0;
}

.offer__title a {
    color: inherit;
    text-decoration: none;
}

.offer__title a:hover {
    color: var(--accent);
}

/*
 * Score de mise en avant : le signal le plus déterminant de la carte, sorti
 * de la rangée d'étiquettes descriptives pour ne pas s'y noyer.
 */
.offer__score {
    flex-shrink: 0;
    padding: 0.2rem 0.55rem;
    font-size: 0.85rem;
    font-weight: 800;
    letter-spacing: 0;
}

.offer__badges {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
}

.offer__meta {
    margin: 0;
    font-size: 0.8rem;
}

.offer__percentile {
    margin: 0;
    font-size: 0.72rem;
}

.offer__foot {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-top: auto;
    padding-top: var(--sp-2);
}

.offer__price {
    font-size: 1.2rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
}

.offer__shipping {
    display: block;
    font-size: 0.72rem;
}

/* Cible tactile un peu plus large que .btn--icon par défaut : seuls le favori
   et le déclencheur du menu « Plus d'actions » restent visibles en permanence
   sur la carte, autant les rendre faciles à toucher. */
.offer__actions {
    display: flex;
    gap: var(--sp-2);
    flex-shrink: 0;
}

.offer__actions .btn--icon {
    width: 2.25rem;
    height: 2.25rem;
}

/* Regroupe les actions secondaires (vendue, pas d'édition française…) sous un
   seul déclencheur : les empiler en icônes sur la carte ne passe déjà plus à
   deux, et n'aurait fait qu'empirer à chaque nouvelle action ajoutée.

   Volontairement sans `position: relative` : le panneau se cale sur
   .offer__foot, la seule boîte de la carte assez large pour le borner. Ancré
   sur le bouton lui-même, il n'aurait eu que 2 rem de référence. */
.offer-menu {
    position: static;
}

.offer-menu > summary {
    list-style: none;
}

.offer-menu > summary::-webkit-details-marker {
    display: none;
}

/* Dans un tableau (voir l'écran Santé, « Annonces à identifier »), il n'y a
   pas de `.offer__foot` pour servir de repère : sans containing block à
   proximité, le panneau remontait tout le document jusqu'au premier ancêtre
   positionné — c'est-à-dire aucun avant `<html>` — et s'ouvrait n'importe où
   sur la page plutôt que sous le bouton. La cellule elle-même sert de repère
   ici ; `position: relative` sur un `<td>` ne perturbe pas la mise en page du
   tableau. Descendant, pas enfant direct : `.offer-menu` partage désormais sa
   cellule avec le bouton « Corriger », dans un `<span class="row">`. */
.table td:has(.offer-menu) {
    position: relative;
}

.offer-menu__panel {
    position: absolute;
    right: 0;
    top: calc(100% + var(--sp-1));
    z-index: 20;
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    /* Assez large pour ne pas casser les intitulés sur trois lignes, jamais
       plus large que la carte : sur une grille dense, 14 rem fixes débordaient.
       Le pourcentage se lit sur .offer__foot, qui sert de repère (voir plus bas). */
    min-width: 12rem;
    max-width: 100%;
    padding: var(--sp-2);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-float);
}

.offer-menu__panel .btn {
    justify-content: flex-start;
    width: 100%;
    text-align: left;
    white-space: normal;
}

.offer__source {
    margin: 0;
    font-size: 0.72rem;
}

.btn.is-favorite {
    border-color: var(--warn);
    color: var(--warn-ink);
}

/* Le signalement déjà posé par ce compte (voir partials/_report_button.html.twig).
   Sienne comme les pastilles qui alertent, et non safran comme le favori :
   l'un met de côté ce qu'on veut revoir, l'autre signale ce qui cloche. */
.btn.is-reported {
    border-color: var(--bad);
    color: var(--bad-ink);
}

/* --------------------------------------------------------------------------
   8. Filtres
   -------------------------------------------------------------------------- */

/* margin-bottom : jamais collée au tableau/bloc qui suit — constaté sur
   l'écran Santé (onglets « À identifier », « Alias ») depuis l'ajout de la
   recherche par titre, sans le moindre espacement vers le bas. Sans risque de
   double marge là où le bloc suivant porte déjà son propre margin-top (offre
   « Explorer ») : des marges verticales de blocs adjacents se fusionnent au
   lieu de s'additionner. */
.filters {
    padding: var(--sp-4);
    margin-bottom: var(--sp-5);
}

/* Seule rangée toujours visible : la recherche par nom, le geste le plus
   courant, ne doit jamais exiger de déplier quoi que ce soit. */
.filters__quick {
    display: flex;
    align-items: flex-end;
    gap: var(--sp-3);
}

.filters__quick .field {
    flex: 1;
    margin-bottom: 0;
}

/* Panneau repliable : tout le reste (facettes, cases d'éligibilité), replié
   par défaut pour ne pas occuper toute la largeur de l'écran en permanence —
   ouvert de lui-même seulement si un de ces filtres est déjà actif (voir
   OfferFilter::hasAdvancedCriteria()). Même marqueur ▸/▾ que .expansions
   (fiche jeu), sans reprendre son propre cadre : celui-ci vit déjà dans une
   carte, un second cadre imbriqué de la même couleur n'ajouterait rien. */
.filters__more {
    margin-top: var(--sp-3);
}

.filters__more > summary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2) var(--sp-3);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--border);
    cursor: pointer;
    list-style: none;
}

.filters__more > summary::-webkit-details-marker {
    display: none;
}

.filters__more > summary::before {
    content: "▸";
    color: var(--text-muted);
}

.filters__more[open] > summary::before {
    content: "▾";
}

.filters__body {
    padding-top: var(--sp-4);
}

.filters__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
    gap: var(--sp-3) var(--sp-4);
}

.filters__grid .field {
    margin-bottom: 0;
}

.filters__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-3) var(--sp-4);
    margin-top: var(--sp-4);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--border);
}

/* Regroupe les cases à cocher par intention (mettre en avant / élargir
   l'éligibilité) plutôt qu'un unique bloc indifférencié — voir _filters.html.twig.
   Le label prend toute la largeur pour forcer un retour à la ligne avant les
   cases qui le suivent, dans le même conteneur flex. */
.filters__group {
    width: 100%;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-3) var(--sp-4);
}

.filters__group-label {
    width: 100%;
    margin: 0;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.checkbox {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: 0.85rem;
    color: var(--text-muted);
    cursor: pointer;
}

.checkbox input {
    accent-color: var(--accent);
}

.pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-4);
    margin-top: var(--sp-5);
}

/* --------------------------------------------------------------------------
   9. Fiche jeu, collection, favoris
   -------------------------------------------------------------------------- */

.game-layout {
    display: grid;
    grid-template-columns: 18rem minmax(0, 1fr);
    gap: var(--sp-5);
    align-items: start;
}

.game-aside {
    position: sticky;
    top: calc(var(--sp-6) + var(--sp-4));
}

.game-cover {
    display: block;
    width: 100%;
    border-bottom: 1px solid var(--border);
}

.datalist {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--sp-1) var(--sp-3);
    margin: 0;
    font-size: 0.85rem;
}

.datalist dt {
    color: var(--text-muted);
}

.datalist dd {
    margin: 0;
    text-align: right;
    font-weight: 600;
}

/* Note BGG mise en avant en tête de fiche jeu : le chiffre le plus regardé
   mérite plus de poids visuel qu'une ligne parmi d'autres dans le datalist. */
.game-rating {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
}

.game-rating__value {
    font-size: 2.25rem;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.02em;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}

.game-rating__scale {
    font-size: 0.9rem;
    color: var(--text-muted);
}

.game-rating__meta {
    margin: var(--sp-1) 0 0;
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* Quatre repères lus d'un coup d'œil (joueurs, durée, complexité, année),
   plutôt que noyés au milieu d'un datalist de treize lignes toutes égales. */
/*
 * La bande de repères d'un dos de boîte : des cases JOINTIVES dans un seul
 * cadre, séparées d'un filet — pas quatre cartes qui flottent côte à côte.
 * C'est la disposition qui fait lire la bande comme une bande.
 */
.mini-stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /*
     * Les filets de séparation sont les gouttières de la grille, teintées par
     * le fond de la bande : une bordure par case laissait un filet parasite
     * le long du cadre sur la dernière rangée, et ne savait pas suivre une
     * grille qui passe à la ligne.
     */
    gap: 1px;
    background: var(--border);
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-sm);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
}

/* Les mêmes repères, mais dans la colonne principale : quatre de front tant
   qu'ils tiennent, deux ensuite. La variante de base reste à deux colonnes,
   calibrée pour la barre latérale de 18 rem. */
/*
 * Les quatre repères sur une seule ligne : c'est ainsi qu'une bande figure au
 * dos d'une boîte. En carré de deux sur deux, ils donnaient au bloc la hauteur
 * d'un écran pour quatre chiffres.
 *
 * Bande intégrée à une carte : elle en occupe toute la largeur, d'un bord à
 * l'autre. Ni cadre latéral, ni coins arrondis, ni ombre — c'est une pièce DE
 * la carte, et l'encadrer comme un bloc autonome ajoutait un trait à
 * l'intérieur d'un autre trait, qui se lisait comme une bavure.
 */
.mini-stats--strip {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    border-width: 2px 0;
    border-radius: 0;
    box-shadow: none;
}

/*
 * Une case au quart d'une colonne de dix-neuf rem, c'est environ soixante-dix
 * pixels : les marges latérales se resserrent et l'étiquette rétrécit, sans
 * quoi « 42 min » et « 1,89/5 » se faisaient tronquer en « 42 … » et « 1,8… ».
 */
.mini-stats--strip .mini-stat {
    padding-inline: var(--sp-1);
}

.mini-stats--strip .mini-stat__label {
    font-size: 0.58rem;
    letter-spacing: 0.02em;
}

/*
 * Sous seize rem, la bande repasse à deux colonnes : quatre cases de quatre
 * rem ne tiennent plus rien. Le seuil était à vingt-deux rem, si haut que la
 * colonne d'une fiche — dix-huit rem — n'a jamais vu la bande à quatre.
 *
 * Il reste ici, et c'est la valeur de la durée qui a cédé : l'unité est
 * passée dans l'intitulé (« 60–90 » / « MINUTES »), parce que « 60–90 min »
 * se coupait en « 60–9… » dans une case au quart de dix-huit rem — sur
 * toutes les fiches, téléphone ET grand écran. Remonter le seuil aurait
 * rendu la bande à deux colonnes partout, y compris là où elle a la place.
 */
@container (max-width: 16rem) {
    .mini-stats--strip {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.mini-stats--wide {
    grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
}

/*
 * La bande de pictogrammes d'un dos de boîte — joueurs, durée, âge, note.
 * Des cases jointives séparées d'un filet, comme sur un vrai emballage, et
 * non des pastilles flottantes.
 */
.mini-stat {
    padding: var(--sp-3) var(--sp-2);
    background: var(--bg-raised);
    text-align: center;
}

.mini-stat__icon {
    display: block;
    margin-bottom: 0.15rem;
    color: var(--accent-ink);
}

.mini-stat__icon svg {
    width: 1.25rem;
    height: 1.25rem;
}

.mini-stat__value {
    display: block;
    font-family: var(--font-display);
    font-weight: 400;
    font-size: 1.05rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mini-stat__label {
    display: block;
    margin-top: 0.1rem;
    font-size: 0.64rem;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Statistiques secondaires (moyenne brute, rangs, popularité BGG…) : utiles
   mais pas ce qu'on scanne en premier — repliées plutôt qu'empilées avec
   les quatre repères principaux. */
.more-stats > summary {
    cursor: pointer;
    list-style: none;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
}

.more-stats > summary::-webkit-details-marker {
    display: none;
}

.more-stats > summary::before {
    content: "▸ ";
}

.more-stats[open] > summary::before {
    content: "▾ ";
}

.more-stats > summary:hover {
    color: var(--text);
}

.more-stats .datalist {
    margin-top: var(--sp-2);
}

.chart-wrap {
    position: relative;
    width: 100%;
    min-height: 240px;
}

/* --------------------------------------------------------------------------
   Fiche jeu — lecture du marché
   -------------------------------------------------------------------------- */

/*
 * Le bandeau qui surmonte les offres : l'étalon auquel les juger, là où on les
 * regarde. Deux chiffres, pas trois — au-delà, on relit une fiche au lieu de
 * comparer des prix. Le détail vit plus bas, dans « Le marché de ce jeu ».
 */
.market-pulse {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2) var(--sp-4);
    margin: 0 0 var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    background: var(--bg-accent-soft);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    font-size: 0.85rem;
}

.market-pulse__item strong {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.market-pulse__more {
    margin-left: auto;
    font-size: 0.8rem;
    white-space: nowrap;
}

/* Une valeur qui n'a rien d'anormal en soi, mais que son contexte rend
   suspecte — un stock encore actif sous une source qui ne répond plus. */
.is-warning {
    color: var(--danger);
    font-weight: 700;
}

/* Second niveau d'une cellule de tableau : percentile, tendance… Sur une seule
   ligne, en plus petit, pour ne pas doubler la hauteur de chaque offre. */
.offer-row__aside {
    display: inline-block;
    margin-top: 0.1rem;
    font-size: 0.72rem;
    white-space: nowrap;
}

/* Titre interne à une carte : au-dessus d'une grille de repères ou d'un
   graphe, là où un card__head ferait une deuxième carte. */
.section-title {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2);
    margin: 0 0 var(--sp-3);
    font-family: var(--font-display);
    font-size: 0.82rem;
    font-weight: 400;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

.section-title .muted {
    font-size: 0.78rem;
    font-weight: 400;
}

.chart-block {
    margin: 0;
    min-width: 0;
}

.chart-block__title {
    margin-bottom: var(--sp-2);
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Une précision sous un titre de graphe : sur sa propre ligne, et sans les
   majuscules ni l'interlettrage du titre — collée à sa suite, elle se lisait
   comme la fin de celui-ci. */
.chart-block__note {
    display: block;
    margin-top: 0.2rem;
    font-size: 0.72rem;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
}

/* Les réserves méthodologiques, repliées : nécessaires pour ne pas mentir sur
   ce que valent ces chiffres, mais elles ne doivent pas occuper la page. */
/* Pleine largeur de la carte, et non une colonne de lecture étroite : le bloc
   est replié par défaut, donc déplié il a toute la place — et une colonne de
   62 caractères au milieu d'une carte large laissait un vide plus gênant que
   la longueur de ligne qu'elle évitait. */
.prose-note {
    margin-top: var(--sp-3);
    font-size: 0.82rem;
    color: var(--text-muted);
}

.prose-note p,
.prose-note li {
    margin: 0 0 var(--sp-2);
}

.prose-note p:last-child,
.prose-note li:last-child {
    margin-bottom: 0;
}

/* Une liste sans puces : chaque point s'ouvre déjà sur son intitulé en gras,
   la puce n'ajouterait qu'un décrochage. */
ul.prose-note {
    list-style: none;
    padding: 0;
}

.prose-note strong {
    color: var(--text);
}

/*
 * `min(20rem, 100%)` et non `20rem` : le minimum d'un minmax() dans un
 * repeat(auto-fit) est un plancher dur, que la colonne ne franchit pas même
 * quand le conteneur est plus étroit — c'est alors la page qui s'élargit.
 * Constaté à 320 px sur une fiche jeu : les deux graphes du marché
 * débordaient de trente-quatre pixels, et la page entière glissait avec eux.
 * Tous les repeat(auto-*) de cette feuille passent par min() pour la même
 * raison.
 */
.two-col {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
    gap: var(--sp-4);
    align-items: start;
}

/* max-height + défilement propre : une recherche renvoie souvent le jeu de
   base et une demi-douzaine d'extensions/goodies promo du même nom — sans ce
   plafond, la liste pousserait toute la boîte de dialogue à s'allonger. */
.hit-list {
    list-style: none;
    margin: var(--sp-3) 0 0;
    padding: 0;
    max-height: 20rem;
    overflow-y: auto;
}

.hit-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-2) 0;
    border-bottom: 1px solid var(--border);
}

/* Le nom tronque plutôt que de renvoyer les boutons à la ligne suivante :
   c'était ce repli qui doublait la hauteur de chaque résultat. */
.hit-list__info {
    flex: 1 1 auto;
    min-width: 0;
}

.hit-list__name {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}

.hit-list__actions {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* Le résultat qui correspond exactement au terme cherché — le jeu de base,
   la plupart du temps — ressort du reste plutôt que de se noyer avec le même
   poids visuel que ses extensions et goodies promo. */
.hit-list__exact .hit-list__name {
    font-weight: 700;
}

.hit-list__thumb {
    width: 2rem;
    height: 2rem;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    flex-shrink: 0;
}

/* --------------------------------------------------------------------------
   7bis. Carte de jeu (ludothèque)
   -------------------------------------------------------------------------- */

.game-card {
    display: flex;
    flex-direction: column;
    background: var(--bg-raised);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
    transition: transform var(--transition), box-shadow var(--transition);
}

/* `display: flex` ci-dessus l'emporterait sinon sur la règle native
   `[hidden] { display: none }` : même spécificité, et une feuille auteur
   prime toujours sur la feuille par défaut du navigateur. */
.game-card[hidden] {
    display: none;
}

.game-card:hover {
    transform: translate(-2px, -3px);
    box-shadow: var(--shadow-md);
}

.game-card__visual {
    display: grid;
    place-items: center;
    aspect-ratio: 4 / 3;
    background: var(--bg-sunken);
    overflow: hidden;
}

.game-card__visual img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.game-card__placeholder {
    font-size: 2.5rem;
    opacity: 0.35;
}

.game-card__body {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4) var(--sp-4);
    flex: 1;
}

.game-card__title {
    margin: 0;
    font-size: 0.95rem;
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.game-card__title a {
    color: inherit;
    text-decoration: none;
}

.game-card__title a:hover {
    color: var(--accent);
}

.game-card__badges {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
}

.game-card__meta {
    margin: 0;
    font-size: 0.8rem;
}

.game-card__foot {
    margin-top: auto;
    padding-top: var(--sp-2);
}

.game-card__foot select {
    width: 100%;
}

/* Offre provenant d'une plateforme favorite */
/*
 * Dix colonnes ne tiennent pas toujours dans la largeur disponible, et c'est
 * la dernière — les commandes : ouvrir l'annonce, épingler, corriger — qui
 * sortait de l'écran, donc précisément ce qu'on vient chercher. Elle reste
 * collée à droite pendant que le reste défile.
 */
.table--offers td:last-child,
.table--offers th:last-child {
    position: sticky;
    right: 0;
    background: var(--bg-raised);
    box-shadow: inset 1px 0 0 0 var(--border);
}

/*
 * Sur un écran étroit, le tableau abandonne trois colonnes plutôt que de se
 * laisser défiler. Mesuré à 390 px de large : ses huit colonnes réclamaient
 * sept cent douze pixels pour trois cent cinquante disponibles, et c'est le
 * TOTAL — la seule qu'on vient lire — qui se retrouvait hors champ, derrière
 * un défilement horizontal que rien n'annonçait. Les commandes, elles,
 * restaient visibles : la règle collante ci-dessus les avait sauvées, et
 * seulement elles.
 *
 * Les trois qui partent ne perdent rien d'irremplaçable : le prix article et
 * le port sont déjà comptés dans le total, et l'âge reste au survol du reste
 * de la ligne.
 */
@media (max-width: 46rem) {
    .table--offers .col--aside {
        display: none;
    }

    .table--offers th,
    .table--offers td {
        padding: 0.6rem 0.375rem;
    }

    /* L'intitulé, et non son contenu, fixait la largeur de la première
       colonne : « PLATEFORME » réclamait cent deux pixels là où « Okkazeo »
       s'en contentait de soixante. L'interlettrage part avec, pour la même
       raison — il ajoute cinq centièmes de cadratin par lettre, soit une
       dizaine de pixels sur un mot de dix. */
    .table--offers th {
        font-size: 0.62rem;
        letter-spacing: 0;
    }

    /*
     * La colonne des commandes était la plus large des cinq restantes — cent
     * soixante-huit pixels sur trois cent cinquante-huit — et elle est
     * collée à droite : elle mangeait donc la moitié de la fenêtre pendant
     * que le reste défilait dessous. Quatre cibles plus serrées valent mieux
     * qu'une colonne qui ne laisse rien voir, et 1,75 rem reste au-dessus du
     * pouce.
     */
    .offer-actions {
        gap: 2px;
    }

    .offer-actions .btn--icon {
        width: 1.75rem;
        height: 1.75rem;
        padding: 0.15rem;
    }

    /* Signaler une annonce sort de la ligne : c'est le geste le plus rare des
       quatre, et le seul qui ait déjà sa place ailleurs — le menu « Plus
       d'actions » de la carte du Radar. Les trois qui restent — ouvrir,
       épingler, corriger — sont ceux qu'on fait vraiment depuis une fiche. */
    .offer-actions .report-button {
        display: none;
    }
}

.offer-row--favorite > td {
    background: color-mix(in srgb, var(--good) 12%, transparent);
}

/* La cellule collée est opaque : sans ce rappel, la teinte du favori
   disparaîtrait derrière son fond neutre. */
.table--offers tr.offer-row--favorite > td:last-child {
    background: color-mix(in srgb, var(--good) 12%, var(--bg-raised));
}

.offer-row--favorite > td:first-child {
    box-shadow: inset 3px 0 0 0 var(--good);
}

.table tbody tr.offer-row--favorite:hover > td {
    background: color-mix(in srgb, var(--good) 20%, transparent);
}

/* Choix des plateformes favorites, dans les réglages */
.platform-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
    gap: var(--sp-2) var(--sp-4);
}

/* Résumé BoardGameGeek, en anglais : signalé comme une citation */
.bgg-summary {
    margin: 0;
    padding: var(--sp-3) var(--sp-4);
    border-left: 3px solid var(--border-strong);
    background: var(--bg-sunken);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    font-size: 0.9rem;
}

.bgg-summary footer {
    margin-top: var(--sp-2);
    font-size: 0.75rem;
}

/* « Traduire avec DeepL », sous le crédit du résumé anglais : sur sa propre
   ligne, décollé du texte qu'il complète. */
.bgg-summary footer form {
    margin-top: var(--sp-2);
}

/* Liste des extensions rattachées à un jeu */
.expansion-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid var(--border);
}

.expansion-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2) var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--border);
}

.expansion-list li:hover {
    background: var(--bg-sunken);
}

/* Bloc dépliable des offres d'extension */
.expansions {
    margin-top: var(--sp-4);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
}

.expansions > summary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2) var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    cursor: pointer;
    list-style: none;
    border-radius: var(--radius);
}

.expansions > summary::-webkit-details-marker {
    display: none;
}

.expansions > summary::before {
    content: "▸";
    color: var(--text-muted);
    transition: transform var(--transition);
}

.expansions[open] > summary::before {
    content: "▾";
}

.expansions > summary:hover {
    background: var(--bg-sunken);
}

.expansions__label {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-weight: 620;
}

.expansions__hint {
    font-size: 0.8rem;
}

.expansions .table-wrap {
    border-top: 1px solid var(--border);
}

.expansions__filter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2) var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--border);
    background: var(--bg-sunken);
}

/* Corrections manuelles d'une offre, depuis le tableau d'offres */
.correct-dialog {
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    /* Élargie de 28 à 40rem quand deux champs sont passés côte à côte : à
       l'ancienne largeur, chaque colonne tombait sous dix caractères de large
       et les intitulés se coupaient en trois lignes. */
    width: min(40rem, 94vw);
}

.correct-dialog::backdrop {
    background: rgb(0 0 0 / 0.5);
}

.correct-dialog__form {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    padding: var(--sp-4);
}

.correct-dialog__title {
    margin: 0;
    font-size: 1rem;
}

.correct-dialog__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--sp-2);
}

.expansions__filter label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
}

.expansions__filter select {
    width: auto;
    max-width: 22rem;
}

.expansions .card {
    border: 0;
    border-radius: 0 0 var(--radius) var(--radius);
    box-shadow: none;
}

/* Modale de correction d'une offre : les champs y sont plus serrés que dans un
   formulaire de page, la marge basse de .field ferait respirer dans le vide. */
.correct-dialog {
    max-height: 88vh;
    overflow-y: auto;
}

.correct-dialog .field {
    margin-bottom: 0;
}

/* Les questions d'une même famille sous un même intitulé : la modale posait
   dix champs à la suite, sans rien pour dire lesquels répondaient ensemble. */
.correct-dialog__section {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    margin: 0;
    padding: var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

/* Comme .field ci-dessus : le `display: flex` battrait la règle native. */
.correct-dialog__section[hidden] {
    display: none;
}

.correct-dialog__section > legend {
    padding: 0 var(--sp-1);
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Deux champs courts côte à côte tant que la largeur le permet : la modale
   tenait sur deux écrans de haut à force de tout empiler. */
/* Deux champs courts côte à côte quand la place de les lire existe, une seule
   colonne sinon : un seuil trop bas produisait deux colonnes trop étroites,
   c'est-à-dire le pire des deux dispositions. */
.field-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
    gap: var(--sp-3) var(--sp-4);
    align-items: start;
}

.checkbox-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
    gap: var(--sp-2) var(--sp-4);
}

/* L'icône de l'étiquette qui sera posée sur l'annonce, à côté de la case qui
   la pose : c'est ce qui rend la pastille reconnaissable plus tard, sans
   survol. */
.checkbox__icon {
    display: inline-flex;
    color: var(--bad-ink);
}

.checkbox__icon svg {
    width: 1.1em;
    height: 1.1em;
    flex: none;
}

.correct-dialog__form .field__hint {
    margin: 0;
    font-size: 0.72rem;
}

/* Infobulle maison, à la place de celle du système (voir
   tooltip_controller.js, qui la pose et la place). Celle du navigateur n'est
   pas stylable : grise, en petite police, et surtout étirée sur une seule
   ligne — illisible pour les explications de plusieurs phrases que cette
   application affiche au survol. */
.tooltip {
    position: fixed;
    z-index: 100;
    max-width: min(22rem, calc(100vw - 2 * var(--sp-4)));
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    color: var(--text);
    box-shadow: var(--shadow-float);
    font-size: 0.78rem;
    line-height: 1.45;
    /* Les retours à la ligne du texte sont respectés, le reste continue de se
       replier tout seul : le détail d'un score tient en une ligne par
       composante (voir ScoreExplanation::asText()), et une bulle qui les
       recollerait en un pavé ne serait plus lisible. Sans effet sur les cent
       quarante-huit autres infobulles, écrites d'un seul tenant. */
    white-space: pre-line;
    /* Une bulle qui n'attrape pas la souris : sans quoi, apparaissant sous le
       curseur, elle déclencherait le `mouseout` de l'élément qu'elle décrit et
       clignoterait. */
    pointer-events: none;
}

/* Le petit bec vers l'élément décrit, du côté où la bulle a trouvé la place
   (voir place()). Deux carrés tournés : le premier porte la bordure, le second
   masque le trait qui traverserait la bulle. */
.tooltip::before,
.tooltip::after {
    content: '';
    position: absolute;
    /* Posé par le contrôleur, qui vise le centre de l'élément décrit — une
       bulle recalée contre un bord de fenêtre n'est plus centrée sur lui. */
    left: var(--tip-arrow, 50%);
    width: 8px;
    height: 8px;
    margin-left: -4px;
    transform: rotate(45deg);
    background: var(--bg-raised);
}

.tooltip::before {
    border: 1px solid var(--border-strong);
}

.tooltip[data-side="top"]::before,
.tooltip[data-side="top"]::after {
    bottom: -5px;
}

.tooltip[data-side="bottom"]::before,
.tooltip[data-side="bottom"]::after {
    top: -5px;
}

.tooltip[data-side="top"]::after {
    bottom: -3px;
}

.tooltip[data-side="bottom"]::after {
    top: -3px;
}

/* Un repère dont le détail est au survol : le pointillé et le curseur sont les
   deux seules choses qui le disent. */
.correct-dialog__form abbr[title] {
    text-decoration: underline dotted;
    cursor: help;
}

/* Ajout/import de jeux dans la ludothèque (voir dialog_controller.js) : plus
   large que .correct-dialog, son contenu tient en deux colonnes. */
.add-dialog {
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    width: min(48rem, 92vw);
    max-height: 85vh;
}

.add-dialog::backdrop {
    background: rgb(0 0 0 / 0.5);
}

.add-dialog__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-4);
    border-bottom: 1px solid var(--border);
}

.add-dialog__head h2 {
    margin: 0;
    font-size: 1rem;
}

.add-dialog__body {
    padding: var(--sp-4);
    overflow-y: auto;
}

/* Détail d'une synchronisation (voir run_details_controller.js et
   _sync_run_details.html.twig), dans le même habillage add-dialog que les
   suggestions de rattachement — une boîte distincte, propre à l'onglet
   Synchronisations, mais pas de raison d'inventer un style à part. */
.run-details__meta {
    margin: 0 0 var(--sp-3);
    font-size: 0.8rem;
}

.run-details__section {
    margin-top: var(--sp-4);
}

.run-details__section:first-of-type {
    margin-top: 0;
}

.run-details__section h4 {
    margin: 0 0 var(--sp-2);
    font-size: 0.85rem;
}

.run-details__error {
    margin: 0;
    padding: var(--sp-3);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--bad) 12%, transparent);
    white-space: pre-wrap;
}

.run-details__more {
    margin: var(--sp-2) 0 0;
    font-size: 0.8rem;
}

/* Résultats de la recherche de jeu à la frappe (voir game_picker_controller.js).
   `position: relative` sur le champ ancre la liste juste en dessous, plutôt
   que le pavé natif — non stylable, aussi long que le référentiel entier —
   qu'affichait un <datalist>. */
.field--game-picker {
    position: relative;
}

.game-picker__results {
    position: absolute;
    z-index: 20;
    top: 100%;
    left: 0;
    right: 0;
    margin: var(--sp-1) 0 0;
    padding: var(--sp-1);
    max-height: 14rem;
    overflow-y: auto;
    list-style: none;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-float);
}

.game-picker__option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
    font-size: 0.85rem;
    cursor: pointer;
}

.game-picker__option:hover,
.game-picker__option.is-active {
    background: var(--bg-sunken);
}

/* Aperçu de la fiche BoardGameGeek d'une suggestion, sans quitter la liste
   pour autant — voir game_picker_controller.js#suggest(). */
.game-picker__bgg-link {
    flex-shrink: 0;
    font-size: 0.75rem;
    color: var(--text-muted);
}

.game-picker__bgg-link:hover {
    color: var(--text);
}

/* Ligne non sélectionnable annonçant l'absence de suggestion. */
.game-picker__empty {
    padding: var(--sp-2) var(--sp-3);
    font-size: 0.85rem;
    color: var(--text-muted);
    cursor: default;
}

/* Jeux déjà ajoutés à la composition d'un lot (voir game_picker_controller.js#renderChips()). */
.game-picker__chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin: var(--sp-2) 0 0;
    padding: 0;
    list-style: none;
}

.game-picker__chip {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: var(--sp-1) var(--sp-2);
    font-size: 0.8rem;
    background: var(--bg-sunken);
    border-radius: var(--radius-pill);
}

/* Une puce venue de la reconnaissance du titre, et non d'une décision : le
   trait discontinu dit qu'elle est proposée, pas retenue — et qu'enregistrer
   sans y toucher ne la figera pas (voir game-picker#applyInitial()). */
.game-picker__chip--proposed {
    background: transparent;
    border: 1px dashed var(--border);
    color: var(--text-muted);
}

/* Les extensions du jeu rattaché, proposées d'un clic. */
.game-picker__suggestions {
    margin-top: var(--sp-3);
}

.game-picker__suggestion-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-top: var(--sp-1);
}

.game-picker__suggestion-all {
    font-weight: 600;
}

/* Une action secondaire au fil du texte : « Repartir du titre » appartient à
   la phrase qui explique l'état, pas à une rangée de boutons. */
.link-button {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: var(--good-ink);
    text-decoration: underline;
    cursor: pointer;
}

.game-picker__chip-remove {
    background: none;
    border: 0;
    padding: 0 0 0 var(--sp-1);
    font-size: 0.9rem;
    line-height: 1;
    cursor: pointer;
    color: inherit;
}

/* Offre marquée vendue à la main : le bouton de correction le dit sans qu'on
   ait à ouvrir la modale. */
.btn.is-sold {
    border-color: var(--bad);
    color: var(--bad-ink);
}

/* Une offre déjà corrigée à la main : la commande de correction le dit
   d'elle-même, depuis que la colonne « Nature » — qui portait ce ✎ — a été
   retirée faute d'apprendre quoi que ce soit sur les autres lignes. */
.btn.is-corrected {
    border-color: var(--accent);
    color: var(--accent);
}

.favorite--gone {
    opacity: 0.72;
    border-left: 3px solid var(--bad);
}

@media (max-width: 60rem) {
    .game-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    .game-aside {
        position: static;
    }
}

/* --------------------------------------------------------------------------
   10. Écrans sans compte connecté
   -------------------------------------------------------------------------- */

.guest {
    display: grid;
    place-items: center;
    min-height: 100vh;
    padding: var(--sp-4);
    background: var(--bg);
}

.guest__panel {
    width: min(24rem, 100%);
    padding: var(--sp-5);
    border-radius: var(--radius);
    background: var(--bg-raised);
    box-shadow: 0 1px 3px rgb(0 0 0 / 8%), 0 8px 24px rgb(0 0 0 / 6%);
}

.guest__brand {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin: 0 0 var(--sp-5);
    font-family: var(--font-display);
    font-size: 1.5rem;
    font-weight: 400;
    letter-spacing: -0.03em;
    text-transform: none;
}

.guest__brand-mark {
    width: 2.75rem;
    height: 2.75rem;
    flex: none;
}

/* Les deux moitiés colorées du nom, comme dans la barre latérale : « Game »
   en cyan, « Lead » en orange — les deux accents de la marque, et le même
   découpage aux deux endroits. */
.guest__brand-g { color: var(--info-ink); }
.guest__brand-l { color: var(--accent-ink); }

.guest__brand-name {
    font-size: 1.5rem;
    letter-spacing: -0.03em;
    color: var(--text);
}

.guest__intro {
    margin: 0 0 var(--sp-4);
    font-size: 0.875rem;
}

/* Pleine largeur : c'est l'action principale de la page, et la seule. */
.guest__submit {
    width: 100%;
    margin-top: var(--sp-2);
    padding: 0.55rem 0.9rem;
}

.guest__aside {
    margin: var(--sp-4) 0 0;
    text-align: center;
    font-size: 0.85rem;
}

.guest .checkbox {
    margin-bottom: var(--sp-2);
    font-size: 0.85rem;
}

/* Sentinelle de cohérence (voir App\Health\ConsistencyAudit).

   Une liste plutôt qu'un tableau : chaque ligne porte un titre et sa raison
   sur deux niveaux, ce qui se lit mal en cellules — et le nombre de colonnes
   ne justifiait pas l'appareil d'un tableau. */
.audit {
    display: grid;
    gap: var(--sp-4);
}

.audit__why {
    margin: 0 0 var(--sp-4);
    color: var(--text-muted);
    max-width: 68ch;
}

/* Le remède sur sa propre ligne : c'est ce qu'on relit quand on décide d'agir,
   pas ce qu'on parcourt en diagonale. */
.audit__remedy {
    display: block;
    margin-top: var(--sp-1);
}

.audit__list {
    display: grid;
    gap: var(--sp-1);
    margin: 0;
    padding: 0;
    list-style: none;
}

.audit__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    padding: var(--sp-3);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}

.audit__what {
    display: block;
    min-width: 0;
}

.audit__detail {
    display: block;
    margin-top: var(--sp-1);
    font-size: 0.85rem;
}

/* Groupe des commandes d'une ligne (Voir la fiche, C'est normal, Corriger…) :
   sans lui, `justify-content: space-between` sur .audit__row les répartissait
   chacune sur toute la largeur restante, comme si elles n'avaient aucun
   rapport entre elles plutôt que d'être les actions d'une même ligne. Ne doit
   jamais se comprimer sous un long titre d'annonce. */
.audit__actions {
    display: flex;
    flex: none;
    align-items: center;
    gap: var(--sp-2);
}

.audit__more {
    margin: var(--sp-3) 0 0;
    font-size: 0.85rem;
}

@media (max-width: 640px) {
    .audit__row {
        flex-direction: column;
        gap: var(--sp-2);
    }
}

/* Contenu estimé d'un lot (voir App\Market\LotContentEstimator), sur sa propre
   ligne sous l'annonce : la liste des jeux ne tient dans aucune colonne. */
.offer-row__lot-row > td {
    padding-top: 0;
    border-top: none;
}

.lot-estimate {
    padding: var(--sp-3);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}

.lot-estimate p {
    margin: 0;
}

.lot-estimate__head {
    font-size: 0.9rem;
}

.lot-estimate__games,
.lot-estimate__caveat {
    margin-top: var(--sp-2) !important;
    font-size: 0.82rem;
    line-height: 1.5;
}

/* Sur une carte de lot, l'espacement vient du `gap` du corps : ces marges s'y
   ajoutaient et doublaient l'écart. La marge basse valait le plus cher — le
   `.lot-estimate p { margin: 0 }` du dessus ne s'applique qu'au conteneur du
   tableau d'offres, si bien que sur la carte ces paragraphes gardaient la marge
   par défaut du navigateur, haut *et* bas. Le `!important` d'origine sert ce
   même tableau, d'où une remise à zéro plutôt qu'une suppression. */
.lot-card .lot-estimate__games,
.lot-card .lot-estimate__caveat {
    margin: 0 !important;
}

/* La réserve compte autant que le chiffre : sans elle, un plancher se lirait
   comme une valeur ferme. */
.lot-estimate__caveat {
    font-style: italic;
}

/* --------------------------------------------------------------------------
   Page des lots (voir App\Controller\LotController)

   Trois zones, et la hiérarchie visuelle porte le verdict : les lots dont le
   chiffré couvre déjà le prix en cartes larges avec vignette, ceux qui
   attendent un regard en lignes compactes, les écartés en un simple compteur.
   Une seule liste plate de cartes identiques laissait au lecteur le travail de
   trancher, alors que c'est exactement ce que la page est censée faire.
   -------------------------------------------------------------------------- */

.lot-index__lede {
    max-width: 52rem;
    margin-top: calc(-1 * var(--sp-3));
    margin-bottom: var(--sp-4);
}

.lot-legend {
    margin-bottom: var(--sp-5);
}

.lot-legend__items {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
    gap: var(--sp-3) var(--sp-4);
    margin: 0;
    padding: var(--sp-4);
    list-style: none;
    font-size: 0.82rem;
}

.lot-legend__item {
    display: flex;
    gap: var(--sp-3);
}

.lot-legend__swatch {
    flex: 0 0 2.5rem;
    height: 0.85rem;
    margin-top: 0.2rem;
    border-radius: 3px;
    background: var(--bg-sunken);
}

.lot-legend__swatch--price {
    background: var(--text-muted);
}

.lot-legend__swatch--priced {
    background: var(--neutral);
}

.lot-legend__swatch--over {
    background: var(--accent);
}

/* Le filet clair d'un jeu réclamé par les goûts : une troisième teinte, mais
   sur un autre axe que le prix et le surplus — elle ne dit pas « combien »,
   elle dit « pour vous » (voir LotScorer). */
.lot-legend__swatch--wanted {
    background: var(--neutral);
    box-shadow: inset 0 2px 0 var(--highlight);
}

.lot-legend__swatch--unknown {
    border: 1px solid var(--border);
    background: repeating-linear-gradient(
        -45deg,
        color-mix(in srgb, var(--neutral) 42%, transparent) 0 2px,
        transparent 2px 5px
    );
}

/* Zones ------------------------------------------------------------------ */

/* La file à qualifier suit la liste des affaires : un filet et de l'air
   au-dessus, sans quoi son titre se lisait comme la légende de la dernière
   carte. */
.lot-zone {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    margin-top: var(--sp-7);
    padding-top: var(--sp-5);
    border-top: 1px solid var(--border);
}

/* Une grille plutôt qu'un flex : le sous-titre doit passer à la ligne quoi
   qu'il arrive. En flex, son `max-width` le laissait partager la ligne du
   titre dès que la fenêtre était assez large, et `flex-basis: 100%` n'y
   changeait rien — c'est la largeur maximale qui gagnait. */
.lot-zone__head {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: start;
    align-items: baseline;
    column-gap: var(--sp-3);
    row-gap: var(--sp-2);
}

.lot-zone__count {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--text-muted);
}

.lot-zone__hint {
    grid-column: 1 / -1;
    max-width: 46rem;
    margin: 0;
    color: var(--text-muted);
    font-size: 0.84rem;
}

/* Carte de lot ----------------------------------------------------------- */

.lot-card {
    display: flex;
    gap: var(--sp-5);
    padding: var(--sp-4);
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

/* Un lot est le cas où l'image dit en un quart de seconde ce que le titre met
   trois lignes à mal dire : une pile de boîtes se reconnaît, une énumération en
   capitales se déchiffre. */
.lot-card__visual {
    position: relative;
    /* Le panneau prend toute la hauteur de la carte : `stretch` est le défaut
       d'un conteneur flex, il suffit de ne plus lui opposer ni `align-self`
       ni `aspect-ratio`. La pile, elle, se cale dedans (voir .lot-card__stack)
       plutôt que de dicter la hauteur — sans quoi une carte au corps plus haut
       laissait un vide sous la vignette. */
    flex: 0 0 13rem;
    /* Jamais moins haut que large : la pile est un carré borné par la largeur
       (voir .lot-card__stack), et un panneau plus plat que ça le ferait rogner
       par `max-height` — le carré cesse alors d'en être un et toute la
       géométrie en pourcentages se décale. */
    min-height: 13rem;
    display: grid;
    place-items: center;
    overflow: hidden;
    border-radius: var(--radius);
    /* Une lueur d'accent au ras du sol, là où la pile se pose : sans elle, les
       boîtes flottaient sur une dalle plate et le cadre se lisait comme un
       trou dans la carte. */
    background:
        radial-gradient(105% 70% at 42% 96%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 68%),
        var(--bg-sunken);
    /* Un liseré intérieur : le panneau se lit comme un renfoncement où la pile
       est posée, plutôt que comme un aplat collé sur la carte. */
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 7%, transparent);
}

/* Faute de boîte au référentiel : la photo du vendeur, cadrée au plus large. */
.lot-card__photo {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.lot-card__placeholder {
    font-size: 1.6rem;
    opacity: 0.5;
}

/* Les boîtes des jeux du lot, empilées en cascade.

   Un rang horizontal les mettait côte à côte, chacune obligée d'être petite
   pour que les trois tiennent en largeur — trois timbres illisibles. La
   cascade règle les deux problèmes d'un coup : chaque boîte peut prendre plus
   de la moitié de la vignette, et le recouvrement ne lui mange qu'un coin,
   parce que la suivante monte en diagonale plutôt que de se poser à côté.

   Le décalage et l'inclinaison se déduisent de `--i`, le rang dans la pile,
   posé par le gabarit. Le z-index vient à l'inverse : la boîte la plus chère
   est la première du tableau et doit finir devant. */
/* Le cadre de la pile : le plus grand carré qui tienne dans le panneau, centré.

   Carré à dessein. Toute la géométrie ci-dessous est en pourcentages, et sur un
   cadre carré un pourcentage de largeur vaut exactement un pourcentage de
   hauteur — les décalages des deux axes se calculent alors dans la même unité,
   ce qu'un cadre de proportion quelconque interdisait. */
.lot-card__stack {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    max-height: 100%;
}

/* Chaque boîte lit sa place dans trois variables, posées plus bas par
   arrangement. Une formule unique — un pas constant appliqué au rang — avait
   été essayée d'abord : elle donnait le même escalier quel que soit le nombre
   de boîtes, penché d'un côté du cadre à deux et à l'étroit à quatre. Quatre
   dispositions composées à la main valent mieux qu'une moyenne qui ne convient
   à aucun cas. */
.lot-card__box {
    position: absolute;
    width: var(--box);
    aspect-ratio: 1 / 1;
    left: var(--x);
    bottom: var(--y);
    transform: rotate(var(--r));
    /* `cover` : la vignette remplit sa boîte, quel que soit son format.
       `contain` avait été essayé pour ne rien recadrer — les vignettes
       BoardGameGeek n'ont pas toutes les mêmes proportions — mais il laissait
       autour des images non carrées des bandes de fond vide qui se lisaient
       comme un trou dans la boîte, pas comme l'encadrement d'un tirage. Le
       recadrage coûte les bords de la jaquette ; il ne coûte pas
       l'identification, puisque les noms sont listés juste en dessous et que
       chaque boîte porte le sien en infobulle. */
    object-fit: cover;
    border-radius: 5px;
    background: var(--bg-raised);
    /* Un liseré d'un pixel *dans* l'ombre plutôt qu'une bordure : une bordure
       franche cerclait chaque boîte d'un trait gris qui les aplatissait toutes
       au même plan. Le liseré détache, l'ombre portée donne la profondeur — et
       c'est elle, avec le recouvrement, qui met la boîte de devant en avant.
       Une décroissance de taille par rang, essayée aussi, donnait plutôt
       l'impression d'une perspective ratée. */
    box-shadow:
        0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
        0 3px 10px rgb(0 0 0 / 30%);
}

/* Une seule boîte : droite et large. Ni inclinaison ni décalage n'auraient
   quoi que ce soit à suggérer, et une boîte inclinée toute seule se lit comme
   une image mal posée. */
.lot-card__stack--n1 {
    --box: 74%;
}

.lot-card__stack--n1 .lot-card__box:nth-child(1) {
    --x: 13%;
    --y: 13%;
    --r: 0deg;
}

/* Deux boîtes : appuyées l'une contre l'autre, inclinées en sens contraire.
   Un simple décalage en diagonale les faisait pencher du même côté, ce qui
   laissait le cadre visiblement déséquilibré. */
.lot-card__stack--n2 {
    --box: 60%;
}

.lot-card__stack--n2 .lot-card__box:nth-child(1) {
    --x: 5%;
    --y: 11%;
    --r: -5deg;
}

.lot-card__stack--n2 .lot-card__box:nth-child(2) {
    --x: 35%;
    --y: 27%;
    --r: 6deg;
}

/* Trois boîtes : la cascade qui monte vers la droite, celle d'une pile qu'on
   vient de poser. */
.lot-card__stack--n3 {
    --box: 52%;
}

.lot-card__stack--n3 .lot-card__box:nth-child(1) {
    --x: 4%;
    --y: 6%;
    --r: -6deg;
}

.lot-card__stack--n3 .lot-card__box:nth-child(2) {
    --x: 25%;
    --y: 23%;
    --r: 2deg;
}

.lot-card__stack--n3 .lot-card__box:nth-child(3) {
    --x: 44%;
    --y: 41%;
    --r: 9deg;
}

/* Quatre boîtes : deux rangs de deux, en quinconce. Prolonger la cascade
   jusqu'au quatrième rang ne laissait plus à chacune qu'un liseré visible ;
   deux rangs gardent à chaque boîte un angle entier. C'est le maximum retenu —
   au-delà, le compte « +N » dit le reste bien mieux qu'une cinquième vignette
   grande comme un timbre. */
.lot-card__stack--n4 {
    --box: 46%;
}

.lot-card__stack--n4 .lot-card__box:nth-child(1) {
    --x: 6%;
    --y: 4%;
    --r: -7deg;
}

.lot-card__stack--n4 .lot-card__box:nth-child(2) {
    --x: 46%;
    --y: 11%;
    --r: 5deg;
}

.lot-card__stack--n4 .lot-card__box:nth-child(3) {
    --x: 3%;
    --y: 45%;
    --r: 4deg;
}

.lot-card__stack--n4 .lot-card__box:nth-child(4) {
    --x: 44%;
    --y: 51%;
    --r: -4deg;
}

/* Ce que la pile ne montre pas : elle s'arrête à trois boîtes, le compte dit
   combien de jeux restent derrière. */
.lot-card__more {
    position: absolute;
    right: var(--sp-2);
    top: var(--sp-2);
    display: grid;
    place-items: center;
    min-width: 1.4rem;
    height: 1.4rem;
    padding: 0 0.25rem;
    border-radius: var(--radius-pill);
    /* Translucide et floutée : posée sur la pile, elle doit se lire comme une
       annotation par-dessus l'image, pas comme une quatrième boîte. */
    background: color-mix(in srgb, var(--bg-sunken) 78%, transparent);
    backdrop-filter: blur(3px);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.66rem;
    font-weight: 650;
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent);
}

.lot-card__body {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    /* Serré : à `--sp-3` entre six blocs, la carte montait à deux fois la
       hauteur de sa vignette et laissait un grand vide de part et d'autre de
       la pile. */
    gap: var(--sp-2);
    justify-content: center;
}

.lot-card__head {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
}

.lot-card__title {
    margin: 0;
    font-size: 1.02rem;
}

/* Le titre du vendeur n'est pas un titre : il passe sous celui qu'on a déduit,
   en une ligne tronquée, et garde son rôle de lien vers l'annonce. */
.lot-card__raw {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.78rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.lot-card__foot {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--sp-3);
}

.lot-card__verdict {
    display: flex;
    flex-direction: column;
}

/* Le chiffre que la carte existe pour dire : c'est lui qu'on doit lire en
   premier après le titre, avant le prix et avant la jauge. */
.lot-card__surplus {
    font-family: var(--font-mono);
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.03em;
    color: var(--accent-hover);
}

.lot-card__note {
    color: var(--text-muted);
    font-size: 0.74rem;
}

.lot-card__actions {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

/* Couverture ------------------------------------------------------------- */

/* Une pastille par jeu, pleine quand son prix est connu. Remplace le badge
   « plancher », vrai sur presque toutes les lignes et donc muet. */
.coverage {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 650;
    white-space: nowrap;
}

.coverage__dots {
    display: inline-flex;
    gap: 2px;
}

.coverage__dot {
    width: 6px;
    height: 6px;
    border: 1px solid var(--neutral);
    border-radius: 50%;
}

.coverage__dot--on {
    background: var(--neutral);
}

/* Jauge valeur/prix (voir App\Market\LotGauge) --------------------------- */

/* Deux pistes de trois cellules : intitulé, barre, montant.

   Le repère de prix n'est **pas** une cellule de cette grille, et c'est
   volontaire : un élément de grille placé explicitement réserve ses cellules,
   et les éléments auto-placés sautent alors par-dessus. Le repère occupant la
   colonne du milieu sur les deux lignes, les barres se retrouvaient rejetées
   dans la colonne des montants et toute la figure partait en morceaux. Il est
   donc posé en surimpression (voir .gauge__guide), et les deux variables
   ci-dessous disent où commence et où finit la piste pour qu'il s'y aligne
   sans avoir à participer à la grille. */
.gauge {
    --gauge-track-left: calc(4.25rem + var(--sp-3));
    --gauge-track-right: calc(7rem + var(--sp-3));

    position: relative;
    display: grid;
    grid-template-columns: 4.25rem 1fr 7rem;
    column-gap: var(--sp-3);
    row-gap: var(--sp-2);
    align-items: center;
}

/* Sans intitulés de piste : la file à qualifier est un écran de travail, les
   deux mots « prix » et « chiffré » y coûtent plus qu'ils n'apportent. */
.gauge--compact {
    --gauge-track-left: 0px;
    --gauge-track-right: calc(5.5rem + var(--sp-3));

    grid-template-columns: 1fr 5.5rem;
    row-gap: 3px;
}

.gauge__label {
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: right;
}

.gauge__track {
    display: flex;
    height: 0.9rem;
    /* Arrondi complet : les extrémités carrées donnaient à la figure un air de
       tableau de bord industriel, quand elle ne dit qu'une chose simple. */
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
    overflow: hidden;
}

.gauge--compact .gauge__track {
    height: 0.6rem;
}

.gauge__figure {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    text-align: right;
    white-space: nowrap;
}

.gauge--compact .gauge__figure {
    font-size: 0.72rem;
}

/* Le prix reste neutre. Le colorer en faisait un jugement — l'orange de cette
   feuille sert partout ailleurs à avertir — alors qu'il n'est que la
   référence : c'est ce qui le dépasse qui mérite une couleur. */
.gauge__price {
    height: 100%;
    background: var(--text-muted);
}

/* Un bloc par jeu chiffré, large comme son prix de référence. Le séparateur
   est un inset de la couleur de la carte : les frontières se voient sans
   introduire une seconde teinte, qui suggérerait des catégories inexistantes. */
.gauge__seg {
    height: 100%;
    background: var(--neutral);
    box-shadow: inset -1px 0 0 var(--bg-raised);
}

/* Ce qui dépasse le repère de prix : le surplus, seule affirmation certaine de
   la page, et la seule place où l'accent est dépensé. */
.gauge__seg--over {
    background: var(--accent);
}

/* Le bloc suivant décrit le même jeu, de l'autre côté du repère : pas de
   séparateur, pour que le jeu se lise d'un seul tenant qui change de couleur
   au passage du prix. Avec le séparateur, un lot porté par un seul jeu cher
   affichait deux blocs voisins nommant le même jeu au même prix — on croyait
   le voir compté deux fois. */
.gauge__seg--continues {
    box-shadow: none;
}

/* Les jeux réclamés par les goûts ne sont plus marqués sur la jauge.

   Un filet doré y courait le long des segments concernés : sur une barre de
   quatorze pixels qui porte déjà « sous ou au-dessus du prix », il se lisait
   comme une troisième série de données et non comme une annotation. La carte
   le dit déjà deux fois, mieux et sans encombrer — la pastille « N jeux de vos
   goûts » dans les badges, et un 💛 devant chaque nom concerné dans la liste
   des jeux. La classe reste posée par le gabarit : elle sert d'accroche si
   l'envie revient de la marquer autrement, sans dessin par-dessus la mesure. */
.gauge__seg--wanted {
    /* Rien de plus que le séparateur commun. */
    box-shadow: inset -1px 0 0 var(--bg-raised);
}

/* Jeu jamais vu d'occasion, valorisé sur son seul prix neuf (voir
   LotEstimate::totalWithApproximateCents()) : un bloc plein, contrairement à
   .gauge__unknown — on connaît une valeur, elle n'est simplement pas fiable,
   presque toujours surestimée. Une teinte à elle, jamais confondue avec un
   segment certain (.gauge__seg) ou le surplus (.gauge__seg--over). */
.gauge__seg--approximate {
    /* Mêlé au gris des segments ordinaires plutôt que le crimson pur : sur
       une largeur pleine, la teinte franche employée pour un simple badge
       devenait criarde — constaté en production le 2026-09-09. */
    background: color-mix(in srgb, var(--bad) 55%, var(--neutral));
}

/* Jeu reconnu sans prix de référence, ou annoncé sans être reconnu : l'inconnu
   occupe une largeur physique au lieu d'être une réserve en fin de carte que
   personne ne lit. Hachuré, jamais plein — il n'entre pas dans le total. */
.gauge__unknown {
    height: 100%;
    box-shadow: inset -1px 0 0 var(--bg-raised);
    background: repeating-linear-gradient(
        -45deg,
        color-mix(in srgb, var(--neutral) 42%, transparent) 0 2px,
        transparent 2px 5px
    );
}

/* Le repère : un trait tombé du bout de la barre de prix à travers celle du
   dessous, qui rend la comparaison exacte plutôt qu'approximative.

   En surimpression et non en cellule de grille (voir .gauge ci-dessus), et
   calé sur les bornes de la piste plutôt que sur une colonne.

   Une pastille surmontait ce trait jusqu'au 2026-09-09 : elle ne disait rien
   que le trait ne dise, et se lisait comme une poussière sur l'écran dès que
   le trait manquait de contraste. Retirée — un ornement qui pose une question
   au lecteur coûte plus qu'il ne rapporte. */
.gauge__guide {
    position: absolute;
    z-index: 2;
    top: 0;
    bottom: 0;
    left: var(--gauge-track-left);
    right: var(--gauge-track-right);
    pointer-events: none;
}

.gauge__guide::before {
    content: "";
    position: absolute;
    top: -3px;
    bottom: -3px;
    left: var(--at);
    width: 0;
    /* Assez marqué pour se voir sur les deux thèmes : à 0,55 d'opacité, il
       disparaissait sur le fond sombre et ne laissait que la pastille. */
    border-left: 1px dashed var(--text-muted);
}

/* File à qualifier ------------------------------------------------------- */

.qualify {
    display: flex;
    flex-direction: column;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.qualify__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(14rem, 20rem) auto;
    align-items: center;
    gap: var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
}

.qualify__row + .qualify__row {
    border-top: 1px solid var(--border);
}

.qualify__what {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.qualify__title {
    font-size: 0.88rem;
    font-weight: 620;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.qualify__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-2);
    color: var(--text-muted);
    font-size: 0.74rem;
}

.qualify__unread {
    margin: 0;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.78rem;
}

.qualify__gap {
    font-family: var(--font-mono);
    font-size: 0.85rem;
    font-weight: 650;
    color: var(--text-muted);
    white-space: nowrap;
}

.qualify__actions {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

.lot-rejected {
    padding: var(--sp-3) var(--sp-4);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
    color: var(--text-muted);
    font-size: 0.84rem;
}

@media (max-width: 46rem) {
    .lot-card {
        flex-direction: column;
        gap: var(--sp-3);
    }

    /* Le panneau passe au-dessus du texte, et deux fois plus large que haut :
       la pile étant un carré borné par la hauteur (voir .lot-card__stack), un
       panneau très plat — 16/7 auparavant — la réduisait à un timbre au milieu
       d'une bande vide. Le `min-height` du grand écran doit céder ici, sans
       quoi il l'emporterait sur la proportion en dessous de 320 px de large. */
    .lot-card__visual {
        flex: 0 0 auto;
        width: 100%;
        min-height: 0;
        aspect-ratio: 4 / 3;
    }

    /* Le panneau devenant plus large que haut, le carré de la pile se borne à
       la hauteur et non plus à la largeur. Sans cette bascule, `max-height` le
       rognait : les boîtes du bas se retrouvaient tranchées par le bord du
       panneau, alors que les arrangements, eux, restent valables — ils sont
       exprimés en pourcentages d'un carré, quelle que soit sa taille. */
    .lot-card__stack {
        width: auto;
        height: 100%;
    }

    /* Les intitulés passent au-dessus de leur piste plutôt qu'à côté : à cette
       largeur, une colonne de 4 rem laisse la barre trop courte pour que la
       comparaison se lise encore.

       Les bornes de la piste suivent, sinon le repère de prix — posé en
       surimpression et calé sur elles — se décalait de toute la largeur de la
       colonne d'intitulés disparue. La variante compacte garde les siennes :
       elle n'a jamais eu d'intitulés à déplacer. */
    .gauge:not(.gauge--compact) {
        --gauge-track-left: 0px;
        --gauge-track-right: calc(6rem + var(--sp-3));

        grid-template-columns: 1fr 6rem;
    }

    .gauge:not(.gauge--compact) .gauge__label {
        grid-column: 1 / 3;
        text-align: left;
    }

    .qualify__row {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* --------------------------------------------------------------------------
   Onglets (voir tabs_controller.js)

   Un soulignement plutôt que des pastilles : l'écran Santé porte déjà
   beaucoup de pastilles — statuts, compteurs, badges de source — et une
   navigation en pastilles s'y serait confondue avec le contenu.
   -------------------------------------------------------------------------- */
.tabs {
    display: flex;
    gap: var(--sp-1);
    margin-bottom: var(--sp-5);
    border-bottom: 1px solid var(--border);
    /* Sur petit écran, les onglets défilent plutôt que de passer à la ligne :
       un « tablist » sur deux rangées ne se lit plus comme une barre. */
    overflow-x: auto;
    scrollbar-width: none;
}

.tabs::-webkit-scrollbar {
    display: none;
}

.tab {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    border: none;
    /* Chevauche la bordure du groupe : le trait actif la remplace au lieu de
       s'empiler dessus. */
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-weight: 600;
    font-size: 0.9rem;
    white-space: nowrap;
    cursor: pointer;
    transition: color var(--transition), border-color var(--transition);
}

.tab:hover {
    color: var(--text);
}

.tab.is-active {
    color: var(--accent);
    border-bottom-color: var(--accent);
}

/* Le compteur porte à lui seul l'intérêt de l'onglet fermé : sans lui, rien
   ne dirait qu'il reste du travail derrière. */
.tab__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.35rem;
    padding: 0 0.35rem;
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.tab.is-active .tab__count {
    background: var(--bg-accent-soft);
    color: var(--accent-hover);
}

/* Un compteur qui appelle à agir, distinct d'un simple décompte. */
.tab__count--alert {
    background: color-mix(in srgb, var(--bad) 18%, transparent);
    color: var(--bad-ink);
}

.tab.is-active .tab__count--alert {
    background: color-mix(in srgb, var(--bad) 22%, transparent);
    color: var(--bad-ink);
}

/* Sur fond sombre, le sienna soutenu tombe sous le contraste exigible pour un
   texte aussi petit : même parti que .badge--info, on passe au blanc. */
:root[data-theme="dark"] .tab__count--alert {
    color: #fff;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .tab__count--alert {
        color: #fff;
    }
}

/* Barre d'outils de la ludothèque (voir collection/index.html.twig) : les
   onglets et l'action « + Ajouter un jeu » se partagent la même ligne, sous
   un seul filet — .tabs perd le sien pour ne pas en doubler un second juste
   au-dessus du bouton. */
.collection-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    flex-wrap: wrap;
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--sp-5);
}

.collection-toolbar__tabs {
    flex: 1;
    min-width: 0;
    margin-bottom: 0;
    border-bottom: none;
}

/* --------------------------------------------------------------------------
   Verdict de l'écran Santé (voir App\Health\HealthDigest)

   Le seul bloc qui reste visible quel que soit l'onglet : répartir le détail
   en onglets aurait sinon caché les problèmes derrière un clic à deviner.
   -------------------------------------------------------------------------- */
.verdict {
    margin-bottom: var(--sp-5);
    padding: var(--sp-4);
    border: 1px solid var(--border);
    border-left: 3px solid var(--warn);
    border-radius: var(--radius);
    background: var(--bg-raised);
    box-shadow: var(--shadow-sm);
}

/* Rien à signaler : le bloc se fait discret et tient sur une ligne. */
.verdict--clear {
    border-left-color: var(--accent);
    background: var(--bg-accent-soft);
}

.verdict--danger {
    border-left-color: var(--danger);
}

.verdict__head {
    display: flex;
    align-items: baseline;
    gap: var(--sp-3);
    font-weight: 650;
}

.verdict__icon {
    font-size: 1.1rem;
    line-height: 1;
}

.verdict__lede {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.85rem;
    font-weight: 400;
}

.verdict__list {
    display: grid;
    gap: var(--sp-2);
    margin: var(--sp-4) 0 0;
    padding: 0;
    list-style: none;
}

.verdict__item {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    padding: var(--sp-3);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}

.verdict__what {
    min-width: 0;
}

.verdict__detail {
    display: block;
    margin-top: var(--sp-1);
    font-size: 0.85rem;
    /* Un message d'échec cite parfois l'URL en cause (voir
       ProviderCircuitBreaker::lastFailureReason()) : une seule « ligne »
       sans espace, jamais coupée par défaut — elle poussait toute la carte,
       puis toute la page, bien au-delà du viewport sur mobile. */
    overflow-wrap: anywhere;
}

/* Le renvoi ne doit jamais se comprimer sous un long libellé. */
.verdict__go {
    flex: none;
    font-size: 0.85rem;
    font-weight: 600;
    white-space: nowrap;
}

@media (max-width: 640px) {
    .verdict__item {
        flex-direction: column;
        gap: var(--sp-2);
    }
}

/* --------------------------------------------------------------------------
   Lanceur de tâches, groupé par famille (écran Santé)

   Quatorze boutons alignés formaient un mur : on n'y retrouvait une tâche
   qu'en lisant les quatorze libellés.
   -------------------------------------------------------------------------- */
.launcher {
    display: grid;
    gap: var(--sp-4);
}

.launcher__group {
    display: grid;
    gap: var(--sp-2);
}

.launcher__title {
    margin: 0;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}


/* --------------------------------------------------------------------------
   La colonne de gauche d'une fiche jeu, en dos de boîte
   -------------------------------------------------------------------------- */

/*
 * Conteneur de requête : la bande de repères se replie selon la largeur de LA
 * COLONNE et non de la fenêtre — la même bande sert dans une colonne étroite
 * à côté du contenu et sur toute la largeur en dessous de lui, à taille de
 * fenêtre identique.
 */
.game-card-sheet {
    container-type: inline-size;
    /* La bande de repères va d'un bord à l'autre : sans quoi ses angles
       dépasseraient des coins arrondis de la carte. */
    overflow: hidden;
}

.sheet__hero {
    position: relative;
}

.sheet__hero .game-cover {
    display: block;
}

/*
 * La note, en autocollant posé de travers sur l'illustration : l'endroit où un
 * éditeur la met, et ce qu'on lit en premier de la colonne.
 */
.sheet__rating {
    position: absolute;
    top: var(--sp-3);
    right: var(--sp-3);
    padding: 0.3rem 0.55rem;
    border: 3px solid var(--c-cream-text);
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--c-navy);
    text-align: center;
    transform: rotate(4deg);
    /* Une ombre propre, indépendante des jetons : elle est posée sur une
       illustration quelconque, donc sur une couleur inconnue. */
    box-shadow: 2px 3px 0 rgb(10 14 36 / 45%);
}

.sheet__rating b {
    display: block;
    font-family: var(--font-display);
    font-size: 1.35rem;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.sheet__rating i {
    font-style: normal;
    font-size: 0.58rem;
    letter-spacing: 0.04em;
}

/*
 * Les étiquettes au bas de l'illustration, dans un bandeau OPAQUE.
 *
 * Elles étaient d'abord posées à même l'image sur un voile dégradé — la
 * maquette le supportait parce que sa fausse couverture était un aplat. Sur
 * une vraie illustration, chargée et imprévisible, elles devenaient
 * illisibles (constaté sur « Mysterium », sombre et détaillée jusqu'au bord).
 * Un dégradé ne rattrape pas un fond inconnu ; un bandeau, si, parce qu'il
 * devient lui-même le fond — et les contrastes s'y mesurent.
 */
/*
 * Les étiquettes sous l'illustration, jamais dessus.
 *
 * Trois essais y ont été faits — nues, sous un voile dégradé, puis sur un
 * bandeau opaque — et chacun a échoué à sa manière : illisibles dans les deux
 * premiers cas sur une couverture chargée, lisibles dans le troisième mais au
 * prix d'une bande pleine en travers de l'image. Une illustration de jeu est
 * trop imprévisible pour qu'on y pose du texte fin. La note y reste : son
 * autocollant est opaque et ne porte que quatre chiffres.
 */
.sheet__tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
    margin-bottom: var(--sp-1);
}


/* Le chiffre qu'on vient chercher : encadré, pas en note de bas de page. */
/*
 * En pointillé : le plus bas prix relevé est un repère d'historique, pas une
 * offre qu'on peut saisir — l'annonce qui l'a produit est le plus souvent
 * close. Le trait discontinu dit cette différence sans un mot de plus, là où
 * un cadre plein le faisait ressembler aux blocs d'action voisins.
 */
.sheet__floor {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-top: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border: 2px dashed var(--frame-ink);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}

.sheet__floor-icon {
    flex: none;
    color: var(--good-ink);
}

.sheet__floor-icon svg {
    width: 1.25rem;
    height: 1.25rem;
}

.sheet__floor-text b {
    display: block;
    font-family: var(--font-display);
    font-size: 1.05rem;
    color: var(--good-ink);
    font-variant-numeric: tabular-nums;
}

.sheet__floor-text i {
    display: block;
    font-style: normal;
    font-size: 0.68rem;
    color: var(--text-muted);
}


/* --------------------------------------------------------------------------
   Page des statistiques : les blocs de graphe
   -------------------------------------------------------------------------- */

/*
 * L'intertitre qui sépare les quatre questions de la page (voir
 * StatsController). Sans eux, vingt blocs se suivaient sans qu'on sache
 * lesquels répondaient ensemble — le même défaut que la modale de correction
 * avant ses sections.
 */
.stats-section {
    margin: 0 0 var(--sp-3);
    padding-bottom: var(--sp-2);
    border-bottom: 2px solid var(--frame-ink);
    font-size: 1rem;
}

.stats-section:not(:first-of-type) {
    margin-top: var(--sp-6);
}

.chart-card {
    padding: var(--sp-4);
    margin-bottom: var(--sp-4);
    /* Un graphe ne sait pas se rétrécir tout seul : sans cette largeur
       minimale nulle, Chart.js impose la taille de sa toile à la colonne de
       grille, qui déborde alors au lieu de le contraindre. */
    min-width: 0;
}

.chart-card__title {
    margin-bottom: var(--sp-3);
    font-family: var(--font-display);
    font-size: 0.82rem;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* La réserve méthodologique sous un graphe : nécessaire pour ne pas mentir sur
   ce que le graphe montre, mais elle ne doit pas peser autant que lui. */
.chart-card__foot {
    margin: var(--sp-3) 0 0;
    font-size: 0.72rem;
    line-height: 1.45;
}

.chart-card__empty {
    margin: 0;
    padding: var(--sp-5) 0;
    text-align: center;
    font-size: 0.85rem;
}

/* Les deux séries temporelles portent cinquante-deux et douze points : elles
   ont besoin de plus de hauteur que les distributions à huit ou dix barres. */
.chart-wrap--tall {
    min-height: 280px;
}


/* --------------------------------------------------------------------------
   Pages d'erreur
   -------------------------------------------------------------------------- */

.errorpage {
    max-width: 34rem;
    margin: var(--sp-6) auto;
    text-align: center;
}

.errorpage__box {
    width: min(15rem, 70%);
    height: auto;
    margin-bottom: var(--sp-2);
}

/*
 * Le code en gros, mais dans la couleur d'accent et non en rouge : une page
 * introuvable n'est pas une panne, et un grand 404 écarlate en fait un
 * incident. Le rouge reste pour le 500, qui en est un.
 */
.errorpage__code {
    margin: 0;
    font-family: var(--font-display);
    font-size: 3.4rem;
    line-height: 0.9;
    letter-spacing: -0.04em;
    color: var(--accent-ink);
}

.errorpage__title {
    margin: var(--sp-2) 0 var(--sp-1);
    font-size: 1.25rem;
}

.errorpage__message {
    margin: 0 auto var(--sp-5);
    max-width: 44ch;
    color: var(--text-muted);
}

.errorpage__actions {
    display: flex;
    gap: var(--sp-2);
    justify-content: center;
    flex-wrap: wrap;
}

.errorpage__hint {
    margin: var(--sp-4) 0 0;
    font-size: 0.78rem;
}


/* --------------------------------------------------------------------------
   Page d'accueil publique
   --------------------------------------------------------------------------
   La seule page sans barre latérale : son visiteur n'a pas de compte, donc
   rien à quoi naviguer. Elle a son propre bandeau, qui porte les deux seules
   actions possibles — se connecter, ou demander un essai.
   -------------------------------------------------------------------------- */

.land { background: var(--bg); color: var(--text); }

.land__top {
    position: sticky;
    top: 0;
    z-index: 50;
    background: var(--band);
    color: var(--band-text);
    padding: var(--sp-3) 0 calc(var(--sp-3) + 4px);
}

.land__top::after {
    content: '';
    position: absolute;
    inset: auto 0 0;
    height: 4px;
    background: repeating-linear-gradient(90deg,
        var(--accent) 0 26px, var(--info) 26px 52px);
}

.land__w {
    max-width: 66rem;
    margin: 0 auto;
    padding: 0 var(--sp-5);
}

.land__top .land__w {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}

.land__brand {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    text-decoration: none;
    color: inherit;
}

.land__brand-die { width: 2.1rem; height: 2.1rem; flex: none; }

/* Le bandeau public est navy comme celui de l'application : le cadre du dé y
   passe au crème, sinon il disparaît (voir --logo-frame). */
.land__top { --logo-frame: var(--c-cream-text); }

.land__brand b {
    font-family: var(--font-display);
    font-size: 1rem;
    letter-spacing: -0.035em;
}

.land__acts { margin-left: auto; display: flex; align-items: center; gap: var(--sp-4); }

/* L'action pleine du bandeau ne se comprime pas : dans une rangée flex, un
   bouton se laisse rétrécir jusqu'à couper son propre libellé — « Je veux /
   essayer » sur deux lignes, puis débordant de l'écran. */
.land__acts .btn { flex: none; white-space: nowrap; }

/* Qui a déjà un compte vient pour ça : le lien passe avant le bouton, mais en
   retrait — l'action pleine reste l'essai, que la page vient proposer. */
.land__login {
    color: var(--band-muted);
    font-weight: 700;
    font-size: 0.86rem;
    text-decoration: none;
    white-space: nowrap;
}

.land__login:hover { color: var(--band-text); }

/* --- héros --- */
.land__hero { position: relative; padding: var(--sp-7) 0 var(--sp-6); overflow: hidden; }

.land__hero::before {
    content: '';
    position: absolute;
    inset: -40% -10% auto;
    height: 120%;
    background: radial-gradient(ellipse 60% 50% at 50% 0,
        color-mix(in srgb, var(--info) 16%, transparent), transparent 70%);
    pointer-events: none;
}

.land__hero-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    gap: var(--sp-6);
    align-items: center;
}

/* Le sujet, au-dessus du titre : il dit DE QUOI on parle, là où le titre dit
   ce qu'on promet. Un visiteur qui arrive de nulle part a besoin des deux, et
   dans cet ordre. */
.land__eyebrow {
    margin: 0 0 var(--sp-2);
    font-family: var(--font-display);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--accent-ink);
}

.land h1 { font-size: clamp(2.1rem, 6vw, 3.8rem); line-height: 0.99; }
.land h1 em { font-style: normal; color: var(--good-ink); }
.land__lede { color: var(--text-muted); font-size: 1.1rem; max-width: 34ch; margin: 1em 0 1.6em; }
.land__free { font-size: 0.8rem; color: var(--text-muted); }
.land__row { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap; }

/*
 * La main d'annonces qui se distribue à l'arrivée : la promesse de la page —
 * les affaires viennent à toi — montrée plutôt qu'écrite. Les cartes sont
 * décoratives et masquées aux lecteurs d'écran, le texte du héros disant déjà
 * tout ce qu'elles illustrent.
 */
/*
 * Le rayon du pli tient dans trois variables plutôt que dans huit valeurs
 * recopiées : la largeur d'une carte, l'écart entre la première et la
 * dernière, l'inclinaison. Sans ça, resserrer la main sur un téléphone
 * demandait de réécrire les quatre transformations — et c'est comme ça que
 * la main débordait de l'écran des deux côtés.
 */
.land__deck {
    position: relative;
    height: 21rem;
    --card-w: 10.6rem;
    --fan: 8rem;
    --tilt: 13deg;
}

.land__card {
    position: absolute;
    left: 50%;
    top: 1.4rem;
    width: var(--card-w);
    padding: var(--sp-3);
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-lg);
    background: var(--bg-raised);
    box-shadow: 0 14px 34px rgb(10 14 36 / 28%);
    animation: land-deal 0.8s cubic-bezier(0.2, 0.9, 0.2, 1) backwards;
}

/*
 * Trois cartes et non quatre, à toute largeur. À quatre, l'écart entre deux
 * cartes voisines valait deux tiers du rayon du pli, soit moins que la
 * largeur d'un nom : chaque carte recouvrait le titre de la précédente, et on
 * lisait « Panden… », « Carcas… » jusque sur un grand écran. À trois, l'écart
 * vaut le rayon entier, et chaque carte garde son nom et son prix lisibles.
 *
 * La quatrième reste dans le gabarit : c'est la même boucle qui l'écrit, et
 * la masquer ici plutôt que la retirer garde la main réglable d'un seul
 * endroit — la CSS — si la largeur disponible change un jour.
 */
.land__card:nth-child(1) { transform: translateX(-50%) rotate(calc(var(--tilt) * -1)) translate(calc(var(--fan) * -1), 0.25rem); animation-delay: 0.05s; }
.land__card:nth-child(2) { transform: translateX(-50%) rotate(calc(var(--tilt) * -0.1)) translate(0, -0.6rem); animation-delay: 0.25s; }
.land__card:nth-child(3) { transform: translateX(-50%) rotate(var(--tilt)) translate(var(--fan), 0.5rem); animation-delay: 0.45s; }
.land__card:nth-child(4) { display: none; }

@keyframes land-deal {
    from { transform: translateX(-50%) translateY(9rem) scale(0.8); opacity: 0; }
}

.land__card-art {
    height: 6rem;
    border-radius: var(--radius-sm);
    margin-bottom: var(--sp-2);
    background: linear-gradient(145deg,
        color-mix(in srgb, var(--info) 30%, var(--bg-sunken)), var(--bg-sunken));
}

.land__card-name { font-family: var(--font-display); font-size: 0.82rem; line-height: 1.1; }
.land__card-price { display: flex; align-items: baseline; gap: var(--sp-1); margin-top: 0.3rem; }
.land__card-price b { font-family: var(--font-display); font-size: 1.05rem; }
.land__card-price s { color: var(--text-muted); font-size: 0.74rem; }

/* Le même autocollant que sur une vraie carte d'annonce. */
.land__card-flag {
    position: absolute;
    top: -0.7rem;
    left: 0.6rem;
    padding: 0.16rem 0.5rem;
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-sm);
    background: var(--good);
    color: var(--accent-contrast);
    font-family: var(--font-display);
    font-size: 0.6rem;
    text-transform: uppercase;
    transform: rotate(-4deg);
}

.land__toast {
    position: absolute;
    right: 0;
    bottom: 0.4rem;
    width: 16.5rem;
    padding: var(--sp-3);
    border: 2px solid var(--good);
    border-radius: var(--radius);
    background: var(--bg-raised);
    box-shadow: 0 12px 30px rgb(10 14 36 / 32%);
    animation: land-pop 0.7s cubic-bezier(0.2, 0.9, 0.2, 1) 1.5s backwards;
}

@keyframes land-pop {
    from { transform: translateY(1.6rem) scale(0.92); opacity: 0; }
}

.land__toast b { display: block; font-family: var(--font-display); font-size: 0.82rem; color: var(--good-ink); }
.land__toast span { font-size: 0.78rem; color: var(--text-muted); }

/* --- bandes --- */
.land__sec { padding: var(--sp-7) 0; }
.land__sec--alt { background: var(--bg-sunken); }
.land h2 { font-size: clamp(1.5rem, 4vw, 2.4rem); line-height: 1.04; max-width: 24ch; }
.land__sub { color: var(--text-muted); font-size: 1.04rem; max-width: 52ch; margin: 0.8em 0 0; }

.land__two {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
    gap: var(--sp-4);
    margin-top: var(--sp-5);
}

.land__panel {
    border: 2px solid var(--border-strong);
    border-radius: var(--radius-lg);
    padding: var(--sp-5);
    background: var(--bg-raised);
}

.land__panel--win {
    border-color: var(--good);
    background: linear-gradient(160deg, color-mix(in srgb, var(--good) 9%, transparent), transparent);
}

.land__k {
    font-family: var(--font-display);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-muted);
    margin: 0 0 var(--sp-3);
}

.land__panel--win .land__k { color: var(--good-ink); }
.land__big { font-family: var(--font-display); font-size: 1.2rem; margin: 0 0 var(--sp-2); }
.land__panel p { margin: 0; color: var(--text-muted); font-size: 0.95rem; }

/* Les onglets d'un navigateur, simulés : six sites à ouvrir un par un. */
.land__tabs { display: flex; gap: var(--sp-1); flex-wrap: wrap; margin-bottom: var(--sp-3); }

.land__tabs span {
    font-size: 0.64rem;
    padding: 0.2rem 0.45rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    color: var(--text-muted);
}

/* --- goûts --- */
.land__chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-5) 0 var(--sp-1); }

.land__chip {
    padding: 0.32rem 0.78rem;
    border: 2px solid var(--border-strong);
    border-radius: var(--radius-pill);
    font-size: 0.84rem;
    font-weight: 700;
    color: var(--text-muted);
    background: var(--bg-raised);
}

.land__chip--on {
    border-color: var(--good);
    color: var(--good-ink);
    background: color-mix(in srgb, var(--good) 12%, transparent);
}

.land__line {
    display: flex;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: 0.42rem 0;
    font-size: 0.92rem;
    border-bottom: 1px solid var(--border);
}

.land__line b { font-variant-numeric: tabular-nums; color: var(--text-muted); }
.land__line--dim { opacity: 0.42; }
.land__line--hit b { color: var(--good-ink); }
.land__line--hit { animation: land-slide 0.5s cubic-bezier(0.2, 0.9, 0.2, 1) backwards; }
.land__panel--win .land__line--hit:nth-of-type(1) { animation-delay: 0.1s; }
.land__panel--win .land__line--hit:nth-of-type(2) { animation-delay: 0.2s; }
.land__panel--win .land__line--hit:nth-of-type(3) { animation-delay: 0.3s; }

@keyframes land-slide {
    from { transform: translateX(-0.9rem); opacity: 0; }
}

.land__note { margin: var(--sp-3) 0 0; font-size: 0.8rem; color: var(--text-muted); }

/* --- sources --- */
.land__srcs { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-5); }

/* « et d'autres » n'est pas une plateforme : le trait discontinu le dit sans
   un mot, et évite de le lire comme un nom de boutique. */
.land__src--more {
    border-style: dashed;
    border-color: var(--border-strong);
    color: var(--text-muted);
}

.land__src {
    padding: 0.42rem 0.85rem;
    border: 2px solid var(--info);
    border-radius: var(--radius-pill);
    font-size: 0.86rem;
    font-weight: 700;
    background: var(--bg-raised);
}

/* --- diaporama --- */
.land__slides { margin-top: var(--sp-5); }
.land__slide { display: none; animation: land-fade 0.45s ease; }
.land__slide.is-on { display: block; }

@keyframes land-fade {
    from { opacity: 0; transform: translateY(0.6rem); }
}

.land__frame {
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-lg);
    background: var(--bg-raised);
    overflow: hidden;
    box-shadow: 0 18px 44px rgb(10 14 36 / 22%);
}

.land__frame-band {
    position: relative;
    background: var(--band);
    color: var(--band-text);
    padding: var(--sp-2) var(--sp-4);
    font-family: var(--font-display);
    font-size: 0.76rem;
    text-transform: uppercase;
}

.land__frame-band::after {
    content: '';
    position: absolute;
    inset: auto 0 0;
    height: 3px;
    background: repeating-linear-gradient(90deg,
        var(--accent) 0 22px, var(--info) 22px 44px);
}

.land__frame-in { padding: var(--sp-4); }
.land__cap { display: flex; gap: var(--sp-3); align-items: baseline; margin-top: var(--sp-4); flex-wrap: wrap; }
.land__cap b { font-family: var(--font-display); font-size: 1.05rem; }
.land__cap span { color: var(--text-muted); font-size: 0.95rem; }
.land__dots { display: flex; gap: var(--sp-2); justify-content: center; margin-top: var(--sp-4); }

.land__dot {
    width: 2rem;
    height: 5px;
    border: 0;
    padding: 0;
    border-radius: var(--radius-pill);
    background: var(--border-strong);
    cursor: pointer;
}

.land__dot.is-on { background: var(--accent); }

/* --- compteurs --- */
.land__proof { display: flex; flex-wrap: wrap; gap: var(--sp-6); justify-content: center; margin-top: var(--sp-5); }
.land__proof b { display: block; font-family: var(--font-display); font-size: 1.6rem; line-height: 1; font-variant-numeric: tabular-nums; }

.land__proof span {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--text-muted);
    font-weight: 700;
}

/* --- essai --- */
.land__trial {
    padding: var(--sp-7) 0;
    background:
        radial-gradient(ellipse 70% 60% at 50% 0,
            color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%),
        var(--bg-sunken);
}

.land__form { margin: var(--sp-5) auto 0; display: grid; gap: var(--sp-3); max-width: 31rem; text-align: left; }
.land__center { text-align: center; }
.land__small { font-size: 0.76rem; color: var(--text-muted); }
.land__foot { padding: var(--sp-5) 0; font-size: 0.78rem; color: var(--text-muted); border-top: 1px solid var(--border); }

/* --------------------------------------------------------------------------
   Bilans mensuels du marché — pages publiques
   --------------------------------------------------------------------------
   Elles réutilisent la coque et les bandes de la page d'accueil ; ne vit ici
   que ce qui leur est propre, l'histogramme en tête.
   -------------------------------------------------------------------------- */

.mkt__head { padding-bottom: var(--sp-6); }
.mkt__lede { max-width: 46ch; }

/* Une date d'arrêté, pas un avertissement : le ton reste factuel, mais elle
   doit se voir — sans elle, un relevé en cours se lit comme un bilan. */
.mkt__asof {
    display: inline-block;
    margin: 0 0 var(--sp-4);
    padding: 0.35rem 0.7rem;
    border: 2px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: 0.86rem;
    color: var(--text-muted);
}

/*
 * Des repères en cases séparées, et non la bande jointive du dos de boîte
 * (.mini-stats). Celle-ci fait des filets de ses gouttières, teintées par le
 * fond de la grille : avec trois chiffres dans une grille à deux colonnes —
 * ce qui arrive dès qu'un mois n'a pas de durées publiables — la quatrième
 * case restait vide ET colorée, comme une donnée manquante.
 */
.mkt__figures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
    gap: var(--sp-3);
    margin-top: var(--sp-5);
}

.mkt__figures .mini-stat {
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

/*
 * L'histogramme : une grille de trois colonnes — l'intitulé, la piste, la
 * part. Une grille et non trois flex imbriqués, pour que toutes les pistes
 * démarrent à la même abscisse quelle que soit la longueur des intitulés.
 */
.mkt__bars {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--sp-2) var(--sp-3);
    align-items: center;
    margin-top: var(--sp-5);
}

.mkt__bar { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; }

.mkt__bar-key {
    font-size: 0.84rem;
    font-weight: 700;
    color: var(--text-muted);
    white-space: nowrap;
}

.mkt__bar-track {
    height: 1.35rem;
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    overflow: hidden;
}

/* La part vient d'une propriété personnalisée posée par le gabarit. La barre
   garde une largeur minimale visible : à 0,4 %, un remplissage proportionnel
   ne dessine rien du tout, et la tranche semblait absente plutôt que rare. */
.mkt__bar-fill {
    display: block;
    width: max(var(--share), 3px);
    height: 100%;
    background: var(--info);
}

/* Une seule teinte pour toutes les barres, et l'accent pour la plus fournie.
   Un dégradé par barre — le premier jet — ne codait rien : la couleur y
   variait avec la longueur, donc elle répétait ce que la longueur disait
   déjà, et faisait croire à une échelle qui n'existe pas. */
.mkt__bar--peak .mkt__bar-fill { background: var(--accent); }
.mkt__bar--peak .mkt__bar-key,
.mkt__bar--peak .mkt__bar-val { color: var(--accent-ink); }

.mkt__bar-val { font-size: 0.84rem; font-weight: 700; white-space: nowrap; }

.mkt__note {
    margin: var(--sp-4) 0 0;
    max-width: 62ch;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.mkt__split {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
    gap: var(--sp-4);
    margin-top: var(--sp-5);
}

.mkt__slice {
    padding: var(--sp-4);
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-lg);
    background: var(--bg-raised);
    box-shadow: var(--shadow-sm);
}

.mkt__slice b { display: block; font-family: var(--font-display); font-size: 1.9rem; line-height: 1; }
.mkt__slice span { display: block; margin-top: var(--sp-2); font-weight: 700; }
.mkt__slice small { display: block; margin-top: var(--sp-1); font-size: 0.78rem; color: var(--text-muted); }

/* Les trois paliers d'état, dans l'ordre du meilleur au plus fatigué : la
   couleur suit le rôle, pas l'ordre d'écriture. */
.mkt__slice--neuf { border-color: var(--info); }
.mkt__slice--bon { border-color: var(--good); }
.mkt__slice--use { border-color: var(--border-strong); }

.mkt__month-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin: var(--sp-5) 0 0;
    padding: 0;
    list-style: none;
}

.mkt__month {
    display: inline-block;
    padding: 0.42rem 0.85rem;
    border: 2px solid var(--border-strong);
    border-radius: var(--radius-pill);
    background: var(--bg-raised);
    font-weight: 700;
    font-size: 0.88rem;
    text-decoration: none;
    color: var(--text);
}

.mkt__month:hover { border-color: var(--accent); color: var(--accent-ink); }

.mkt__month--here {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--accent-contrast);
}

@media (max-width: 30rem) {
    /* L'intitulé passe au-dessus de sa piste : « moins de 10 € » et une piste
       lisible ne tiennent pas côte à côte sur trois cent cinquante pixels. */
    .mkt__bars { grid-template-columns: minmax(0, 1fr) auto; }
    .mkt__bar { grid-template-columns: subgrid; grid-template-areas: 'key val' 'track track'; }
    .mkt__bar-key { grid-area: key; }
    .mkt__bar-val { grid-area: val; }
    .mkt__bar-track { grid-area: track; }
    .mkt__bar { row-gap: 2px; }
}

/*
 * Le pli se resserre par paliers, et chacun est mesuré : la main doit tenir
 * dans la largeur disponible — largeur du bloc moins la gouttière — rotation
 * comprise, sans quoi les deux cartes du bord sortent de l'écran.
 */
@media (max-width: 52rem) {
    .land__hero-grid { grid-template-columns: minmax(0, 1fr); }
    .land__deck { height: 19rem; --card-w: 10rem; --fan: 7.4rem; }
}

@media (max-width: 40rem) {
    .land__deck { --card-w: 9.5rem; --fan: 7rem; }
}

/*
 * Le téléphone. Mesuré à 390 px de large, la largeur la plus courante : le
 * bandeau débordait de quatorze pixels et le tableau du diaporama de
 * soixante-sept, ce qui faisait glisser la page entière vers la droite.
 */
@media (max-width: 30rem) {
    /* Une gouttière de 1 rem plutôt que 1,5 : vingt-quatre pixels rendus au
       contenu, c'est ce qui manquait au bandeau. */
    .land__w { padding: 0 var(--sp-4); }
    .land__acts { gap: var(--sp-3); }

    /* Le dé seul porte la marque : le mot entier réclame cent vingt pixels
       que « Se connecter » et le bouton se disputent déjà. Le nom reste dans
       le titre de l'onglet et dans le pied de page. */
    .land__brand b { display: none; }

    /* La main resserrée une dernière fois. */
    .land__deck { --card-w: 8rem; --fan: 5.6rem; --tilt: 10deg; }
    .land__card { padding: var(--sp-2); }
    .land__card-art { height: 4.2rem; }
    .land__card-name { font-size: 0.78rem; }

    /* Le prix barré disparaît : à deux sur une carte de huit rem, la rangée
       se comprimait et coupait le prix lui-même — « 41,00 » d'un côté, « € »
       de l'autre. L'écart reste dit par l'autocollant en haut de la carte. */
    .land__card-price s { display: none; }
    .land__card-price b { white-space: nowrap; }

    /*
     * La bulle d'alerte passe SOUS la main plutôt que par-dessus : à cette
     * largeur elle couvrait les trois quarts des cartes qu'elle était censée
     * commenter.
     *
     * D'où la hauteur du pli devenue une marge intérieure haute, et non plus
     * une hauteur : les cartes sont en position absolue, donc hors du flux,
     * et une bulle simplement remise dans le flux se rangeait AU-DESSUS
     * d'elles, contre le haut du bloc.
     */
    .land__deck { height: auto; padding-top: 13rem; }
    .land__toast { position: static; width: auto; }

    /* Le tableau du Radar perd l'état et l'ancienneté : cinq colonnes
       demandaient trois cent soixante-treize pixels pour trois cent six
       disponibles. Ce que la diapositive illustre — « trié par affaire » —
       tient dans le lieu, le total et l'écart. */
    .land__frame .land__opt { display: none; }

    .land__proof { gap: var(--sp-5); }
}

/* --------------------------------------------------------------------------
   Bannière de consentement (mesure d'audience)
   --------------------------------------------------------------------------
   Fixée en bas, sur les deux mises en page (voir partials/_cookie_banner.html.twig).
   Les deux boutons ont le même poids visuel : la CNIL exige que refuser soit
   aussi simple qu'accepter, ce qu'un lien discret à côté d'un bouton plein
   ne serait pas.
   -------------------------------------------------------------------------- */

.consent {
    position: fixed;
    inset: auto 0 0;
    z-index: 2000;
    display: flex;
    justify-content: center;
    padding: var(--sp-4);
    pointer-events: none;
}

.consent[hidden] {
    display: none;
}

.consent__box {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-4);
    max-width: 48rem;
    padding: var(--sp-4) var(--sp-5);
    border: 2px solid var(--frame-ink);
    border-radius: var(--radius-lg);
    background: var(--bg-raised);
    box-shadow: var(--shadow-float);
    pointer-events: auto;
}

.consent__text {
    flex: 1 1 22rem;
    margin: 0;
    font-size: 0.9rem;
    color: var(--text);
}

.consent__text a { color: var(--accent-ink); }

.consent__actions {
    display: flex;
    flex: none;
    gap: var(--sp-2);
}

@media (max-width: 30rem) {
    .consent { padding: var(--sp-3); }

    .consent__box {
        flex-direction: column;
        align-items: stretch;
        padding: var(--sp-4);
    }

    /* Refuser et accepter restent l'un sous l'autre plutôt que côte à
       côte : sur un écran étroit, deux boutons de même poids qui se
       partagent la largeur finissaient par couper leur propre libellé. */
    .consent__actions { flex-direction: column; }
}

/* --------------------------------------------------------------------------
   Pages légales (mentions légales, confidentialité)
   --------------------------------------------------------------------------
   De la prose à lire, pas une page de vente : une seule colonne à largeur de
   lecture, sans les titres XXL du reste du site public.
   -------------------------------------------------------------------------- */

.land__legal {
    max-width: 42rem;
}

.land__legal h1 { font-size: clamp(1.8rem, 4vw, 2.4rem); margin: 0 0 var(--sp-4); }

.land__legal h2 {
    max-width: none;
    margin: var(--sp-6) 0 var(--sp-2);
    font-size: 1.15rem;
}

.land__legal h2:first-of-type { margin-top: var(--sp-2); }

.land__legal h3 {
    margin: var(--sp-4) 0 var(--sp-1);
    font-size: 1rem;
}

.land__legal p { margin: 0 0 var(--sp-3); max-width: none; line-height: 1.6; }
