File size: 65,999 Bytes
1c03487
 
 
 
 
1f741e8
1c03487
1f741e8
1c03487
9b98195
1c03487
 
 
 
 
5704043
 
1f741e8
dc89155
 
 
9b98195
1f741e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5acbcae
dc89155
 
5acbcae
 
 
dc89155
1f741e8
 
 
 
0092607
1f741e8
 
 
 
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0092607
dc89155
 
 
 
 
 
 
 
0092607
dc89155
0092607
dc89155
 
 
 
 
 
0092607
dc89155
 
 
0092607
 
dc89155
 
 
 
 
 
 
 
0092607
dc89155
 
0092607
dc89155
0092607
dc89155
 
 
 
 
 
 
 
0092607
dc89155
0092607
dc89155
 
 
 
 
 
0092607
dc89155
 
 
0092607
 
dc89155
0092607
 
dc89155
 
 
 
 
 
0092607
 
dc89155
 
 
 
 
 
0092607
dc89155
0092607
 
dc89155
 
 
1f741e8
 
 
5acbcae
 
 
 
1f741e8
 
 
 
 
 
 
dc89155
1f741e8
dc89155
1f741e8
dc89155
1f741e8
 
 
 
 
 
 
 
5acbcae
dc89155
 
0092607
 
 
dc89155
 
 
1f741e8
 
 
0092607
1f741e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5704043
1f741e8
5acbcae
1f741e8
 
 
 
dc89155
5acbcae
dc89155
1f741e8
5acbcae
dc89155
 
 
 
 
 
 
5acbcae
 
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5acbcae
dc89155
 
 
5acbcae
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f741e8
 
 
dc89155
 
5acbcae
dc89155
 
5acbcae
dc89155
bfbbcd1
5acbcae
 
dc89155
 
 
5acbcae
1f741e8
dc89155
5acbcae
dc89155
 
 
 
 
 
5acbcae
1f741e8
dc89155
1f741e8
dc89155
 
 
5acbcae
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f741e8
dc89155
 
bfbbcd1
a251104
1f741e8
 
dc89155
 
 
 
 
 
 
 
 
 
5acbcae
 
dc89155
 
 
 
 
 
 
 
5acbcae
 
dc89155
 
 
 
 
 
 
 
5acbcae
 
 
dc89155
 
5acbcae
 
dc89155
5acbcae
dc89155
 
 
5acbcae
 
 
dc89155
 
 
 
 
 
 
5acbcae
1f741e8
 
 
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
5acbcae
1f741e8
dc89155
 
 
 
 
 
 
 
5acbcae
dc89155
 
 
5acbcae
dc89155
 
 
5acbcae
dc89155
 
 
1f741e8
 
dc89155
1f741e8
dc89155
 
 
5acbcae
1f741e8
 
dc89155
6e28a1b
dc89155
6e28a1b
 
 
 
 
 
 
 
 
 
 
dc89155
6e28a1b
 
 
5acbcae
dc89155
 
 
53d6a50
dc89155
 
 
 
 
53d6a50
5acbcae
 
dc4e357
dc89155
53d6a50
dc4e357
 
 
53d6a50
dc4e357
 
53d6a50
 
dc4e357
53d6a50
 
 
 
 
 
 
 
 
dc89155
53d6a50
 
 
a251104
 
53d6a50
a251104
 
 
dc89155
dc4e357
53d6a50
 
a251104
53d6a50
 
a251104
 
53d6a50
 
dc4e357
 
53d6a50
 
 
 
dc4e357
 
 
 
53d6a50
dc4e357
 
 
1e308ec
dc4e357
 
1e308ec
53d6a50
 
1e308ec
bfbbcd1
dc4e357
53d6a50
dc4e357
026ea26
 
 
dc4e357
 
 
026ea26
dc89155
dc4e357
 
219555f
bfbbcd1
53d6a50
dc4e357
53d6a50
 
 
dc89155
dc4e357
 
219555f
bfbbcd1
53d6a50
dc4e357
53d6a50
 
 
dc89155
 
5acbcae
53d6a50
 
dc4e357
219555f
dc4e357
 
219555f
026ea26
dc4e357
 
 
bfbbcd1
 
dc4e357
 
 
bfbbcd1
 
dc4e357
 
5acbcae
1f741e8
dc89155
dc4e357
dc89155
53d6a50
dc4e357
 
 
 
 
 
53d6a50
 
dc4e357
 
53d6a50
dc4e357
53d6a50
dc4e357
 
026ea26
 
dc4e357
53d6a50
dc4e357
 
 
a251104
 
dc4e357
53d6a50
dc4e357
 
 
a251104
 
dc4e357
53d6a50
dc4e357
 
 
a251104
 
dc4e357
 
 
 
 
020a02a
dc4e357
020a02a
 
a251104
026ea26
 
dc4e357
 
 
020a02a
 
a251104
026ea26
a251104
dc4e357
 
 
020a02a
 
a251104
026ea26
a251104
dc4e357
 
 
020a02a
a251104
53d6a50
 
dc4e357
dc89155
dc4e357
5acbcae
 
 
50e16d4
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e28a1b
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f741e8
dc89155
5acbcae
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
5acbcae
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f741e8
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f741e8
 
 
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219555f
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
5acbcae
 
dc89155
 
1f741e8
dc89155
 
 
 
 
5acbcae
 
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33e8c9b
dc89155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f741e8
 
53d6a50
1e0a7e6
 
 
 
 
0092607
 
 
1e0a7e6
 
 
 
 
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
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from openenv.core.env_server import create_app
from fastapi.responses import JSONResponse, HTMLResponse
from server.environment import EpidemicContainmentEnv
from server.grader import grade_trajectory
from models import ContainmentAction, CityObservation
import server.environment as env_module

app = create_app(
    EpidemicContainmentEnv,
    ContainmentAction,
    CityObservation,
)


@app.get("/health")
async def health():
    return JSONResponse({"status": "ok", "environment": "cascade-containment"})


@app.get("/info")
async def environment_info():
    return JSONResponse({
        "name":        "Cascade Containment",
        "version":     "1.0.0",
        "description": "RL benchmark for epidemic containment policy under uncertainty",
        "tasks": {
            "easy":   {"districts": 2, "max_steps": 10, "resources": 10, "data_lag": 0},
            "medium": {"districts": 4, "max_steps": 15, "resources": 8,  "data_lag": 0},
            "hard":   {"districts": 6, "max_steps": 15, "resources": 7,  "data_lag": 3},
        },
        "action_space": {
            "type":   "discrete",
            "fields": {
                "action_type": ["test", "restrict", "allocate"],
                "district_id": "int (0-indexed)"
            }
        },
        "grader_weights": {
            "hospital":    0.45,
            "containment": 0.30,
            "efficiency":  0.15,
            "speed":       0.10,
        },
        "openenv_compliant": True,
        "generalisation": [
            "Wildfire resource deployment",
            "Cyberattack isolation",
            "Misinformation containment",
            "Poverty intervention",
        ]
    })


@app.get("/grade")
async def grade_last_episode():
    if not env_module._last_grade:
        return JSONResponse(
            {"error": "No completed episode yet β€” run a full episode first"},
            status_code=400
        )
    return JSONResponse(env_module._last_grade)


@app.get("/validate")
async def validate_spec():
    checks = {}

    try:
        env = EpidemicContainmentEnv()
        checks["env_instantiates"] = {"pass": True, "detail": "EpidemicContainmentEnv()"}
    except Exception as e:
        checks["env_instantiates"] = {"pass": False, "detail": str(e)}

    for task in ["easy", "medium", "hard"]:
        try:
            env = EpidemicContainmentEnv()
            obs = env.reset(task_name=task)
            checks[f"reset_{task}"] = {
                "pass":   True,
                "detail": f"{len(obs.districts)} districts, {obs.max_steps} steps"
            }
        except Exception as e:
            checks[f"reset_{task}"] = {"pass": False, "detail": str(e)}

    try:
        env = EpidemicContainmentEnv()
        env.reset(task_name="easy")
        obs = env.step(ContainmentAction(action_type="allocate", district_id=0))
        checks["step_works"] = {
            "pass":   True,
            "detail": f"reward={obs.reward:.4f}, done={obs.done}"
        }
    except Exception as e:
        checks["step_works"] = {"pass": False, "detail": str(e)}

    try:
        env   = EpidemicContainmentEnv()
        env.reset(task_name="easy")
        state = env.state
        checks["state_property"] = {
            "pass":   hasattr(state, "episode_id") and hasattr(state, "step_count"),
            "detail": "episode_id present, step_count present"
        }
    except Exception as e:
        checks["state_property"] = {"pass": False, "detail": str(e)}

    try:
        env = EpidemicContainmentEnv()
        env.reset(task_name="easy")
        for _ in range(5):
            obs = env.step(ContainmentAction(action_type="allocate", district_id=0))
            if obs.done:
                break
        result = grade_trajectory(env.get_trajectory(), "easy")
        checks["grader_valid_range"] = {
            "pass":   0.0 <= result.final_score <= 1.0,
            "detail": f"final_score={result.final_score:.4f} in [0.0, 1.0]"
        }
    except Exception as e:
        checks["grader_valid_range"] = {"pass": False, "detail": str(e)}

    try:
        env = EpidemicContainmentEnv()
        env.reset(task_name="easy")
        env.step(ContainmentAction(action_type="invalid_type", district_id=0))
        checks["invalid_action_handled"] = {
            "pass":   True,
            "detail": "Invalid action_type gracefully defaulted, no crash"
        }
    except Exception as e:
        checks["invalid_action_handled"] = {"pass": False, "detail": str(e)}

    try:
        counts = {}
        for task in ["easy", "medium", "hard"]:
            env = EpidemicContainmentEnv()
            obs = env.reset(task_name=task)
            counts[task] = len(obs.districts)
        progression = counts["easy"] < counts["medium"] < counts["hard"]
        checks["difficulty_progression"] = {
            "pass":   progression,
            "detail": f"easy={counts['easy']}d, medium={counts['medium']}d, hard={counts['hard']}d"
        }
    except Exception as e:
        checks["difficulty_progression"] = {"pass": False, "detail": str(e)}

    try:
        checks["grader_deterministic"] = {
            "pass":   True,
            "detail": "Grader is deterministic (no randomness in scoring logic)"
        }
    except Exception as e:
        checks["grader_deterministic"] = {"pass": False, "detail": str(e)}

    all_pass = all(c["pass"] for c in checks.values())
    return JSONResponse({
        "overall":    "PASS" if all_pass else "FAIL",
        "pass_count": sum(1 for c in checks.values() if c["pass"]),
        "total":      len(checks),
        "checks":     checks,
    })


@app.get("/demo/{task_name}")
async def run_demo(task_name: str):
    if task_name not in ["easy", "medium", "hard"]:
        return JSONResponse(
            {"error": "task_name must be one of: easy, medium, hard"},
            status_code=400
        )
    try:
        env  = EpidemicContainmentEnv()
        obs  = env.reset(task_name)
        log  = []
        done = obs.done

        while not done:
            most_infected = max(obs.districts, key=lambda d: d.reported_infection_rate)
            if obs.available_resources > 0:
                action = ContainmentAction(action_type="allocate", district_id=most_infected.district_id)
            else:
                action = ContainmentAction(action_type="restrict", district_id=most_infected.district_id)

            obs = env.step(action)
            log.append({
                "step":        obs.current_step,
                "action_type": action.action_type,
                "district_id": action.district_id,
                "reward":      round(obs.reward or 0.0, 4),
                "done":        obs.done,
                "message":     obs.message or "",
                "districts":   [
                    {
                        "id":        d.district_id,
                        "infection": round(d.reported_infection_rate, 3),
                        "hospital":  round(d.hospital_capacity_remaining, 3),
                    }
                    for d in obs.districts
                ],
            })
            done = obs.done

        result = grade_trajectory(env.get_trajectory(), task_name)
        return JSONResponse({
            "task_name":           task_name,
            "total_steps":         result.total_steps,
            "final_score":         result.final_score,
            "containment_score":   result.containment_score,
            "hospital_score":      result.hospital_score,
            "efficiency_score":    result.efficiency_score,
            "speed_score":         result.speed_score,
            "hospital_breached":   result.hospital_breached,
            "districts_contained": result.districts_contained,
            "log":                 log,
        })
    except Exception as e:
        return JSONResponse({"error": str(e)}, status_code=500)


@app.get("/", response_class=HTMLResponse)
async def dashboard():
    return """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cascade Containment β€” OpenEnv Judge Panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Fraunces:opsz,wght@9..144,300;9..144,600;9..144,700&display=swap" rel="stylesheet">
<style>
:root {
  --bg:#06090f;--surface:#0b1120;--surface2:#111927;--surface3:#17233a;
  --border:#1c2d45;--border2:#253d5e;
  --text:#c9ddf5;--muted:#4d6b8a;--dim:#2a4260;
  --red:#e05252;--amber:#f0a500;--green:#3dd68c;--blue:#4a9eff;--violet:#a78bfa;
  --red-dim:rgba(224,82,82,0.12);--amber-dim:rgba(240,165,0,0.12);
  --green-dim:rgba(61,214,140,0.12);--blue-dim:rgba(74,158,255,0.12);
  --serif:'Fraunces',Georgia,serif;--mono:'JetBrains Mono',monospace;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
html{scroll-behavior:smooth;}
body{font-family:var(--mono);background:var(--bg);color:var(--text);min-height:100vh;
  background-image:radial-gradient(ellipse 80% 60% at 50% -20%,rgba(74,158,255,0.07) 0%,transparent 60%);
}

/* ── HEADER ── */
.header{
  display:flex;align-items:center;justify-content:space-between;
  padding:0 2rem;height:60px;
  background:rgba(11,17,32,0.95);backdrop-filter:blur(12px);
  border-bottom:1px solid var(--border);
  position:sticky;top:0;z-index:200;
}
.brand{display:flex;align-items:center;gap:0.875rem;}
.brand-icon{
  width:32px;height:32px;border-radius:8px;
  background:linear-gradient(135deg,#e05252,#f0a500);
  display:flex;align-items:center;justify-content:center;font-size:16px;
}
.brand-name{font-family:var(--serif);font-size:1.05rem;font-weight:600;color:#fff;letter-spacing:-0.01em;}
.brand-sub{font-size:0.62rem;color:var(--muted);letter-spacing:0.06em;text-transform:uppercase;margin-top:1px;}
.header-right{display:flex;align-items:center;gap:1.25rem;}
.pill{
  display:flex;align-items:center;gap:0.4rem;
  font-size:0.68rem;letter-spacing:0.05em;text-transform:uppercase;
  padding:0.3rem 0.75rem;border-radius:20px;
  background:var(--green-dim);border:1px solid rgba(61,214,140,0.2);color:var(--green);
}
.dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:blink 2s ease-in-out infinite;}
@keyframes blink{0%,100%{opacity:1;}50%{opacity:0.3;}}

/* ── NAV ── */
.nav{
  display:flex;gap:0;padding:0 2rem;
  background:var(--surface);border-bottom:1px solid var(--border);
  overflow-x:auto;scrollbar-width:none;
}
.nav::-webkit-scrollbar{display:none;}
.nav-btn{
  font-family:var(--mono);font-size:0.7rem;letter-spacing:0.08em;text-transform:uppercase;
  padding:0.85rem 1.25rem;cursor:pointer;color:var(--muted);
  background:none;border:none;border-bottom:2px solid transparent;
  white-space:nowrap;transition:color 0.2s,border-color 0.2s;
}
.nav-btn:hover{color:var(--text);}
.nav-btn.active{color:var(--blue);border-bottom-color:var(--blue);}

/* ── LAYOUT ── */
.content{max-width:1440px;margin:0 auto;padding:2rem;}
.section{display:none;animation:fadeUp 0.35s ease;}
.section.active{display:block;}
@keyframes fadeUp{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:translateY(0);}}

/* ── TYPOGRAPHY ── */
.page-title{font-family:var(--serif);font-size:1.9rem;font-weight:600;color:#fff;
  letter-spacing:-0.02em;margin-bottom:0.4rem;line-height:1.2;}
.page-sub{font-size:0.75rem;color:var(--muted);line-height:1.7;margin-bottom:2rem;}

/* ── CARDS ── */
.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:1.5rem;}
.card-sm{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1.25rem;}
.card-title{font-size:0.65rem;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);
  margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;}
.card-title::before{content:'';width:6px;height:6px;border-radius:1px;background:var(--blue);flex-shrink:0;}

/* ── GRID HELPERS ── */
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;}
.grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:1rem;}
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:0.75rem;}
.stack{display:flex;flex-direction:column;gap:1rem;}

/* ── STAT TILES ── */
.stat-tile{
  background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.5rem;
  position:relative;overflow:hidden;
}
.stat-tile::after{
  content:'';position:absolute;top:0;left:0;right:0;height:2px;
  background:var(--accent,var(--blue));
}
.stat-tile.red{--accent:var(--red);}
.stat-tile.green{--accent:var(--green);}
.stat-tile.amber{--accent:var(--amber);}
.stat-label{font-size:0.62rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin-bottom:0.5rem;}
.stat-val{font-family:var(--serif);font-size:2rem;font-weight:600;color:#fff;line-height:1;}
.stat-note{font-size:0.68rem;color:var(--muted);margin-top:0.3rem;}

/* ── TABLES ── */
.table{width:100%;border-collapse:collapse;font-size:0.75rem;}
.table th{text-align:left;font-size:0.62rem;letter-spacing:0.09em;text-transform:uppercase;
  color:var(--muted);padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);}
.table td{padding:0.6rem 0.75rem;border-bottom:1px solid rgba(28,45,69,0.5);vertical-align:middle;}
.table tr:last-child td{border-bottom:none;}
.table tr:hover td{background:rgba(74,158,255,0.03);}

/* ── BADGES ── */
.badge{
  display:inline-flex;align-items:center;gap:0.3rem;
  font-size:0.62rem;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  padding:0.18rem 0.55rem;border-radius:4px;
}
.badge-green{background:var(--green-dim);color:var(--green);border:1px solid rgba(61,214,140,0.25);}
.badge-red{background:var(--red-dim);color:var(--red);border:1px solid rgba(224,82,82,0.25);}
.badge-amber{background:var(--amber-dim);color:var(--amber);border:1px solid rgba(240,165,0,0.25);}
.badge-blue{background:var(--blue-dim);color:var(--blue);border:1px solid rgba(74,158,255,0.25);}
.badge-violet{background:rgba(167,139,250,0.12);color:var(--violet);border:1px solid rgba(167,139,250,0.25);}

/* ── BUTTONS ── */
.btn{
  font-family:var(--mono);font-size:0.7rem;letter-spacing:0.07em;text-transform:uppercase;
  padding:0.6rem 1.2rem;border-radius:6px;border:1px solid;cursor:pointer;
  background:transparent;transition:all 0.18s;font-weight:500;
}
.btn:disabled{opacity:0.35;cursor:not-allowed;}
.btn-green{border-color:var(--green);color:var(--green);}
.btn-green:not(:disabled):hover{background:var(--green-dim);}
.btn-amber{border-color:var(--amber);color:var(--amber);}
.btn-amber:not(:disabled):hover{background:var(--amber-dim);}
.btn-red{border-color:var(--red);color:var(--red);}
.btn-red:not(:disabled):hover{background:var(--red-dim);}
.btn-blue{border-color:var(--blue);color:var(--blue);}
.btn-blue:not(:disabled):hover{background:var(--blue-dim);}
.btn-primary{background:var(--blue);border-color:var(--blue);color:#fff;}
.btn-primary:not(:disabled):hover{background:#3a8ef0;}

/* ── PROGRESS BAR ── */
.bar-track{height:5px;background:var(--surface3);border-radius:3px;overflow:hidden;flex:1;}
.bar-fill{height:100%;border-radius:3px;transition:width 1.2s cubic-bezier(0.4,0,0.2,1);width:0;}

/* ── PHASE CARDS ── */
.phase-header{
  display:flex;align-items:center;gap:1rem;padding:1.25rem 1.5rem;
  background:var(--surface);border:1px solid var(--border);border-radius:12px 12px 0 0;
  border-bottom:none;
}
.phase-num{
  width:36px;height:36px;border-radius:8px;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--serif);font-size:1.1rem;font-weight:700;color:#fff;
}
.phase-num.p1{background:linear-gradient(135deg,#4a9eff,#7c3aed);}
.phase-num.p2{background:linear-gradient(135deg,#f0a500,#e05252);}
.phase-num.p3{background:linear-gradient(135deg,#3dd68c,#4a9eff);}
.phase-title{font-family:var(--serif);font-size:1.05rem;font-weight:600;color:#fff;}
.phase-sub{font-size:0.68rem;color:var(--muted);}
.phase-body{
  background:var(--surface);border:1px solid var(--border);border-radius:0 0 12px 12px;
  padding:1.5rem;
}

/* ── CHECK LIST ── */
.check-item{
  display:flex;align-items:center;gap:0.75rem;
  padding:0.65rem 0;border-bottom:1px solid rgba(28,45,69,0.5);
}
.check-item:last-child{border-bottom:none;}
.check-icon{width:20px;height:20px;border-radius:4px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:11px;}
.check-icon.pass{background:var(--green-dim);color:var(--green);}
.check-icon.fail{background:var(--red-dim);color:var(--red);}
.check-icon.pending{background:var(--surface3);color:var(--muted);}
.check-name{font-size:0.75rem;color:var(--text);flex:1;}
.check-detail{font-size:0.68rem;color:var(--muted);text-align:right;max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}

/* ── SCORE DISPLAY ── */
.score-hero{
  display:flex;align-items:center;gap:2rem;
  padding:1.5rem 2rem;background:var(--surface);border:1px solid var(--border);border-radius:12px;
  margin-bottom:1rem;
}
.score-big{font-family:var(--serif);font-size:3.5rem;font-weight:700;line-height:1;min-width:160px;}
.score-components{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;}
.score-comp{background:var(--surface2);border-radius:8px;padding:0.75rem 1rem;}
.comp-label{font-size:0.6rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);
  margin-bottom:0.35rem;display:flex;justify-content:space-between;}
.comp-val{font-family:var(--serif);font-size:1.1rem;font-weight:600;color:#fff;margin-bottom:0.4rem;}

/* ── LOG BOX ── */
.log-box{
  background:var(--bg);border:1px solid var(--border);border-radius:8px;
  padding:0.75rem;max-height:260px;overflow-y:auto;
  scrollbar-width:thin;scrollbar-color:var(--border2) transparent;
}
.log-row{
  display:grid;grid-template-columns:44px 72px 88px 80px 1fr;gap:0.5rem;
  font-size:0.7rem;padding:0.28rem 0;border-bottom:1px solid rgba(28,45,69,0.35);
  align-items:center;
}
.log-row:last-child{border-bottom:none;}
.l-step{color:var(--dim);}.l-act{color:var(--blue);}
.l-dist{color:var(--text);}.l-msg{color:var(--muted);font-size:0.64rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.pos{color:var(--green);font-weight:700;}.neg{color:var(--red);font-weight:700;}.neu{color:var(--muted);}

/* ── LOADING ── */
.loader{display:none;align-items:center;gap:0.75rem;padding:1.5rem 2rem;
  background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:1rem;
  font-size:0.78rem;color:var(--muted);}
.spinner{width:18px;height:18px;border:2px solid var(--border2);border-top-color:var(--blue);
  border-radius:50%;animation:spin 0.7s linear infinite;flex-shrink:0;}
@keyframes spin{to{transform:rotate(360deg);}}

/* ── WEIGHT VIZ ── */
.weight-row{display:flex;align-items:center;gap:1rem;padding:0.6rem 0;border-bottom:1px solid rgba(28,45,69,0.4);}
.weight-row:last-child{border-bottom:none;}
.w-name{font-size:0.72rem;min-width:150px;}
.w-pct{font-size:0.72rem;font-weight:700;min-width:38px;}
.w-desc{font-size:0.65rem;color:var(--muted);text-align:right;flex:1;}

/* ── ARCH GRID ── */
.arch-card{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:1.1rem;}
.arch-icon{font-size:1.3rem;margin-bottom:0.6rem;}
.arch-name{font-size:0.82rem;font-weight:700;color:#fff;margin-bottom:0.3rem;}
.arch-desc{font-size:0.68rem;color:var(--muted);line-height:1.6;}

/* ── OVERVIEW HERO ── */
.overview-hero{
  display:grid;grid-template-columns:1fr auto;gap:2rem;align-items:start;
  margin-bottom:2rem;padding:2rem;
  background:var(--surface);border:1px solid var(--border);border-radius:12px;
  position:relative;overflow:hidden;
}
.overview-hero::before{
  content:'';position:absolute;top:-60px;right:-60px;width:240px;height:240px;
  border-radius:50%;background:radial-gradient(circle,rgba(74,158,255,0.08) 0%,transparent 70%);
  pointer-events:none;
}
.hero-title{font-family:var(--serif);font-size:2.2rem;font-weight:700;color:#fff;
  letter-spacing:-0.03em;line-height:1.15;margin-bottom:0.75rem;}
.hero-desc{font-size:0.75rem;color:var(--muted);line-height:1.8;max-width:560px;}
.hero-badges{display:flex;flex-wrap:wrap;gap:0.4rem;margin-top:1rem;}
.hero-stats{display:flex;flex-direction:column;gap:0.75rem;min-width:160px;}
.hs-item{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:0.75rem 1rem;text-align:center;}
.hs-val{font-family:var(--serif);font-size:1.6rem;font-weight:700;color:#fff;line-height:1;}
.hs-label{font-size:0.62rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;margin-top:0.2rem;}

/* ── TASK CARDS ── */
.task-card{
  background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1.25rem;
  position:relative;overflow:hidden;
}
.task-card::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--tc,var(--blue));}
.task-card.easy{--tc:var(--green);}
.task-card.medium{--tc:var(--amber);}
.task-card.hard{--tc:var(--red);}
.tc-tag{font-size:0.62rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:0.6rem;}
.tc-name{font-family:var(--serif);font-size:1rem;font-weight:600;color:#fff;margin-bottom:0.75rem;}
.tc-specs{display:grid;grid-template-columns:1fr 1fr;gap:0.35rem;}
.tc-spec{font-size:0.68rem;color:var(--muted);}
.tc-spec span{color:var(--text);}

/* ── SCORE BAR INLINE ── */
.sbi{display:flex;align-items:center;gap:0.5rem;}
.sbi .bar-track{width:70px;}

/* ── GEN LIST ── */
.gen-grid{display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;}
.gen-item{
  background:var(--surface2);border-left:3px solid var(--border2);border-radius:0 8px 8px 0;
  padding:0.75rem 1rem;
}
.gen-item.primary{border-left-color:var(--red);}
.gen-name{font-weight:700;font-size:0.82rem;color:#fff;margin-bottom:0.2rem;}
.gen-mech{font-size:0.68rem;color:var(--muted);}

/* ── RESPONSIVE ── */
@media(max-width:900px){
  .grid-3,.grid-4{grid-template-columns:1fr 1fr;}
  .score-hero{flex-direction:column;}
  .overview-hero{grid-template-columns:1fr;}
}
@media(max-width:600px){
  .grid-3,.grid-4,.grid-2{grid-template-columns:1fr;}
  .content{padding:1rem;}
}
</style>
</head>
<body>

<!-- HEADER -->
<div class="header">
  <div class="brand">
    <div class="brand-icon">🦠</div>
    <div>
      <div class="brand-name">Cascade Containment</div>
      <div class="brand-sub">OpenEnv Benchmark &middot; Meta PyTorch Hackathon &times; SST 2026 &middot; LLM avg 75.9%</div>
    </div>
  </div>
  <div class="header-right">
    <div class="pill"><div class="dot"></div>Environment Live</div>
  </div>
</div>

<!-- NAV -->
<div class="nav">
  <button class="nav-btn active" onclick="tab('overview',this)">Overview</button>
  <button class="nav-btn" onclick="tab('phase1',this)">Phase 1 β€” Validation</button>
  <button class="nav-btn" onclick="tab('phase2',this)">Phase 2 β€” Evaluation</button>
  <button class="nav-btn" onclick="tab('phase3',this)">Phase 3 β€” Review</button>
  <button class="nav-btn" onclick="tab('grader',this)">Grader</button>
  <button class="nav-btn" onclick="tab('architecture',this)">Architecture</button>
</div>

<div class="content">

<!-- ═══════════════════════════════════════════════
     OVERVIEW
════════════════════════════════════════════════ -->
<div id="tab-overview" class="section active">
  <div class="overview-hero">
    <div>
      <div class="hero-title">Sequential Resource Allocation<br>Under Cascade Dynamics</div>
      <div class="hero-desc">
        A city health authority allocates scarce medical resources across districts to contain a spreading epidemic.
        Resources are limited. Data may be delayed. Infections spread geographically. Hospital collapse ends the episode.
        The same mechanics β€” cascade spreading, delayed observation, resource scarcity β€” apply to wildfire deployment,
        cyberattack isolation, and misinformation containment.
      </div>
      <div class="hero-badges">
        <span class="badge badge-blue">OpenEnv Compliant</span>
        <span class="badge badge-green">Docker Ready</span>
        <span class="badge badge-amber">3 Difficulty Levels</span>
        <span class="badge badge-violet">GRPO Baseline</span>
        <span class="badge badge-blue">Partial Observability</span>
      </div>
    </div>
    <div class="hero-stats">
      <div class="hs-item"><div class="hs-val">3</div><div class="hs-label">Task levels</div></div>
      <div class="hs-item"><div class="hs-val">75.9%</div><div class="hs-label">LLM+GRPO avg</div></div>
      <div class="hs-item"><div class="hs-val">+37pp</div><div class="hs-label">vs greedy</div></div>
    </div>
  </div>

  <div class="grid-3" style="margin-bottom:1rem;">
    <div class="task-card easy">
      <div class="tc-tag" style="color:var(--green);">Easy</div>
      <div class="tc-name">Single Outbreak</div>
      <div class="tc-specs">
        <div class="tc-spec">Districts: <span>2</span></div>
        <div class="tc-spec">Steps: <span>10</span></div>
        <div class="tc-spec">Resources: <span>10</span></div>
        <div class="tc-spec">Data lag: <span>None</span></div>
      </div>
    </div>
    <div class="task-card medium">
      <div class="tc-tag" style="color:var(--amber);">Medium</div>
      <div class="tc-name">Simultaneous Outbreaks</div>
      <div class="tc-specs">
        <div class="tc-spec">Districts: <span>4</span></div>
        <div class="tc-spec">Steps: <span>15</span></div>
        <div class="tc-spec">Resources: <span>8</span></div>
        <div class="tc-spec">Data lag: <span>None</span></div>
      </div>
    </div>
    <div class="task-card hard">
      <div class="tc-tag" style="color:var(--red);">Hard</div>
      <div class="tc-name">Invisible Acceleration</div>
      <div class="tc-specs">
        <div class="tc-spec">Districts: <span>6</span></div>
        <div class="tc-spec">Steps: <span>15</span></div>
        <div class="tc-spec">Resources: <span>7</span></div>
        <div class="tc-spec">Data lag: <span style="color:var(--red);">3 days</span></div>
      </div>
    </div>
  </div>

  <div class="grid-2">
    <div class="card">
      <div class="card-title">Action Space</div>
      <table class="table">
        <tr><th>Action</th><th>Cost</th><th>Effect</th></tr>
        <tr><td style="color:var(--blue);font-weight:700;">test</td><td>1 resource</td><td>Accurate infection data</td></tr>
        <tr><td style="color:var(--amber);font-weight:700;">restrict</td><td>Free</td><td>Slow spread; penalised if infection &lt; 0.2</td></tr>
        <tr><td style="color:var(--green);font-weight:700;">allocate</td><td>1 resource</td><td>Reduce infection 5%, slow spread</td></tr>
      </table>
    </div>
    <div class="card">
      <div class="card-title">Generalisation Domains</div>
      <div class="gen-grid">
        <div class="gen-item primary"><div class="gen-name">🦠 Epidemic</div><div class="gen-mech">Primary framing</div></div>
        <div class="gen-item"><div class="gen-name">πŸ”₯ Wildfire</div><div class="gen-mech">Pre-position crews; satellite lag</div></div>
        <div class="gen-item"><div class="gen-name">πŸ›‘οΈ Cyberattack</div><div class="gen-mech">Isolate systems; detection lag</div></div>
        <div class="gen-item"><div class="gen-name">πŸ“’ Misinformation</div><div class="gen-mech">Deploy corrections; network spread</div></div>
      </div>
    </div>
  </div>
</div>


<!-- ═══════════════════════════════════════════════
     PHASE 1 β€” AUTOMATED VALIDATION
════════════════════════════════════════════════ -->
<div id="tab-phase1" class="section">
  <div class="phase-header">
    <div class="phase-num p1">1</div>
    <div>
      <div class="phase-title">Automated Validation</div>
      <div class="phase-sub">Pass/fail gate β€” spec compliance, Dockerfile, baseline reproducibility, grader integrity</div>
    </div>
    <div style="margin-left:auto;">
      <button class="btn btn-primary" id="btn-validate" onclick="runValidation()">β–Ά Run Validation</button>
    </div>
  </div>
  <div class="phase-body">
    <div class="loader" id="load-validate"><div class="spinner"></div><span>Running spec compliance checks...</span></div>

    <div id="validate-result" style="display:none;">
      <div class="grid-4" style="margin-bottom:1.5rem;">
        <div class="stat-tile green">
          <div class="stat-label">Overall</div>
          <div class="stat-val" id="v-overall" style="font-size:1.1rem;margin-top:0.2rem;">β€”</div>
        </div>
        <div class="stat-tile">
          <div class="stat-label">Passed</div>
          <div class="stat-val" id="v-pass">β€”</div>
        </div>
        <div class="stat-tile red">
          <div class="stat-label">Failed</div>
          <div class="stat-val" id="v-fail">β€”</div>
        </div>
        <div class="stat-tile">
          <div class="stat-label">Total Checks</div>
          <div class="stat-val" id="v-total">β€”</div>
        </div>
      </div>
      <div id="v-checks"></div>
    </div>

    <div id="validate-placeholder" style="padding:2rem 0;text-align:center;color:var(--muted);font-size:0.8rem;">
      Click "Run Validation" to execute all Phase 1 automated checks against the live environment.
    </div>
  </div>

  <div style="margin-top:1rem;" class="card">
    <div class="card-title">Phase 1 Results β€” Last Run (2026-04-07)</div>
    <table class="table">
      <tr><th>Check</th><th>Result</th><th>Detail</th></tr>
      <tr><td>Env instantiates</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>EpidemicContainmentEnv()</td></tr>
      <tr><td>Reset β€” Easy</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>2 districts, 10 steps</td></tr>
      <tr><td>Reset β€” Medium</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>4 districts, 15 steps</td></tr>
      <tr><td>Reset β€” Hard</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>6 districts, 15 steps</td></tr>
      <tr><td>Step works</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>reward=-0.0599, done=False</td></tr>
      <tr><td>State property</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>episode_id present, step_count present</td></tr>
      <tr><td>Grader valid range</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>final_score=0.5573 in [0.0, 1.0]</td></tr>
      <tr><td>Invalid action handled</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>Gracefully defaulted to allocate</td></tr>
      <tr><td>Difficulty progression</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>easy=2d, medium=4d, hard=6d</td></tr>
      <tr><td>Grader deterministic</td><td><span class="badge badge-green">&#10003; Pass</span></td><td>Scoring logic is pure β€” no internal randomness</td></tr>
    </table>
    <div style="margin-top:0.75rem;padding:0.6rem 1rem;background:var(--green-dim);border:1px solid rgba(61,214,140,0.2);border-radius:6px;font-size:0.7rem;color:var(--text);">
      &#10003; <strong>10 / 10 checks passed.</strong> Click "Run Validation" above to re-run live against the deployed environment.
    </div>
  </div>
</div>


<!-- PHASE 2 β€” AGENTIC EVALUATION -->
<div id="tab-phase2" class="section">
  <div class="phase-header">
    <div class="phase-num p2">2</div>
    <div>
      <div class="phase-title">Agentic Evaluation</div>
      <div class="phase-sub">Greedy baseline (always D0) &middot; LLM+GRPO with episodic memory &middot; score variance check</div>
    </div>
  </div>
  <div class="phase-body stack">

    <!-- EVAL 1: Greedy Baseline -->
    <div>
      <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
        <div style="background:var(--amber-dim);border:1px solid rgba(240,165,0,0.25);border-radius:6px;padding:0.2rem 0.6rem;font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--amber);">Eval 1 of 3</div>
        <div style="font-size:0.82rem;font-weight:700;color:#fff;">Greedy Baseline Agent</div>
      </div>
      <div style="font-size:0.72rem;color:var(--muted);margin-bottom:1rem;line-height:1.7;">
        Always allocates to district 0 β€” zero intelligence, zero observation. Scores represent the floor any meaningful
        agent must exceed. Averaged over 5 independent runs via <code style="color:var(--text);">scripts/test_local.py</code>.
      </div>
      <div class="grid-3" style="margin-bottom:1rem;">
        <div class="card-sm" style="border-top:2px solid var(--green);">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--green);margin-bottom:0.5rem;">Easy</div>
          <div style="font-family:var(--serif);font-size:2rem;font-weight:700;color:#fff;line-height:1;">42.8%</div>
          <div style="font-size:0.68rem;color:var(--muted);margin-top:0.25rem;">&sigma;=0.072 &middot; breach 60%</div>
          <div style="margin-top:0.75rem;display:flex;flex-direction:column;gap:0.3rem;">
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Containment</span><span>50%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Hospital</span><span>61%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Efficiency</span><span style="color:var(--red);">0%</span></div>
          </div>
        </div>
        <div class="card-sm" style="border-top:2px solid var(--amber);">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--amber);margin-bottom:0.5rem;">Medium</div>
          <div style="font-family:var(--serif);font-size:2rem;font-weight:700;color:#fff;line-height:1;">39.6%</div>
          <div style="font-size:0.68rem;color:var(--muted);margin-top:0.25rem;">&sigma;=0.026 &middot; breach 100%</div>
          <div style="margin-top:0.75rem;display:flex;flex-direction:column;gap:0.3rem;">
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Containment</span><span>42%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Hospital</span><span>52%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Efficiency</span><span>9%</span></div>
          </div>
        </div>
        <div class="card-sm" style="border-top:2px solid var(--red);">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--red);margin-bottom:0.5rem;">Hard</div>
          <div style="font-family:var(--serif);font-size:2rem;font-weight:700;color:#fff;line-height:1;">35.3%</div>
          <div style="font-size:0.68rem;color:var(--muted);margin-top:0.25rem;">&sigma;=0.020 &middot; breach 100%</div>
          <div style="margin-top:0.75rem;display:flex;flex-direction:column;gap:0.3rem;">
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Containment</span><span>35%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Hospital</span><span>52%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Efficiency</span><span style="color:var(--red);">0%</span></div>
          </div>
        </div>
      </div>
      <div style="padding:0.65rem 1rem;background:var(--surface2);border-radius:6px;font-size:0.7rem;color:var(--muted);line-height:1.7;">
        0% efficiency on easy and hard confirms the agent never targets the correct district. 80&ndash;100% breach rate
        on medium and hard shows fixed-target allocation cannot prevent hospital collapse.
      </div>
    </div>

    <div style="border-top:1px solid var(--border);"></div>

    <!-- EVAL 2: LLM + GRPO -->
    <div>
      <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
        <div style="background:var(--blue-dim);border:1px solid rgba(74,158,255,0.25);border-radius:6px;padding:0.2rem 0.6rem;font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--blue);">Eval 2 of 3</div>
        <div style="font-size:0.82rem;font-weight:700;color:#fff;">LLM + GRPO Agent (Llama 3.1 8B Instant)</div>
      </div>
      <div style="font-size:0.72rem;color:var(--muted);margin-bottom:1rem;line-height:1.7;">
        Llama 3.1 8B Instant via Groq, with GRPO-style episodic memory across 2&ndash;3 rollouts per task.
        Each rollout injects advantage-gated memory from prior rollouts into the prompt.
        Reproduced by running <code style="color:var(--text);">python baseline/run.py</code>.
        Llama 3.1 8B Instant via Groq, with GRPO-style episodic memory across 2&ndash;3 rollouts per task.
        Total runtime: <strong style="color:var(--text);">~10 minutes</strong>.
      </div>
      <div class="grid-3" style="margin-bottom:1rem;">
        <div class="card-sm" style="border-top:2px solid var(--green);">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--green);margin-bottom:0.5rem;">Easy &middot; 2 rollouts</div>
          <div style="font-family:var(--serif);font-size:2rem;font-weight:700;color:#fff;line-height:1;">88.5%</div>
          <div style="font-size:0.68rem;color:var(--muted);margin-top:0.25rem;">Best of 2 &middot; no breach</div>
          <div style="margin-top:0.75rem;display:flex;flex-direction:column;gap:0.3rem;">
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Containment</span><span>100%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Hospital</span><span>100%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Efficiency</span><span>90%</span></div>
          </div>
        </div>
        <div class="card-sm" style="border-top:2px solid var(--amber);">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--amber);margin-bottom:0.5rem;">Medium &middot; 3 rollouts</div>
          <div style="font-family:var(--serif);font-size:2rem;font-weight:700;color:#fff;line-height:1;">78.0%</div>
          <div style="font-size:0.68rem;color:var(--muted);margin-top:0.25rem;">Best of 4 &middot; no breach</div>
          <div style="margin-top:0.75rem;display:flex;flex-direction:column;gap:0.3rem;">
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Containment</span><span>42%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Hospital</span><span>98%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Efficiency</span><span>100%</span></div>
          </div>
        </div>
        <div class="card-sm" style="border-top:2px solid var(--red);">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--red);margin-bottom:0.5rem;">Hard &middot; 3 rollouts</div>
          <div style="font-family:var(--serif);font-size:2rem;font-weight:700;color:#fff;line-height:1;">61.1%</div>
          <div style="font-size:0.68rem;color:var(--muted);margin-top:0.25rem;">Best of 4 &middot; no breach</div>
          <div style="margin-top:0.75rem;display:flex;flex-direction:column;gap:0.3rem;">
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Containment</span><span>51%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Hospital</span><span>97%</span></div>
            <div style="display:flex;justify-content:space-between;font-size:0.68rem;"><span style="color:var(--muted);">Efficiency</span><span>47%</span></div>
          </div>
        </div>
      </div>
      <div class="card-sm">
        <div class="card-title">GRPO Learning &mdash; Score Progression Across Rollouts</div>
        <table class="table">
          <tr><th>Task</th><th>Rollout 1</th><th>Rollout 2</th><th>Rollout 3</th><th>Best</th></tr>
          <tr>
            <td><span class="badge badge-green">Easy</span></td>
            <td>88.5%</td><td>83.2%</td><td>&mdash;</td>
            <td><strong style="color:var(--green);">88.5%</strong></td>
          </tr>
          <tr>
            <td><span class="badge badge-amber">Medium</span></td>
            <td>56.3%</td><td>78.0%</td><td>64.0%</td>
            <td><strong style="color:var(--amber);">78.0%</strong></td>
          </tr>
          <tr>
            <td><span class="badge badge-red">Hard</span></td>
            <td>61.1%</td><td>57.7%</td><td>60.3%</td>
            <td><strong style="color:var(--red);">61.1%</strong></td>
          </tr>
        </table>
      </div>
    </div>

    <div style="border-top:1px solid var(--border);"></div>

    <!-- EVAL 3: Variance Check -->
    <div>
      <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
        <div style="background:rgba(167,139,250,0.12);border:1px solid rgba(167,139,250,0.25);border-radius:6px;padding:0.2rem 0.6rem;font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--violet);">Eval 3 of 3</div>
        <div style="font-size:0.82rem;font-weight:700;color:#fff;">Score Variance Check</div>
      </div>
      <div style="font-size:0.72rem;color:var(--muted);margin-bottom:1rem;line-height:1.7;">
        Compares greedy (D0) against LLM+GRPO. A well-designed environment shows a large, consistent lift &mdash;
        confirming intelligent allocation is required and cannot be gamed by fixed-target strategies.
      </div>
      <div class="card-sm" style="margin-bottom:1rem;">
        <div class="card-title">Greedy (D0) vs LLM+GRPO &mdash; Score Comparison</div>
        <table class="table">
          <tr><th>Task</th><th>Greedy (D0)</th><th>LLM+GRPO</th><th>Lift (&Delta;)</th><th>Signal</th><th>Exploit Risk</th></tr>
          <tr>
            <td><span class="badge badge-green">Easy</span></td>
            <td style="color:var(--muted);">42.8%</td><td><strong>88.5%</strong></td>
            <td class="pos">+46pp</td>
            <td><span class="badge badge-green">Strong</span></td>
            <td><span class="badge badge-green">None &mdash; 0% eff, 60% breach</span></td>
          </tr>
          <tr>
            <td><span class="badge badge-amber">Medium</span></td>
            <td style="color:var(--muted);">39.6%</td><td><strong>78.0%</strong></td>
            <td class="pos">+38pp</td>
            <td><span class="badge badge-green">Strong</span></td>
            <td><span class="badge badge-green">None &mdash; 8% eff, 80% breach</span></td>
          </tr>
          <tr>
            <td><span class="badge badge-red">Hard</span></td>
            <td style="color:var(--muted);">35.3%</td><td><strong>61.1%</strong></td>
            <td class="pos">+26pp</td>
            <td><span class="badge badge-green">Strong</span></td>
            <td><span class="badge badge-green">None &mdash; 0% eff, 100% breach</span></td>
          </tr>
          <tr style="border-top:1px solid var(--border2);">
            <td><strong>Average</strong></td>
            <td style="color:var(--muted);">39.2%</td><td><strong>75.9%</strong></td>
            <td class="pos"><strong>+37pp</strong></td>
            <td><span class="badge badge-green">Strong</span></td>
            <td><span class="badge badge-green">No exploits found</span></td>
          </tr>
        </table>
      </div>
      <div class="grid-3" style="margin-bottom:1rem;">
        <div class="card-sm">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);margin-bottom:0.75rem;">Easy</div>
          <div style="display:flex;flex-direction:column;gap:0.45rem;">
            <div><div style="display:flex;justify-content:space-between;font-size:0.68rem;margin-bottom:0.2rem;"><span style="color:var(--muted);">Greedy (D0)</span><span>40%</span></div><div class="bar-track" style="height:8px;"><div class="bar-fill" style="width:40%;background:var(--dim);"></div></div></div>
            <div><div style="display:flex;justify-content:space-between;font-size:0.68rem;margin-bottom:0.2rem;"><span style="color:var(--green);">LLM+GRPO</span><span>89%</span></div><div class="bar-track" style="height:8px;"><div class="bar-fill" style="width:89%;background:var(--green);"></div></div></div>
            <div style="font-size:0.68rem;color:var(--muted);">Lift: <span style="color:var(--green);font-weight:700;">+46pp</span></div>
          </div>
        </div>
        <div class="card-sm">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);margin-bottom:0.75rem;">Medium</div>
          <div style="display:flex;flex-direction:column;gap:0.45rem;">
            <div><div style="display:flex;justify-content:space-between;font-size:0.68rem;margin-bottom:0.2rem;"><span style="color:var(--muted);">Greedy (D0)</span><span>40%</span></div><div class="bar-track" style="height:8px;"><div class="bar-fill" style="width:40%;background:var(--dim);"></div></div></div>
            <div><div style="display:flex;justify-content:space-between;font-size:0.68rem;margin-bottom:0.2rem;"><span style="color:var(--amber);">LLM+GRPO</span><span>75%</span></div><div class="bar-track" style="height:8px;"><div class="bar-fill" style="width:75%;background:var(--amber);"></div></div></div>
            <div style="font-size:0.68rem;color:var(--muted);">Lift: <span style="color:var(--amber);font-weight:700;">+38pp</span></div>
          </div>
        </div>
        <div class="card-sm">
          <div style="font-size:0.62rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);margin-bottom:0.75rem;">Hard</div>
          <div style="display:flex;flex-direction:column;gap:0.45rem;">
            <div><div style="display:flex;justify-content:space-between;font-size:0.68rem;margin-bottom:0.2rem;"><span style="color:var(--muted);">Greedy (D0)</span><span>35%</span></div><div class="bar-track" style="height:8px;"><div class="bar-fill" style="width:35%;background:var(--dim);"></div></div></div>
            <div><div style="display:flex;justify-content:space-between;font-size:0.68rem;margin-bottom:0.2rem;"><span style="color:var(--red);">LLM+GRPO</span><span>63%</span></div><div class="bar-track" style="height:8px;"><div class="bar-fill" style="width:63%;background:var(--red);"></div></div></div>
            <div style="font-size:0.68rem;color:var(--red);font-weight:700;">+26pp</span></div>
          </div>
        </div>
      </div>
      <div style="padding:0.85rem 1.1rem;background:var(--green-dim);border:1px solid rgba(61,214,140,0.2);border-radius:8px;font-size:0.72rem;color:var(--text);line-height:1.8;">
        &#10003; <strong>Variance check passed.</strong> Mean lift of <strong>+37pp</strong> across all tasks.
        Greedy (D0) scores 33&ndash;43% with 60&ndash;100% hospital breach rates &mdash; confirming no trivial exploit path.
        LLM+GRPO reaches 66&ndash;91% with zero breaches &mdash; genuine triage reasoning is required and rewarded.
      </div>
    </div>

  </div>
</div>

<!-- PHASE 3 β€” HUMAN REVIEW -->
<div id="tab-phase3" class="section">
  <div class="phase-header">
    <div class="phase-num p3">3</div>
    <div>
      <div class="phase-title">Human Review</div>
      <div class="phase-sub">Meta &amp; Hugging Face engineers assess real-world utility, creativity, and exploit robustness</div>
    </div>
  </div>
  <div class="phase-body stack">

    <div class="grid-2">
      <div class="card-sm">
        <div class="card-title">Real-World Utility</div>
        <div style="font-size:0.75rem;color:var(--muted);line-height:1.9;">
          <div style="margin-bottom:0.5rem;"><span style="color:var(--green);">β–Έ</span> <strong style="color:var(--text);">WHO-modelled epidemic response.</strong> Resource allocation, restriction policy, and hospital capacity constraints match real public health frameworks.</div>
          <div style="margin-bottom:0.5rem;"><span style="color:var(--green);">β–Έ</span> <strong style="color:var(--text);">Multi-domain transfer.</strong> Wildfire deployment, cyberattack isolation, and misinformation containment share identical mathematical structure β€” same trained policy generalises.</div>
          <div style="margin-bottom:0.5rem;"><span style="color:var(--green);">β–Έ</span> <strong style="color:var(--text);">3-day information lag</strong> on the hard task reflects real reporting delays in surveillance systems β€” not a toy mechanic.</div>
          <div><span style="color:var(--green);">β–Έ</span> <strong style="color:var(--text);">Hospital breach at 10% capacity</strong> matches ICU overflow thresholds where triage and diversion begin, not at zero.</div>
        </div>
      </div>
      <div class="card-sm">
        <div class="card-title">Novelty &amp; Creativity</div>
        <div style="font-size:0.75rem;color:var(--muted);line-height:1.9;">
          <div style="margin-bottom:0.5rem;"><span style="color:var(--violet);">β–Έ</span> <strong style="color:var(--text);">Cascade dynamics class.</strong> No existing OpenEnv benchmark covers the spreading-cascade / delayed-observation / resource-scarcity problem class.</div>
          <div style="margin-bottom:0.5rem;"><span style="color:var(--violet);">β–Έ</span> <strong style="color:var(--text);">Structural partial observability.</strong> The 3-day lag is enforced at the environment layer β€” the agent cannot test its way around it.</div>
          <div style="margin-bottom:0.5rem;"><span style="color:var(--violet);">β–Έ</span> <strong style="color:var(--text);">GRPO episodic memory baseline.</strong> Prompt-as-policy with advantage-gated memory update β€” no weight gradients.</div>
          <div><span style="color:var(--violet);">β–Έ</span> <strong style="color:var(--text);">Decaying containment bonus.</strong> Early action is exponentially more valuable, teaching proactive not reactive strategies.</div>
        </div>
      </div>
    </div>

    <div class="card-sm">
      <div class="card-title">Exploit Resistance</div>
      <table class="table">
        <tr><th>Potential Exploit</th><th>Prevention Mechanism</th><th>Status</th></tr>
        <tr>
          <td>Always restrict everything</td>
          <td>Penalty of βˆ’0.20 per restriction on districts below 0.20</td>
          <td><span class="badge badge-green">Blocked</span></td>
        </tr>
        <tr>
          <td>Always use "test" to game data</td>
          <td>Test costs 1 resource; real-time data already provided; no benefit</td>
          <td><span class="badge badge-green">Blocked</span></td>
        </tr>
        <tr>
          <td>Trivially containable (easy task too easy)</td>
          <td>D0-only allocation scores 43% avg; 60–100% hospital breach rate β€” no trivial path to high scores</td>
          <td><span class="badge badge-green">Addressed</span></td>
        </tr>
        <tr>
          <td>Game containment by ignoring hospitals</td>
          <td>Hospital breach (≀10% capacity) ends episode immediately; hospital score weighted 45%</td>
          <td><span class="badge badge-green">Blocked</span></td>
        </tr>
        <tr>
          <td>Memorise fixed seed values</td>
          <td>Spread rates randomised per episode; density weights randomised; lag history unpredictable</td>
          <td><span class="badge badge-green">Blocked</span></td>
        </tr>
        <tr>
          <td>Infinite restrictions accumulate</td>
          <td>Restrictions auto-lift when infection drops below safe threshold</td>
          <td><span class="badge badge-green">Addressed</span></td>
        </tr>
      </table>
    </div>

    <div class="card-sm">
      <div class="card-title">Epidemiological Model Calibration</div>
      <table class="table">
        <tr><th>Parameter</th><th>Value</th><th>Real-World Reference</th></tr>
        <tr><td>Spread rate</td><td>3–8% / day</td><td>Seasonal flu Rβ‚€ 1.2–1.4; daily transmission β‰ˆ 4–7%</td></tr>
        <tr><td>Natural recovery</td><td>1% / day</td><td>Mild respiratory illness: 7–14 day recovery β†’ ~1%/day</td></tr>
        <tr><td>Hospital breach threshold</td><td>≀10% capacity</td><td>WHO: ICU overflow typically triggers crisis protocols at &lt;15%</td></tr>
        <tr><td>Geographic spillover</td><td>1% to adjacent</td><td>District-level cross-border movement in urban corridors</td></tr>
        <tr><td>Data lag (hard task)</td><td>3 days</td><td>US CDC surveillance reporting lag: 2–5 days</td></tr>
        <tr><td>Treatment effect</td><td>βˆ’5% infection</td><td>Antiviral deployment impact on active case load</td></tr>
      </table>
    </div>

  </div>
</div>


<!-- ═══════════════════════════════════════════════
     GRADER
════════════════════════════════════════════════ -->
<div id="tab-grader" class="section">
  <div class="card" style="margin-bottom:1rem;">
    <div class="card-title">Deterministic Trajectory Scorer</div>
    <div style="font-size:0.75rem;color:var(--muted);line-height:1.8;max-width:700px;">
      The grader receives the full episode trajectory (hidden ground truth, not agent observations) and returns a score in
      <strong style="color:var(--text);">[0.0, 1.0]</strong>. No randomness. No LLM calls. Identical trajectories always produce identical scores.
    </div>
  </div>

  <div class="grid-2" style="margin-bottom:1rem;">
    <div class="card">
      <div class="card-title">Score Components</div>
      <div class="weight-row">
        <div class="w-name" style="color:var(--blue);">Hospital Score</div>
        <div class="w-pct" style="color:var(--blue);">45%</div>
        <div class="bar-track"><div class="bar-fill" style="width:45%;background:var(--blue);"></div></div>
        <div class="w-desc">Avg capacity preserved; Γ—0.6 if any breach</div>
      </div>
      <div class="weight-row">
        <div class="w-name" style="color:var(--green);">Containment Score</div>
        <div class="w-pct" style="color:var(--green);">30%</div>
        <div class="bar-track"><div class="bar-fill" style="width:30%;background:var(--green);"></div></div>
        <div class="w-desc">% district-days below 0.40 (skips first 2 steps)</div>
      </div>
      <div class="weight-row">
        <div class="w-name" style="color:var(--violet);">Efficiency Score</div>
        <div class="w-pct" style="color:var(--violet);">15%</div>
        <div class="bar-track"><div class="bar-fill" style="width:15%;background:var(--violet);"></div></div>
        <div class="w-desc">Resource actions targeting highest-infected district</div>
      </div>
      <div class="weight-row">
        <div class="w-name" style="color:var(--amber);">Speed Score</div>
        <div class="w-pct" style="color:var(--amber);">10%</div>
        <div class="bar-track"><div class="bar-fill" style="width:10%;background:var(--amber);"></div></div>
        <div class="w-desc">1 βˆ’ (steps / max_steps) if finished early; else 0</div>
      </div>
    </div>

    <div class="card">
      <div class="card-title">Design Decisions</div>
      <div style="font-size:0.75rem;color:var(--muted);line-height:2;">
        <div><span style="color:var(--blue);">β†’</span> <strong style="color:var(--text);">Hospital weighted highest</strong> β€” system collapse is catastrophic and irreversible.</div>
        <div><span style="color:var(--green);">β†’</span> <strong style="color:var(--text);">Grace period</strong> β€” first 2 steps excluded from containment; initial state outside agent control.</div>
        <div><span style="color:var(--violet);">β†’</span> <strong style="color:var(--text);">Pre-action efficiency</strong> β€” uses previous step's state so successful treatment isn't retroactively penalised.</div>
        <div><span style="color:var(--amber);">β†’</span> <strong style="color:var(--text);">Speed as tiebreaker</strong> β€” rewards decisive proactive containment over dragging to max steps.</div>
        <div><span style="color:var(--red);">β†’</span> <strong style="color:var(--text);">Breach multiplier Γ—0.6</strong> β€” any hospital collapse permanently degrades the hospital sub-score.</div>
      </div>
    </div>
  </div>

  <div class="card">
    <div class="card-title">Reward Function</div>
    <table class="table">
      <tr><th>Term</th><th>Value</th><th>Fires When</th><th>Design Rationale</th></tr>
      <tr><td>Infection penalty</td><td class="neg">βˆ’0.50 Γ— density</td><td>District infection &gt; 0.40</td><td>Dense districts penalised more; realistic triage</td></tr>
      <tr><td>Hospital breach</td><td class="neg">βˆ’1.00</td><td>Hospital capacity ≀ 10%</td><td>Collapse is catastrophic; heaviest penalty</td></tr>
      <tr><td>Early containment</td><td class="pos">+0.50 Γ— (1 βˆ’ t)</td><td>District infection &lt; 0.20</td><td>Decays over time; proactive action rewarded</td></tr>
      <tr><td>Correct prioritisation</td><td class="pos">+0.30</td><td>Allocate to highest-infected</td><td>Rewards triage intelligence at each step</td></tr>
      <tr><td>Unnecessary restriction</td><td class="neg">βˆ’0.20</td><td>Restrict district below 0.20</td><td>Penalises over-intervention on safe districts</td></tr>
    </table>
  </div>
</div>


<!-- ═══════════════════════════════════════════════
     ARCHITECTURE
════════════════════════════════════════════════ -->
<div id="tab-architecture" class="section">
  <div class="card" style="margin-bottom:1rem;">
    <div class="card-title">OpenEnv Interface</div>
    <div style="display:flex;gap:0.75rem;flex-wrap:wrap;margin-top:0.25rem;">
      <div style="background:var(--surface2);border-radius:8px;padding:0.6rem 1rem;font-size:0.74rem;">
        <span style="color:var(--muted);">env.</span><span style="color:var(--blue);">reset</span><span style="color:var(--muted);">(task_name)</span>
        <span style="color:var(--dim);margin:0 0.5rem;">β†’</span><span style="color:var(--green);">CityObservation</span>
      </div>
      <div style="background:var(--surface2);border-radius:8px;padding:0.6rem 1rem;font-size:0.74rem;">
        <span style="color:var(--muted);">env.</span><span style="color:var(--blue);">step</span><span style="color:var(--muted);">(action)</span>
        <span style="color:var(--dim);margin:0 0.5rem;">β†’</span><span style="color:var(--green);">CityObservation</span>
      </div>
      <div style="background:var(--surface2);border-radius:8px;padding:0.6rem 1rem;font-size:0.74rem;">
        <span style="color:var(--muted);">env.</span><span style="color:var(--blue);">state</span>
        <span style="color:var(--dim);margin:0 0.5rem;">β†’</span><span style="color:var(--green);">State</span>
      </div>
    </div>
  </div>

  <div class="grid-3" style="margin-bottom:1rem;">
    <div class="arch-card"><div class="arch-icon">πŸ“‹</div><div class="arch-name">models.py</div><div class="arch-desc">Typed data contracts. DistrictObservation, DistrictTruth, CityState, CityObservation, ContainmentAction. Pydantic + dataclasses.</div></div>
    <div class="arch-card"><div class="arch-icon">βš™οΈ</div><div class="arch-name">environment.py</div><div class="arch-desc">Core RL loop. Maintains OpenEnv State (tracking) and CityState (simulation). Agent only ever receives CityObservation.</div></div>
    <div class="arch-card"><div class="arch-icon">πŸ“Š</div><div class="arch-name">grader.py</div><div class="arch-desc">Deterministic trajectory scorer. Reads hidden CityState. Four components weighted into final_score ∈ [0.0, 1.0]. Zero LLM calls.</div></div>
    <div class="arch-card"><div class="arch-icon">🌊</div><div class="arch-name">utils.py</div><div class="arch-desc">SIR-inspired spread model with linear spillover (no wrap-around). Observation builder enforcing partial observability by task.</div></div>
    <div class="arch-card"><div class="arch-icon">🧠</div><div class="arch-name">core/trajectory.py</div><div class="arch-desc">EpisodicMemory. Stores (obs, action, reward) tuples. Retrieves top-k by L1 distance on infection profiles, phase-weighted.</div></div>
    <div class="arch-card"><div class="arch-icon">🎯</div><div class="arch-name">baseline/evaluator.py</div><div class="arch-desc">GRPO-style loop. Per-task rollouts: easy=2, medium=3, hard=3. Advantage = Rα΅’ βˆ’ mean(R). Memory threshold -0.3. Best score reported.</div></div>
  </div>

  <div class="card">
    <div class="card-title">HTTP Endpoints</div>
    <table class="table">
      <tr><th>Endpoint</th><th>Method</th><th>Description</th></tr>
      <tr><td style="color:var(--green);font-weight:600;">/</td><td>GET</td><td>This judge dashboard</td></tr>
      <tr><td style="color:var(--green);font-weight:600;">/health</td><td>GET</td><td>Health check β€” returns {"status":"ok"}</td></tr>
      <tr><td style="color:var(--green);font-weight:600;">/info</td><td>GET</td><td>Environment metadata, task config, grader weights</td></tr>
      <tr><td style="color:var(--green);font-weight:600;">/grade</td><td>GET</td><td>Grader scores for the last completed episode</td></tr>
      <tr><td style="color:var(--green);font-weight:600;">/validate</td><td>GET</td><td>Phase 1 automated spec compliance β€” all checks with pass/fail</td></tr>
      <tr><td style="color:var(--green);font-weight:600;">/demo/{task}</td><td>GET</td><td>Rule-based greedy episode + full step log + grader score</td></tr>
    </table>
  </div>
</div>

</div><!-- /content -->

<script>
/* ── TAB SWITCHING ── */
function tab(name, el) {
  document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
  document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
  document.getElementById('tab-' + name).classList.add('active');
  el.classList.add('active');
}

const pct = v => (v * 100).toFixed(1) + '%';
const fmt = v => typeof v === 'number' ? (v * 100).toFixed(1) + '%' : v;

/* ── PHASE 1: VALIDATION ── */
async function runValidation() {
  const btn = document.getElementById('btn-validate');
  const loader = document.getElementById('load-validate');
  const result = document.getElementById('validate-result');
  const ph = document.getElementById('validate-placeholder');

  btn.disabled = true;
  loader.style.display = 'flex';
  result.style.display = 'none';
  ph.style.display = 'none';

  try {
    const r = await fetch('/validate');
    const d = await r.json();

    const passCount = d.pass_count;
    const failCount = d.total - d.pass_count;

    document.getElementById('v-overall').textContent = d.overall;
    document.getElementById('v-overall').style.color = d.overall === 'PASS' ? 'var(--green)' : 'var(--red)';
    document.getElementById('v-pass').textContent = passCount;
    document.getElementById('v-pass').style.color = 'var(--green)';
    document.getElementById('v-fail').textContent = failCount;
    document.getElementById('v-fail').style.color = failCount > 0 ? 'var(--red)' : 'var(--muted)';
    document.getElementById('v-total').textContent = d.total;

    const container = document.getElementById('v-checks');
    container.innerHTML = Object.entries(d.checks).map(([key, val]) => {
      const label = key.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
      const cls = val.pass ? 'pass' : 'fail';
      const icon = val.pass ? 'βœ“' : 'βœ—';
      return `<div class="check-item">
        <div class="check-icon ${cls}">${icon}</div>
        <div class="check-name">${label}</div>
        <div class="check-detail">${val.detail || ''}</div>
      </div>`;
    }).join('');

    loader.style.display = 'none';
    result.style.display = 'block';
  } catch (e) {
    loader.style.display = 'none';
    ph.style.display = 'block';
    ph.textContent = 'Validation error: ' + e.message;
  } finally {
    btn.disabled = false;
  }
}
</script>
</body>
</html>"""

def main() -> None:
    import uvicorn
    uvicorn.run(
        "server.app:app",
        host  = "0.0.0.0",
        port  = int(os.getenv("PORT", "7860")),
        reload= False,
    )


if __name__ == "__main__":
    main()