/* ══════════════════════════════════════════════════════════════
   RADAR — mobile.css
   Mobile-first. Base styles target phones; content grows into
   wider viewports via min-width breakpoints only — never the
   reverse.

   Palette/typo — redesign "façon Linear" (15/09/2026, local uniquement,
   desktop non touché) : fond quasi-noir, ladder de surfaces à 4 niveaux,
   bordures hairline (pas d'ombre sur le fond sombre), typo Sora
   (substitut libre documenté pour Linear Display/Text), tracking négatif
   sur les titres. Un seul accent chromatique (--accent), cohérent avec le
   principe Linear "no second chromatic color" — plusieurs teintes essayées
   sur demande explicite le même jour (lavande Linear, jaune ancien
   --jaune-fill, vert rgb(21,250,19)) avant de se fixer sur le vert-citron
   #B6F23A actuel. Les couleurs de catégorie
   (CAT_COLORS, config.js, palette éditoriale partagée desktop/mobile)
   restent inchangées : ce ne sont pas l'accent du site, comparable au
   "richer color-tag palette" que Linear réserve à ses propres surfaces
   produit. Layout system (gouttières/rythme/chrome) inchangé (voir
   §"container/section" plus bas). */

:root {
  /* Surface ladder (canvas → surface-4), du plus profond au plus élevé */
  --canvas:            #010102;
  --surface-1:          #0f1011;
  --surface-2:          #141516;
  --surface-3:          #18191a;
  --surface-4:          #191a1b;

  /* Bordures hairline — remplacent les ombres sur fond sombre */
  --hairline:           #23252a;
  --hairline-strong:    #34343a;
  --hairline-tertiary:  #3e3e44;

  /* Texte — ink = couleur de texte par défaut (claire, contrairement à
     l'ancien --ink sombre pré-redesign) */
  --ink:                #f7f8f8;
  --ink-muted:          #d0d6e0;
  --ink-subtle:         #8a8f98;
  --ink-tertiary:       #62666d;

  /* Accent unique — vert-citron #B6F23A demandé explicitement (remplace
     le vert rgb(21,250,19) essayé juste avant). --on-accent reste foncé
     pour le texte posé SUR un remplissage accent plein (pastilles/ronds
     sélectionnés) — trop clair/saturé pour porter du texte blanc lisible. */
  --accent:             #B6F23A;
  --accent-hover:       #C9F56B;
  --accent-focus:       #9DD62B;
  --on-accent:          #0F1117;

  /* Sémantique — seules exceptions à l'accent unique (badges
     ouverture/fermeture), gardées proches du spec Linear (success) et
     assombries pour rester lisibles sur fond sombre (danger, non
     documenté par Linear qui n'a pas ce besoin en marketing) */
  --success:            #1a7f37;
  --danger:             #d92d20;

  /* Neutres et calques transverses (mêmes valeurs qu'avant, nommées) */
  --white: #FFFFFF;
  --map-ink: #1f1f1f;
  --map-ink-muted: #5f6368;
  --map-ink-subtle: #6b6f76;
  --map-line: #eee;
  --map-hover: #f4f4f4;
  --scrim: rgba(0,0,0,0.6);
  --scrim-soft: rgba(0,0,0,0.55);
  --glass: rgba(10,10,12,0.72);
  --glass-soft: rgba(10,10,12,0.6);
  --veil-3: rgba(255,255,255,0.03);
  --veil-4: rgba(255,255,255,0.04);
  --veil-6: rgba(255,255,255,0.06);
  --veil-10: rgba(255,255,255,0.10);
  --shadow-lift: 0 16px 28px rgba(0,0,0,0.45);
  --shadow-pin: 0 1px 4px rgba(0,0,0,0.35);
  --shadow-float: 0 2px 6px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2);
  --shadow-drop: rgba(0,0,0,0.4);

  /* Rayons — échelle Linear */
  --r-xs:  4px;
  --r-sm:  6px;
  --r-md:  8px;
  --r-lg:  12px;
  --r-xl:  16px;
  --r-pill: 9999px;

  /* ── Fondations layout : gouttières + rythme vertical des sections ──
     Un seul endroit à modifier pour que ça change partout. */
  --gutter:      16px;
  --section-pad: clamp(3.75rem, 8vw, 6.25rem);
  --stack-gap:   clamp(3rem, 6vw, 4rem);
  --card-gap:    clamp(1rem, 2.5vw, 1.25rem);

  /* Chrome fixe (app bar + onglets + filtre) — mesuré en JS pour que le
     feed ne démarre jamais dessous. */
  --chrome-h: 0px;
  --filter-bar-top: 0px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  min-height: 100%;
  /* Desactive l'auto-inflation de texte de Safari iOS (elargit
     l'espacement des petits textes, ex. .m-filter-count a 10px) —
     invisible en emulation mobile Chrome/F12, qui ne reproduit pas ce
     comportement specifique a WebKit, seulement sur un vrai iPhone. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  font-family: 'Sora', -apple-system, 'SF Pro Display', system-ui, sans-serif;
  background: var(--canvas);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  letter-spacing: -0.05px;
}

button { font-family: inherit; }

/* ══════════════════════════════════════════
   APP BAR — fixe, toujours visible. Fond canvas quasi-noir, logo RADAR
   centré — image (frontend/img/radar-logo.svg) plutôt que du texte CSS :
   le "D" central est un cadran/antenne radar stylisé (cercle + barre
   verticale), en vert accent dans le fichier ; les lettres sont en blanc.
   Le logo est un lien vers l'accueil (rechargement complet = réinitialisation).
   ══════════════════════════════════════════ */
#m-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--gutter);
  background: var(--canvas);
  z-index: 100;
}
.m-logo { display: block; }
.m-logo img {
  display: block;
  height: 22px;
  width: auto;
}
#m-map-toggle {
  position: absolute;
  right: var(--gutter);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--ink);
  cursor: pointer;
}
#m-map-toggle svg {
  display: block;
}

/* ══════════════════════════════════════════
   GROUPE — 4 onglets (Nouveautés / Ouvertures / Temporaires / Derniers
   jours). Depuis le redesign Linear (15/09/2026), bandeau surface-1 sur
   canvas ; l'onglet actif se distingue par un soulignement accent (lavande)
   + poids de police, plus le fond jaune-boulevard/soulignement noir
   d'origine. Slider horizontal à largeur naturelle (comme .m-tab-group
   juste en dessous, ou le bandeau rouge de TimeOut) plutôt que 4 cases
   comprimées à largeur égale — les noms longs ("Derniers jours") restent
   lisibles sur un seul mot au lieu d'être écrasés/tronqués. Remplace
   l'ancien carrousel "cadran" à 3 cellules fixes + texte glissant (archivé
   dans frontend/reference/mobile-group-tabs-carousel.html — comportement
   gardé au cas où, juste plus adapté à 4 groupes : la logique de rotation
   supposait exactement 3 cellules).
   ══════════════════════════════════════════ */
#m-group-tabs {
  position: fixed;
  top: 56px; left: 0; right: 0;
  height: 56px; /* même hauteur que #m-header — les deux bandes s'alignent */
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 0 var(--gutter);
  overflow-x: auto;
  justify-content: safe center;
  scrollbar-width: none;
  z-index: 99;
  /* Un cran au-dessus du canvas (sub-nav, cf. Linear surface-3) avec une
     ligne hairline pour séparer visuellement les deux bandes sans
     recourir à une ombre (bannie sur fond sombre côté Linear). */
  background: var(--surface-1);
  border-bottom: 1px solid var(--hairline);
}
#m-group-tabs::-webkit-scrollbar { display: none; }
#m-group-tabs:not(.m-aligned) > * { visibility: hidden; }
/* Espace invisible avant "Nouveautés" — sans lui, centrer "Ouvertures"
   (scrollIntoView({inline:'center'}), centerActiveGroupTab() mobile-app.js)
   demande un scrollLeft légèrement négatif (mesuré ≈ -1 à -3px selon le
   rendu des polices), impossible (clampé à 0) : "Ouvertures" retombait
   presque, mais pas exactement, centré — contrairement à "Temporaires"
   (assez de contenu avant lui pour ne jamais toucher cette limite).
   20px reste largement sous les ~120px qu'il faudrait pour que
   "Nouveautés" lui-même se centre parfaitement (jamais demandé, doit
   garder son comportement actuel, tout comme "Derniers jours" côté
   opposé) — donc aucun effet sur ces deux-là, seul "Ouvertures" en profite
   réellement. */
.m-group-tabs-spacer {
  flex-shrink: 0;
  width: 20px;
}
.m-group-tab {
  flex-shrink: 0;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--ink-subtle);
  font-family: 'Sora', sans-serif;
  font-style: normal;
  text-transform: uppercase;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.2px;
  white-space: nowrap;
  padding: 0 14px;
  cursor: pointer;
}
.m-group-tab.active {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--accent);
}

/* ══════════════════════════════════════════
   FLÈCHE FILTRES + PANNEAU — remplace les anciennes barres sous-onglets
   ("depuis moins de X") et catégorie qui étaient toujours affichées.
   Désormais une flèche flottante (façon LeBonbon mobile —
   https://www.lebonbon.fr/paris/..., demande explicite) qui ouvre/ferme un
   menu déroulant en overlay (position fixed, au-dessus du feed, backdrop
   cliquable pour fermer) — pas un tiroir qui pousse le contenu.

   La flèche n'est PAS dans une barre statique : c'est un bouton rond
   flottant dont le `top` est animé en JS (layoutChrome/openFilterPanel/
   closeFilterPanel, mobile-app.js) entre `filterBarTop - FILTER_TOGGLE_SIZE/2`
   (CENTRÉ sur le bord bas de #m-group-tabs, fermé — demande explicite,
   moitié dans le bandeau/moitié dessous, façon LeBonbon mobile ; le
   z-index de #m-group-tabs (99) > celui de la flèche (98) cache sa moitié
   haute) et `filterBarTop + hauteur du panneau - FILTER_TOGGLE_SIZE/2`
   (centrée pareil sur le bord bas du panneau ouvert) — même durée de
   transition (0.3s) que #m-filter-panel pour qu'elle suive visuellement le
   bord du tiroir qui descend/remonte. --chrome-h ne réserve que la MOITIÉ
   de FILTER_TOGGLE_SIZE (mobile-app.js) sous #m-group-tabs pour la
   position fermée (l'autre moitié empiète sur le bandeau, pas sur le
   feed), que le panneau soit ouvert ou non (overlay, ne pousse jamais le
   feed). */
#m-filter-toggle {
  position: fixed;
  top: 96px; /* défaut avant mesure JS (layoutChrome) — centré sur le bord bas de #m-group-tabs (112px), donc -16px (demi-diamètre, FILTER_TOGGLE_SIZE=32) */
  left: 50%;
  transform: translateX(-50%);
  width: 32px; height: 32px; /* rétréci depuis 40px (facteur 0.8, à ajuster si besoin), SVG 20px assorti dans mobile.html */
  border-radius: 50%;
  /* Rond surface-2 + hairline (contrôle de nav standard, pas une CTA).
     Icône accent (vert) en permanence, ouvert ou fermé — demande
     explicite d'harmoniser tous les chevrons de l'app sur l'accent. */
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  box-shadow: none;
  color: var(--accent);
  cursor: pointer;
  z-index: 98;
  transition: top 0.3s ease;
}
/* L'icône est positionnée en absolu (pas centrée en flex) : fermé, le rond
   est à moitié caché derrière #m-group-tabs (z-index 99 > 98) — si la
   flèche était centrée dans les 32px du rond, sa moitié haute serait elle
   aussi cachée. top:~10px n'est PAS un réglage à l'œil : c'est la valeur
   calculée (mesurée via path.getBBox() + demi-stroke-width en Playwright,
   voir historique de session) pour que la BASE du chevron — le haut
   ouvert des 2 branches, PAS la pointe basse (testé et écarté : la pointe
   pile sur le bord rend tout le chevron invisible, entièrement caché
   derrière le bandeau vu sa faible hauteur) — tombe exactement sur le
   bord bas de #m-group-tabs (demande explicite). Le viewBox du path est
   0 0 20 20, le trait va de (4,7) à (10,13) à (16,7) : le haut visible des
   branches (avec l'arrondi de stroke-linecap="round") est à
   (7 - strokeWidth/2)=5.75 unités viewBox — le SVG rendu fait maintenant
   20×20 (mobile.html, échelle 1:1 avec le viewBox, plus de mise à
   l'échelle contrairement à l'ancien 26px) donc 5.75 unités = 5.75px sous
   le haut du SVG. Recalculer si le path, le viewBox ou la taille du SVG
   (mobile.html) changent à nouveau. Ouvert, le rond est entier (rien ne le
   recouvre) donc l'icône revient centrée normalement (top: (32-20)/2=6px),
   non concernée par ce calcul. */
#m-filter-toggle svg {
  position: absolute;
  top: 10.25px;
  left: 50%;
  transform: translateX(-50%);
  transition: transform 0.25s ease, top 0.25s ease;
}
#m-filter-toggle.open svg {
  top: 6px;
  transform: translateX(-50%) rotate(180deg);
}

#m-filter-backdrop {
  position: fixed;
  inset: 0;
  background: var(--scrim-soft);
  z-index: 96;
  opacity: 0;
  transition: opacity 0.25s ease;
}
#m-filter-backdrop.visible { opacity: 1; }

/* Tiroir en translateY (PAS max-height/opacity, essayé d'abord puis
   écarté — retour utilisateur direct : "je n'aime pas l'espèce de fondu,
   je veux que toute la section remonte, pas que ça disparaisse dans un
   fondu". Même remède que l'ancien #m-tabs, §13ter : un fondu sur place
   ne rend pas l'impression d'un tiroir qui glisse). Fermé =
   translateY(-100%) — remonte le panneau de la totalité de SA PROPRE
   hauteur, peu importe le mode actif (date : ~4 lignes ; catégorie :
   jusqu'à ~15 chips qui wrappent), donc pas besoin de connaître une
   hauteur cible à l'avance contrairement à max-height. `top:152px` reste
   fixe ; visuellement le panneau glisse jusqu'à disparaître complètement
   au-dessus de cette ligne. Le masquage derrière #m-header/#m-group-tabs
   (z-index 99 > 97 ici) fait le reste : pendant la translation, la partie
   du panneau qui traverse leur zone d'écran est occultée par eux — MÊME
   mécanisme que la flèche à moitié cachée (voir plus haut) — donnant
   l'effet "vient se cacher sous le bandeau du dessus" plutôt qu'un simple
   fondu. `max-height` + `overflow-y:auto` restent en place mais
   uniquement comme garde-fou STATIQUE (pas animé) pour ne pas déborder de
   l'écran sur un très long contenu catégorie — mobile-app.js n'a plus
   besoin de mesurer `scrollHeight`/poser un `max-height` en px : il lit
   juste `getBoundingClientRect().height` (déjà cappée par ce garde-fou)
   pour positionner la flèche au bord bas réel. */
#m-filter-panel {
  position: fixed;
  top: 112px; /* défaut avant mesure JS (layoutChrome) — header 56px + #m-group-tabs 56px, même valeur que filterBarTop calculé en JS. Un défaut à 152px (stale) laissait dépasser le bas du panneau fermé sous le bandeau au premier paint. */
  left: 0; right: 0;
  z-index: 97;
  background: var(--surface-1);
  border-bottom: 1px solid var(--hairline);
  box-shadow: var(--shadow-lift);
  max-height: min(70vh, calc(100vh - 110px));
  overflow-y: auto;
  transform: translateY(-100%);
  transition: transform 0.3s ease;
}
#m-filter-panel.open {
  transform: translateY(0);
}

/* Boutons de mode — petits, centrés, forme elliptique (pill, même
   border-radius que .m-cat-chip plus bas) plutôt que le rectangle à coins
   arrondis d'origine ("le rectangle d'encerclement doit être comme celui
   qui entoure les catégories", demande explicite). Police 2px sous celle
   de .m-group-tab (13px, "Nouveautés"/"Ouvertures"...) = 11px — un
   premier essai à 8px de moins (5px) s'est révélé illisible à l'écran,
   corrigé sur retour utilisateur direct. */
.m-filter-mode-tabs {
  display: flex;
  justify-content: center;
  gap: 8px;
  padding: 14px var(--gutter) 4px;
}
.m-filter-mode-tab {
  flex: 0 0 auto;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--ink-subtle);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  padding: 6px 14px;
  border-radius: var(--r-pill);
  cursor: pointer;
}
.m-filter-mode-tab.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.m-filter-mode-tab[hidden] { display: none; }

.m-filter-mode-panel { padding: 10px var(--gutter) 20px; }
.m-filter-mode-panel[hidden] { display: none; }

/* Liste "Par date d'ouverture" — empilée verticalement (plus le scroll
   horizontal de l'ancien #m-tabs : ici on est dans un menu déroulant, la
   place verticale ne manque pas). */
.m-filter-mode-panel .m-tab-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.m-filter-mode-panel .m-tab-group[hidden] { display: none; }
.m-tab {
  text-align: left;
  background: none;
  border: none;
  border-bottom: 1px solid var(--hairline);
  color: var(--ink-muted);
  font-size: 14px;
  font-weight: 500;
  padding: 13px 6px;
  cursor: pointer;
}
.m-tab-group > .m-tab:last-child { border-bottom: none; }
.m-tab.active {
  font-weight: 600;
  color: var(--accent);
  background: var(--surface-2);
  border-radius: var(--r-md);
}

/* Liste "Par date d'ouverture" — 4 ronds (0/7/30/90 jours) centrés en
   ligne, libellé sur 2 lignes en dessous (retour à la ligne après "Moins",
   demande explicite), plutôt que la liste verticale de labels seuls
   utilisée avant. Sélecteur plus spécifique que .m-tab/.m-tab-group
   ci-dessus (même classes, juste scopées à ce panneau) pour n'écraser que
   ce qu'il faut sans dupliquer toute la logique JS (selectTab cible déjà
   .m-tab générique). */
.m-filter-mode-panel[data-mode-panel="date"] .m-tab-group {
  flex-direction: row;
  justify-content: center;
  gap: 20px;
  padding: 16px 0 14px;
}
.m-filter-mode-panel[data-mode-panel="date"] .m-tab,
.m-filter-mode-panel[data-mode-panel="date"] .m-tab.active {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 66px;
  background: none;
  border: none;
  border-bottom: none;
  border-radius: 0;
  padding: 0;
}
.m-tab-circle {
  position: relative; /* ancre .m-filter-count, voir plus bas */
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--ink);
  font-size: 22px;
  font-weight: 700;
  line-height: 1;
}
/* Badge de comptage — nombre d'événements derrière ce filtre (date ou
   catégorie), posé en haut à droite du rond, chevauchant son bord. Taille
   de police volontairement minuscule (7px, à ajuster si besoin) : ce n'est
   qu'un indicateur secondaire, pas le contenu principal du rond. Pas de
   largeur fixe — `right`/`top` seuls (pas de `left`) laissent la boîte se
   dimensionner à son contenu (shrink-to-fit), donc "4" et "200" (jusqu'à 3
   chiffres) s'affichent tous les deux sans troncature ni fixe arbitraire.
   Bordure de la couleur du fond du panneau (surface-1) : détoure le badge
   du rond derrière lui, façon pastille de notification. */
.m-filter-count {
  position: absolute;
  top: -8px;
  right: -8px;
  box-sizing: border-box;
  min-width: 20px;
  padding: 2px 4px;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  text-align: center;
  border: 1.5px solid var(--surface-1);
  white-space: nowrap;
}
.m-tab-label {
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-subtle);
  text-align: center;
  line-height: 1.25;
}
/* Sélection = fond accent plein, pas d'effet d'ombre décalée (le "3D
   brutalist" d'origine n'a pas d'équivalent dans le langage Linear, qui
   porte la profondeur par la ladder de surfaces + bordures hairline). */
.m-tab.active .m-tab-circle {
  background: var(--accent);
  border: 1px solid var(--accent);
  color: var(--on-accent);
  box-shadow: none;
}
.m-tab.active .m-tab-label {
  font-weight: 600;
  color: var(--ink);
}

/* Liste "Par catégorie" — même format que les ronds "Par date d'ouverture"
   juste au-dessus (rond avec l'emoji + libellé en dessous, même diamètre),
   organisée en grille 2x4 (2 lignes de 4). Au-delà de 8 catégories, la
   grille est paginée horizontalement : la 1ère page montre 7 catégories +
   un rond "+X" (X = catégories restantes) à la 8e place ; cliquer dessus le
   transforme en 8e catégorie réelle et fait apparaître le reste sur d'autres
   pages 2x4 accessibles en scrollant à droite (renderCatChips, mobile-app.js).
   Chaque .m-cat-page occupe exactement 100% de la largeur visible (flex:0 0
   100%) et centre sa propre grille .m-cat-grid en interne — nécessaire pour
   qu'une page suivante s'affiche elle aussi centrée plutôt que de laisser
   dépasser un bout de la page précédente/suivante sur les côtés (bug
   constaté avec un simple justify-content:center sur la liste entière, qui
   ne centre que l'ensemble des pages bout à bout, pas chaque page
   individuellement). scroll-snap aligne proprement une page à la fois. */
.m-cat-list {
  display: flex;
  flex-direction: row;
  overflow-x: auto;
  padding: 16px 0 14px;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;
}
.m-cat-page {
  display: flex;
  justify-content: center;
  flex: 0 0 100%;
  scroll-snap-align: start;
}
.m-cat-grid {
  display: grid;
  grid-template-columns: repeat(4, 66px);
  column-gap: 20px;
  row-gap: 16px;
}
.m-cat-tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 66px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
.m-cat-tab-circle {
  position: relative; /* ancre .m-filter-count, même mécanisme que .m-tab-circle */
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  font-size: 22px;
  line-height: 1;
  /* Nécessaire depuis le passage aux icônes SVG (stroke="currentColor") —
     un <button> a son propre `color` UA (buttontext, noir) non hérité du
     body, invisible ici puisque le fond est sombre. Les emoji d'avant
     n'avaient pas le problème (glyphes couleur, indifférents à `color`). */
  color: var(--ink);
}
.m-cat-tab-label {
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-subtle);
  text-align: center;
  line-height: 1.25;
}
.m-cat-tab.active .m-cat-tab-circle {
  background: var(--accent);
  border: 1px solid var(--accent);
  box-shadow: none;
  color: var(--on-accent);
}
.m-cat-tab.active .m-cat-tab-label {
  font-weight: 600;
  color: var(--ink);
}
/* Style inerte PARTAGÉ par tout rond sans événement derrière — catégorie
   vide dans l'onglet actif ET rond "+X" quand rien de présent ne s'y cache
   (renderCatChips, mobile-app.js) : non cliquable dans les deux cas (JS,
   disabled), simple transparence plutôt qu'un fond grisé en dur sur le
   cercle — plus propre visuellement (retour utilisateur), même traitement
   partout. */
.m-cat-tab-disabled {
  cursor: default;
  opacity: 0.45;
}

/* ══════════════════════════════════════════
   CONTAINER / SECTION — le système réutilisable
   ══════════════════════════════════════════
   Chaque <section> a ce padding horizontal (gouttières) et vertical
   (rythme d'espacement) ; le .container à l'intérieur centre le
   contenu, grandit jusqu'à 1280px, et empile ses enfants avec un
   gap uniforme. Aucune largeur ni hauteur fixe en dessous. */
section {
  padding: var(--section-pad) var(--gutter);
}
.container {
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--stack-gap);
}

main {
  padding-top: var(--chrome-h);
  transition: padding-top 0.25s ease;
}
#m-feed-section { padding-top: var(--gutter); }

#m-map-section {
  position: relative;
  padding: 0;
  margin-top: calc(var(--filter-bar-top) - var(--chrome-h));
  height: calc(100dvh - var(--filter-bar-top));
}
#m-map-canvas {
  width: 100%;
  height: 100%;
}
.m-map-pin {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  line-height: 1;
  color: var(--white);
  border: 2px solid var(--white);
  box-shadow: var(--shadow-pin);
  cursor: pointer;
}
.m-map-search-pin {
  font-size: 32px;
  line-height: 1;
  color: var(--danger);
  filter: drop-shadow(0 2px 3px var(--shadow-drop));
}
#m-map-search-wrap {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  width: calc(100% - 32px);
  max-width: 420px;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--white);
  color: var(--map-ink);
  border-radius: var(--r-pill);
  padding: 12px 16px;
  box-shadow: var(--shadow-float);
}
#m-map-search-wrap svg {
  flex: none;
  color: var(--map-ink-muted);
}
#m-map-search {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: none;
  font-family: inherit;
  font-size: 16px;
  color: var(--map-ink);
}
#m-map-search::placeholder {
  color: var(--map-ink-subtle);
}
#m-map-suggestions {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  right: 0;
  list-style: none;
  background: var(--white);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-float);
  overflow: hidden;
  max-height: 260px;
  overflow-y: auto;
}
#m-map-suggestions li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  cursor: pointer;
  border-bottom: 1px solid var(--map-line);
}
#m-map-suggestions li:last-child {
  border-bottom: none;
}
#m-map-suggestions li:hover {
  background: var(--map-hover);
}
#m-map-suggestions li svg {
  flex: none;
  color: var(--map-ink-muted);
}
#m-map-suggestions li span {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
#m-map-suggestions li strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--map-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#m-map-suggestions li small {
  font-size: 12px;
  color: var(--map-ink-subtle);
}

/* ══════════════════════════════════════════
   FEED — grille 12 colonnes, cards en li
   ══════════════════════════════════════════ */
.m-card-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--card-gap);
}
.m-card-grid[hidden] { display: none; }
#m-feed-section .m-card-grid {
  margin-top: calc(36px - var(--stack-gap) - 12px);
}

#m-rails[hidden] { display: none; }
#m-rails {
  display: flex;
  flex-direction: column;
  gap: 32px;
  margin-top: calc(36px - var(--stack-gap) - 12px);
}
.m-rail-section {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.m-rail-title {
  font-family: 'Sora', sans-serif;
  font-size: clamp(28px, 7.5vw, 34px);
  font-weight: 600;
  color: var(--ink);
  margin: 0;
  padding: 0;
}
.m-rail-count {
  color: var(--accent);
}
.m-rail-blurb {
  margin: -6px 0 0;
  font-size: 14px;
  line-height: 1.4;
  color: var(--ink-muted);
}
.m-rail {
  display: flex;
  align-items: stretch;
  gap: var(--card-gap);
  overflow-x: auto;
  padding: 0 0 4px;
  scroll-snap-type: x proximity;
  scrollbar-color: var(--accent) transparent;
}
.m-rail::-webkit-scrollbar {
  height: 4px;
}
.m-rail::-webkit-scrollbar-track {
  background: transparent;
}
.m-rail::-webkit-scrollbar-thumb {
  background: var(--accent);
  border-radius: 2px;
}
.m-rail .m-card {
  grid-column: unset;
  flex: 0 0 calc((100% - var(--card-gap)) / 2);
  width: calc((100% - var(--card-gap)) / 2);
  scroll-snap-align: start;
}
.m-rail-more {
  flex: 0 0 auto;
  align-self: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.m-page-chevron {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
}
.m-page-chevron:disabled { color: var(--ink-subtle); opacity: 0.3; cursor: default; }
.m-page-chevron-left svg { display: block; }
.m-page-chevron-right svg { display: block; }

.m-page-nav-bottom[hidden] { display: none; }
.m-page-nav-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Même distance (36px) que sous-titre → 1ère card (voir #m-feed-section
     .m-card-grid juste au-dessus) : ici pas de padding-bottom équivalent à
     celui de #m-feed-intro à soustraire, juste le gap flex du conteneur
     (--stack-gap) à ramener à 36px. */
  margin-top: calc(36px - var(--stack-gap));
}

.m-card {
  grid-column: span 6;
  background: var(--surface-1);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  overflow: hidden;
  cursor: pointer;
  display: flex;
  flex-direction: column;
}
.m-card-visual {
  position: relative;
  width: 100%;
  /* Hauteur FIXE (pas aspect-ratio) — demande explicite après retour
     utilisateur : les posters offi.fr (théâtre/cinéma), très hauts en
     format natif, produisaient des cards visiblement plus grandes que les
     autres (ex. les cards Ville de Paris/Exposition Alula) malgré
     object-fit:cover, l'aspect-ratio proportionnel ne suffisait pas à
     garantir une taille identique perçue. Une hauteur en dur élimine toute
     variation, quels que soient la source/le format de l'image ou la
     largeur de la card (1/2/3 colonnes selon le breakpoint). */
  height: 200px;
  /* Noir pur (--canvas), pas --surface-2 (gris) — même fond que le body et
     la carte MapLibre, demande explicite (partie haute de la card, visible
     quand il n'y a pas de photo). */
  background: var(--canvas);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 40px;
}
.m-card-visual img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
/* offi_* uniquement (POSTER_SOURCES, mobile-app.js) : image entière visible
   (contain) sur un fond flouté agrandi de la même photo, plutôt qu'une
   simple couleur plate, pour combler l'espace laissé vide par le contain
   dans une boîte quasi carrée. Les autres sources ne portent pas ces
   classes et gardent .m-card-visual img (cover) sans changement. */
.m-card-visual-poster { overflow: hidden; }
/* Expo Time Out sans visuel (repérage provisoire) : fond vert d'accent, libellé
   catégorie en noir pour rester lisible dessus. */
.m-card-visual-flag { background: var(--accent); }
.m-card-visual-flag .m-card-tag-label { color: var(--canvas); }
.m-card-visual-bg {
  position: absolute;
  inset: 0;
  /* contain, pas cover : même cadrage que .m-card-visual-fg (même image,
     même boîte, même règle de fit) — le flou prolonge donc les vraies
     couleurs de bord de l'affiche. Avec cover, ce calque échantillonnait
     une bande horizontale du milieu vertical de l'image (recadrage
     totalement différent de ce qu'affiche le contain au premier plan),
     d'où des couleurs de flou sans rapport avec les bords réels visibles. */
  background-size: contain;
  background-position: center;
  filter: blur(20px);
  /* scale() nettement > 1 : contain laisse un déficit sur l'axe non
     contraint (jusqu'à ~50% selon le ratio de l'affiche, voir les échantillons
     offi.fr testés) — il faut agrandir suffisamment pour que ce calque
     déborde de la boîte sur CET axe aussi, sinon la couleur de fond plate
     (var(--surface-2)) resterait visible dans les coins. */
  transform: scale(1.5);
}
/* Spécificité (0,2,0) > .m-card-visual img (0,1,1) : passe cette image en
   contain sans toucher à la règle de base dont dépendent toutes les autres
   sources. position:relative (pas static) est nécessaire pour que cette
   image soit peinte dans le même groupe d'empilement que .m-card-visual-bg/
   .m-card-tag/.m-card-source-badge (tous position:absolute) — sinon, en
   restant "static", elle serait peinte AVANT eux quel que soit l'ordre DOM,
   et le fond flou (positionné) la recouvrirait entièrement. */
.m-card-visual-poster .m-card-visual-fg {
  position: relative;
  object-fit: contain;
  /* Masque en dégradé radial : l'image nette reste pleinement opaque au
     centre puis s'estompe vers ses bords, qui se fondent progressivement
     dans .m-card-visual-bg (flou) en dessous, plutôt qu'une jonction nette
     entre les deux calques. Taille explicite "75% 75%" (pas un mot-clé
     ellipse closest-side/farthest-corner) : les pourcentages sont relatifs
     à la distance centre→bord sur chaque axe, ce qui permet de régler le
     diamètre de l'ellipse indépendamment des stops de couleur — ajustée à
     l'œil (13/09/2026) avec black 50%/transparent 70% (calculés sur ce
     rayon explicite, pas sur closest-side d'origine). */
  -webkit-mask-image: radial-gradient(75% 75% at center, black 50%, transparent 70%);
  mask-image: radial-gradient(75% 75% at center, black 50%, transparent 70%);
}
.m-card-tag {
  position: absolute;
  top: 10px; left: 10px;
  /* Chip translucide sombre + hairline plutôt qu'un chip clair — reste
     lisible sur n'importe quelle photo tout en restant dans le langage
     sombre du reste de l'app (le clair "flottant sur photo" de l'ancien
     design tranchait avec le fond noir partout ailleurs). */
  background: var(--glass);
  border: 1px solid var(--hairline-strong);
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.4px;
  padding: 4px 9px;
  border-radius: var(--r-pill);
  color: var(--ink);
  /* .m-card-tag-right affiche un texte à casse normale ("Demain", "3
     mois") : les majuscules forcées gardent les badges de même taille
     apparente (sinon l'écriture normale laisse un espace visuel qui donne
     l'impression d'un badge plus petit). */
  text-transform: uppercase;
}
/* Badge catégorie de la grille — emoji seul, plus de libellé texte (retour
   utilisateur : le nom de catégorie encombrait le badge). Émoji agrandi
   pour rester lisible une fois le texte 8px retiré ; le reste (fond,
   bordure, position) vient toujours de .m-card-tag. La feuille détail
   (#m-detail-tag) garde emoji + libellé, pas concernée par ce changement. */
.m-card-tag-emoji {
  font-size: 13px;
  padding: 5px 8px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--accent);
}
/* Card sans photo : plus de badge icône (doublon illisible avec la grande
   icône centrale d'avant, noir sur noir) — nom de catégorie en toutes
   lettres à la place, bas-gauche du visuel plutôt que haut-gauche comme le
   badge icône qu'il remplace (demande explicite). Pas de chip/fond,
   simple texte blanc ; mêmes distances aux bords que les autres badges du
   coin (10px horizontal comme .m-card-tag, 8px vertical comme
   .m-card-source-badge). */
.m-card-tag-label {
  position: absolute;
  bottom: 8px; left: 10px;
  font-size: 24px;
  font-weight: 600;
  color: var(--white);
}
/* Duplicat de .m-card-tag (même gabarit), même alignement (top:10px) mais
   côté droit — couleurs reprises de l'ancienne .m-timing-closing (rouge)
   pour signaler visuellement "ferme dans". */
.m-card-tag-right {
  left: auto;
  right: 10px;
  font-size: 10px;
  background: var(--danger);
  border-color: var(--danger);
  color: var(--white);
}
.m-card-tag-right-open {
  background: var(--success);
  border-color: var(--success);
  color: var(--white);
}
.m-card-source-badge {
  flex-shrink: 0;
  margin-left: auto;
  width: 24px; height: 24px;
  border-radius: var(--r-sm);
  overflow: hidden;
  /* Fond blanc gardé volontairement : ce sont des logos de marque externes
     (SortirAParis, LeBonbon...) conçus pour un fond clair, pas un élément
     de chrome de l'app — les repeindre casserait leur lisibilité. */
  background: var(--white);
}
/* cover, sans padding : le rectangle du badge est volontairement un peu
   plus petit que le carré du logo, qui le remplit entièrement — ses coins
   débordent donc légèrement et se font tronquer par le bord arrondi
   (overflow:hidden) du badge. */
.m-card-source-badge img { display: block; width: 100%; height: 100%; object-fit: cover; }
.m-card-body { padding: 12px 14px 14px; }
.m-card-title {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.1px;
  color: var(--ink);
  margin-bottom: 6px;
  /* Plafonné à 2 lignes — même raison que .editorial-card-title (desktop,
     main.css) : un titre long (ex. offi.fr) grandissait toute la card,
     cassant l'uniformité de la grille. min-height réserve TOUJOURS la
     place de 2 lignes (1.35 × 2), même pour un titre court sur 1 seule
     ligne — sinon le repère 📍 qui suit n'est pas à la même hauteur d'une
     card à l'autre (retour utilisateur, cf. screenshot). */
  min-height: 2.7em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.m-card-footer {
  display: flex;
  align-items: center;
  min-height: 24px;
}
.m-card-postal {
  font-size: 12px;
  color: var(--white);
}
.m-card-meta { display: flex; gap: 8px; flex-wrap: wrap; }
.m-timing-open, .m-timing-closing, .m-timing-upcoming {
  font-size: 10px;
  font-weight: 500;
  padding: 2px 8px;
  border-radius: var(--r-pill);
}
.m-timing-open    { background: var(--success); color: var(--white); }
.m-timing-closing { background: var(--danger); color: var(--white); }
.m-timing-upcoming { background: var(--accent); color: var(--white); }

.m-card-empty { opacity: 0.7; cursor: default; }
.m-card-empty-visual {
  font-size: 12px;
  color: var(--ink-muted);
  text-align: center;
  padding: 0 24px;
  line-height: 1.4;
  background: var(--veil-3);
}
/* Sous-squelettes de la card vide — mêmes classes de positionnement que les
   vrais éléments (.m-card-tag/.m-card-tag-right pour top-left/top-right),
   juste vidées de contenu et grisées. Sélecteur composé pour primer sur le
   fond/texte de .m-card-tag et .m-card-tag-right (rouge) sans dépendre de
   l'ordre des règles. */
.m-card-tag.m-card-empty-tag {
  width: 46px;
  height: 17px;
  padding: 0;
  background: var(--veil-6);
  border-color: transparent;
  color: transparent;
}
.m-card-empty-title {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.m-card-empty-line,
.m-card-empty-postal {
  height: 11px;
  border-radius: var(--r-xs);
  background: var(--veil-6);
}
.m-card-empty-line:last-child { width: 60%; }
.m-card-empty-postal { width: 34%; }

/* Intro — titre (suivi du nombre d'événements, ex. "Sur le radar
   aujourd'hui: 80") + sous-texte, au-dessus de la grille
   (mobile-app.js::renderFeedIntro), contenu changeant par onglet actif.
   Inspiré du site mobile OFFI (demande explicite) : réserve environ la
   moitié de l'écran par défaut en dessous du chrome fixe (--chrome-h,
   mesuré en JS), contenu centré verticalement dans cet espace. Fait partie
   du flux normal du document (pas de position fixed/sticky) donc défile et
   disparaît naturellement avec le reste du feed — aucune logique JS de
   scroll nécessaire pour ça. */
#m-feed-intro[hidden] { display: none; }
#m-feed-intro {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  gap: 14px;
  padding-top: 20px;
  text-align: center;
  padding-bottom: 12px;
}
.m-feed-intro-title {
  font-family: 'Sora', sans-serif;
  font-size: clamp(28px, 7.5vw, 34px);
  font-weight: 600;
  letter-spacing: -0.8px;
  color: var(--ink);
  line-height: 1.15;
  /* #m-feed-intro est en flex column + align-items:center (pas stretch) :
     le <h2> se dimensionne à son contenu (shrink-to-fit), pas à la largeur
     du conteneur — sans max-width, un titre "limite" (ex. "Repéré
     aujourd'hui: 58") peut tomber pile sous le seuil de retour à la ligne
     et s'afficher en UNE ligne quasi bord-à-bord, visuellement différent
     des titres qui wrappent proprement sur 2 lignes équilibrées (ex. "Sur
     le radar aujourd'hui") ou qui tiennent clairement sur une ligne courte
     (ex. "Ouvert aujourd'hui: 0") — repéré sur l'onglet Temporaires,
     retour utilisateur direct. max-width force un rythme plus prévisible
     (repasse en 2 lignes équilibrées dès que ça approche la limite) sans
     affecter les titres nettement courts ou longs.
     Décalage vertical du bloc désormais géré par `padding-top` sur
     #m-feed-intro (voir plus haut, plus de margin-top négatif ici — le
     `justify-content:flex-start` remplace le centrage, donc plus besoin de
     "tricher" avec une marge négative pour remonter le bloc). */
  /* min(330px, 92%) force le retour à la ligne pour un titre à la limite
     (ex. "Repéré aujourd'hui: 58" ≈ 342px en une ligne, quasi bord-à-bord
     — signalé par retour utilisateur sur l'onglet Temporaires) sans
     forcer le wrap des titres nettement plus courts (ex. "Ouvert
     aujourd'hui: 0" ≈ 319px, reste sur 1 ligne) ; ("ch" essayé d'abord —
     écarté : le "0" du Poppins 700 est presque aussi large qu'un em dans
     cette police, 20ch ≈ 360px ne contraignait quasiment rien en
     pratique). text-wrap:balance (Chromium/Safari récents, dégrade
     proprement en wrap normal sinon) choisit un point de coupure équilibré
     plutôt que de laisser un mot isolé sur la 2e ligne (ex. "Ouvert
     aujourd'hui:" / "0" tout seul — repéré en testant un premier seuil
     plus bas). */
  max-width: min(330px, 92%);
  text-wrap: balance;
}
.m-feed-intro-count {
  /* Nombre en accent plein, sans contour/ombre 3D — l'effet "brutalist"
     d'origine n'a pas d'équivalent Linear ; l'accent seul suffit à le
     distinguer du reste du titre. */
  color: var(--accent);
  -webkit-text-stroke: 0;
  text-shadow: none;
}
.m-feed-intro-subtitle {
  font-size: 14px;
  color: var(--ink-muted);
  max-width: 34ch;
  line-height: 1.4;
  min-height: 2.8em;
}

#m-feed-intro.m-feed-intro-with-cats .m-feed-intro-subtitle { min-height: 0; }
#m-feed-intro.m-feed-intro-with-cats .m-feed-intro-cats { margin: 8.4px 0 -12px; }
.m-feed-intro-cats[hidden] { display: none; }
.m-feed-intro-cats {
  display: flex;
  justify-content: center;
  gap: 10px;
  cursor: pointer;
}
.m-feed-intro-cat {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--accent);
  font-size: 18px;
  line-height: 1;
}

.m-page-indicator {
  font-size: 12px;
  color: var(--ink-subtle);
  text-align: center;
}

/* Squelettes de chargement — réutilise le gabarit de la card "vide"
   (.m-card-empty-visual/.m-card-empty-tag/.m-card-empty-title, voir
   renderFeed) plutôt qu'un squelette dupliqué : mêmes tailles/positions,
   juste un shimmer animé à la place du grisé statique. */
.m-skeleton .m-card-empty-visual,
.m-skeleton .m-card-empty-tag,
.m-skeleton .m-card-empty-line,
.m-skeleton .m-card-empty-postal {
  background: linear-gradient(90deg, var(--veil-4) 25%, var(--veil-10) 50%, var(--veil-4) 75%);
  background-size: 200% 100%;
  animation: m-shimmer 1.4s infinite;
}
@keyframes m-shimmer {
  0%   { background-position:  200% 0; }
  100% { background-position: -200% 0; }
}

/* ══════════════════════════════════════════
   FEUILLE DÉTAIL — plein écran, glisse depuis le bas
   ══════════════════════════════════════════ */
#m-detail-backdrop {
  position: fixed; inset: 0;
  background: var(--scrim);
  z-index: 200;
  opacity: 0;
  transition: opacity 0.25s ease;
}
#m-detail-backdrop.visible { opacity: 1; }

#m-detail-sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  top: 6vh;
  background: var(--canvas);
  border: 1px solid var(--hairline);
  border-bottom: none;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  z-index: 201;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  transform: translateY(100%);
  transition: transform 0.3s cubic-bezier(0.4,0,0.2,1);
  padding-bottom: env(safe-area-inset-bottom, 20px);
}
#m-detail-sheet.open { transform: translateY(0); }

#m-detail-close {
  position: absolute;
  top: 14px; right: 14px;
  z-index: 5;
  width: 36px; height: 36px;
  border-radius: 50%;
  border: none;
  background: var(--glass-soft);
  color: var(--white);
  font-size: 14px;
  cursor: pointer;
}

/* #m-detail-sheet est un <aside>, pas un <section> — la règle
   "section { padding: ... var(--gutter) }" (gouttières) ne s'applique
   donc jamais à lui, et .container seul n'a pas de padding horizontal
   propre (il ne fait que centrer/limiter la largeur). Sans ce padding
   explicite, titre/timing/adresse/lien étaient collés aux bords de la
   feuille. */
.m-detail-body { padding: 20px var(--gutter) 0; gap: 16px; }

#m-detail-visual {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  background: var(--surface-2);
  font-size: 104px;
  cursor: pointer;
  overflow: hidden;
}
#m-detail-visual-media { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.m-detail-visual-bg {
  position: absolute;
  inset: 0;
  background-size: contain;
  background-position: center;
  filter: blur(20px);
  transform: scale(1.5);
}
.m-detail-visual-fg {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: contain;
  -webkit-mask-image: radial-gradient(75% 75% at center, black 50%, transparent 70%);
  mask-image: radial-gradient(75% 75% at center, black 50%, transparent 70%);
}
#m-detail-tag {
  position: absolute;
  bottom: 12px; left: 14px;
  background: var(--glass);
  border: 1px solid var(--hairline-strong);
  font-size: 10px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  color: var(--ink);
}
#m-detail-source-badge {
  position: absolute;
  bottom: 14px; right: 14px;
  width: 40px; height: 40px;
  border-radius: var(--r-lg);
  overflow: hidden;
  /* Fond blanc gardé volontairement, même raison que .m-card-source-badge
     plus haut — logo de marque externe, pas du chrome d'app. */
  background: var(--white);
}
#m-detail-source-badge img { display: block; width: 100%; height: 100%; object-fit: cover; }

#m-detail-title { font-size: 20px; font-weight: 600; letter-spacing: -0.4px; line-height: 1.3; color: var(--ink); }
.m-detail-timing { display: flex; gap: 8px; flex-wrap: wrap; }
.m-detail-field { font-size: 14px; line-height: 1.6; color: var(--ink-muted); }
#m-detail-real-description { text-align: justify; }
#m-detail-map-wrap { width: 100%; aspect-ratio: 1 / 1; border-radius: var(--r-xl); overflow: hidden; border: 1px solid var(--hairline); }
#m-detail-map { width: 100%; height: 100%; }
#m-detail-link { font-size: 14px; font-weight: 600; color: var(--accent); text-decoration: none; }

/* ══════════════════════════════════════════
   BREAKPOINTS — mobile grows up, never shrinks down
   ══════════════════════════════════════════ */
@media (min-width: 560px) {
  .m-card { grid-column: span 4; }   /* 3×N grid */
  .m-rail .m-card { flex-basis: calc((100% - 2 * var(--card-gap)) / 3); width: calc((100% - 2 * var(--card-gap)) / 3); }
}
@media (min-width: 900px) {
  .m-card { grid-column: span 3; }   /* 4×N grid */
  .m-rail .m-card { flex-basis: calc((100% - 3 * var(--card-gap)) / 4); width: calc((100% - 3 * var(--card-gap)) / 4); }
  #m-detail-sheet { left: 50%; right: auto; width: 560px; transform: translate(100%, 0); top: 0; border-radius: 0; }
  #m-detail-sheet.open { transform: translate(0, 0); }
}
