File size: 44,756 Bytes
8ba0889
 
9504baf
 
 
8ba0889
 
9504baf
8ba0889
 
 
 
 
 
 
 
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
9504baf
 
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
9504baf
 
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
9504baf
 
8ba0889
 
 
 
 
 
 
 
 
 
 
 
 
9504baf
8ba0889
 
9504baf
 
 
 
8ba0889
 
9504baf
8ba0889
9504baf
 
 
 
 
 
 
 
 
 
 
8ba0889
9504baf
 
8ba0889
9504baf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ba0889
9504baf
 
 
 
8ba0889
9504baf
 
 
 
 
 
8ba0889
 
9504baf
 
 
8ba0889
9504baf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ba0889
 
9504baf
8ba0889
9504baf
 
 
 
 
 
 
 
 
 
8ba0889
 
9504baf
8ba0889
9504baf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ba0889
 
9504baf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
#!/usr/bin/env python3
"""
GÉO-RÉVISION — Tectonique des Plaques
Application Gradio compatible HuggingFace Spaces
76 notions · CH.3 · CH.4 · CH.5 · Synthèse
"""

import gradio as gr
import random
import re

# ══════════════════════════════════════════════════════════
#  DONNÉES — 76 cartes
# ══════════════════════════════════════════════════════════
CARDS = [
    {"id": 1,  "ch": "3", "section": "CH.3 — Limites de plaques",
     "q": "Quels sont les 3 grands types de limites de plaques ?",
     "hint": "💡 Pense : naissance / destruction / collision",
     "a": "**1. DORSALES** (divergence / accrétion océanique)\n**2. FOSSES OCÉANIQUES** (convergence / subduction)\n**3. CHAÎNES DE MONTAGNES** (convergence / collision)"},
    {"id": 2,  "ch": "3", "section": "CH.3 — Dorsales",
     "q": "Quelles roches sont produites à la dorsale ? Quelle est leur texture ?",
     "hint": "💡 Deux roches, deux textures",
     "a": "**BASALTE** : texture microlithique (refroidissement *rapide* en surface)\n**GABBRO** : texture grenue (refroidissement *lent* en profondeur)"},
    {"id": 3,  "ch": "3", "section": "CH.3 — Dorsales",
     "q": "Quel type de failles caractérise les dorsales ? Pourquoi ?",
     "hint": "💡 Extension ou compression ?",
     "a": "**FAILLES NORMALES** car la divergence crée une **EXTENSION** (étirement) de la croûte."},
    {"id": 4,  "ch": "3", "section": "CH.3 — Dorsales",
     "q": "Caractéristiques sismiques et thermiques d'une dorsale ?",
     "hint": "💡 Profondeur + intensité + flux",
     "a": "Sismicité **SUPERFICIELLE** et **FAIBLE MAGNITUDE**.\nFlux géothermique **ÉLEVÉ** (magmatisme ascendant)."},
    {"id": 5,  "ch": "3", "section": "CH.3 — Fosses",
     "q": "Quelles roches plutoniques et volcaniques sont produites en subduction ?",
     "hint": "💡 4 roches, 2 textures différentes",
     "a": "Plutoniques (grenues) : **DIORITE**, **GRANITE**\nVolcaniques (microlithiques) : **ANDÉSITE**, **RHYOLITE**"},
    {"id": 6,  "ch": "3", "section": "CH.3 — Fosses",
     "q": "Comment se répartissent les foyers sismiques en zone de subduction ? Jusqu'où ?",
     "hint": "💡 Un plan célèbre...",
     "a": "Plan de **WADATI-BENIOFF** : plan incliné suivant la plaque plongeante.\nAtteint **700 KM** de profondeur."},
    {"id": 7,  "ch": "3", "section": "CH.3 — Fosses",
     "q": "Décris le flux géothermique en zone de subduction (deux zones).",
     "hint": "💡 Froid ici, chaud là-bas",
     "a": "**BAS** au niveau de la plaque froide qui plonge.\n**FORT** au niveau de l'arc volcanique sus-jacent."},
    {"id": 8,  "ch": "3", "section": "CH.3 — Collision",
     "q": "Caractéristiques géologiques et sismiques d'une zone de collision ?",
     "hint": "💡 Type de failles + profondeur séismes",
     "a": "**FAILLES INVERSES**, flux géothermique **FAIBLE**,\nsismicité principalement **SUPERFICIELLE**."},
    {"id": 9,  "ch": "3", "section": "CH.3 — Points chauds",
     "q": "Comment un alignement de volcans inactifs prouve-t-il le mouvement des plaques ?",
     "hint": "💡 Âge + distance",
     "a": "Le point chaud est **FIXE** dans le manteau profond.\nL'âge des volcans **AUGMENTE avec la distance** au volcan actif."},
    {"id": 10, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
     "q": "Qu'est-ce qu'une anomalie magnétique positive vs négative ?",
     "hint": "💡 Intensité mesurée vs moyenne",
     "a": "**POSITIVE** : intensité > moyenne → polarité **NORMALE**\n**NÉGATIVE** : intensité < moyenne → polarité **INVERSE** (pôles inversés)"},
    {"id": 11, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
     "q": "Décris le patron des anomalies magnétiques autour d'une dorsale.",
     "hint": "💡 Symétrie + image",
     "a": "Bandes **PARALLÈLES ET SYMÉTRIQUES** par rapport à l'axe de la dorsale.\nComme un **DOUBLE TAPIS ROULANT**.\nBasaltes d'autant plus vieux qu'ils sont éloignés."},
    {"id": 12, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
     "q": "Que révèle l'âge et l'épaisseur des sédiments sur le plancher océanique ?",
     "hint": "💡 Loi simple distance/âge",
     "a": "Épaisseur et âge des sédiments **AUGMENTENT** avec la distance à la dorsale.\n→ Preuve de l'**expansion océanique**."},
    {"id": 13, "ch": "3", "section": "CH.3 — Géodésie spatiale",
     "q": "Que permet la géodésie spatiale (GPS) en tectonique ?",
     "hint": "💡 Précision + deux types de vitesses",
     "a": "Localisation au **MILLIMÈTRE PRÈS**.\nCalcul des vitesses **ABSOLUES** (repères fixes) et **RELATIVES** (entre deux plaques)."},
    {"id": 14, "ch": "3", "section": "CH.3 — Résumé frontières",
     "q": "Quelle frontière présente une vallée axiale (rift) ? Quel mouvement ?",
     "hint": "💡 Rift = fossé d'effondrement",
     "a": "La **DORSALE** : mouvement de **DIVERGENCE**.\nLa vallée axiale (rift) témoigne de l'extension de la croûte."},
    {"id": 15, "ch": "3", "section": "CH.3 — Structure générale",
     "q": "Lithosphère terrestre : structure de base et activité aux frontières.",
     "hint": "💡 Découpe + concentration",
     "a": "Découpée en **PLAQUES LITHOSPHÉRIQUES RIGIDES**.\nFrontières concentrent l'**ACTIVITÉ SISMIQUE ET VOLCANIQUE**."},
    {"id": 16, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
     "q": "Comment les basaltes enregistrent-ils le champ magnétique ?",
     "hint": "💡 Moment clé = refroidissement",
     "a": "En se **REFROIDISSANT**, les minéraux magnétiques (magnétite) se figent dans la direction du champ magnétique terrestre du moment."},
    {"id": 17, "ch": "3", "section": "CH.3 — Dorsales",
     "q": "Que produit l'accrétion océanique à la dorsale ?",
     "hint": "💡 Naît quoi ?",
     "a": "La **CROÛTE OCÉANIQUE** :\n- Basalte en surface\n- Gabbro en profondeur\n- Péridotite mantellique sous-jacente"},
    {"id": 18, "ch": "3", "section": "CH.3 — Fosses",
     "q": "Qu'est-ce qu'une fosse océanique ? Quel processus y est associé ?",
     "hint": "💡 Plongement...",
     "a": "Dépression profonde marquant une **ZONE DE SUBDUCTION** :\nplongement d'une plaque océanique dense dans le manteau."},
    {"id": 19, "ch": "3", "section": "CH.3 — Collision",
     "q": "Qu'est-ce qu'une chaîne de montagnes en termes tectoniques ?",
     "hint": "💡 Fermeture d'un océan",
     "a": "Zone de **COLLISION CONTINENTALE** : résultat de la convergence de deux plaques continentales après fermeture d'un océan."},
    {"id": 20, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Quelle est la vitesse d'écartement d'une dorsale rapide ?",
     "hint": "💡 10–16 cm/an — Quel océan ?",
     "a": "**10 à 16 CM/AN**.\nExemple : dorsale du **Pacifique Est** (Pacifique)."},
    {"id": 21, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Morphologie d'une dorsale rapide : rift, bombement ?",
     "hint": "💡 Opposé de la dorsale lente",
     "a": "**BOMBEMENT LARGE** mais **VALLÉE AXIALE QUASI ABSENTE** (magmatisme abondant comble le rift)."},
    {"id": 22, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Décris le processus de fusion partielle sous une dorsale (3 étapes).",
     "hint": "💡 Convection → décompression → solidus",
     "a": "**1.** Remontée des péridotites par **CONVECTION**\n**2.** **DÉCOMPRESSION ADIABATIQUE** : le géotherme recoupe le solidus\n**3.** **FUSION PARTIELLE** (~15%) → gouttelettes de magma"},
    {"id": 23, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Où se loge la chambre magmatique ? Quelles roches en sortent ?",
     "hint": "💡 Profondeur + 2 roches",
     "a": "Entre **2 et 7 KM** de profondeur.\nRefroidissement lent → **GABBROS** (grenu)\nRefroidissement rapide → **BASALTES** (microlithique)"},
    {"id": 24, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Quelle épaisseur de croûte produit une dorsale rapide ?",
     "hint": "💡 Chiffre précis en km",
     "a": "Croûte océanique régulière de **5 À 7 KM** d'épaisseur."},
    {"id": 25, "ch": "4", "section": "CH.4 — Dorsales lentes",
     "q": "Quelle est la vitesse d'écartement d'une dorsale lente ?",
     "hint": "💡 1–5 cm/an — Quel océan ?",
     "a": "**1 à 5 CM/AN**.\nExemple : dorsale **médio-atlantique** (Atlantique)."},
    {"id": 26, "ch": "4", "section": "CH.4 — Dorsales lentes",
     "q": "Morphologie d'une dorsale lente : rift, magmatisme, expansion.",
     "hint": "💡 Opposé de rapide",
     "a": "**RIFT TRÈS MARQUÉ** + grandes failles de détachement.\nMagmatisme **RÉDUIT ET DISCONTINU**.\nExpansion parfois assurée par **ÉTIREMENT TECTONIQUE**."},
    {"id": 27, "ch": "4", "section": "CH.4 — Dorsales lentes",
     "q": "Qu'est-ce que la serpentinisation et quand se produit-elle ?",
     "hint": "💡 Roche + eau + dorsale lente",
     "a": "Hydratation des **PÉRIDOTITES** (manteau) remontées en surface.\nL'eau transforme l'olivine en **SERPENTINE**."},
    {"id": 28, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Qu'est-ce que le métamorphisme hydrothermal ?",
     "hint": "💡 Eau + chaleur + minéraux",
     "a": "Transformation **À L'ÉTAT SOLIDE** des minéraux de la croûte océanique par infiltration d'eau de mer chauffée. Produit des minéraux **HYDRATÉS**."},
    {"id": 29, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Séquence de faciès métamorphiques lors du vieillissement océanique ?",
     "hint": "💡 Deux températures, deux faciès",
     "a": "**> 700°C** : faciès **AMPHIBOLITE** (hornblende)\n**500–700°C** : faciès **SCHISTE VERT** (chlorite + actinote)"},
    {"id": 30, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Comment évolue la densité de la lithosphère océanique avec le temps ?",
     "hint": "💡 Refroidissement → épaississement → densité",
     "a": "La lithosphère **SE REFROIDIT**, **S'ÉPAISSIT** (isotherme 1300°C s'approfondit),\nsa **DENSITÉ AUGMENTE** jusqu'à dépasser celle de l'asthénosphère."},
    {"id": 31, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Où et comment naissent les fumeurs noirs ?",
     "hint": "💡 Cycle eau de mer",
     "a": "Eau de mer s'infiltre, se **RÉCHAUFFE** au contact du magma,\nse charge en minéraux et ressort : **SOURCES HYDROTHERMALES CHAUDES**."},
    {"id": 32, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Qu'est-ce que la décompression adiabatique ? Pourquoi déclenche-t-elle la fusion ?",
     "hint": "💡 Pas de perte de chaleur",
     "a": "Remontée de matériel **SANS ÉCHANGE DE CHALEUR**.\nPression baisse → solidus baisse → géotherme **RECOUPE LE SOLIDUS** → fusion."},
    {"id": 33, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Quelle est la signification de l'isotherme 1300°C pour la lithosphère ?",
     "hint": "💡 Limite LAB",
     "a": "**LIMITE LITHOSPHÈRE-ASTHÉNOSPHÈRE (LAB)**.\nElle **S'APPROFONDIT** avec le refroidissement de la plaque éloignée de la dorsale."},
    {"id": 34, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Basalte en coussin vs basalte en filon : quelle différence ?",
     "hint": "💡 Eau vs fissure",
     "a": "**COUSSIN** : refroidissement très rapide au contact de l'eau (lave sous-marine).\n**FILON (dyke)** : injection de magma dans des fissures verticales."},
    {"id": 35, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Quelle minéralogie correspond au faciès schiste vert ?",
     "hint": "💡 Température < 500°C",
     "a": "**CHLORITE** + **ACTINOTE** (amphibole calcique) : minéraux hydratés de basse température."},
    {"id": 36, "ch": "4", "section": "CH.4 — Dorsales lentes",
     "q": "Pourquoi une dorsale lente a-t-elle un rift très marqué ?",
     "hint": "💡 Magma insuffisant pour...",
     "a": "Magmatisme trop faible pour **COMBLER L'ESPACE** créé par l'écartement.\nL'extension tectonique forme un **FOSSÉ D'EFFONDREMENT PROFOND**."},
    {"id": 37, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Transformation minérale lors de l'hydrothermalisme > 700°C ?",
     "hint": "💡 Faciès amphibolite",
     "a": "**Pyroxène + Plagioclase** (gabbro) → **HORNBLENDE** (amphibole)\nSous l'action de l'hydrothermalisme à > 700°C."},
    {"id": 38, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "La péridotite fond-elle entièrement sous une dorsale ? Quel pourcentage ?",
     "hint": "💡 Partielle !",
     "a": "Non : seulement **FUSION PARTIELLE (~15%)**.\nLe reste reste **SOLIDE**. C'est le liquide qui monte former la croûte."},
    {"id": 39, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Rôle de la convection mantellique dans le fonctionnement d'une dorsale ?",
     "hint": "💡 Mouvement ascendant",
     "a": "La **CONVECTION** assure la **REMONTÉE DES PÉRIDOTITES** sous la dorsale,\ndéclenchant la décompression adiabatique et la fusion partielle."},
    {"id": 40, "ch": "4", "section": "CH.4 — Dorsales lentes",
     "q": "Qu'est-ce qu'une faille de détachement aux dorsales lentes ?",
     "hint": "💡 Grande faille, faible pendage",
     "a": "Grande **FAILLE À FAIBLE PENDAGE** (presque horizontale)\npermettant l'exhumation de péridotites directement sur le plancher océanique."},
    {"id": 41, "ch": "5", "section": "CH.5 — Marqueurs de subduction",
     "q": "Quels sont les 3 marqueurs principaux d'une zone de subduction ?",
     "hint": "💡 1 relief / 1 alignement / 1 sismique",
     "a": "**1.** FOSSE OCÉANIQUE profonde\n**2.** ALIGNEMENT VOLCANIQUE (arc insulaire ou cordillère)\n**3.** PLAN DE WADATI-BENIOFF (sismicité profonde)"},
    {"id": 42, "ch": "5", "section": "CH.5 — Marqueurs de subduction",
     "q": "Pourquoi la sismicité suit-elle un plan incliné en subduction ?",
     "hint": "💡 Rigidité de la plaque",
     "a": "La plaque reste **RIGIDE** en plongeant (elle est froide).\nLes contraintes génèrent des séismes le long du plan de contact : **WADATI-BENIOFF**."},
    {"id": 43, "ch": "5", "section": "CH.5 — Magmatisme",
     "q": "Pourquoi le volcanisme de subduction est-il explosif ?",
     "hint": "💡 Composition du magma",
     "a": "Magma riche en **SILICE** (très visqueux) et en **GAZ DISSOUS**.\nViscosité élevée → dégazage brutal → **EXPLOSION VIOLENTE**."},
    {"id": 44, "ch": "5", "section": "CH.5 — Magmatisme",
     "q": "Quelle est la chaîne de réactions menant au volcanisme de subduction ?",
     "hint": "💡 Eau → péridotite → magma",
     "a": "**1.** Métamorphisme → libération d'**EAU**\n**2.** Eau monte dans le **COIN MANTELLIQUE**\n**3.** Abaisse le point de fusion des péridotites\n**4.** **FUSION PARTIELLE** → magma → volcanisme"},
    {"id": 45, "ch": "5", "section": "CH.5 — Magmatisme",
     "q": "Quelles sont les 4 roches de subduction et leur texture ?",
     "hint": "💡 2 volcaniques + 2 plutoniques",
     "a": "Volcaniques (microlithique) : **ANDÉSITE**, **RHYOLITE**\nPlutoniques (grenue) : **DIORITE**, **GRANITE**"},
    {"id": 46, "ch": "5", "section": "CH.5 — Magmatisme",
     "q": "Pourquoi existe-t-il une telle diversité de roches en subduction ?",
     "hint": "💡 2 processus",
     "a": "**CRISTALLISATION FRACTIONNÉE** (différenciation du magma)\n+ **CONTAMINATION** par la croûte continentale traversée."},
    {"id": 47, "ch": "5", "section": "CH.5 — Magmatisme",
     "q": "Quel est le signe minéralogique caractéristique des roches de subduction ?",
     "hint": "💡 OH dans les minéraux",
     "a": "Présence de **MINÉRAUX HYDRATÉS** portant des groupements **OH**\n(ex. : amphibole, biotite, muscovite)."},
    {"id": 48, "ch": "5", "section": "CH.5 — Métamorphisme",
     "q": "Séquence de faciès métamorphiques de la plaque subduite ?",
     "hint": "💡 3 faciès successifs",
     "a": "**SCHISTE VERT** → **SCHISTE BLEU** (glaucophane, HP-BT) → **ÉCLOGITE** (grenat + jadéite, > 50 km)"},
    {"id": 49, "ch": "5", "section": "CH.5 — Métamorphisme",
     "q": "Quelle minéralogie caractérise le faciès schiste bleu ?",
     "hint": "💡 Minéral bleu, HP-BT",
     "a": "**GLAUCOPHANE** (amphibole sodique bleue) :\nindicateur de conditions **HAUTE PRESSION – BASSE TEMPÉRATURE**."},
    {"id": 50, "ch": "5", "section": "CH.5 — Métamorphisme",
     "q": "Minéralogie du faciès éclogite et profondeur de formation ?",
     "hint": "💡 > 50 km",
     "a": "**GRENAT** + **JADÉITE** (pyroxène sodique)\nFormés à **> 50 KM** de profondeur, sous très haute pression."},
    {"id": 51, "ch": "5", "section": "CH.5 — Métamorphisme",
     "q": "En quoi le contexte P-T de la subduction est-il unique ?",
     "hint": "💡 Comparer avec métamorphisme classique",
     "a": "**HAUTE PRESSION / BASSE TEMPÉRATURE** :\nLa plaque s'enfonce vite et reste froide.\nContraire du métamorphisme de contact (BT/BP)."},
    {"id": 52, "ch": "5", "section": "CH.5 — Moteur",
     "q": "Quel est le moteur principal du mouvement des plaques ?",
     "hint": "💡 Force de tirage",
     "a": "**TRACTION DE LA PLAQUE SUBDUITE** (slab pull) :\nPlaque froide et dense → gravité → tire la plaque,\nentraîne les mouvements descendants de la convection mantellique."},
    {"id": 53, "ch": "5", "section": "CH.5 — Moteur",
     "q": "Quel lien y a-t-il entre subduction et convection mantellique ?",
     "hint": "💡 Cause-conséquence",
     "a": "La subduction **ENTRETIENT** les courants descendants froids de la convection.\nLes plaques ne sont pas portées passivement : elles **TIRENT ACTIVEMENT**."},
    {"id": 54, "ch": "5", "section": "CH.5 — Collision",
     "q": "Comment se produit une collision continentale ?",
     "hint": "💡 Après quoi ?",
     "a": "Après la **FERMETURE COMPLÈTE D'UN OCÉAN** par subduction.\nLes deux marges continentales entrent en contact : croûte trop légère pour plonger."},
    {"id": 55, "ch": "5", "section": "CH.5 — Collision",
     "q": "Quelles structures géologiques témoignent d'une collision en surface ?",
     "hint": "💡 3 structures",
     "a": "**PLIS** + **FAILLES INVERSES** + **NAPPES DE CHARRIAGE**\n(grandes masses de roches déplacées horizontalement)"},
    {"id": 56, "ch": "5", "section": "CH.5 — Collision",
     "q": "Qu'est-ce qu'une racine crustale ? À quelle profondeur descend le Moho ?",
     "hint": "💡 Iceberg crustal",
     "a": "Épaississement de la croûte vers le bas.\nLe **MOHO** peut descendre jusqu'à **70 KM** (normal : ~35 km)."},
    {"id": 57, "ch": "5", "section": "CH.5 — Collision",
     "q": "Quels types de roches métamorphiques se forment en collision ?",
     "hint": "💡 Roches foliées",
     "a": "Roches **FOLIÉES** :\n- **MICASCHISTES** (métamorphisme faible à modéré)\n- **GNEISS** (fort métamorphisme)"},
    {"id": 58, "ch": "5", "section": "CH.5 — Marqueurs",
     "q": "Différence entre arc insulaire et cordillère en subduction ?",
     "hint": "💡 Océan-Océan vs Océan-Continent",
     "a": "**ARC INSULAIRE** : subduction Océan/Océan → volcanisme en **MER**.\n**CORDILLÈRE** : subduction Océan/Continent → volcanisme en **BORDURE DE CONTINENT**."},
    {"id": 59, "ch": "5", "section": "CH.5 — Magmatisme",
     "q": "Qu'est-ce que le coin mantellique et quel est son rôle ?",
     "hint": "💡 Wedge",
     "a": "Portion du **MANTEAU DE LA PLAQUE CHEVAUCHANTE** entre les deux plaques.\nReçoit l'**EAU libérée** → péridotite fond partiellement → magmatisme."},
    {"id": 60, "ch": "5", "section": "CH.5 — Risques",
     "q": "Risque volcanique caractéristique des zones de subduction ?",
     "hint": "💡 Nuage mortel",
     "a": "**NUÉES ARDENTES** (coulées pyroclastiques) :\nmélanges brûlants de gaz, cendres, débris à très grande vitesse.\nLié à la viscosité élevée du magma."},
    {"id": 61, "ch": "synth", "section": "SYNTHÈSE — Cycle lithosphérique",
     "q": "Résume en 5 étapes le cycle de la lithosphère océanique.",
     "hint": "💡 Naissance → mort",
     "a": "**1.** Naissance à la **DORSALE** (HT, BP)\n**2.** Hydratation + refroidissement → schistes verts\n**3.** Densification → **SUBDUCTION**\n**4.** Métamorphisme HP-BT (schiste bleu → éclogite) + libération d'eau\n**5.** Déclenchement du **MAGMATISME** → croûte continentale"},
    {"id": 62, "ch": "synth", "section": "SYNTHÈSE — Moteur",
     "q": "Quel processus assure la continuité de la convection mantellique globale ?",
     "hint": "💡 Traction + plongement",
     "a": "**TRACTION DE LA PLAQUE PLONGEANTE** (slab pull) :\nforce gravitaire exercée par la plaque froide et dense,\nmoteur principal du cycle convectif global."},
    {"id": 63, "ch": "synth", "section": "SYNTHÈSE — Eau",
     "q": "Quel est le rôle de l'eau dans le cycle de la lithosphère ?",
     "hint": "💡 Partout : dorsale → subduction",
     "a": "**DORSALE** : hydratation hydrothermale (schistes verts)\n**SUBDUCTION** : expulsion par métamorphisme → abaissement du solidus → magmatisme"},
    {"id": 64, "ch": "synth", "section": "SYNTHÈSE — Cycle",
     "q": "Où naît et où meurt la lithosphère océanique ?",
     "hint": "💡 Deux lieux opposés",
     "a": "**NAISSANCE** : à la **DORSALE** (accrétion)\n**MORT** : à la **FOSSE DE SUBDUCTION** (réintégration dans le manteau)"},
    {"id": 65, "ch": "synth", "section": "SYNTHÈSE — Croûte continentale",
     "q": "Comment la croûte continentale est-elle créée via la subduction ?",
     "hint": "💡 Eau → fusion → granite",
     "a": "Eau libérée → fusion coin mantellique → magma riche en silice → volcanisme + intrusions\n→ **GRANITE ET DIORITE** = croûte continentale"},
    {"id": 66, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
     "q": "Qu'est-ce qu'une polarité magnétique normale vs inverse ?",
     "hint": "💡 Pôles Nord et Sud",
     "a": "**NORMALE** : orientation actuelle (pôle magnétique Nord ≈ Nord géographique)\n**INVERSE** : pôles **NORD ET SUD MAGNÉTIQUES INVERSÉS**"},
    {"id": 67, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "Pourquoi la lithosphère océanique devient-elle plus dense avec le temps ?",
     "hint": "💡 Refroidissement + manteau",
     "a": "En se refroidissant, elle intègre du manteau refroidi par sa base.\nDensité dépasse celle de l'**ASTHÉNOSPHÈRE** (~3,3 g/cm³)."},
    {"id": 68, "ch": "5", "section": "CH.5 — Métamorphisme",
     "q": "Pourquoi le métamorphisme de subduction est-il HP-BT ?",
     "hint": "💡 Vitesse d'enfouissement",
     "a": "La plaque **S'ENFONCE RAPIDEMENT** : pression augmente vite\nmais plaque froide ne se réchauffe pas assez → **HAUTE PRESSION, BASSE TEMPÉRATURE**."},
    {"id": 69, "ch": "5", "section": "CH.5 — Collision",
     "q": "Pourquoi la croûte continentale ne peut-elle pas subduire ?",
     "hint": "💡 Densité vs manteau",
     "a": "Croûte continentale : ~**2,7 g/cm³**\nManteau : ~**3,3 g/cm³**\nElle est **MOINS DENSE** → **FLOTTE** → ne peut pas plonger → **COLLISION**."},
    {"id": 70, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Qu'est-ce que le géotherme et le solidus dans le contexte des dorsales ?",
     "hint": "💡 Deux courbes sur un graphique P-T",
     "a": "**GÉOTHERME** : température en fonction de la profondeur.\n**SOLIDUS** : limite en dessous de laquelle la roche est solide.\nSi géotherme **RECOUPE** le solidus → **LA ROCHE FOND**."},
    {"id": 71, "ch": "3", "section": "CH.3 — Preuves expansion",
     "q": "Quels indices prouvent l'expansion océanique (3 types) ?",
     "hint": "💡 Magnétique + sédimentaire + géodésique",
     "a": "**1.** ANOMALIES MAGNÉTIQUES SYMÉTRIQUES\n**2.** ÂGE/ÉPAISSEUR croissants des sédiments\n**3.** MESURES GPS (géodésie spatiale)"},
    {"id": 72, "ch": "4", "section": "CH.4 — Dorsales rapides",
     "q": "Composition minéralogique initiale d'un gabbro (avant métamorphisme) ?",
     "hint": "💡 2 minéraux principaux",
     "a": "**PYROXÈNE** (clinopyroxène) + **PLAGIOCLASE** (feldspath calcique).\nRemplacés par hornblende puis chlorite + actinote lors du métamorphisme hydrothermal."},
    {"id": 73, "ch": "5", "section": "CH.5 — Moteur",
     "q": "Quelle force initie le mouvement de la plaque vers la fosse ?",
     "hint": "💡 Densité + gravité",
     "a": "Plaque océanique vieillie : **PLUS DENSE QUE L'ASTHÉNOSPHÈRE**.\nGravité → **PLONGEMENT SPONTANÉ** (slab pull) → tire le reste de la plaque."},
    {"id": 74, "ch": "5", "section": "CH.5 — Magmatisme",
     "q": "Quelle roche est l'équivalent plutonique de l'andésite ?",
     "hint": "💡 Roche à grain visible",
     "a": "La **DIORITE** : même composition que l'andésite\nmais refroidissement lent en profondeur → texture **GRENUE**."},
    {"id": 75, "ch": "4", "section": "CH.4 — Vieillissement",
     "q": "À quelle température se place la limite faciès amphibolite / faciès schiste vert ?",
     "hint": "💡 Deux seuils de température",
     "a": "**AMPHIBOLITE** : > 700°C\n**SCHISTE VERT** : entre 300 et 500°C\nTransition : **500–700°C**"},
    {"id": 76, "ch": "synth", "section": "SYNTHÈSE — Les 3 faciès du voyage",
     "q": "Cite les 3 faciès jalonnant le parcours d'une plaque océanique (dorsale → arc).",
     "hint": "💡 Dorsale → plaque âgée → subduction",
     "a": "**1. SCHISTE VERT** (hydrothermalisme dorsale + vieillissement)\n**2. SCHISTE BLEU** (début subduction, HP-BT)\n**3. ÉCLOGITE** (subduction profonde > 50 km)"},
]

WRONG_POOL = [
    "Cristallisation fractionnée", "Plan de Wadati-Benioff", "Décompression adiabatique",
    "Fusion partielle", "Serpentinisation", "Dorsale médio-atlantique", "Glaucophane",
    "Faille inverse", "Faille normale", "Basalte en coussin", "Jadéite", "Grenat",
    "Hornblende", "Chlorite", "Actinote", "Gabbro", "Andésite", "Diorite",
    "Schiste bleu", "Schiste vert", "Éclogite", "Slab pull", "Coin mantellique",
    "Chambre magmatique", "Isotherme 1300°C", "Solidus", "Géotherme",
    "Arc insulaire", "Cordillère", "Nuée ardente", "5 à 7 km", "10 à 16 cm/an",
    "1 à 5 cm/an", "700 km de profondeur", "50 km de profondeur",
    "Fumeurs noirs", "Micaschiste", "Gneiss", "Péridotite", "Pyroxène",
    "Plagioclase", "Serpentine", "Nappe de charriage", "Racine crustale",
    "Rhyolite", "Granite", "Basalte en filon", "Moho à 70 km",
]

FILTER_CHOICES = [
    "Tout (76)", "CH.3 — Mobilité (19)", "CH.4 — Divergence (21)",
    "CH.5 — Convergence (20)", "Synthèse (5+)", "⚠ À revoir",
]

# ══════════════════════════════════════════════════════════
#  LOGIQUE MÉTIER
# ══════════════════════════════════════════════════════════
def get_filtered(ch_filter, status_dict):
    mapping = {
        "Tout (76)":              list(CARDS),
        "CH.3 — Mobilité (19)":  [c for c in CARDS if c["ch"] == "3"],
        "CH.4 — Divergence (21)":[c for c in CARDS if c["ch"] == "4"],
        "CH.5 — Convergence (20)":[c for c in CARDS if c["ch"] == "5"],
        "Synthèse (5+)":         [c for c in CARDS if c["ch"] == "synth"],
        "⚠ À revoir":            [c for c in CARDS if status_dict.get(str(c["id"])) in (None, "bad")],
    }
    result = mapping.get(ch_filter, list(CARDS))
    return result if result else list(CARDS)

def ch_badge(ch):
    return {"3":"🔴 CH.3","4":"🔵 CH.4","5":"🟢 CH.5","synth":"🟡 SYNTH"}.get(ch, ch)

def status_emoji(s):
    return {"good":"✅ Maîtrisé","ok":"🟡 Hésitant","bad":"❌ À revoir"}.get(s, "⬜ Non vu")

def compute_stats(status_dict):
    good   = sum(1 for v in status_dict.values() if v == "good")
    ok     = sum(1 for v in status_dict.values() if v == "ok")
    bad    = sum(1 for v in status_dict.values() if v == "bad")
    unseen = 76 - good - ok - bad
    pct    = int(good / 76 * 100)
    filled = int(pct / 5)
    bar    = "█" * filled + "░" * (20 - filled)
    return (f"✅ **{good}** maîtrisées &nbsp;|&nbsp; 🟡 **{ok}** hésitantes &nbsp;|&nbsp; "
            f"❌ **{bad}** à revoir &nbsp;|&nbsp; ⬜ **{unseen}** non vues\n\n"
            f"`[{bar}]` **{pct}%** complété")

# ── Flashcard ──
def init_flashcard(ch_filter, state):
    status_dict = state.get("status", {})
    deck = get_filtered(ch_filter, status_dict)
    random.shuffle(deck)
    state["deck"]    = [c["id"] for c in deck]
    state["fc_idx"]  = 0
    state["flipped"] = False
    return _render_fc(state)

def _render_fc(state):
    deck_ids    = state.get("deck", [])
    idx         = state.get("fc_idx", 0)
    flipped     = state.get("flipped", False)
    status_dict = state.get("status", {})
    if not deck_ids:
        return ("—", "### Aucune carte. Change de filtre.", "", "",
                "👁 Voir la réponse", gr.update(visible=False),
                compute_stats(status_dict), state)
    idx  = idx % len(deck_ids)
    card = next(c for c in CARDS if c["id"] == deck_ids[idx])
    s    = status_dict.get(str(card["id"]))
    prog = f"**{idx+1} / {len(deck_ids)}** &nbsp;·&nbsp; {ch_badge(card['ch'])} &nbsp;·&nbsp; {card['section']} &nbsp;·&nbsp; {status_emoji(s)}"
    q_md = f"## ❓ {card['q']}\n\n{card['hint']}" if not flipped else f"## ❓ {card['q']}"
    a_md = f"---\n### ✅ Réponse\n\n{card['a']}" if flipped else ""
    flip_label = "🔄 Masquer la réponse" if flipped else "👁 Voir la réponse"
    return (prog, q_md, a_md, "",
            flip_label, gr.update(visible=flipped),
            compute_stats(status_dict), state)

def flip_fc(state):
    state["flipped"] = not state.get("flipped", False)
    return _render_fc(state)

def next_fc(state):
    deck_ids = state.get("deck", [c["id"] for c in CARDS])
    state["fc_idx"] = (state.get("fc_idx", 0) + 1) % len(deck_ids)
    state["flipped"] = False
    return _render_fc(state)

def prev_fc(state):
    deck_ids = state.get("deck", [c["id"] for c in CARDS])
    state["fc_idx"] = (state.get("fc_idx", 0) - 1) % len(deck_ids)
    state["flipped"] = False
    return _render_fc(state)

def rate_fc(rating, state):
    deck_ids = state.get("deck", [c["id"] for c in CARDS])
    idx = state.get("fc_idx", 0) % len(deck_ids)
    state.setdefault("status", {})[str(deck_ids[idx])] = rating
    state["fc_idx"] = (idx + 1) % len(deck_ids)
    state["flipped"] = False
    return _render_fc(state)

def shuffle_fc(state):
    deck = state.get("deck", [c["id"] for c in CARDS])
    random.shuffle(deck)
    state["deck"] = deck
    state["fc_idx"] = 0
    state["flipped"] = False
    return _render_fc(state)

FC_OUTS = ["prog_md","q_md","a_md","_","flip_btn","rating_row","stats_md","state"]

# ── Quiz ──
def _build_mcq(card):
    raw = re.sub(r"\*\*", "", card["a"].split("\n")[0])
    correct = re.sub(r"^\d+\.\s*", "", raw).strip()[:80]
    pool = [w for w in WRONG_POOL if w.lower() not in correct.lower()]
    random.shuffle(pool)
    opts = pool[:3] + [correct]
    random.shuffle(opts)
    return correct, opts

def start_quiz(ch_filter, state):
    status_dict = state.get("status", {})
    deck = get_filtered(ch_filter, status_dict)
    random.shuffle(deck)
    state["qz_deck"]     = [c["id"] for c in deck]
    state["qz_idx"]      = 0
    state["qz_score"]    = 0
    state["qz_answered"] = False
    return _render_qz(state)

def _render_qz(state):
    deck_ids = state.get("qz_deck", [c["id"] for c in CARDS])
    idx      = state.get("qz_idx", 0)
    score    = state.get("qz_score", 0)
    if idx >= len(deck_ids):
        total = len(deck_ids)
        pct   = int(score / total * 100) if total else 0
        ico   = "🏆" if pct>=80 else "👍" if pct>=60 else "⚠️" if pct>=40 else "📚"
        msg   = ("Excellent !" if pct>=80 else "Bien !" if pct>=60 else
                 "Des lacunes — flashcards !" if pct>=40 else "Reprends tout !")
        q_md  = f"# {ico} Score final : {pct}%\n\n**{score}/{total}** bonnes réponses\n\n*{msg}*"
        return (q_md, gr.update(choices=[], visible=False),
                gr.update(value="", visible=False),
                gr.update(visible=False, value="✅ Valider"),
                gr.update(visible=True), state)
    card = next(c for c in CARDS if c["id"] == deck_ids[idx])
    correct, opts = _build_mcq(card)
    state["qz_correct"]  = correct
    state["qz_answered"] = False
    state["qz_opts"]     = opts
    q_md = (f"**{idx+1}/{len(deck_ids)}** &nbsp;·&nbsp; {ch_badge(card['ch'])} &nbsp;·&nbsp; "
            f"Score : **{score}/{idx}**\n\n---\n\n## ❓ {card['q']}")
    return (q_md,
            gr.update(choices=opts, value=None, visible=True, interactive=True),
            gr.update(value="", visible=False),
            gr.update(visible=True, value="✅ Valider"),
            gr.update(visible=False), state)

def answer_qz(chosen, btn_label, state):
    if btn_label != "✅ Valider":
        state["qz_idx"] = state.get("qz_idx", 0) + 1
        state["qz_answered"] = False
        return _render_qz(state)
    if not chosen or state.get("qz_answered"):
        return _render_qz(state)
    correct  = state.get("qz_correct", "")
    is_ok    = (chosen == correct)
    deck_ids = state.get("qz_deck", [])
    idx      = state.get("qz_idx", 0)
    card_id  = deck_ids[idx] if idx < len(deck_ids) else None
    card     = next((c for c in CARDS if c["id"] == card_id), None)
    state["qz_answered"] = True
    if is_ok:
        state["qz_score"] = state.get("qz_score", 0) + 1
        if card:
            state.setdefault("status", {})[str(card_id)] = "good"
        fb = f"### ✅ Bonne réponse !\n\n{card['a'] if card else ''}"
    else:
        if card:
            state.setdefault("status", {})[str(card_id)] = "bad"
        fb = f"### ❌ Incorrect\n\n**Bonne réponse :** {correct}\n\n---\n\n{card['a'] if card else ''}"
    deck_ids2 = state.get("qz_deck", [])
    idx2      = state.get("qz_idx", 0)
    score2    = state.get("qz_score", 0)
    card2     = next(c for c in CARDS if c["id"] == deck_ids2[idx2])
    _, opts2  = _build_mcq(card2)
    q_md2 = (f"**{idx2+1}/{len(deck_ids2)}** &nbsp;·&nbsp; {ch_badge(card2['ch'])} &nbsp;·&nbsp; "
             f"Score : **{score2}/{idx2}**\n\n---\n\n## ❓ {card2['q']}")
    return (q_md2,
            gr.update(choices=opts2, value=chosen, interactive=False),
            gr.update(value=fb, visible=True),
            gr.update(visible=True, value="Question suivante →"),
            gr.update(visible=False), state)

# ── Récap ──
def build_recap(ch_filter):
    groups = {
        "Tout (76)":             [("3","🔴 CH.3"), ("4","🔵 CH.4"), ("5","🟢 CH.5"), ("synth","🟡 SYNTHÈSE")],
        "CH.3 — Mobilité (19)":  [("3","🔴 CH.3")],
        "CH.4 — Divergence (21)":[("4","🔵 CH.4")],
        "CH.5 — Convergence (20)":[("5","🟢 CH.5")],
        "Synthèse (5+)":         [("synth","🟡 SYNTHÈSE")],
    }
    chapters = groups.get(ch_filter, groups["Tout (76)"])
    md = ""
    for ch, label in chapters:
        md += f"\n---\n# {label}\n\n"
        for c in [x for x in CARDS if x["ch"] == ch]:
            md += f"**#{c['id']:02d} · {c['section']}**\n\n❓ *{c['q']}*\n\n✅ {c['a']}\n\n"
    return md

# ── Carte de progression ──
def build_map(state):
    sd = state.get("status", {})
    md = "## 🗺 Carte de progression\n\n"
    for ch, label, ico in [("3","CH.3","🔴"),("4","CH.4","🔵"),("5","CH.5","🟢"),("synth","SYNTH","🟡")]:
        cards = [c for c in CARDS if c["ch"] == ch]
        g = sum(1 for c in cards if sd.get(str(c["id"]))=="good")
        o = sum(1 for c in cards if sd.get(str(c["id"]))=="ok")
        b = sum(1 for c in cards if sd.get(str(c["id"]))=="bad")
        bar = "█"*int(g/len(cards)*10) + "░"*(10-int(g/len(cards)*10))
        md += f"{ico} **{label}** `[{bar}]` ✅{g} 🟡{o}{b} / {len(cards)}\n\n"
    md += "\n---\n### Détail par carte\n\n"
    row = []
    for c in CARDS:
        s = sd.get(str(c["id"]))
        ico2 = {"good":"✅","ok":"🟡","bad":"❌"}.get(s,"⬜")
        row.append(f"{ico2}`{c['id']:02d}`")
        if len(row) == 10:
            md += "  ".join(row) + "\n\n"; row = []
    if row: md += "  ".join(row) + "\n\n"
    md += "\n**Légende :** ✅ Maîtrisé &nbsp; 🟡 Hésitant &nbsp; ❌ À revoir &nbsp; ⬜ Non vu"
    return md

def reset_progress(state):
    state["status"] = {}
    return compute_stats({}), build_map(state), state

# ══════════════════════════════════════════════════════════
#  CSS
# ══════════════════════════════════════════════════════════
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@700;800&display=swap');
body, .gradio-container { font-family: 'Space Mono', monospace !important; }
.app-header { background: linear-gradient(135deg,#111420,#181d2e); border:1px solid #252a3d;
  border-radius:14px; padding:28px 36px; margin-bottom:12px; }
.app-header h1 { font-family:'Syne',sans-serif !important; font-size:2.4rem !important;
  font-weight:800 !important; letter-spacing:-2px; margin:0 !important; }
.app-header h1 span { color:#e8440a; }
.app-header p { color:#6b7090; font-size:.73rem; letter-spacing:2px; margin:6px 0 0; }
"""

# ══════════════════════════════════════════════════════════
#  UI GRADIO
# ══════════════════════════════════════════════════════════
with gr.Blocks(css=CSS, title="GéoRévision — Tectonique des Plaques",
               theme=gr.themes.Base(
                   primary_hue="orange", neutral_hue="slate",
                   font=[gr.themes.GoogleFont("Space Mono"), "monospace"]
               )) as demo:

    state = gr.State({})

    gr.HTML("""
    <div class="app-header">
      <h1>GÉO<span>RÉVISION</span></h1>
      <p>TECTONIQUE DES PLAQUES &nbsp;·&nbsp; 76 NOTIONS &nbsp;·&nbsp; CH.3 · CH.4 · CH.5 · SYNTHÈSE &nbsp;·&nbsp; MÉTHODE EBBINGHAUS + TESTING EFFECT</p>
    </div>
    """)

    stats_md = gr.Markdown(compute_stats({}))

    with gr.Tabs():

        # ═══ FLASHCARDS ═══
        with gr.Tab("🃏 Flashcards"):
            with gr.Row():
                fc_filter = gr.Dropdown(FILTER_CHOICES, value="Tout (76)", label="Filtre", scale=3)
                fc_start  = gr.Button("▶ Démarrer", variant="primary", scale=1)
                fc_shuf   = gr.Button("⟳ Mélanger", variant="secondary", scale=1)

            fc_prog = gr.Markdown("*Sélectionne un filtre et clique sur ▶ Démarrer*")

            with gr.Group():
                fc_q = gr.Markdown("### ← Lance une session pour commencer !")

            fc_flip = gr.Button("👁 Voir la réponse", variant="primary")

            with gr.Group():
                fc_a = gr.Markdown("")
                fc_s = gr.Markdown("")

            with gr.Row(visible=False) as fc_rating:
                fc_bad  = gr.Button("❌ À revoir",  variant="secondary", scale=1)
                fc_ok   = gr.Button("🟡 Hésitant",  variant="secondary", scale=1)
                fc_good = gr.Button("✅ Maîtrisé",  variant="secondary", scale=1)

            with gr.Row():
                fc_prev = gr.Button("← Précédent", variant="secondary", scale=1)
                fc_next = gr.Button("Suivant →",   variant="secondary", scale=1)

            gr.Markdown("*💡 Méthode : lis la question, formule mentalement, puis révèle. Note honnêtement. Reviens sur le filtre **⚠ À revoir** régulièrement.*")

            FC = [fc_prog, fc_q, fc_a, fc_s, fc_flip, fc_rating, stats_md, state]
            fc_start.click(init_flashcard, [fc_filter, state], FC)
            fc_shuf.click(shuffle_fc,  [state], FC)
            fc_flip.click(flip_fc,     [state], FC)
            fc_prev.click(prev_fc,     [state], FC)
            fc_next.click(next_fc,     [state], FC)
            fc_bad.click( lambda s: rate_fc("bad",  s), [state], FC)
            fc_ok.click(  lambda s: rate_fc("ok",   s), [state], FC)
            fc_good.click(lambda s: rate_fc("good", s), [state], FC)

        # ═══ QUIZ ═══
        with gr.Tab("🎯 Quiz QCM"):
            with gr.Row():
                qz_filt  = gr.Dropdown(FILTER_CHOICES, value="Tout (76)", label="Filtre", scale=3)
                qz_start = gr.Button("▶ Lancer", variant="primary", scale=1)

            qz_q    = gr.Markdown("### ← Lance le quiz pour commencer !")
            qz_opts = gr.Radio(choices=[], label="Ta réponse", visible=False)
            qz_fb   = gr.Markdown("", visible=False)

            with gr.Row():
                qz_val     = gr.Button("✅ Valider", variant="primary", visible=False, scale=2)
                qz_restart = gr.Button("🔁 Recommencer", variant="secondary", visible=False, scale=1)

            QZ = [qz_q, qz_opts, qz_fb, qz_val, qz_restart, state]
            qz_start.click(start_quiz, [qz_filt, state], QZ)
            qz_val.click(answer_qz, [qz_opts, qz_val, state], QZ)
            qz_restart.click(start_quiz, [qz_filt, state], QZ)

        # ═══ RÉCAP ═══
        with gr.Tab("📚 Récapitulatif"):
            recap_filt = gr.Dropdown(
                [c for c in FILTER_CHOICES if c != "⚠ À revoir"],
                value="Tout (76)", label="Chapitre"
            )
            gr.Button("📖 Afficher", variant="primary").click(
                build_recap, [recap_filt], gr.Markdown("*Clique sur Afficher.*")
            )
            recap_out = gr.Markdown("*Clique sur Afficher pour voir toutes les notions.*")
            gr.Button("📖 Afficher", variant="primary", visible=False)
            # Rebind correctement
            recap_filt.change(build_recap, [recap_filt], recap_out)

        # ═══ PROGRESSION ═══
        with gr.Tab("🗺 Progression"):
            with gr.Row():
                map_btn   = gr.Button("🔄 Actualiser", variant="primary", scale=2)
                reset_btn = gr.Button("🗑 Réinitialiser", variant="stop", scale=1)
            map_out = gr.Markdown(build_map({}))
            map_btn.click(lambda s: build_map(s), [state], map_out)
            reset_btn.click(reset_progress, [state], [stats_md, map_out, state])

        # ═══ AIDE ═══
        with gr.Tab("ℹ️ Aide"):
            gr.Markdown("""
## 🧠 Méthode scientifique

| Principe | Source | Application |
|----------|--------|-------------|
| **Testing Effect** | Roediger & Karpicke, 2006 | Se tester > relire → chaque flashcard force une récupération active |
| **Répétition espacée** | Ebbinghaus, 1885 | Filtre ⚠ À revoir → cible les lacunes au bon moment |
| **Interleaving** | Kornell & Bjork, 2008 | Mode Tout (76) mélangé → améliore la discrimination |

---

## 🃏 Flashcards
1. Sélectionne un filtre → **▶ Démarrer**
2. Lis la question, formule **mentalement** ta réponse
3. Clique **👁 Voir la réponse** pour comparer
4. Note honnêtement : ❌ / 🟡 / ✅
5. Reviens régulièrement sur **⚠ À revoir**

## 🎯 Quiz
- 4 propositions par question
- Feedback immédiat avec l'explication complète
- Met à jour automatiquement la progression

## 📅 Plan de révision recommandé

| Quand | Action |
|-------|--------|
| J-7 | Flashcards CH.3 complet |
| J-5 | Flashcards CH.4 complet |
| J-3 | Flashcards CH.5 + Synthèse |
| J-2 | Quiz tout + ⚠ À revoir |
| J-1 | Récap complet + Quiz final |
| Jour J | Cartes ❌ uniquement |
""")

    demo.load(lambda s: (compute_stats(s.get("status", {})), build_map(s)),
              [state], [stats_md, map_out])

if __name__ == "__main__":
    demo.launch()