File size: 135,990 Bytes
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505c280
 
 
 
 
 
 
 
 
 
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505c280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505c280
41b7d63
505c280
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505c280
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505c280
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505c280
41b7d63
 
 
 
 
 
 
 
 
 
505c280
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505c280
 
41b7d63
 
 
 
 
 
 
505c280
41b7d63
 
 
 
505c280
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3b8162
 
 
 
 
 
 
 
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42a25fd
 
 
 
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42a25fd
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42a25fd
 
 
 
 
41b7d63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
;
;
/* ============================================================================

   TUTORIAL — KEEL, ship liaison intelligence, Lance of Morning

   ----------------------------------------------------------------------------

   Two jobs live in this file:



     1. A first-drop tutorial, narrated in character rather than read off a

        card. Every step is gated on the SAME state the rest of the game

        already tracks — bldLive, unit arrays, ability cooldowns, kill counts

        — polled from a private setInterval. Nothing here edits the frame

        loop, and nothing here can desync from what actually happened: if the

        player builds a Factory before a Reactor, or wins a fight before they

        ever open the build menu, the step order just adapts around them

        instead of stalling on a script it expected.



     2. A small set of reactive barks that outlive the tutorial — first base

        attack, first loss, a hazard telegraph, a wave massing, the grid

        running dry. Each fires at most once per match. The mechanical coach

        in hud.js already tells you WHAT to fix; KEEL never repeats that,

        it only ever reacts to what it means.



   KEEL is written the way the dispatches are written: short, declarative,

   never a pep talk. It respects that the player can read a build menu; it

   just narrates the war while they do.



   File ownership: this module speaks to the rest of the game entirely

   through globals that already exist (toast, sfx, $, META, metaSave,

   bldLive, TYPES, BT, the unit arrays) and extends exactly one function it

   does not own — renderSettings() — using the same wrap-and-call-through

   pattern src/game/commander.js and src/game/meta.js already use on

   econTick() and heroXP(). Nothing here is imported; it is one more script

   in the shared boot scope, loaded after meta.js and before main.js.

   ============================================================================ */
(function(){

/* ---------------------------------------------------------------------------

   THE SCRIPT

   Every line KEEL says lives here, in one place, so the voice stays one

   voice. Step copy is fixed (a lesson should say the same thing every time);

   reactive lines get two or three variants because a player who finishes a

   dozen matches will hear the same trigger a dozen times.

   --------------------------------------------------------------------------- */
var KEEL_NAME='KEEL';
var KEEL_TAG='UGA SHIP LIAISON · LANCE OF MORNING';
var GUIDE_VERSION=5;
var TRAINING_COMPLETE_EVENT='massfront:protected-training-complete';
var TRAINING_SKIP_EVENT='massfront:protected-training-skipped';

var GREETING='KEEL online — UGA ship liaison, Lance of Morning. Follow the gold signal: it marks the exact control for your next action. '+
             'Tap SKIP any time; replay this guide from Settings.';

var SKIP_LINE='Understood. Find me again under Settings → Tutorial if you change your mind.';

var GRADUATION='Field orientation complete. Camera, economy, orders, logistics, formations, scouting, technology and extraction are certified. '+
               'Real operations are chosen at the WAR TABLE: system, planet, region, then a site. '+
               "I'll go quiet now — unless something's on fire.";
var BASIC_GRADUATION='Planetary basics complete. Camera, landing, Commander control, economy, production, army selection, orders and extraction are confirmed. '+
                     'Next: choose your faction and receive that faction\'s Commander 1.';

/* The recorded KEEL takes are keyed to the authored lines below. Keep those

   lines for voice, but show one compact action at a time on the battlefield.

   This also gives players with voice disabled the complete control sequence. */
var GREETING_CUE='Follow the gold marker to your next control. SKIP is always available.';
var GRADUATION_CUE='Orientation complete. Choose your next operation at the War Table.';
var BASIC_GRADUATION_CUE='Basics complete. Choose a faction to commission its first Commander.';

/* Each step tests REAL state. `say` retains the authored voice take while

   STEP_CUES below supplies the concise visible prompt. `done` fires once,

   the moment its test flips true — but only if

   it was the step on screen when that happened, so a player who does things

   out of order gets fast-forwarded in silence instead of congratulated for

   a step they finished five minutes ago. */
var STEPS=[
 { id:'camera', icon:'◇',
   say:'DRAG the battlefield to pan. PINCH to zoom and twist, or use VIEW → ROT / TILT. Move the camera once to confirm control.',
   done:'Command view responding. The minimap can jump across the whole battlefield.',
   test:function(){ return MATCH.sawCamera; } },

 { id:'deploy', icon:'⚓',
   say:'Tap open ground to fly the carrier there. When the landing signal turns green, tap DEPLOY BASE HERE.',
   done:"Hull's down, engines cold. That was the easy part.",
   test:function(){ return typeof matchLive!=='undefined' && !!matchLive; } },

  { id:'commander', icon:'★',
    say:'Your COMMANDER is the marked hero and a mobile builder. Tap the Commander bar at top-left to select and center it.',
   done:'Commander identified. Keep it alive: losing every Commander loses the match.',
   test:function(){ return typeof heroIdx==='number'&&heroIdx>=0&&!!usel[heroIdx]; } },

 { id:'pickup', icon:'◆',
   say:'A gold SUPPLY CACHE is marked nearby. Select the Commander or Constructor, then tap beside the cache so the unit collects it.',
   done:'Cache secured. Pickups can grant resources, repairs, scans, veterancy or rare strike codes.',
   test:function(){ return MATCH.sawPickup; } },

 /* Moved ahead of the economy steps. `power` tells the player to place a

    Reactor "inside the blue grid" — and the step that explains what the blue

    grid is used to sit three objectives further down. */
 { id:'territory', icon:'⌁', manual:'territoryAck', action:'GOT IT',
   say:'LOGISTICS: blue grid is legal build territory. Builders extend a mobile zone; Factories and Uplinks anchor it. Silos prevent resource overflow. Tap GOT IT.',
   done:'Logistics and territory rules logged.',
   test:function(){ return MATCH.territoryAck; } },

 { id:'mex', icon:'⛏',
   say:'Mass buys units and structures. Tap BUILD → ECONOMY → Extractor, drag its hologram onto a ◆ deposit, then tap ✓.',
   done:"Extractor placed. That's your first income line.",
   test:function(){ return hasPlayerBld('mex'); } },

 { id:'power', icon:'⚡',
   say:'Production also needs energy. Tap BUILD → ECONOMY → Reactor, place it on open ground inside the blue grid, then tap ✓.',
   done:'Reactor placed. Mass and energy must both stay positive while building.',
   test:function(){ return hasPlayerBld('pgen')||hasPlayerBld('geo'); } },

 { id:'fac', icon:'▣',
   say:"Build a Factory under BUILD → PRODUCTION. It is this army's barracks and vehicle plant, and expands construction territory.",
   done:'Factory placed. Open it to train the first field unit.',
   test:function(){ return hasPlayerBld('fac'); } },

 { id:'queue', icon:'▤',
   say:'Tap the Factory on the map, then tap the Striker unit card. The queue line shows what is being built. Each unit also costs population — the HUD counter reads pop n/500, shared by your combat faction.',
   done:'Striker queued. Production burns mass and energy over the build time. Pop n/500 is live faction headcount — adding commanders does not multiply the cap.',
   test:function(){ return MATCH.sawQueue; } },

 { id:'train', icon:'⚔',
   say:'Keep the Factory supplied until the Striker rolls out. Tap ARMY when it appears to select every field unit.',
   done:'Combat unit trained and selected. Unit cards show role, range, armor and ammunition.',
   test:function(){ return MATCH.sawCombatSel&&combatCount()>MATCH.startCombat; } },

 /* ORDERS ARE A GESTURE LANGUAGE AND NOTHING ON THE HUD SAYS SO.

    Ground tap, enemy tap and double-tap ground write three different unit

    states in orderMove()/orderAttack() (src/ui/input.js). Two of them a

    player discovers by accident within a minute; the double-tap retreat is

    the one nobody finds, and it is the one that saves an army — which is

    why this step gates on the retreat specifically rather than on any

    order at all. */
 { id:'orders', icon:'➤',
   say:'Orders are gestures. SINGLE-TAP GROUND is attack-move: units advance and fight what they meet. TAP AN ENEMY and they chase it. DOUBLE-TAP OPEN GROUND to RETREAT: they drop the target and reposition without stopping to fight. Break contact once now.',
   done:'Retreat confirmed — double-tap is how an army leaves a fight it is losing. Single-tap remains attack-move. Stop in the command row and Hold under ORDERS both plant units where they stand: they fire at whatever enters weapon range and never chase it out. Stop is the panic button, Hold is the same order given on purpose.',
   test:function(){ return MATCH.sawRetreat; } },

 /* THE ONE STEP THAT TEACHES THE GAME RATHER THAN THE BUTTONS.

    Every other objective here is "tap X to make Y happen", and the training

    match is deliberately combat-free — trainingSafety() parks the wave timers

    and floors resources — so a player can complete all of it without ever

    making a wrong-weapon decision. Meanwhile the counter card already renders

    the whole 0.45x-1.85x chip grid, splash, minimum range and shield-pierce

    prose, and it opens on a 520 ms long-press that nothing ever asks for.

    This step exists to make the player perform that gesture once. */
 { id:'intel', icon:'⬢',
   say:'Long-press any unit — yours or the enemy\'s — to open its counter card. Weapon class against armour class swings damage from 0.45x to 1.85x: KINETIC shreds LIGHT and dents HEAVY, GAUSS is the exact reverse.',
   done:'Counter card read. The wrong weapon against the wrong plate is a quarter of the damage you thought you had — and SONIC is the only thing in the roster that ignores Bulwark shields.',
   test:function(){ return shown(document.getElementById('unitCard')); } },

 { id:'turret', icon:'⛨',
   say:'Protect the economy before the first push. Tap BUILD → DEFENCE → Sentinel and place it on the likely approach.',
   done:'Basic perimeter online. Towers buy your army time; they do not replace it. Long-ranged guns — Bastions, Thumpers, Bombards — also have a MIN RANGE, and an enemy inside it is an enemy they cannot shoot.',
   test:function(){ return hasPlayerBld('turret'); } },

 { id:'platoon', icon:'Ⅳ',
   say:'With combat units selected, open PLATOONS and HOLD P1 to save them. Tap P1 later to recall; double-tap focuses the camera.',
   done:'Platoon P1 stored. Saved groups remember membership and formation.',
   test:function(){ return MATCH.sawPlatoon; } },

 { id:'formation', icon:'⌃',
   say:'Open ORDERS, tap the formation button, then drag the hologram on the map and release. Every marker is one unit slot.',
   done:'Formation placed. Spacing keeps mixed units readable and prevents stacking.',
   test:function(){ return MATCH.sawFormation; } },

 { id:'fog', icon:'⌾',
   say:'Black fog hides enemies; grey terrain is remembered, not currently visible. Move the marked SCOUT toward the signal to reveal the sector.',
   done:'Sensor contact expanded. Enemy positions remain hidden until a friendly sensor sees them.',
   test:function(){ return MATCH.sawScout; } },

 { id:'attack', icon:'➜',
   say:'Leave A-MOVE active and SINGLE-TAP unexplored ground. That is attack-move: units advance and engage contacts. DOUBLE-TAP is still retreat. Watch what actually does the killing: a unit with SPLASH on its card hits everything inside the radius, which beats raw damage against anything bunched up.',
   done:'Attack-move confirmed — single-tap ground fights on the way; double-tap ground breaks contact. Long-press a unit or structure for its full counter card.',
   test:function(){ return MATCH.sawAttackMove; } },

 { id:'tech', icon:'⌬',
   say:'Field rank 3 granted for training. Build BUILD → TECH → Research Complex; it unlocks studies and banks account-level ◆ Data after battle.',
   done:'Research Complex online. Its shield buffer protects studies from a sudden rush.',
   test:function(){ return hasPlayerBld('techlab'); } },

  { id:'ability', icon:'✦',
    say:'Select the Commander, then tap BLAST in the action bar above the selection panel. Abilities cost energy and have cooldowns.',
   done:'Special ability confirmed. Research and Commander levels unlock additional powers.',
   test:function(){ return MATCH.usedAbility; } },

 { id:'objective', icon:'◎', manual:'objectiveAck', action:'CALL EXTRACTION',
   say:'Normal operations end by their selected objective: destroy Commanders, hold territory, purge hives or survive. Training is secure—tap CALL EXTRACTION.',
   done:'Extraction acknowledged. Match resources, rewards and research are summarized after every operation.',
   test:function(){ return MATCH.objectiveAck; } },

 { id:'cloud', icon:'☁', manual:'cloudAck', action:'FINISH TRAINING',
   say:'Your career autosaves locally. After extraction, ACCOUNT can compare and sync a cloud save; PROFILE can export a portable .mfsave backup. Tap FINISH TRAINING.',
   done:'Career storage confirmed.',
   test:function(){ return MATCH.cloudAck; } }
];

/* Each objective is already a gated stage. The visible cue states only the

   action and the rule needed for that stage; the longer authored narration

   remains available through the existing voice bank without stale audio. */
var STEP_CUES={
  camera:'Drag to pan. Pinch to zoom or twist; VIEW has ROT and TILT.',
  deploy:'Tap clear ground for a green landing signal, then DEPLOY BASE HERE.',
  commander:'Tap the Commander bar to select and center your hero. Keep it alive.',
  pickup:'Select Commander or Constructor, then tap beside the marked ◆ cache.',
  territory:'Blue grid = build zone. Builders extend it; Factories and Uplinks anchor it. Silos prevent overflow. Tap GOT IT.',
  mex:'BUILD → ECONOMY → Extractor. Drag it onto a ◆ deposit; tap ✓.',
  power:'BUILD → ECONOMY → Reactor. Place inside the blue grid; tap ✓.',
  fac:'BUILD → PRODUCTION → Factory. It trains units and extends territory.',
  queue:'Tap Factory, then Striker. Watch the queue and population counter (max 500).',
  train:'Wait for Striker. Tap ARMY to select your field units.',
  orders:'Single-tap ground: attack-move. Tap an enemy: focus attack. Double-tap open ground: retreat. Try retreat now.',
  intel:'Long-press any unit for counters. Kinetic beats light; Gauss beats heavy. Damage can swing 0.45–1.85×.',
  turret:'BUILD → DEFENCE → Sentinel. Place it on the likely approach.',
  platoon:'Select units. Open PLATOONS; hold P1 to save. Tap P1 to recall, double-tap to center.',
  formation:'Open ORDERS, tap formation, then drag its hologram onto the map.',
  fog:'Black fog hides enemies; grey is remembered. Move the marked SCOUT to reveal the signal.',
  attack:'Select ARMY. Single-tap unexplored ground to attack-move; double-tap to retreat. SPLASH hits grouped enemies.',
  tech:'BUILD → TECH → Research Complex. It unlocks studies and earns ◆ Data after battle.',
  ability:'Select Commander, tap BLAST, then a target. Abilities cost energy and have cooldowns.',
  objective:'Operations vary: destroy, hold, purge, or survive. Training is secure—tap CALL EXTRACTION.',
  cloud:'Career autosaves locally. ACCOUNT syncs cloud saves; PROFILE exports .mfsave backups. Tap FINISH TRAINING.'
};
var STEP_ACKS={
  queue:'Striker queued. Check mass, energy, and the shared 500-unit cap.',
  orders:'Retreat confirmed. Stop and Hold keep units firing in place; neither chases.',
  intel:'Counter card read. Weapon class matters; SONIC bypasses Bulwark shields.',
  turret:'Perimeter online. Long-range guns also have a minimum range.',
  attack:'Attack-move confirmed. Tap ground to fight forward; double-tap to disengage.'
};

/* The first-career course is intentionally super-basic. The existing longer

   certification remains available from Operations/Settings, but a new player

   reaches faction commissioning after these ten essential actions instead of

   being held inside a twenty-one-objective systems tour. Reusing the authored

   step objects keeps every gate tied to real simulation state. */
var BASIC_STEP_IDS=['camera','deploy','commander','mex','power','fac','queue','train','orders','objective'];
var BASIC_STEPS=BASIC_STEP_IDS.map(function(id){
  for(var i=0;i<STEPS.length;i++)if(STEPS[i].id===id)return STEPS[i];
  throw new Error('Missing basic tutorial step '+id);
});

var BASE_ATTACK_LINES=[
 'Contact on the perimeter — tap the ⚠ alert, top of screen, and go see for yourself.',
 "Something's testing your walls. The ⚠ alert will take you straight there."
];
var UNIT_LOST_LINES=[
 "That one's not getting up. Filed under acceptable losses — keep the file short.",
 'Lost one. It happens to everyone who actually fights. Keep moving.'
];
var LOW_POWER_LINE="Grid's in the red. Whatever's queued behind it just stopped moving.";
var WAVE_LINE="Enemy's massing for a real push — you'll feel this one land. Get your army somewhere useful.";
var HAZARD_LINES={
 vanguard:'Dust front inbound — sight and speed go with it. Get exposed units under cover.',
 highland:"The slope's about to become the valley floor. Clear the marked ground.",
 isles:"Storm's earthing through whatever's biggest and made of metal. Spread out.",
 crater:"The relic's stirring. The basin's about to go dark — don't be standing in it.",
 _default:"Something's inbound that isn't the enemy. Read the warning; it isn't decoration."
};

/* ---------------------------------------------------------------------------

   STATE

   --------------------------------------------------------------------------- */
var TUT={ active:false, stepIdx:0, doneFlags:[], shownStepIdx:-1, graduated:false, forceNext:false,
          trainingMode:false, basicMode:false, course:'certification', finishTimer:0 };
var trainingPrev=null, trainingLaunched=false;
/* Set only while re-entering tutSkip from its own confirmation callback, so the

   dialog cannot ask twice. */
var tutSkipConfirmed=false;
/* Facts about THIS match, tracked whether or not the tutorial is running, so

   a mid-match "replay" (from Settings) can credit things the player already

   demonstrably knows how to do instead of waiting for them to happen again. */
var MATCH={ sawSel:false, sawCombatSel:false, sawMove:false, sawQueue:false, sawCamera:false,
            sawPickup:false, sawPlatoon:false, sawFormation:false, sawAttackMove:false,sawScout:false,
            sawRetreat:false,
            territoryAck:false,objectiveAck:false,cloudAck:false,usedAbility:false,
            startCombat:0,cameraBase:null,pickup:null,scoutIdx:-1,scoutStart:null,lastAbCool:[0,0,0,0] };
/* Reactive one-per-match latches. */
var REACT={ baseAttack:false, lowPower:false, hazard:false, unitLost:false, wave:false,
            lastAlarmT:0, lastStallE:false, lastHazWarn:false, lastWarned:false };
var prevDropping=false;

function tutMeta(){
  if(typeof META==='undefined'||!META) return {done:false,skipped:false,version:0};
  META.tutorial=META.tutorial||{done:false,skipped:false,version:0};
  /* `progress` is additive save metadata: old profiles load exactly as before,

     while the briefing can distinguish new, interrupted and completed runs.

     It is a high-water mark only; the live-state gates below remain the

     authority whenever a mission is running. */
  if(typeof META.tutorial.progress!=='number') META.tutorial.progress=META.tutorial.done?STEPS.length:0;
  META.tutorial.progress=Math.max(0,Math.min(STEPS.length,META.tutorial.progress|0));
  if(typeof META.tutorial.basicProgress!=='number') META.tutorial.basicProgress=META.tutorial.done?BASIC_STEPS.length:0;
  META.tutorial.basicProgress=Math.max(0,Math.min(BASIC_STEPS.length,META.tutorial.basicProgress|0));
  if(META.tutorial.done)META.tutorial.basicDone=true;
  return META.tutorial;
}
function courseSteps(){return TUT.basicMode?BASIC_STEPS:STEPS;}
function courseName(){return TUT.basicMode?'basic':'certification';}
function newCareerTrainingRequested(){
  try{
    var gate=window.MFNewCareerFactionGate;
    var state=gate&&typeof gate.state==='function'?gate.state():null;
    return !!(state&&state.pending&&state.phase==='training');
  }catch(e){return false;}
}
function trainingSignal(type,detail){
  if(typeof window==='undefined'||typeof window.dispatchEvent!=='function')return false;
  try{window.dispatchEvent(new CustomEvent(type,{detail:detail}));return true;}catch(e){return false;}
}
function pick(arr){ return arr[(Math.random()*arr.length)|0]; }
function hasPlayerBld(type){
  if(typeof bldLive==='undefined') return false;
  for(var i=0;i<bldLive.length;i++){
    var B=bldLive[i];
    /* The lesson is about making a valid placement. Waiting through the full

       construction timer left the highlight pointing back at BUILD after the

       player had already done the requested action, which looked like a loop. */
    if(B.alive!==false && B.team===0 && B.type===type) return true;
  }
  return false;
}
function playerBld(type){
  if(typeof bldLive==='undefined') return null;
  for(var i=0;i<bldLive.length;i++) if(bldLive[i].alive!==false&&bldLive[i].team===0&&bldLive[i].type===type) return bldLive[i];
  return null;
}
function isCombatUnit(i){
  return typeof ualive!=='undefined'&&ualive[i]&&uteam[i]===0&&i!==heroIdx&&utype[i]!==19;
}
function hasCombatUnit(){
  if(typeof unitHigh!=='number') return false;
  for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)) return true;
  return false;
}
function combatCount(){
  if(typeof unitHigh!=='number') return 0;
  var n=0;for(var i=0;i<unitHigh;i++)if(isCombatUnit(i))n++;return n;
}
function hasSelectedCombatUnit(){
  if(typeof unitHigh!=='number') return false;
  for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)&&usel[i]) return true;
  return false;
}
function sawQueuedUnit(){
  if(typeof bldLive==='undefined') return false;
  /* A queued item stays at B.queue[0] for its full build time (>=1.1s for the

     cheapest unit — see TYPES[0].bt) and is only shifted off on completion,

     so a non-empty queue is never a one-frame flicker: it is guaranteed to be

     visible across many polls of this timer, with no need for a population

     backstop that would have to guess at a baseline. */
  for(var i=0;i<bldLive.length;i++){
    var B=bldLive[i];
    if(B.team===0 && B.queue && B.queue.length>0 &&
       (B.type==='fac'||B.type==='tgate'||B.type==='harbor'||B.type==='airfield')) return true;
  }
  return false;
}

/* ---------------------------------------------------------------------------

   THE BUBBLE — one element, two moods. "Speaking" (a fresh line just landed,

   full emphasis, brief pulse) settles into "resting" (the current step's

   objective, held at lower emphasis) once its hold time elapses. Reactive

   barks are not tied to a step: they interrupt, hold briefly, then hand back

   to whatever the resting text was (or hide, if the tutorial isn't running).

   --------------------------------------------------------------------------- */
var queue=[], holdUntil=0, restingText='', restingTag='', restingId='', lastText=null, domReady=false;
var keelMapSpeakingUntil=0;
var focusEls=[], mapCue=null;

function syncTrainingChrome(){
  var skipBtn=document.getElementById('keelSkip');
  if(skipBtn){
    skipBtn.classList.toggle('keelSkipEnd',!!TUT.trainingMode);
    skipBtn.textContent=TUT.trainingMode?(TUT.basicMode?'SKIP TRAINING':'END'):'SKIP';
    skipBtn.setAttribute('aria-label',TUT.trainingMode?(TUT.basicMode?'Skip training and continue to faction commissioning':'End training operation'):'Skip tutorial guidance');
  }
  var mode=document.getElementById('keelModeBadge');
  if(mode){var b=mode.querySelector('b');if(b)b.textContent=TUT.basicMode?'PLANETARY BASICS':'TRAINING OPERATION';}
}
function buildDOM(){
  if(domReady){syncTrainingChrome();return;}
  domReady=true;
  var wrap=document.createElement('div');
  wrap.id='keelWrap';
  wrap.setAttribute('aria-live','polite');
  wrap.innerHTML=
    '<div id="keelBar">'+
      '<div id="keelBadge" aria-hidden="true"><span class="kr kr1"></span><span class="kr kr2"></span><span class="kd"></span></div>'+
      '<div id="keelBody">'+
        '<div id="keelHd"><span id="keelName" title="'+KEEL_TAG+'">'+KEEL_NAME+'</span><span id="keelStepTag"></span></div>'+
        '<div id="keelTxt"></div>'+
        '<div id="keelProgress" role="progressbar" aria-label="Training objective progress" aria-valuemin="0"><i></i></div>'+
        '<button id="keelNext" type="button">GOT IT</button>'+
      '</div>'+
      '<button id="keelSkip" type="button" aria-label="Skip tutorial">SKIP</button>'+
    '</div>';
  document.body.appendChild(wrap);
  var cue=document.createElement('div');
  cue.id='keelMapCue'; cue.setAttribute('aria-hidden','true'); cue.innerHTML='<span></span>';
  document.body.appendChild(cue);
  var mode=document.createElement('div');
  mode.id='keelModeBadge'; mode.setAttribute('aria-live','polite');
  mode.innerHTML='<b>TRAINING OPERATION</b><span>UGA KEEL GUIDANCE ACTIVE</span>';
  document.body.appendChild(mode);
  var skipBtn=document.getElementById('keelSkip');
  skipBtn.addEventListener('pointerdown',function(ev){ ev.stopPropagation(); tutSkip(); });
  /* A control that abandons a match should not be styled as a hint. In a

     training match the label becomes END and the button takes the warning

     treatment, so the destructive one is never the quiet one. */
  syncTrainingChrome();
  var nextBtn=document.getElementById('keelNext');
  nextBtn.addEventListener('pointerdown',function(ev){
    ev.preventDefault(); ev.stopPropagation();
    var active=courseSteps(),S=TUT.active&&active[TUT.stepIdx];
    if(!S||!S.manual) return;
    MATCH[S.manual]=true;
    if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
  });
  /* Tapping the body re-shows the full-strength card if it has settled into

     its dim resting state — a quiet "say that again" without extra chrome. */
  document.getElementById('keelBar').addEventListener('pointerdown',function(ev){
    if(ev.target===skipBtn||ev.target===nextBtn) return;
    if(restingText) speak(restingText,4.2,'recall',restingId);
  });
  mapCue=cue;
}
function renderBubble(text,tag){
  if(!domReady) return;
  if(text!==lastText){ var t=document.getElementById('keelTxt'); if(t) t.textContent=text; lastText=text; }
  var tg=document.getElementById('keelStepTag'); if(tg) tg.textContent=tag||'';
  var pg=document.getElementById('keelProgress');
  if(pg){
    var active=courseSteps(),at=Math.min(active.length,TUT.stepIdx),pct=active.length?at/active.length*100:0;
    pg.setAttribute('aria-valuemax',active.length);pg.setAttribute('aria-valuenow',at);
    var fill=pg.querySelector('i');if(fill)fill.style.width=pct+'%';
  }
}
function showWrap(fresh){
  if(!domReady) return;
  var w=document.getElementById('keelWrap');
  if(w) w.classList.add('show');
  var bar=document.getElementById('keelBar');
  if(bar){
    bar.classList.toggle('resting',!fresh);
    if(fresh){ bar.classList.remove('pulse'); void bar.offsetWidth; bar.classList.add('pulse'); }
  }
}
function hideWrap(){
  if(!domReady) return;
  var w=document.getElementById('keelWrap');
  if(w) w.classList.remove('show');
}

/* ---------------------------------------------------------------------------

   CONTEXT SIGNAL — the copy names a control and this signal marks that exact

   control. It is recalculated every poll because BUILD changes from button →

   tab → card → world placement without changing tutorial steps. The signal is

   purely visual (pointer-events:none), so it can never become an input shield.

   --------------------------------------------------------------------------- */
function shown(el){
  if(!el||!el.isConnected) return false;
  var s=getComputedStyle(el),r=el.getBoundingClientRect();
  return s.display!=='none'&&s.visibility!=='hidden'&&r.width>1&&r.height>1;
}
function byText(rootId,selector,text){
  var root=document.getElementById(rootId); if(!root) return null;
  var list=root.querySelectorAll(selector),want=String(text).toUpperCase();
  for(var i=0;i<list.length;i++) if((list[i].textContent||'').toUpperCase().indexOf(want)>=0) return list[i];
  return null;
}
function clearFocus(){
  for(var i=0;i<focusEls.length;i++){
    focusEls[i].classList.remove('keelFocus');
    if(focusEls[i].getAttribute('aria-describedby')==='keelTxt') focusEls[i].removeAttribute('aria-describedby');
  }
  focusEls.length=0;
  if(mapCue){ mapCue.classList.remove('show'); mapCue.style.removeProperty('left'); mapCue.style.removeProperty('top'); }
}
function focusEl(el){
  if(!shown(el)||focusEls.indexOf(el)>=0) return;
  el.classList.add('keelFocus'); el.setAttribute('aria-describedby','keelTxt'); focusEls.push(el);
}
function cueAt(x,y,label){
  if(!mapCue) return;
  var pad=62,bot=typeof VH==='number'?Math.max(210,VH*.25):220;
  x=Math.max(pad,Math.min((typeof VW==='number'?VW:innerWidth)-pad,x));
  y=Math.max(170,Math.min((typeof VH==='number'?VH:innerHeight)-bot,y));
  mapCue.style.left=x+'px'; mapCue.style.top=y+'px';
  var t=mapCue.querySelector('span'); if(t) t.textContent=label||'TAP MAP';
  mapCue.classList.add('show');
}
function cueWorld(x,y,label){
  try{ var p=w2s(x,y); cueAt(p[0],p[1],label); }
  catch(e){ cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.5,label); }
}
function buildFocus(cat,name,type){
  if(typeof placing!=='undefined'&&placing&&placing.type===type){
    var valid=false;
    try{ valid=!!placementValid(); }catch(e){}
    if(valid) focusEl(document.getElementById('placeOk'));
    if(type==='mex'&&!valid&&typeof deposits!=='undefined'){
      var best=null,bd=Infinity;
      for(var i=0;i<deposits.length;i++) if(!deposits[i].taken){
        var d=placing?Math.hypot(deposits[i].x-placing.x,deposits[i].y-placing.y):i;
        if(d<bd){bd=d;best=deposits[i];}
      }
      if(best) cueWorld(best.x,best.y,'◆ DEPOSIT');
    } else cueWorld(placing.x,placing.y,valid?'TAP ✓':'BLUE GRID');
    return;
  }
  var bm=document.getElementById('buildMenu');
  if(shown(bm)){
    var tab=byText('buildTabs','.tabBtn',cat);
    var card=byText('buildGrid','.bcard',name);
    if(card) focusEl(card); else if(tab) focusEl(tab);
    return;
  }
  focusEl(document.getElementById('buildBtn'));
}
function syncFocus(){
  clearFocus();
  var active=courseSteps();
  var badge=document.getElementById('keelModeBadge');
  /* The KEEL card already carries STEP / TOTAL and the current objective.

     Repeating that information in a second fixed badge consumed the same

     narrow phone lane as the goal bar and rank row, creating the exact stack

     the tutorial is meant to teach through. Keep the legacy node for old OTA

     shells, but it is no longer a visible surface. */
  if(badge) badge.classList.remove('show');
  if(!TUT.active||TUT.stepIdx>=active.length) return;
  var S=active[TUT.stepIdx],next=document.getElementById('keelNext');
  if(badge){
    var bs=badge.querySelector('span'); if(bs) bs.textContent='OBJECTIVE '+(TUT.stepIdx+1)+' / '+active.length+' · '+S.id.toUpperCase();
  }
  if(next){
    next.style.display=S.manual?'inline-flex':'none';next.textContent=S.action||'GOT IT';
    next.setAttribute('aria-label',(S.action||'Confirm')+' — '+S.id+' lesson');
  }
  if(S.id==='camera'){
    focusEl(document.querySelector('.hudDeckBtn[data-deck="view"]'));
    focusEl(document.getElementById('rotL'));focusEl(document.getElementById('zoomIn'));focusEl(document.getElementById('tiltBtn'));focusEl(document.getElementById('zoomOut'));focusEl(document.getElementById('rotR'));
    cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.52,'DRAG / PINCH');
  } else if(S.id==='deploy'){
    var dep=document.getElementById('deployBtn');
    if(shown(dep)) focusEl(dep); else cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.52,'TAP LANDING SITE');
  } else if(S.id==='commander'){
    focusEl(document.getElementById('heroBar'));
  } else if(S.id==='pickup'){
    if(MATCH.pickup&&crates.indexOf(MATCH.pickup)>=0)cueWorld(MATCH.pickup.x,MATCH.pickup.y,'◆ COLLECT CACHE');
    focusEl(document.getElementById('heroBar'));
  } else if(S.id==='mex') buildFocus('ECONOMY','Extractor','mex');
  else if(S.id==='power') buildFocus('ECONOMY','Reactor','pgen');
  else if(S.id==='fac') buildFocus('PRODUCTION','Factory','fac');
  else if(S.id==='territory'){
    focusEl(document.getElementById('buildBtn'));
    var H=playerBld('hq');
    if(H) cueWorld(H.x,H.y,'BLUE BUILD GRID');
  } else if(S.id==='queue'){
    focusEl(document.getElementById('unitRes'));
    var pm=document.getElementById('prodMenu');
    if(shown(pm)) focusEl(byText('prodGrid','.bcard','Striker')||pm.querySelector('.bcard'));
    else { var F=playerBld('fac'); if(F) cueWorld(F.x,F.y,'TAP FACTORY'); }
  } else if(S.id==='train'){
    if(hasCombatUnit()) focusEl(document.getElementById('armyBtn'));
    else { var F2=playerBld('fac'); if(F2) cueWorld(F2.x,F2.y,'UNIT BUILDING'); }
  } else if(S.id==='orders'){
    /* No button teaches this one — the cue has to sit on the ground the

       player is being asked to double-tap, offset clear of the units so the

       marker is not sitting under the very gesture it is asking for. */
    var ox=0,oy=0,on=0;
    if(typeof unitHigh==='number') for(var q=0;q<unitHigh;q++) if(isCombatUnit(q)&&usel[q]){ox+=ux[q];oy+=uy[q];on++;}
    if(on) cueWorld(ox/on-170,oy/on+130,'DOUBLE-TAP = RETREAT');
    else focusEl(document.getElementById('armyBtn'));
  } else if(S.id==='turret') buildFocus('DEFENCE','Sentinel','turret');
  else if(S.id==='platoon'){
    if(shown(document.getElementById('grpRow')))focusEl(document.getElementById('grpBtn1'));
    else focusEl(document.querySelector('.hudDeckBtn[data-deck="platoons"]'));
  } else if(S.id==='formation'){
    if(shown(document.getElementById('tacRow'))){
      focusEl(document.getElementById('formBtn'));
      if(orderPreview)cueWorld(orderPreview.x,orderPreview.y,'DRAG FORMATION');
    } else focusEl(document.querySelector('.hudDeckBtn[data-deck="orders"]'));
  } else if(S.id==='attack'){
    if(shown(document.getElementById('tacRow')))focusEl(document.getElementById('moveBtn'));
    else focusEl(document.querySelector('.hudDeckBtn[data-deck="orders"]'));
    var sx=0,sy=0,n=0;
    if(typeof unitHigh==='number') for(var j=0;j<unitHigh;j++) if(isCombatUnit(j)&&usel[j]){sx+=ux[j];sy+=uy[j];n++;}
    if(n) cueWorld(sx/n+150,sy/n-110,'SINGLE-TAP = A-MOVE');
    else focusEl(document.getElementById('armyBtn'));
  } else if(S.id==='fog'){
    focusEl(document.getElementById('minimapWrap'));
    if(MATCH.scoutIdx>=0&&ualive[MATCH.scoutIdx])cueWorld(ux[MATCH.scoutIdx]+190,uy[MATCH.scoutIdx]-150,'SCOUT THIS SECTOR');
  } else if(S.id==='tech') buildFocus('TECH','Research Complex','techlab');
else if(S.id==='ability'){
        focusEl(document.getElementById('heroBar'));
        if(typeof heroIdx==='number'&&heroIdx>=0&&ualive[heroIdx])cueWorld(ux[heroIdx]+105,uy[heroIdx]-70,'BLAST TARGET');
  } else if(S.id==='objective'||S.id==='cloud'){
    focusEl(next);
  }
}
/* Capped so a burst of state changes (several buildings finishing within the

   same poll, or a mid-match replay that silently back-fills a lot of

   already-true steps) can never queue up a minute of stale dialogue. Once

   the backlog is deep, older lines are dropped in favour of newer ones — the

   step the player is looking at right now matters more than a "done" bark

   for something they finished three actions ago. */
var MAX_QUEUE=3;
/* `id` is the AUTHORED voice key for this line (see keenLineId). It travels with

   the line through the queue instead of being reconstructed from the copy on the

   far side — reconstructing it is what let the gate and the player disagree. */
function speak(text,holdSec,kind,id,display){
  if(!text) return;
  queue.push({text:text,display:display||text,hold:holdSec||4.4,kind:kind||'bark',id:id||null});
  while(queue.length>MAX_QUEUE) queue.shift();
}
function keelBattleReceiverAvailable(){
  if(typeof document==='undefined'||!document.getElementById('cmdrTx')||!document.getElementById('minimapWrap')) return false;
  if(typeof running!=='undefined'&&!running) return false;
  if(typeof matchLive!=='undefined'&&!matchLive) return false;
  if(typeof gameEnded!=='undefined'&&gameEnded) return false;
  var b=document.body;
  return !(b&&b.classList&&(b.classList.contains('mfMenuOpen')||b.classList.contains('menuMode')));
}
/* KEEL owns the same in-match receiver as commanders, but never their faction

   identity. The cancelable event lets the HUD accept the line only when the

   minimap bay is idle; a busy receiver is retried instead of spawning a second

   talking portrait elsewhere on the battlefield. */
function keelPresentAtBattleMap(message,durationMs){
  if(!keelBattleReceiverAvailable()||typeof window==='undefined'||typeof window.dispatchEvent!=='function') return 0;
  var detail={schema:'massfront.keel-hint.v1',hintId:'tutorial-'+(message.id||message.kind||'guidance'),
    context:'protected-training',surface:'battle-minimap',speaker:'KEEL',speakerId:'keel',affiliation:'uga',
    speakerRole:'UGA SHIP LIAISON',channel:'UGA TACTICAL LINK',voiceId:'keen',
    profileId:'uga-keel-expedition-guide',animationId:'keel-tactical-link',text:message.display||message.text,
    durationMs:durationMs,priority:message.kind==='step'?85:70,issuedAt:Date.now(),handled:false};
  try{
    var ev;
    if(typeof CustomEvent==='function') ev=new CustomEvent('massfront:keel-hint',{detail:detail,cancelable:true});
    else if(document.createEvent){ev=document.createEvent('CustomEvent');ev.initCustomEvent('massfront:keel-hint',false,true,detail);}
    if(!ev) return 0;
    window.dispatchEvent(ev);
    return detail.handled?1:2;
  }catch(e){return 0;}
}
function pump(){
  var now=performance.now()/1000;
  if(now<holdUntil) return;
  if(queue.length){
    var m=queue.shift();
    /* Hold time compresses while a backlog remains, so a burst of catch-up

       lines still reads in order without pinning the HUD for the better

       part of a minute. */
    var hold=queue.length?Math.min(m.hold,2.4):m.hold;
    var mapResult=keelPresentAtBattleMap(m,Math.max(1000,Math.round(hold*1000)));
    if(mapResult===2){queue.unshift(m);holdUntil=now+.18;hideWrap();return;}
    holdUntil=now+hold;
    if(mapResult===1){keelMapSpeakingUntil=holdUntil;hideWrap();}
    else{renderBubble(m.display,restingTag);showWrap(true);}
    /* Pass the line's identity through, so a rendered take can be found for it

       instead of every line silently falling through to synthesis. */
    speakVoice(m.text,'keen',keenLineId(m.kind,m.id));
    return;
  }
  if(TUT.active && restingText){
    renderBubble(restingText,restingTag);
    showWrap(false);
  } else hideWrap();
}

/* ---------------------------------------------------------------------------

   VOICE — opt-in, best-effort, silent when unsupported. Never awaited, never

   on the hot path: a browser with no speechSynthesis (or one that throws)

   behaves exactly like voice is off.

   --------------------------------------------------------------------------- */
/* KEEN WAS MUTE FOR THREE REASONS AT ONCE.

   1. This gate required `'speechSynthesis' in window` — the very dependency the

      voice rework existed to remove. A device with a downloaded voice pack and

      no TTS engine (ordinary on Android WebView) reported voice as unavailable

      and never even looked at the pack.

   2. pump() called speakVoice(text) with no action, so the `voPlay` branch was

      unreachable and every line fell through to speech synthesis regardless.

   3. On the platforms that DO expose speechSynthesis, an Android WebView with

      no installed voice makes speak() a silent no-op, so the fallback failed

      quietly too — which is why this reads as "the voice doesn't work" rather

      than as an error.

   Voice is now on when EITHER path can carry it. */
function voiceOn(){
  try{
    if(!(typeof META!=='undefined' && META.settings && META.settings.tutorialVoice)) return false;
    /* voHas asks the BANK MANIFEST; voReady asks whether the buffer is decoded

       yet. Gating on voReady alone made the answer "no voice available" for the

       whole of a cold start, because nothing is decoded until something asks for

       it — and this gate is the thing that asks. */
    if(typeof voHas==='function' && voHas('keen','greeting')) return true;
    if(typeof voReady==='function' && voReady('keen','greeting')) return true;
    return 'speechSynthesis' in window;
  }catch(e){ return false; }
}
/* A stable id per scripted line, so a rendered take can be looked up for it.

   KEEN's script is FIXED — every step's say/done, the greeting, the graduation

   and the reactive barks are constants — which is precisely what makes them

   renderable offline the same way the command acks already are.



   THE ID IS AUTHORED AT THE CALL SITE. It must match an action key under

   `lines.keen` in data:application/json;base64,ewogIl9ub3RlIjogIkdFTkVSQVRFRCBieSB0b29scy9tYWtlLXZvaWNlcy5weSDigJQgZG8gbm90IGhhbmQtZWRpdC4gUmUtcnVuIHRoZSB0b29sOyBzZWUgZG9jcy9WT0lDRS1QSVBFTElORS5tZC4iLAogImdlbmVyYXRlZCI6ICIyMDI2LTA4LTA5VDAzOjA4OjEzWiIsCiAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogInZvaWNlcyI6IHsKICAiYXNjZW5kYW5jeSI6ICJibV9nZW9yZ2UiLAogICJob3JkZSI6ICJibV9mYWJsZSIsCiAgImtlZW4iOiAiYWZfaGVhcnQiLAogICJub3ZhIjogImFtX21pY2hhZWwiLAogICJzeW5kaWNhdGUiOiAiYWZfYmVsbGEiCiB9LAogImxpbmVzIjogewogICJhc2NlbmRhbmN5IjogewogICAiYWJpbGl0eSI6IFsKICAgICJhc2NlbmRhbmN5X2FiaWxpdHlfMCIsCiAgICAiYXNjZW5kYW5jeV9hYmlsaXR5XzEiLAogICAgImFzY2VuZGFuY3lfYWJpbGl0eV8yIgogICBdLAogICAiYXR0YWNrIjogWwogICAgImFzY2VuZGFuY3lfYXR0YWNrXzAiLAogICAgImFzY2VuZGFuY3lfYXR0YWNrXzEiLAogICAgImFzY2VuZGFuY3lfYXR0YWNrXzIiCiAgIF0sCiAgICJidWlsZCI6IFsKICAgICJhc2NlbmRhbmN5X2J1aWxkXzAiLAogICAgImFzY2VuZGFuY3lfYnVpbGRfMSIsCiAgICAiYXNjZW5kYW5jeV9idWlsZF8yIgogICBdLAogICAiZGVwbG95IjogWwogICAgImFzY2VuZGFuY3lfZGVwbG95XzAiLAogICAgImFzY2VuZGFuY3lfZGVwbG95XzEiLAogICAgImFzY2VuZGFuY3lfZGVwbG95XzIiCiAgIF0sCiAgICJob2xkIjogWwogICAgImFzY2VuZGFuY3lfaG9sZF8wIiwKICAgICJhc2NlbmRhbmN5X2hvbGRfMSIsCiAgICAiYXNjZW5kYW5jeV9ob2xkXzIiCiAgIF0sCiAgICJtb3ZlIjogWwogICAgImFzY2VuZGFuY3lfbW92ZV8wIiwKICAgICJhc2NlbmRhbmN5X21vdmVfMSIsCiAgICAiYXNjZW5kYW5jeV9tb3ZlXzIiCiAgIF0sCiAgICJwYXRyb2wiOiBbCiAgICAiYXNjZW5kYW5jeV9wYXRyb2xfMCIsCiAgICAiYXNjZW5kYW5jeV9wYXRyb2xfMSIsCiAgICAiYXNjZW5kYW5jeV9wYXRyb2xfMiIKICAgXSwKICAgInNlbGVjdCI6IFsKICAgICJhc2NlbmRhbmN5X3NlbGVjdF8wIiwKICAgICJhc2NlbmRhbmN5X3NlbGVjdF8xIiwKICAgICJhc2NlbmRhbmN5X3NlbGVjdF8yIgogICBdLAogICAic3RvcCI6IFsKICAgICJhc2NlbmRhbmN5X3N0b3BfMCIsCiAgICAiYXNjZW5kYW5jeV9zdG9wXzEiLAogICAgImFzY2VuZGFuY3lfc3RvcF8yIgogICBdCiAgfSwKICAiaG9yZGUiOiB7CiAgICJhYmlsaXR5IjogWwogICAgImhvcmRlX2FiaWxpdHlfMCIsCiAgICAiaG9yZGVfYWJpbGl0eV8xIiwKICAgICJob3JkZV9hYmlsaXR5XzIiCiAgIF0sCiAgICJhdHRhY2siOiBbCiAgICAiaG9yZGVfYXR0YWNrXzAiLAogICAgImhvcmRlX2F0dGFja18xIiwKICAgICJob3JkZV9hdHRhY2tfMiIKICAgXSwKICAgImJ1aWxkIjogWwogICAgImhvcmRlX2J1aWxkXzAiLAogICAgImhvcmRlX2J1aWxkXzEiLAogICAgImhvcmRlX2J1aWxkXzIiCiAgIF0sCiAgICJkZXBsb3kiOiBbCiAgICAiaG9yZGVfZGVwbG95XzAiLAogICAgImhvcmRlX2RlcGxveV8xIiwKICAgICJob3JkZV9kZXBsb3lfMiIKICAgXSwKICAgImhvbGQiOiBbCiAgICAiaG9yZGVfaG9sZF8wIiwKICAgICJob3JkZV9ob2xkXzEiLAogICAgImhvcmRlX2hvbGRfMiIKICAgXSwKICAgIm1vdmUiOiBbCiAgICAiaG9yZGVfbW92ZV8wIiwKICAgICJob3JkZV9tb3ZlXzEiLAogICAgImhvcmRlX21vdmVfMiIKICAgXSwKICAgInBhdHJvbCI6IFsKICAgICJob3JkZV9wYXRyb2xfMCIsCiAgICAiaG9yZGVfcGF0cm9sXzEiLAogICAgImhvcmRlX3BhdHJvbF8yIgogICBdLAogICAic2VsZWN0IjogWwogICAgImhvcmRlX3NlbGVjdF8wIiwKICAgICJob3JkZV9zZWxlY3RfMSIsCiAgICAiaG9yZGVfc2VsZWN0XzIiCiAgIF0sCiAgICJzdG9wIjogWwogICAgImhvcmRlX3N0b3BfMCIsCiAgICAiaG9yZGVfc3RvcF8xIiwKICAgICJob3JkZV9zdG9wXzIiCiAgIF0KICB9LAogICJrZWVuIjogewogICAiZG9uZV9hYmlsaXR5IjogWwogICAgImtlZW5fZG9uZV9hYmlsaXR5IgogICBdLAogICAiZG9uZV9hdHRhY2siOiBbCiAgICAia2Vlbl9kb25lX2F0dGFjayIKICAgXSwKICAgImRvbmVfY2FtZXJhIjogWwogICAgImtlZW5fZG9uZV9jYW1lcmEiCiAgIF0sCiAgICJkb25lX2Nsb3VkIjogWwogICAgImtlZW5fZG9uZV9jbG91ZCIKICAgXSwKICAgImRvbmVfY29tbWFuZGVyIjogWwogICAgImtlZW5fZG9uZV9jb21tYW5kZXIiCiAgIF0sCiAgICJkb25lX2RlcGxveSI6IFsKICAgICJrZWVuX2RvbmVfZGVwbG95IgogICBdLAogICAiZG9uZV9mYWMiOiBbCiAgICAia2Vlbl9kb25lX2ZhYyIKICAgXSwKICAgImRvbmVfZm9nIjogWwogICAgImtlZW5fZG9uZV9mb2ciCiAgIF0sCiAgICJkb25lX2Zvcm1hdGlvbiI6IFsKICAgICJrZWVuX2RvbmVfZm9ybWF0aW9uIgogICBdLAogICAiZG9uZV9pbnRlbCI6IFsKICAgICJrZWVuX2RvbmVfaW50ZWwiCiAgIF0sCiAgICJkb25lX21leCI6IFsKICAgICJrZWVuX2RvbmVfbWV4IgogICBdLAogICAiZG9uZV9vYmplY3RpdmUiOiBbCiAgICAia2Vlbl9kb25lX29iamVjdGl2ZSIKICAgXSwKICAgImRvbmVfcGlja3VwIjogWwogICAgImtlZW5fZG9uZV9waWNrdXAiCiAgIF0sCiAgICJkb25lX3BsYXRvb24iOiBbCiAgICAia2Vlbl9kb25lX3BsYXRvb24iCiAgIF0sCiAgICJkb25lX3Bvd2VyIjogWwogICAgImtlZW5fZG9uZV9wb3dlciIKICAgXSwKICAgImRvbmVfcXVldWUiOiBbCiAgICAia2Vlbl9kb25lX3F1ZXVlIgogICBdLAogICAiZG9uZV90ZWNoIjogWwogICAgImtlZW5fZG9uZV90ZWNoIgogICBdLAogICAiZG9uZV90ZXJyaXRvcnkiOiBbCiAgICAia2Vlbl9kb25lX3RlcnJpdG9yeSIKICAgXSwKICAgImRvbmVfdHJhaW4iOiBbCiAgICAia2Vlbl9kb25lX3RyYWluIgogICBdLAogICAiZG9uZV90dXJyZXQiOiBbCiAgICAia2Vlbl9kb25lX3R1cnJldCIKICAgXSwKICAgImdyYWR1YXRpb24iOiBbCiAgICAia2Vlbl9ncmFkdWF0aW9uIgogICBdLAogICAiZ3JlZXRpbmciOiBbCiAgICAia2Vlbl9ncmVldGluZyIKICAgXSwKICAgInJlYWN0X2Jhc2VfYXR0YWNrMCI6IFsKICAgICJrZWVuX3JlYWN0X2Jhc2VfYXR0YWNrMCIKICAgXSwKICAgInJlYWN0X2Jhc2VfYXR0YWNrMSI6IFsKICAgICJrZWVuX3JlYWN0X2Jhc2VfYXR0YWNrMSIKICAgXSwKICAgInJlYWN0X2hhemFyZF9jcmF0ZXIiOiBbCiAgICAia2Vlbl9yZWFjdF9oYXphcmRfY3JhdGVyIgogICBdLAogICAicmVhY3RfaGF6YXJkX2RlZmF1bHQiOiBbCiAgICAia2Vlbl9yZWFjdF9oYXphcmRfZGVmYXVsdCIKICAgXSwKICAgInJlYWN0X2hhemFyZF9oaWdobGFuZCI6IFsKICAgICJrZWVuX3JlYWN0X2hhemFyZF9oaWdobGFuZCIKICAgXSwKICAgInJlYWN0X2hhemFyZF9pc2xlcyI6IFsKICAgICJrZWVuX3JlYWN0X2hhemFyZF9pc2xlcyIKICAgXSwKICAgInJlYWN0X2hhemFyZF92YW5ndWFyZCI6IFsKICAgICJrZWVuX3JlYWN0X2hhemFyZF92YW5ndWFyZCIKICAgXSwKICAgInJlYWN0X2xvd19wb3dlciI6IFsKICAgICJrZWVuX3JlYWN0X2xvd19wb3dlciIKICAgXSwKICAgInJlYWN0X3VuaXRfbG9zdDAiOiBbCiAgICAia2Vlbl9yZWFjdF91bml0X2xvc3QwIgogICBdLAogICAicmVhY3RfdW5pdF9sb3N0MSI6IFsKICAgICJrZWVuX3JlYWN0X3VuaXRfbG9zdDEiCiAgIF0sCiAgICJyZWFjdF93YXZlIjogWwogICAgImtlZW5fcmVhY3Rfd2F2ZSIKICAgXSwKICAgInNraXAiOiBbCiAgICAia2Vlbl9za2lwIgogICBdLAogICAic3RlcF9hYmlsaXR5IjogWwogICAgImtlZW5fc3RlcF9hYmlsaXR5IgogICBdLAogICAic3RlcF9hdHRhY2siOiBbCiAgICAia2Vlbl9zdGVwX2F0dGFjayIKICAgXSwKICAgInN0ZXBfY2FtZXJhIjogWwogICAgImtlZW5fc3RlcF9jYW1lcmEiCiAgIF0sCiAgICJzdGVwX2Nsb3VkIjogWwogICAgImtlZW5fc3RlcF9jbG91ZCIKICAgXSwKICAgInN0ZXBfY29tbWFuZGVyIjogWwogICAgImtlZW5fc3RlcF9jb21tYW5kZXIiCiAgIF0sCiAgICJzdGVwX2RlcGxveSI6IFsKICAgICJrZWVuX3N0ZXBfZGVwbG95IgogICBdLAogICAic3RlcF9mYWMiOiBbCiAgICAia2Vlbl9zdGVwX2ZhYyIKICAgXSwKICAgInN0ZXBfZm9nIjogWwogICAgImtlZW5fc3RlcF9mb2ciCiAgIF0sCiAgICJzdGVwX2Zvcm1hdGlvbiI6IFsKICAgICJrZWVuX3N0ZXBfZm9ybWF0aW9uIgogICBdLAogICAic3RlcF9pbnRlbCI6IFsKICAgICJrZWVuX3N0ZXBfaW50ZWwiCiAgIF0sCiAgICJzdGVwX21leCI6IFsKICAgICJrZWVuX3N0ZXBfbWV4IgogICBdLAogICAic3RlcF9vYmplY3RpdmUiOiBbCiAgICAia2Vlbl9zdGVwX29iamVjdGl2ZSIKICAgXSwKICAgInN0ZXBfcGlja3VwIjogWwogICAgImtlZW5fc3RlcF9waWNrdXAiCiAgIF0sCiAgICJzdGVwX3BsYXRvb24iOiBbCiAgICAia2Vlbl9zdGVwX3BsYXRvb24iCiAgIF0sCiAgICJzdGVwX3Bvd2VyIjogWwogICAgImtlZW5fc3RlcF9wb3dlciIKICAgXSwKICAgInN0ZXBfcXVldWUiOiBbCiAgICAia2Vlbl9zdGVwX3F1ZXVlIgogICBdLAogICAic3RlcF90ZWNoIjogWwogICAgImtlZW5fc3RlcF90ZWNoIgogICBdLAogICAic3RlcF90ZXJyaXRvcnkiOiBbCiAgICAia2Vlbl9zdGVwX3RlcnJpdG9yeSIKICAgXSwKICAgInN0ZXBfdHJhaW4iOiBbCiAgICAia2Vlbl9zdGVwX3RyYWluIgogICBdLAogICAic3RlcF90dXJyZXQiOiBbCiAgICAia2Vlbl9zdGVwX3R1cnJldCIKICAgXQogIH0sCiAgIm5vdmEiOiB7CiAgICJhYmlsaXR5IjogWwogICAgIm5vdmFfYWJpbGl0eV8wIiwKICAgICJub3ZhX2FiaWxpdHlfMSIsCiAgICAibm92YV9hYmlsaXR5XzIiCiAgIF0sCiAgICJhdHRhY2siOiBbCiAgICAibm92YV9hdHRhY2tfMCIsCiAgICAibm92YV9hdHRhY2tfMSIsCiAgICAibm92YV9hdHRhY2tfMiIKICAgXSwKICAgImJ1aWxkIjogWwogICAgIm5vdmFfYnVpbGRfMCIsCiAgICAibm92YV9idWlsZF8xIiwKICAgICJub3ZhX2J1aWxkXzIiCiAgIF0sCiAgICJkZXBsb3kiOiBbCiAgICAibm92YV9kZXBsb3lfMCIsCiAgICAibm92YV9kZXBsb3lfMSIsCiAgICAibm92YV9kZXBsb3lfMiIKICAgXSwKICAgImhvbGQiOiBbCiAgICAibm92YV9ob2xkXzAiLAogICAgIm5vdmFfaG9sZF8xIiwKICAgICJub3ZhX2hvbGRfMiIKICAgXSwKICAgIm1vdmUiOiBbCiAgICAibm92YV9tb3ZlXzAiLAogICAgIm5vdmFfbW92ZV8xIiwKICAgICJub3ZhX21vdmVfMiIKICAgXSwKICAgInBhdHJvbCI6IFsKICAgICJub3ZhX3BhdHJvbF8wIiwKICAgICJub3ZhX3BhdHJvbF8xIiwKICAgICJub3ZhX3BhdHJvbF8yIgogICBdLAogICAic2VsZWN0IjogWwogICAgIm5vdmFfc2VsZWN0XzAiLAogICAgIm5vdmFfc2VsZWN0XzEiLAogICAgIm5vdmFfc2VsZWN0XzIiCiAgIF0sCiAgICJzdG9wIjogWwogICAgIm5vdmFfc3RvcF8wIiwKICAgICJub3ZhX3N0b3BfMSIsCiAgICAibm92YV9zdG9wXzIiCiAgIF0KICB9LAogICJzeW5kaWNhdGUiOiB7CiAgICJhYmlsaXR5IjogWwogICAgInN5bmRpY2F0ZV9hYmlsaXR5XzAiLAogICAgInN5bmRpY2F0ZV9hYmlsaXR5XzEiLAogICAgInN5bmRpY2F0ZV9hYmlsaXR5XzIiCiAgIF0sCiAgICJhdHRhY2siOiBbCiAgICAic3luZGljYXRlX2F0dGFja18wIiwKICAgICJzeW5kaWNhdGVfYXR0YWNrXzEiLAogICAgInN5bmRpY2F0ZV9hdHRhY2tfMiIKICAgXSwKICAgImJ1aWxkIjogWwogICAgInN5bmRpY2F0ZV9idWlsZF8wIiwKICAgICJzeW5kaWNhdGVfYnVpbGRfMSIsCiAgICAic3luZGljYXRlX2J1aWxkXzIiCiAgIF0sCiAgICJkZXBsb3kiOiBbCiAgICAic3luZGljYXRlX2RlcGxveV8wIiwKICAgICJzeW5kaWNhdGVfZGVwbG95XzEiLAogICAgInN5bmRpY2F0ZV9kZXBsb3lfMiIKICAgXSwKICAgImhvbGQiOiBbCiAgICAic3luZGljYXRlX2hvbGRfMCIsCiAgICAic3luZGljYXRlX2hvbGRfMSIsCiAgICAic3luZGljYXRlX2hvbGRfMiIKICAgXSwKICAgIm1vdmUiOiBbCiAgICAic3luZGljYXRlX21vdmVfMCIsCiAgICAic3luZGljYXRlX21vdmVfMSIsCiAgICAic3luZGljYXRlX21vdmVfMiIKICAgXSwKICAgInBhdHJvbCI6IFsKICAgICJzeW5kaWNhdGVfcGF0cm9sXzAiLAogICAgInN5bmRpY2F0ZV9wYXRyb2xfMSIsCiAgICAic3luZGljYXRlX3BhdHJvbF8yIgogICBdLAogICAic2VsZWN0IjogWwogICAgInN5bmRpY2F0ZV9zZWxlY3RfMCIsCiAgICAic3luZGljYXRlX3NlbGVjdF8xIiwKICAgICJzeW5kaWNhdGVfc2VsZWN0XzIiCiAgIF0sCiAgICJzdG9wIjogWwogICAgInN5bmRpY2F0ZV9zdG9wXzAiLAogICAgInN5bmRpY2F0ZV9zdG9wXzEiLAogICAgInN5bmRpY2F0ZV9zdG9wXzIiCiAgIF0KICB9CiB9LAogInRha2VzIjogewogICJhc2NlbmRhbmN5X2FiaWxpdHlfMCI6IHsKICAgInNoYSI6ICI2YjdiMDMzNjViZTFhYzFjIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi45MzUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NDMxLAogICAibTRhIjogMTYxMjQKICB9LAogICJhc2NlbmRhbmN5X2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICI3ZTIyNGEyYzJkMzc5YmRkIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wOTksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE2MTM1LAogICAibTRhIjogMTY5MzYKICB9LAogICJhc2NlbmRhbmN5X2FiaWxpdHlfMiI6IHsKICAgInNoYSI6ICI1MGUyOTk5YzEzMWUwZDYxIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wNTEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NzQzLAogICAibTRhIjogMTY3NTQKICB9LAogICJhc2NlbmRhbmN5X2F0dGFja18wIjogewogICAic2hhIjogImNhNjc4ZWU1YzJiMmZhNmIiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjAzNywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE0OTUsCiAgICJtNGEiOiAxMTUyOAogIH0sCiAgImFzY2VuZGFuY3lfYXR0YWNrXzEiOiB7CiAgICJzaGEiOiAiOGE5YjVlYTY1MjM4NTdiNSIsCiAgICJ2b2ljZSI6ICJibV9nZW9yZ2UiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMDIsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1MzgyLAogICAibTRhIjogMTY1MzkKICB9LAogICJhc2NlbmRhbmN5X2F0dGFja18yIjogewogICAic2hhIjogIjkyZTE4MDY5Y2Q3YTFjMTciLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjg1OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTUyNDEsCiAgICJtNGEiOiAxNTgyOQogIH0sCiAgImFzY2VuZGFuY3lfYnVpbGRfMCI6IHsKICAgInNoYSI6ICIzMzdmYjM1NzQyOGEwNGY4IiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4xOTMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE2MzA1LAogICAibTRhIjogMTc0OTUKICB9LAogICJhc2NlbmRhbmN5X2J1aWxkXzEiOiB7CiAgICJzaGEiOiAiY2NhNDUwMjhmY2M1YTAwMyIsCiAgICJ2b2ljZSI6ICJibV9nZW9yZ2UiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMzk5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNzU3MiwKICAgIm00YSI6IDE4NTIyCiAgfSwKICAiYXNjZW5kYW5jeV9idWlsZF8yIjogewogICAic2hhIjogImViZGExZjE4NjRkMWIzZTYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjY0MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ0MTgsCiAgICJtNGEiOiAxNDYzMAogIH0sCiAgImFzY2VuZGFuY3lfZGVwbG95XzAiOiB7CiAgICJzaGEiOiAiNGQwOTcyZGYzYTRhNTg0NyIsCiAgICJ2b2ljZSI6ICJibV9nZW9yZ2UiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMDIyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNTY0MywKICAgIm00YSI6IDE2NjEyCiAgfSwKICAiYXNjZW5kYW5jeV9kZXBsb3lfMSI6IHsKICAgInNoYSI6ICI1MTQzY2NkNmNjMjY2Yzc5IiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi45MjMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NjIzLAogICAibTRhIjogMTYwNzAKICB9LAogICJhc2NlbmRhbmN5X2RlcGxveV8yIjogewogICAic2hhIjogIjVmMTJlZTdiMzZhYzI0YTMiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjg4MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTUyNzksCiAgICJtNGEiOiAxNTg4MwogIH0sCiAgImFzY2VuZGFuY3lfaG9sZF8wIjogewogICAic2hhIjogIjQyNDg2YzAxNzhkNGRkZDciLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI2MywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI3MjQsCiAgICJtNGEiOiAxMjcwOAogIH0sCiAgImFzY2VuZGFuY3lfaG9sZF8xIjogewogICAic2hhIjogImMzODMyNDdiZTgzZDU2M2IiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjgwMywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ3NTIsCiAgICJtNGEiOiAxNTQyNQogIH0sCiAgImFzY2VuZGFuY3lfaG9sZF8yIjogewogICAic2hhIjogImY1MThiYjgzNTFmNGQwYjAiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjM5NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTMxODYsCiAgICJtNGEiOiAxMzQzMgogIH0sCiAgImFzY2VuZGFuY3lfbW92ZV8wIjogewogICAic2hhIjogIjllOGU3YmU2MzE5NzhiMWQiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjQ5NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM3MTAsCiAgICJtNGEiOiAxMzg2OAogIH0sCiAgImFzY2VuZGFuY3lfbW92ZV8xIjogewogICAic2hhIjogIjc0OTRiZWJhNTdjY2NmODkiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjc5OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ1ODksCiAgICJtNGEiOiAxNTM5MwogIH0sCiAgImFzY2VuZGFuY3lfbW92ZV8yIjogewogICAic2hhIjogIjFhNTMyOThjYWY1NTIzYmYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjYwNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAwNTUsCiAgICJtNGEiOiA5NDQ0CiAgfSwKICAiYXNjZW5kYW5jeV9wYXRyb2xfMCI6IHsKICAgInNoYSI6ICI3MzFmZDQ3NjZkMDU3ODc3IiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4zOTUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNjA5LAogICAibTRhIjogMTM0MTYKICB9LAogICJhc2NlbmRhbmN5X3BhdHJvbF8xIjogewogICAic2hhIjogImNjOTBmYWE2MmU0YjVjNDEiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjA2LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjAxNSwKICAgIm00YSI6IDExNjUyCiAgfSwKICAiYXNjZW5kYW5jeV9wYXRyb2xfMiI6IHsKICAgInNoYSI6ICJlMzVjNWVmNmQ2YTE4MTIwIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi42MzEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE0MDkyLAogICAibTRhIjogMTQ2NTgKICB9LAogICJhc2NlbmRhbmN5X3NlbGVjdF8wIjogewogICAic2hhIjogImNmZTJlZjVlYmYwNDljZWYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjk5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNTcwMCwKICAgIm00YSI6IDE2MzkyCiAgfSwKICAiYXNjZW5kYW5jeV9zZWxlY3RfMSI6IHsKICAgInNoYSI6ICIwYWQwZTBhZjVhMDljNTUyIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi42NjYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE0Mjg5LAogICAibTRhIjogMTQ3MjkKICB9LAogICJhc2NlbmRhbmN5X3NlbGVjdF8yIjogewogICAic2hhIjogIjA4YzVjMTE3MTI5YjdhNDMiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjYyOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM5NjIsCiAgICJtNGEiOiAxNDY2NQogIH0sCiAgImFzY2VuZGFuY3lfc3RvcF8wIjogewogICAic2hhIjogImE5ZTQ2MTE4OWI3NGIwMjYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjg1OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ5MjgsCiAgICJtNGEiOiAxNTg4NgogIH0sCiAgImFzY2VuZGFuY3lfc3RvcF8xIjogewogICAic2hhIjogImNmYWFmYzRiOGI3Y2MxZjciLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjIzNSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTIzMTQsCiAgICJtNGEiOiAxMjU1NAogIH0sCiAgImFzY2VuZGFuY3lfc3RvcF8yIjogewogICAic2hhIjogImRlMWI1MWZiZDk3YjdkMzgiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjk3OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE2NTQsCiAgICJtNGEiOiAxMTMxNgogIH0sCiAgImhvcmRlX2FiaWxpdHlfMCI6IHsKICAgInNoYSI6ICI5MDBiZmQyNjFjZGZhMWE3IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjgyNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTUwNjEsCiAgICJtNGEiOiAxNTU4MAogIH0sCiAgImhvcmRlX2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICJhNzZhZDI4OTdkMDAwMDE2IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAzLjE5NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTY3MzgsCiAgICJtNGEiOiAxNzQ3NQogIH0sCiAgImhvcmRlX2FiaWxpdHlfMiI6IHsKICAgInNoYSI6ICIyMjFlYjQ0MTU2MzY2NTIxIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjkwNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTU1MDYsCiAgICJtNGEiOiAxNjA3MQogIH0sCiAgImhvcmRlX2F0dGFja18wIjogewogICAic2hhIjogImQxM2U1ZWJiZmMwZTIwNDYiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzU0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDI4OCwKICAgIm00YSI6IDEwMjA0CiAgfSwKICAiaG9yZGVfYXR0YWNrXzEiOiB7CiAgICJzaGEiOiAiNjgyOGQwMmY1NmJiNTFlZCIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wNjQsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NjYwLAogICAibTRhIjogMTY4NjYKICB9LAogICJob3JkZV9hdHRhY2tfMiI6IHsKICAgInNoYSI6ICI1NWJlNTYxZTg3Y2UwYTllIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjY4OSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQzODMsCiAgICJtNGEiOiAxNDk2NQogIH0sCiAgImhvcmRlX2J1aWxkXzAiOiB7CiAgICJzaGEiOiAiODhmZjc1NjZlYzhkMjVkOSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi43ODMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE0Njg3LAogICAibTRhIjogMTU0NjMKICB9LAogICJob3JkZV9idWlsZF8xIjogewogICAic2hhIjogImJhMWRhN2Y0NDViN2VmNmMiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMjI4LAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiAxNjc0MywKICAgIm00YSI6IDE3NzYyCiAgfSwKICAiaG9yZGVfYnVpbGRfMiI6IHsKICAgInNoYSI6ICI3ZDM2MWQ0NzcyYzUxZGUzIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjEyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjQ2NCwKICAgIm00YSI6IDEyMDgzCiAgfSwKICAiaG9yZGVfZGVwbG95XzAiOiB7CiAgICJzaGEiOiAiMzc0ZmI4NDBkZjcxZmQwMiIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wMDUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NTc4LAogICAibTRhIjogMTY2MDYKICB9LAogICJob3JkZV9kZXBsb3lfMSI6IHsKICAgInNoYSI6ICIwMGQxOThkZGYwMjg3ODdiIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjk2MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTU2NzEsCiAgICJtNGEiOiAxNjM0OQogIH0sCiAgImhvcmRlX2RlcGxveV8yIjogewogICAic2hhIjogIjM0YmFjMWI1MDNiMjBlN2UiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMDE0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNTU1OSwKICAgIm00YSI6IDE2NTM1CiAgfSwKICAiaG9yZGVfaG9sZF8wIjogewogICAic2hhIjogIjk2YTYxNDU3Y2UyYjNjNDYiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuODUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExMDYzLAogICAibTRhIjogMTA2NjUKICB9LAogICJob3JkZV9ob2xkXzEiOiB7CiAgICJzaGEiOiAiOTkwODU1OGI2MWI4NDBjNyIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi42MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM5ODMsCiAgICJtNGEiOiAxNDY3NAogIH0sCiAgImhvcmRlX2hvbGRfMiI6IHsKICAgInNoYSI6ICJkMDkwMDU3NTkwZGU1M2QzIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjg4MywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTExNjQsCiAgICJtNGEiOiAxMDg2MAogIH0sCiAgImhvcmRlX21vdmVfMCI6IHsKICAgInNoYSI6ICJhNTdjNjhkOTkzMTQ1NDU3IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI3NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI5MTcsCiAgICJtNGEiOiAxMjgwMAogIH0sCiAgImhvcmRlX21vdmVfMSI6IHsKICAgInNoYSI6ICJkODI3YzFjOWY5ZTkyMDA4IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI4MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI1MDUsCiAgICJtNGEiOiAxMjg1OQogIH0sCiAgImhvcmRlX21vdmVfMiI6IHsKICAgInNoYSI6ICI1NzNhMWU1NWYwZDUyMzAzIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjMwOCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogOTAwOSwKICAgIm00YSI6IDc3MTgKICB9LAogICJob3JkZV9wYXRyb2xfMCI6IHsKICAgInNoYSI6ICJhZjYwODc5NDExZDRlMTRjIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjEzOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI1OTgsCiAgICJtNGEiOiAxMjE0NAogIH0sCiAgImhvcmRlX3BhdHJvbF8xIjogewogICAic2hhIjogImY4ZjliMDUzZWVhY2I3MmIiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzU3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDg0MCwKICAgIm00YSI6IDEwMTc5CiAgfSwKICAiaG9yZGVfcGF0cm9sXzIiOiB7CiAgICJzaGEiOiAiMmQ1YWI3M2M4MGE1ZjFjZiIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi40ODksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNjgxLAogICAibTRhIjogMTM4NzgKICB9LAogICJob3JkZV9zZWxlY3RfMCI6IHsKICAgInNoYSI6ICI0ZGRmMjFjMDc0YjRkZDRiIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjc2NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ3MDIsCiAgICJtNGEiOiAxNTM0MQogIH0sCiAgImhvcmRlX3NlbGVjdF8xIjogewogICAic2hhIjogImM2MDNlYTQzN2Y1ZDY2ZTIiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTgzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjQ5MCwKICAgIm00YSI6IDEyMzg0CiAgfSwKICAiaG9yZGVfc2VsZWN0XzIiOiB7CiAgICJzaGEiOiAiNzIxOTQ0YjAwNGQ2N2VhMSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi40NjksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzMTc0LAogICAibTRhIjogMTM3NTYKICB9LAogICJob3JkZV9zdG9wXzAiOiB7CiAgICJzaGEiOiAiZWZmNTAzMGIzNGQzM2MxZSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi41NzcsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNzcyLAogICAibTRhIjogMTQ0MjkKICB9LAogICJob3JkZV9zdG9wXzEiOiB7CiAgICJzaGEiOiAiYzI4MTFmMWYyNThjZGZhOSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS44MTMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEwODE0LAogICAibTRhIjogMTA0NzYKICB9LAogICJob3JkZV9zdG9wXzIiOiB7CiAgICJzaGEiOiAiNDMxMWM4ODc2NTQyMTBjMSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS44MTQsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEwOTg4LAogICAibTRhIjogMTA0MTgKICB9LAogICJrZWVuX2RvbmVfYWJpbGl0eSI6IHsKICAgInNoYSI6ICI5OGU5ZDdkMjc4MWY3NDI3IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1Ljk2NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjczNTMsCiAgICJtNGEiOiAzMDEzMwogIH0sCiAgImtlZW5fZG9uZV9hdHRhY2siOiB7CiAgICJzaGEiOiAiNmJmZmYyOGU3YjIyYzFjMiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS41MjksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI1MzgzLAogICAibTRhIjogMjgxNDMKICB9LAogICJrZWVuX2RvbmVfY2FtZXJhIjogewogICAic2hhIjogImRhNDc5OGQ4YmU3NGU4N2EiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDUuMzM1LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAyNDgwMCwKICAgIm00YSI6IDI3MzA1CiAgfSwKICAia2Vlbl9kb25lX2Nsb3VkIjogewogICAic2hhIjogIjQzODMwZTAyNzkwNmRiZjAiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDY3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA3NywKICAgIm00YSI6IDExNzU2CiAgfSwKICAia2Vlbl9kb25lX2NvbW1hbmRlciI6IHsKICAgInNoYSI6ICIzMTczMTgyM2UzYWUxMDljIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1Ljc5OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjYyNjcsCiAgICJtNGEiOiAyOTQ2NAogIH0sCiAgImtlZW5fZG9uZV9kZXBsb3kiOiB7CiAgICJzaGEiOiAiYTg1MDRjZWU3YWM1N2IzMSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy44NjIsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE5MTAxLAogICAibTRhIjogMjA2NzYKICB9LAogICJrZWVuX2RvbmVfZmFjIjogewogICAic2hhIjogImZmYmQ1MGIxMTkwOGU0ZDgiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuODkxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxOTE3MCwKICAgIm00YSI6IDIwODc2CiAgfSwKICAia2Vlbl9kb25lX2ZvZyI6IHsKICAgInNoYSI6ICIzZTkyZDI2NTQzNmYxYzFhIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA2Ljk2NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMzEzOTcsCiAgICJtNGEiOiAzNDg1NAogIH0sCiAgImtlZW5fZG9uZV9mb3JtYXRpb24iOiB7CiAgICJzaGEiOiAiYWE4ZjA4ZDBmMzYwNzlmMCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4yNTgsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI0Nzc3LAogICAibTRhIjogMjcyMjkKICB9LAogICJrZWVuX2RvbmVfaW50ZWwiOiB7CiAgICJzaGEiOiAiNGM4ODhkNmMzOWZkYTI5MiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTEuNDY4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA0ODE3MSwKICAgIm00YSI6IDU1OTAyCiAgfSwKICAia2Vlbl9kb25lX21leCI6IHsKICAgInNoYSI6ICJmNjgzZmZkNDNlNzlmZDAxIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAzLjU0NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTgxODIsCiAgICJtNGEiOiAxOTE1MwogIH0sCiAgImtlZW5fZG9uZV9vYmplY3RpdmUiOiB7CiAgICJzaGEiOiAiNDdmODZjMDY1Y2FmYTk1NCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNy4zNTgsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDMyODc3LAogICAibTRhIjogMzcwNjQKICB9LAogICJrZWVuX2RvbmVfcGlja3VwIjogewogICAic2hhIjogIjNmYjY0ZWU0ZWQwYmI2OGMiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDYuOTYyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzMDg4MywKICAgIm00YSI6IDM0ODUyCiAgfSwKICAia2Vlbl9kb25lX3BsYXRvb24iOiB7CiAgICJzaGEiOiAiZTVhNzQxMWQwOTBkNzExZCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4wMTIsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDIzNDI2LAogICAibTRhIjogMjU2OTEKICB9LAogICJrZWVuX2RvbmVfcG93ZXIiOiB7CiAgICJzaGEiOiAiYTVlNTk1MDQ4OTgxN2I0YyIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4wMjEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDIzNjk1LAogICAibTRhIjogMjYyMzMKICB9LAogICJrZWVuX2RvbmVfcXVldWUiOiB7CiAgICJzaGEiOiAiMDU0NTYxOWZkNmFiYzU0ZiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4wMDEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDIzNDIyLAogICAibTRhIjogMjU2MjMKICB9LAogICJrZWVuX2RvbmVfdGVjaCI6IHsKICAgInNoYSI6ICI5YzIwNTNkODA2YmYwMDYyIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1LjYyNSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjU4OTUsCiAgICJtNGEiOiAyODcwNgogIH0sCiAgImtlZW5fZG9uZV90ZXJyaXRvcnkiOiB7CiAgICJzaGEiOiAiN2FjOWM2ZmVjN2I1ODZlOCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi44NzEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1MzQ0LAogICAibTRhIjogMTU4MzEKICB9LAogICJrZWVuX2RvbmVfdHJhaW4iOiB7CiAgICJzaGEiOiAiYjFlNTAyNTI4ZDc2NTZiMSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNi42OSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjkyMTQsCiAgICJtNGEiOiAzMzQ5NwogIH0sCiAgImtlZW5fZG9uZV90dXJyZXQiOiB7CiAgICJzaGEiOiAiYmIyNWNiMTI0NmIzMzg3YiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTQuNTYyLAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiA1OTE0NSwKICAgIm00YSI6IDcxMTQ1CiAgfSwKICAia2Vlbl9ncmFkdWF0aW9uIjogewogICAic2hhIjogImU5ZDJkNGU5ZjY2MDFhMDAiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEyLjA5NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNTA5MTAsCiAgICJtNGEiOiA1OTAwOQogIH0sCiAgImtlZW5fZ3JlZXRpbmciOiB7CiAgICJzaGEiOiAiM2YyZGQ2M2JjMmExYjQ3NiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTIuMTIzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA1MDA5OSwKICAgIm00YSI6IDU4ODIwCiAgfSwKICAia2Vlbl9yZWFjdF9iYXNlX2F0dGFjazAiOiB7CiAgICJzaGEiOiAiNjdhY2M5NWI5MzliMWQxZSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS40MTUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI1NDAwLAogICAibTRhIjogMjc5OTUKICB9LAogICJrZWVuX3JlYWN0X2Jhc2VfYXR0YWNrMSI6IHsKICAgInNoYSI6ICI2MDk3YWVhZmUwNGM0MDdhIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjA0NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTk4OTIsCiAgICJtNGEiOiAyMTM3NQogIH0sCiAgImtlZW5fcmVhY3RfaGF6YXJkX2NyYXRlciI6IHsKICAgInNoYSI6ICIzOTE3ZDBhODlkZGExMzMwIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjgyMywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjM1NTcsCiAgICJtNGEiOiAyNTIwNwogIH0sCiAgImtlZW5fcmVhY3RfaGF6YXJkX2RlZmF1bHQiOiB7CiAgICJzaGEiOiAiNThhMDNmODJkYjQ5OTM5NSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS43NzUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI2MjM5LAogICAibTRhIjogMjkzNzMKICB9LAogICJrZWVuX3JlYWN0X2hhemFyZF9oaWdobGFuZCI6IHsKICAgInNoYSI6ICJiMTBmNTUxYjFmYmM2NjA1IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjM4NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjA4ODUsCiAgICJtNGEiOiAyMzA0NQogIH0sCiAgImtlZW5fcmVhY3RfaGF6YXJkX2lzbGVzIjogewogICAic2hhIjogImVlM2Q1ZGQ5ZGYxNDgzNjYiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDQuNjIzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAyMjM4NywKICAgIm00YSI6IDI0MDE1CiAgfSwKICAia2Vlbl9yZWFjdF9oYXphcmRfdmFuZ3VhcmQiOiB7CiAgICJzaGEiOiAiNzY1OGRkMTFiNmZkZDdmMyIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS41OTMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI2MDE2LAogICAibTRhIjogMjg3NjIKICB9LAogICJrZWVuX3JlYWN0X2xvd19wb3dlciI6IHsKICAgInNoYSI6ICJlZGE4M2E5ZDBhMmU3NTEzIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjEzOCwKICAgInBlYWsiOiAtMC42LAogICAib2dnIjogMjAyNDIsCiAgICJtNGEiOiAyMjAxOAogIH0sCiAgImtlZW5fcmVhY3RfdW5pdF9sb3N0MCI6IHsKICAgInNoYSI6ICIzMGU4NmQzODFhMzk5NzY3IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1LjI1MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjQ4NTIsCiAgICJtNGEiOiAyNzAyMAogIH0sCiAgImtlZW5fcmVhY3RfdW5pdF9sb3N0MSI6IHsKICAgInNoYSI6ICIxYmEzMzE2MjM4YTVlYWNjIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0Ljk2NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjMzMzksCiAgICJtNGEiOiAyNTg0NAogIH0sCiAgImtlZW5fcmVhY3Rfd2F2ZSI6IHsKICAgInNoYSI6ICIyNjQzMGVmMTU4YjA4M2ViIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA2LjI5NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjc3MjIsCiAgICJtNGEiOiAzMTk3MAogIH0sCiAgImtlZW5fc2tpcCI6IHsKICAgInNoYSI6ICIzNjMwOWJhN2EwNDk4MTc2IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1LjMxNSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjQ5NjgsCiAgICJtNGEiOiAyNzI4NwogIH0sCiAgImtlZW5fc3RlcF9hYmlsaXR5IjogewogICAic2hhIjogIjQ1Yzg1OTdmZjEwNWJkNDAiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDguNjg1LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzNzc2NywKICAgIm00YSI6IDQyODI4CiAgfSwKICAia2Vlbl9zdGVwX2F0dGFjayI6IHsKICAgInNoYSI6ICI4ZmNkYTA5YTViZGQwNDkwIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxNi43NTcsCiAgICJwZWFrIjogLTAuNiwKICAgIm9nZyI6IDY4Njc4LAogICAibTRhIjogODEzNzEKICB9LAogICJrZWVuX3N0ZXBfY2FtZXJhIjogewogICAic2hhIjogIjE5MGRkNTM1MWEzOWI5MTIiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDkuNDE2LAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiA0MDE2NSwKICAgIm00YSI6IDQ2NDk5CiAgfSwKICAia2Vlbl9zdGVwX2Nsb3VkIjogewogICAic2hhIjogIjVhM2ExMTYxNTcxYzAyZjUiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEyLjI0NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNTEzNjMsCiAgICJtNGEiOiA1OTg3OAogIH0sCiAgImtlZW5fc3RlcF9jb21tYW5kZXIiOiB7CiAgICJzaGEiOiAiNjQ2OGI2NTIwMzg3ZjRiMCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNy40NTUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDMzNDQ4LAogICAibTRhIjogMzcyMTcKICB9LAogICJrZWVuX3N0ZXBfZGVwbG95IjogewogICAic2hhIjogIjRlMmQ3MWNhN2I2NDJjNjIiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDYuODkxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzMDE2MiwKICAgIm00YSI6IDM0ODQ5CiAgfSwKICAia2Vlbl9zdGVwX2ZhYyI6IHsKICAgInNoYSI6ICIwNGNhMjMwMjY2ZDE2Y2UyIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA4LjQ4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzNzMzMiwKICAgIm00YSI6IDQyMDc2CiAgfSwKICAia2Vlbl9zdGVwX2ZvZyI6IHsKICAgInNoYSI6ICIxNjI3MzQyNzQ4Y2MyMmVkIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA5LjI0MywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMzk2NzgsCiAgICJtNGEiOiA0NTgwMAogIH0sCiAgImtlZW5fc3RlcF9mb3JtYXRpb24iOiB7CiAgICJzaGEiOiAiNzBlMjllMWJmZmYxMmIwNCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogOC4wODUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDM1MTgwLAogICAibTRhIjogNDAyMzQKICB9LAogICJrZWVuX3N0ZXBfaW50ZWwiOiB7CiAgICJzaGEiOiAiNjc0Mzk1M2U2ZTg4YmMyYyIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTcuNjUyLAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiA3MTMwOCwKICAgIm00YSI6IDg0ODIwCiAgfSwKICAia2Vlbl9zdGVwX21leCI6IHsKICAgInNoYSI6ICIyN2FlMDI1N2NmMTg2ZWJlIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA5Ljk4NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNDI4NzMsCiAgICJtNGEiOiA0OTU0MAogIH0sCiAgImtlZW5fc3RlcF9vYmplY3RpdmUiOiB7CiAgICJzaGEiOiAiZDg1YTY5MDg5N2JkYjM4ZSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTEuNjQ0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA0ODk0NiwKICAgIm00YSI6IDU3MDE1CiAgfSwKICAia2Vlbl9zdGVwX3BpY2t1cCI6IHsKICAgInNoYSI6ICJmMjI2ODY5MjBhNTkyNTU5IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA4LjU5MiwKICAgInBlYWsiOiAtMC42LAogICAib2dnIjogMzc4NjIsCiAgICJtNGEiOiA0MjU2NwogIH0sCiAgImtlZW5fc3RlcF9wbGF0b29uIjogewogICAic2hhIjogImE4NjdhMGU4ZTA3NDFhMTIiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDkuODExLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA0MTY2MSwKICAgIm00YSI6IDQ4NTg1CiAgfSwKICAia2Vlbl9zdGVwX3Bvd2VyIjogewogICAic2hhIjogIjY2NWY3YWVlZjNiNTMxY2MiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEwLjE2NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNDM3NjAsCiAgICJtNGEiOiA1MDI3OQogIH0sCiAgImtlZW5fc3RlcF9xdWV1ZSI6IHsKICAgInNoYSI6ICI4Zjg4MDE5MWRlYTE4YTk5IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA2Ljc1MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjk4MTQsCiAgICJtNGEiOiAzMzgzOQogIH0sCiAgImtlZW5fc3RlcF90ZWNoIjogewogICAic2hhIjogIjM3MWYzZDU5NDA4MzNhNGYiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEwLjA2NiwKICAgInBlYWsiOiAtMC42LAogICAib2dnIjogNDM3MzIsCiAgICJtNGEiOiA0OTY4OQogIH0sCiAgImtlZW5fc3RlcF90ZXJyaXRvcnkiOiB7CiAgICJzaGEiOiAiOWJmYjVmMzlhNjliMGQzMSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTIuMTMzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA1MDk0OCwKICAgIm00YSI6IDU5NDM5CiAgfSwKICAia2Vlbl9zdGVwX3RyYWluIjogewogICAic2hhIjogImJjMjg0MDgwNjc4NjBiY2UiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDcuMzc0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzMjkwMSwKICAgIm00YSI6IDM2ODcxCiAgfSwKICAia2Vlbl9zdGVwX3R1cnJldCI6IHsKICAgInNoYSI6ICIzYTNkZjk1ZDZjMzdkODM4IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA3Ljc4MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMzM5MjAsCiAgICJtNGEiOiAzODY5NQogIH0sCiAgIm5vdmFfYWJpbGl0eV8wIjogewogICAic2hhIjogIjUzNzcwZmZmYTUzZWM4NDgiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4xNzcsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEyNDM3LAogICAibTRhIjogMTIyMzgKICB9LAogICJub3ZhX2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICIxZTFkMjg4YzZmNWNjZTE0IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuNTc0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNDQ0NywKICAgIm00YSI6IDE0MjY1CiAgfSwKICAibm92YV9hYmlsaXR5XzIiOiB7CiAgICJzaGEiOiAiMWE3YmMwZThhYWJjOTVjYSIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjY4OSwKICAgIm00YSI6IDEyODI3CiAgfSwKICAibm92YV9hdHRhY2tfMCI6IHsKICAgInNoYSI6ICJhMzJmMmZmNTIxN2M3ZWFhIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNjM0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA5ODI0LAogICAibTRhIjogOTQ5MgogIH0sCiAgIm5vdmFfYXR0YWNrXzEiOiB7CiAgICJzaGEiOiAiMWUxYWM2NWQ4MzBiNjUxZCIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjMwOCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI5NTcsCiAgICJtNGEiOiAxMjk0MwogIH0sCiAgIm5vdmFfYXR0YWNrXzIiOiB7CiAgICJzaGEiOiAiYWU3MTQ0M2Q1YTM5MTZhNiIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjIzNiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI3OTAsCiAgICJtNGEiOiAxMjU5MAogIH0sCiAgIm5vdmFfYnVpbGRfMCI6IHsKICAgInNoYSI6ICI2NjQyZjg2MzNjOTdjYzQ3IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuNDYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNDU2LAogICAibTRhIjogMTM3MDIKICB9LAogICJub3ZhX2J1aWxkXzEiOiB7CiAgICJzaGEiOiAiMDQ2MTA5YTg4N2Y4ZjcxNyIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjQxOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM3NzEsCiAgICJtNGEiOiAxMzUwMwogIH0sCiAgIm5vdmFfYnVpbGRfMiI6IHsKICAgInNoYSI6ICJiODY5ZGJkYWI0M2JkNjkyIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTkxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTk5NywKICAgIm00YSI6IDExMzk5CiAgfSwKICAibm92YV9kZXBsb3lfMCI6IHsKICAgInNoYSI6ICI2NDliYjMyMjkyMTZmNDY0IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMjg0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjc0NCwKICAgIm00YSI6IDEyODM2CiAgfSwKICAibm92YV9kZXBsb3lfMSI6IHsKICAgInNoYSI6ICI2YWVhYWVjZGMwNzBjMGVkIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTczLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjQyNywKICAgIm00YSI6IDEyMjY5CiAgfSwKICAibm92YV9kZXBsb3lfMiI6IHsKICAgInNoYSI6ICI2NGRiODcwOTE1NzBlYWRiIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTQ5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjM0NywKICAgIm00YSI6IDEyMTQyCiAgfSwKICAibm92YV9ob2xkXzAiOiB7CiAgICJzaGEiOiAiNjcyMTJkYzY5NDU4ODkzNiIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjkxNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTEzOTcsCiAgICJtNGEiOiAxMDkxMQogIH0sCiAgIm5vdmFfaG9sZF8xIjogewogICAic2hhIjogIjhkMDQyYzE4NzkxZDlhNTIiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4xNDEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEyMDgxLAogICAibTRhIjogMTIwNTUKICB9LAogICJub3ZhX2hvbGRfMiI6IHsKICAgInNoYSI6ICJhYjdkMDA1NGQ4NWQ2NDJmIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuODc4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTEwMSwKICAgIm00YSI6IDEwODY4CiAgfSwKICAibm92YV9tb3ZlXzAiOiB7CiAgICJzaGEiOiAiNDNiMTNiYjVmMWEyY2MyYiIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjgxOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTEwNjEsCiAgICJtNGEiOiAxMDQyOAogIH0sCiAgIm5vdmFfbW92ZV8xIjogewogICAic2hhIjogIjNiZjJmYzU5MDgzYjAzYjQiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4wMzYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExODc1LAogICAibTRhIjogMTE1NzcKICB9LAogICJub3ZhX21vdmVfMiI6IHsKICAgInNoYSI6ICIxYzNhNGNlZWU0M2Y4MTJjIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuMzkyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA5MjAzLAogICAibTRhIjogODIwMQogIH0sCiAgIm5vdmFfcGF0cm9sXzAiOiB7CiAgICJzaGEiOiAiMGIxMzY1NmY5ZGU2YTRkYSIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjkzOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE1NDksCiAgICJtNGEiOiAxMTA3OQogIH0sCiAgIm5vdmFfcGF0cm9sXzEiOiB7CiAgICJzaGEiOiAiNDc0YzJjZDFhYzZjODQwMyIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjYzNiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAyNjksCiAgICJtNGEiOiA5NTEwCiAgfSwKICAibm92YV9wYXRyb2xfMiI6IHsKICAgInNoYSI6ICI4NGFmMWYyNWQwZTEwMWNmIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDk5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA3MSwKICAgIm00YSI6IDExOTM5CiAgfSwKICAibm92YV9zZWxlY3RfMCI6IHsKICAgInNoYSI6ICIzZTdmMWFkZmQ0NWJmZDk5IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDkzLAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiAxMjAyMiwKICAgIm00YSI6IDExODI4CiAgfSwKICAibm92YV9zZWxlY3RfMSI6IHsKICAgInNoYSI6ICI0NDFhNjJjOTE4NGIzYjhkIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDcyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjEyMCwKICAgIm00YSI6IDExODA5CiAgfSwKICAibm92YV9zZWxlY3RfMiI6IHsKICAgInNoYSI6ICJjZjU4YjJlM2E2OTA0NzNmIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDQzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTQ0MywKICAgIm00YSI6IDExNTQ4CiAgfSwKICAibm92YV9zdG9wXzAiOiB7CiAgICJzaGEiOiAiYmMwOTdhN2UyZmI5MjQ1YyIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjE4MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTIyMzAsCiAgICJtNGEiOiAxMjI4MQogIH0sCiAgIm5vdmFfc3RvcF8xIjogewogICAic2hhIjogIjJmYzIxMWVmMzUyYWVlNmYiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS42NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAxMjMsCiAgICJtNGEiOiA5NTk5CiAgfSwKICAibm92YV9zdG9wXzIiOiB7CiAgICJzaGEiOiAiYmI0MTNlOTJmNjA4YmQxZCIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjY2LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDIyMSwKICAgIm00YSI6IDk2NTgKICB9LAogICJzeW5kaWNhdGVfYWJpbGl0eV8wIjogewogICAic2hhIjogImNjMzE1OWJiMDdiNjQ5YzkiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDAxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTU5NSwKICAgIm00YSI6IDExNDAwCiAgfSwKICAic3luZGljYXRlX2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICIzMzJhZmEwMWQ5ZmU3MzU5IiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI5NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI5NDIsCiAgICJtNGEiOiAxMzA2OQogIH0sCiAgInN5bmRpY2F0ZV9hYmlsaXR5XzIiOiB7CiAgICJzaGEiOiAiZDJjZmZiZTYxM2E1MzhhOSIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4wMzUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExODc5LAogICAibTRhIjogMTE2MTUKICB9LAogICJzeW5kaWNhdGVfYXR0YWNrXzAiOiB7CiAgICJzaGEiOiAiMWJiMGYyOWJiZTA1MGRiZCIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS41MDUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDkzMDAsCiAgICJtNGEiOiA4Nzc5CiAgfSwKICAic3luZGljYXRlX2F0dGFja18xIjogewogICAic2hhIjogIjA1NDlmNzc2ZmFlZDQ2YTkiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTgzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjM5MCwKICAgIm00YSI6IDEyMzU3CiAgfSwKICAic3luZGljYXRlX2F0dGFja18yIjogewogICAic2hhIjogIjQzYTNhMjg0YTZkYjY4ZDIiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDYxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA4OCwKICAgIm00YSI6IDExNjU2CiAgfSwKICAic3luZGljYXRlX2J1aWxkXzAiOiB7CiAgICJzaGEiOiAiMGIyOWU2YmU1ZGRjZDQ3OSIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4wOTYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEyMDEwLAogICAibTRhIjogMTE4OTYKICB9LAogICJzeW5kaWNhdGVfYnVpbGRfMSI6IHsKICAgInNoYSI6ICI2YjNiMDBkZWQxMWEwODE5IiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjE4NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI1OTksCiAgICJtNGEiOiAxMjQwOAogIH0sCiAgInN5bmRpY2F0ZV9idWlsZF8yIjogewogICAic2hhIjogIjFjNDUzZTQyM2QyMWZiZTMiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzY2LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDgxNywKICAgIm00YSI6IDEwMjA0CiAgfSwKICAic3luZGljYXRlX2RlcGxveV8wIjogewogICAic2hhIjogIjBhNWFlMTczNGFlNmQ5MTgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTE3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA3NiwKICAgIm00YSI6IDEyMDQ1CiAgfSwKICAic3luZGljYXRlX2RlcGxveV8xIjogewogICAic2hhIjogIjM0Y2QxYTM4NTZhYmEwMjkiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTg0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTg0NSwKICAgIm00YSI6IDExMzM5CiAgfSwKICAic3luZGljYXRlX2RlcGxveV8yIjogewogICAic2hhIjogIjFhMWQ3Zjk0NDRiZTg5NjgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExNTc2LAogICAibTRhIjogMTEyMTMKICB9LAogICJzeW5kaWNhdGVfaG9sZF8wIjogewogICAic2hhIjogIjIyMDgzMGU5NTIxMzk5ODgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNjU3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDIxNiwKICAgIm00YSI6IDk2NTQKICB9LAogICJzeW5kaWNhdGVfaG9sZF8xIjogewogICAic2hhIjogIjFkZTBjMzgwNzhiYjJmN2EiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTUyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTQzMSwKICAgIm00YSI6IDExMTMyCiAgfSwKICAic3luZGljYXRlX2hvbGRfMiI6IHsKICAgInNoYSI6ICI5ZDE1NDk1NjFiN2JiZWY4IiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjcxMywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTA0MjIsCiAgICJtNGEiOiA5OTczCiAgfSwKICAic3luZGljYXRlX21vdmVfMCI6IHsKICAgInNoYSI6ICI4MmU1OTY0MWY1Zjk4ZWVmIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjY1MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAxNzgsCiAgICJtNGEiOiA5NzA1CiAgfSwKICAic3luZGljYXRlX21vdmVfMSI6IHsKICAgInNoYSI6ICI4MThmMjNiNzYzMjUxZGJkIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjg3OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTEwMjAsCiAgICJtNGEiOiAxMDg0MQogIH0sCiAgInN5bmRpY2F0ZV9tb3ZlXzIiOiB7CiAgICJzaGEiOiAiNDM3YWVmMmNlM2Q4YjhjZSIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS4zMjMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDg5MDAsCiAgICJtNGEiOiA4MDA2CiAgfSwKICAic3luZGljYXRlX3BhdHJvbF8wIjogewogICAic2hhIjogIjU1ZTJkMzg1YWY4ZTRmYzIiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzcsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEwNzU0LAogICAibTRhIjogMTAyMzAKICB9LAogICJzeW5kaWNhdGVfcGF0cm9sXzEiOiB7CiAgICJzaGEiOiAiNjI3MGI4OWNlNGUxMTU3MiIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS40MzgsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDkyODMsCiAgICJtNGEiOiA4NDg0CiAgfSwKICAic3luZGljYXRlX3BhdHJvbF8yIjogewogICAic2hhIjogIjJkM2Y0MGFjNzk1OGQzOGYiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuODgxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTAyMiwKICAgIm00YSI6IDEwODcwCiAgfSwKICAic3luZGljYXRlX3NlbGVjdF8wIjogewogICAic2hhIjogIjhjYmU4ODUyNmU4MGM1MTIiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTE1LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTM1MCwKICAgIm00YSI6IDEwOTM1CiAgfSwKICAic3luZGljYXRlX3NlbGVjdF8xIjogewogICAic2hhIjogImYxMTg4ZTRhYzg1NzJhODgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzQzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDU2MiwKICAgIm00YSI6IDEwMTAxCiAgfSwKICAic3luZGljYXRlX3NlbGVjdF8yIjogewogICAic2hhIjogIjhkMjg1OWY0NGE2ZjRkMTAiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzQ4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDM3MSwKICAgIm00YSI6IDEwMTQzCiAgfSwKICAic3luZGljYXRlX3N0b3BfMCI6IHsKICAgInNoYSI6ICJiNzcxYzUzZmZhYTgxOWIwIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjA2OSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE4MDQsCiAgICJtNGEiOiAxMTc4MgogIH0sCiAgInN5bmRpY2F0ZV9zdG9wXzEiOiB7CiAgICJzaGEiOiAiMDdhN2E4YzlkNmZlYzZmOCIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS40OTEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDk1NjEsCiAgICJtNGEiOiA4NzcxCiAgfSwKICAic3luZGljYXRlX3N0b3BfMiI6IHsKICAgInNoYSI6ICI4ZTdiYWE5YzY0YjA2NzEwIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjQ4MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogOTI5MiwKICAgIm00YSI6IDg3NzYKICB9CiB9Cn0K (step_camera, done_camera, greeting,

   skip, graduation, react_low_power, react_hazard_default, …), which is what

   verifykeen.mjs diffs. This used to hash the line's own TEXT, which meant the

   gate at voiceOn() probed vo_keen_greeting while playback asked for

   vo_keen_greeting_19hur50, no rendered take could ever match either, and

   correcting a typo silently orphaned a take.



   `kind` no longer contributes to the key — it survives only as the bubble's

   hold/emphasis class. That is deliberate: it is what lets a 'recall' replay

   resolve to the SAME take as the 'step' line it is repeating instead of

   demanding a second recording of identical copy. */
function keenLineId(kind,id){
  if(!id) return null;
  return String(id).toLowerCase().replace(/[^a-z0-9_]/g,'');
}
/* CHARACTER VOICES.



   This used to be window.speechSynthesis. That could not work on the platforms

   this game ships to: Android WebView often has no installed TTS voice at all

   (silent no-op), iOS WKWebView gates speech behind a gesture and drops queued

   utterances on backgrounding, the voice you get is whatever the host OS

   happens to have, and none of it passes through the game's mixer — no volume

   slider, no ducking, no priority culling.



   The lines are now pre-rendered by tools/make-voices.py with Piper (open

   source, one distinct voice per faction) through a comms-radio filter chain,

   and played as ordinary samples. speechSynthesis survives only as the

   last-resort fallback for a build whose voice pack has not downloaded yet. */
function speakVoice(text,faction,action,idx,wx,wy){
  if(!voiceOn()) return;
  var fac=faction||'nova';
  if(action&&typeof voPlay==='function'){
    /* A slot the player has not heard yet is not decoded yet, and the decode is

       asynchronous. Reporting "no take" at that instant is what made the FIRST

       airing of every line — which for KEEN is the only airing — fall through to

       speech synthesis. voPlay now owns the wait and calls back only if the take

       genuinely cannot be produced. */
    if(voPlay(fac,action,wx,wy,idx,function(){ speakVoiceFallback(text,fac); })) return;
  }
  speakVoiceFallback(text,fac);
}
/* Chrome returns an EMPTY voice list on the first getVoices() call and fills it

   asynchronously, so the very first line of the tutorial — the greeting, the

   one a new player is most likely to notice missing — picked no voice and, on

   some builds, did not speak at all. Warm the list once at load. */
try{
  if(window.speechSynthesis && window.speechSynthesis.getVoices &&
     !window.speechSynthesis.getVoices().length){
    window.speechSynthesis.addEventListener('voiceschanged',function once(){
      window.speechSynthesis.removeEventListener('voiceschanged',once);
    });
    window.speechSynthesis.getVoices();
  }
}catch(e){}
function speakVoiceFallback(text,faction){
  try{
    if(!window.speechSynthesis) return;
    window.speechSynthesis.cancel();
    var u=new SpeechSynthesisUtterance(String(text).replace(/[\u2192\u25c6\u26cf\ud83c\udfd7\ud83c\udfed\ud83d\udca5\u26a0\u2715]/g,''));
    var v={keen:[1.06,.94],nova:[1.02,.82],ascendancy:[.92,.66],syndicate:[1.10,1.03],horde:[.78,.52]}[faction]||[1.02,.82];
    u.rate=v[0]; u.pitch=v[1];
    /* The last-resort OS voice cannot enter Web Audio, but it must still obey

       the same Voice Volume setting as the rendered commander/tutorial bank. */
    u.volume=0.85*(typeof audVoiceLevel==='function'?audVoiceLevel():1);
    var voices=window.speechSynthesis.getVoices?window.speechSynthesis.getVoices():[];
    if(voices.length&&faction){
      var prefer=faction==='ascendancy'?'male':(faction==='syndicate'||faction==='keen')?'female':faction==='horde'?'en':null;
      var chosen=voices.find(function(q){return prefer&&String(q.name).toLowerCase().indexOf(prefer)>=0;})||
                 voices.find(function(q){return /^en/i.test(q.lang);});
      if(chosen)u.voice=chosen;
    }
    window.speechSynthesis.speak(u);
  }catch(e){ /* degrade silently — the radio panel already carries the message */ }
}

/* ---------------------------------------------------------------------------

   MATCH-LEVEL SIGNAL TRACKING — runs every tick regardless of whether the

   tutorial is active, so replaying it mid-match can credit what the player

   already demonstrably did.

   --------------------------------------------------------------------------- */
function trackMatchSignals(){
  if(sawQueuedUnit()) MATCH.sawQueue=true;
  if(MATCH.cameraBase){
    var C=MATCH.cameraBase;
    if((typeof camUserT==='number'&&camUserT>0)||Math.abs(yawTarget-C.yaw)>.06||
       Math.abs(pitchTarget-C.pitch)>.035||Math.abs(orthoSpan-C.span)>24)MATCH.sawCamera=true;
  }
  if(typeof unitHigh==='number' && (!MATCH.sawSel||!MATCH.sawCombatSel)){
    for(var i=0;i<unitHigh;i++){
      if(!ualive[i]||uteam[i]!==0) continue;
      if(usel[i]) MATCH.sawSel=true;
      if(isCombatUnit(i)&&usel[i]) MATCH.sawCombatSel=true;
      if(MATCH.sawSel&&MATCH.sawCombatSel) break;
    }
  }
  if(typeof activePlatoon==='number'&&activePlatoon>=0&&ctrlGroups[activePlatoon]&&groupLive(ctrlGroups[activePlatoon]).length)
    MATCH.sawPlatoon=true;
  if(typeof orderConfirm!=='undefined'&&orderConfirm)MATCH.sawFormation=true;
  if(MATCH.scoutIdx>=0&&MATCH.scoutStart&&ualive[MATCH.scoutIdx]&&
     Math.hypot(ux[MATCH.scoutIdx]-MATCH.scoutStart.x,uy[MATCH.scoutIdx]-MATCH.scoutStart.y)>115)MATCH.sawScout=true;
  if(typeof abCool!=='undefined'){
    for(var k=0;k<abCool.length;k++){
      if(abCool[k]>(MATCH.lastAbCool[k]||0)+0.01) MATCH.usedAbility=true;
      MATCH.lastAbCool[k]=abCool[k];
    }
  }
}

/* ---------------------------------------------------------------------------

   TUTORIAL ENGINE

   --------------------------------------------------------------------------- */
function beginRun(){
  buildDOM();
  var active=courseSteps();
  TUT.active=true;
  TUT.graduated=false;
  TUT.stepIdx=0;
  TUT.shownStepIdx=-1;
  TUT.doneFlags=active.map(function(){ return false; });
  MATCH.sawSel=false;MATCH.sawCombatSel=false;MATCH.sawMove=false;MATCH.sawQueue=false;MATCH.sawCamera=false;
  MATCH.sawPickup=false;MATCH.sawPlatoon=false;MATCH.sawFormation=false;MATCH.sawAttackMove=false;MATCH.sawScout=false;MATCH.sawRetreat=false;
  MATCH.territoryAck=false;MATCH.objectiveAck=false;MATCH.cloudAck=false;MATCH.usedAbility=false;
  MATCH.startCombat=combatCount();MATCH.pickup=null;MATCH.scoutIdx=-1;MATCH.scoutStart=null;
  MATCH.cameraBase={yaw:yawTarget,pitch:pitchTarget,span:orthoSpan};
  MATCH.lastAbCool=(typeof abCool!=='undefined')?abCool.slice():[0,0,0,0];
  queue.length=0;keelMapSpeakingUntil=0;
  speak(GREETING,5.5,'greeting','greeting',GREETING_CUE);
}
function evalSteps(){
  if(!TUT.active) return;
  var active=courseSteps();
  for(var i=0;i<active.length;i++){
    if(TUT.doneFlags[i]) continue;
    var ok=false;
    try{ ok=!!active[i].test(); }catch(e){ ok=false; }
    if(ok){
      TUT.doneFlags[i]=true;
      if(i===TUT.stepIdx) speak(active[i].done,3.8,'done','done_'+active[i].id,STEP_ACKS[active[i].id]);
    }
  }
  while(TUT.stepIdx<active.length && TUT.doneFlags[TUT.stepIdx]) TUT.stepIdx++;
  var progressMeta=tutMeta();
  var progressKey=TUT.basicMode?'basicProgress':'progress';
  if(TUT.stepIdx>(progressMeta[progressKey]|0)){
    progressMeta[progressKey]=TUT.stepIdx;
    if(typeof metaSave==='function') metaSave();
  }
  if(TUT.stepIdx>=active.length){
    if(!TUT.graduated){
      TUT.graduated=true; TUT.active=false;
      var M=tutMeta();
      /* Dedicated Training commits completion in finishTrainingMission(),

         where persistence is checked before the commissioning event exists.

         Publishing basicDone here let the faction gate observe an in-memory

         success up to 1.3 seconds before the save transaction ran. */
      if(!TUT.trainingMode){
        M.skipped=false;M.version=GUIDE_VERSION;
        if(TUT.basicMode){M.basicDone=true;M.basicSkipped=false;M.basicProgress=BASIC_STEPS.length;}
        else{M.done=true;M.basicDone=true;M.progress=STEPS.length;M.basicProgress=BASIC_STEPS.length;}
      }
      restingText=''; restingTag=''; restingId=''; keelMapSpeakingUntil=0;
      speak(TUT.basicMode?BASIC_GRADUATION:GRADUATION,6.5,'graduation','graduation',
        TUT.basicMode?BASIC_GRADUATION_CUE:GRADUATION_CUE);
      if(typeof sfx==='function'){ try{ sfx('level'); }catch(e){} }
      if(TUT.trainingMode&&!TUT.finishTimer) TUT.finishTimer=setTimeout(finishTrainingMission,1300);
      else if(typeof metaSave==='function') metaSave();
    }
    return;
  }
  var cur=active[TUT.stepIdx];
  restingText=STEP_CUES[cur.id]||cur.say;
  restingId='step_'+cur.id;
  restingTag=(cur.icon||'◇')+'  STEP '+(TUT.stepIdx+1)+' / '+active.length;
  if(TUT.shownStepIdx!==TUT.stepIdx){
    TUT.shownStepIdx=TUT.stepIdx;
    speak(cur.say,5.5,'step',restingId,restingText);
  }
}
/* SKIP ends GUIDANCE. In a training MATCH it also ends the match — and it

   used to do that on a single tap of a 9px transparent label sitting on the

   guidance bar for the whole session, with no confirmation and no undo. The

   deliberate way out (pause > ABANDON MATCH) has always asked first; the

   accidental way out did not. Same question, same wording, before anything is

   thrown away. Guidance-only skips stay instant: nothing is lost. */
function tutSkip(confirmedState){
  if(!TUT.active) return;
  var liveTraining=TUT.trainingMode&&(typeof running!=='undefined'&&running)&&
                   !(typeof gameEnded!=='undefined'&&gameEnded);
  if(liveTraining&&!tutSkipConfirmed&&typeof accConfirm==='function'){
    var confirmState={trainingMode:!!TUT.trainingMode,basicMode:!!TUT.basicMode};
    accConfirm(TUT.basicMode
      ?'Skip the remaining basics and continue to required faction commissioning? This operation grants no payout.'
      :'Leave training and return to the menu? This operation grants no payout.',
      function(){ tutSkipConfirmed=true; tutSkip(confirmState); tutSkipConfirmed=false; });
    return;
  }
  TUT.active=false;
  var wasTraining=confirmedState?confirmedState.trainingMode:TUT.trainingMode;
  var wasBasic=confirmedState?confirmedState.basicMode:TUT.basicMode;
  var M=tutMeta(),skipSaved=false; M.skipped=true; M.version=GUIDE_VERSION;
  if(wasBasic){M.basicSkipped=true;M.basicDone=false;}
  queue.length=0;
  restingText=''; restingTag=''; restingId=''; keelMapSpeakingUntil=0;
  clearFocus();
  if(wasTraining){
    TUT.trainingMode=false; trainingLaunched=false; restoreTrainingConfig();
    if(typeof metaSave==='function')try{skipSaved=metaSave()!==false;}catch(e){skipSaved=false;}
    if(wasBasic&&typeof window.__MF_RETURN_TO_BASE_FOR_COMMISSIONING__==='function'){
      /* Galactic Command owns ordinary main-menu returns. Commissioning is the

         one protected base-document destination, so mark only this synchronous

         cleanup call and clear the marker before any later navigation can use it. */
      window.__MF_COMMISSIONING_RETURN_ACTIVE__=true;
      try{window.__MF_RETURN_TO_BASE_FOR_COMMISSIONING__();}
      finally{delete window.__MF_COMMISSIONING_RETURN_ACTIVE__;}
    }
    else if(typeof returnToMainMenu==='function') returnToMainMenu();
  } else if(typeof metaSave==='function')try{skipSaved=metaSave()!==false;}catch(e2){skipSaved=false;}
  speak(SKIP_LINE,4.0,'skip','skip');
  if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
  if(wasBasic&&skipSaved){
    var detail={schema:'massfront.protected-training-skipped.v1',course:'basic',
      speaker:'KEEL',affiliation:'uga',nextStep:'required-faction-selection',at:Date.now()};
    trainingSignal(TRAINING_SKIP_EVENT,detail);
    /* A player who changes their mind after landing chose to skip Training,

       not to abandon the new-career workflow. Converge on the same mandatory

       commissioning gate as the pre-drop skip path. */
    setTimeout(function(){
      try{
        var gate=window.MFNewCareerFactionGate;
        if(gate&&typeof gate.openFromRoute==='function')gate.openFromRoute({choice:'skipped'});
      }catch(e){}
    },0);
  }else if(wasBasic&&!skipSaved&&typeof toast==='function')toast('Training skip could not be saved — commissioning remains locked');
  TUT.basicMode=false;TUT.course='certification';
  syncTrainingChrome();
}

/* ---------------------------------------------------------------------------

   REACTIVE NARRATION — beyond the tutorial. Sparse on purpose: each of these

   fires at most once per match. The mechanical coach in hud.js already tells

   the player WHAT to fix (low energy, low mass); these only ever react to

   what just happened, and never repeat that advice.

   --------------------------------------------------------------------------- */
function checkReactive(){
  /* KEEL is the Tutorial guide, not the selected battlefield commander.

     Standard/Campaign radio belongs to the chosen faction commander. */
  if(!TUT.trainingMode) return;
  if(typeof running==='undefined'||!running) return;
  if(typeof demoMode!=='undefined'&&demoMode) return;
  /* Nothing here is meaningful before the HQ is actually down — there is no

     base to attack, no grid to stall, no wave to mass against. Gating on

     matchLive (rather than just `running`) also means any state left over

     from the tail of a PREVIOUS match — a grid still reading "stalled" in

     the last instant before defeat — has already decayed by the time it

     could matter again, instead of firing a premature bark during the next

     drop before the player has even landed. */
  if(typeof matchLive==='undefined'||!matchLive) return;
  if(typeof alarmT!=='undefined' && alarmT>0 && alarmT!==REACT.lastAlarmT){
    REACT.lastAlarmT=alarmT;
    /* pick() throws the chosen index away, and the index is exactly what the

       voice key needs — the two variants are two separate recordings. Indexed

       inline rather than changing pick(), which has other callers. */
    if(!REACT.baseAttack){ REACT.baseAttack=true;
      var bi=(Math.random()*BASE_ATTACK_LINES.length)|0;
      speak(BASE_ATTACK_LINES[bi],5.0,'reactive','react_base_attack'+bi); }
  }
  if(typeof stallE!=='undefined'){
    var nowStall=stallE>0;
    if(nowStall && !REACT.lastStallE && !REACT.lowPower){ REACT.lowPower=true; speak(LOW_POWER_LINE,4.6,'reactive','react_low_power'); }
    REACT.lastStallE=nowStall;
  }
  if(typeof HAZ!=='undefined'){
    var nowWarn=HAZ.warn>0;
    if(nowWarn && !REACT.lastHazWarn && !REACT.hazard){
      REACT.hazard=true;
      var key=(typeof curMap!=='undefined')?curMap:'';
      /* HAZARD_LINES._default is keyed with a leading underscore so it cannot

         collide with a map id; the rendered take is plain `react_hazard_default`.

         Guard the underscore explicitly — a map literally named "_default" would

         otherwise ask for a take that was never rendered. */
      var hk=(key&&key.charAt(0)!=='_'&&HAZARD_LINES[key])?key:'default';
      speak(HAZARD_LINES[key]||HAZARD_LINES._default,5.2,'reactive','react_hazard_'+hk);
    }
    REACT.lastHazWarn=nowWarn;
  }
  if(typeof stats!=='undefined' && stats.kills && !REACT.unitLost && ((stats.kills[1]|0)+(stats.kills[2]|0))>0){
    REACT.unitLost=true;
    var ui=(Math.random()*UNIT_LOST_LINES.length)|0;
    speak(UNIT_LOST_LINES[ui],4.2,'reactive','react_unit_lost'+ui);
  }
  if(typeof AI!=='undefined'){
    if(AI.warned && !REACT.lastWarned && !REACT.wave){ REACT.wave=true; speak(WAVE_LINE,5.0,'reactive','react_wave'); }
    REACT.lastWarned=!!AI.warned;
  }
}

/* ---------------------------------------------------------------------------

   MATCH-START DETECTION — the carrier goes active/phase 0 exactly once per

   drop (see newSkirmish() in src/main.js), so that edge is what arms a fresh

   run and resets every once-per-match latch. Polled, not hooked: this file

   never touches newSkirmish itself. */
function onNewMatchBegin(){
  REACT.baseAttack=false; REACT.lowPower=false; REACT.hazard=false; REACT.unitLost=false; REACT.wave=false;
  REACT.lastAlarmT=(typeof alarmT!=='undefined')?alarmT:0;
  REACT.lastStallE=false; REACT.lastHazWarn=false; REACT.lastWarned=false;
  MATCH.sawSel=false;MATCH.sawCombatSel=false;MATCH.sawMove=false;MATCH.sawQueue=false;MATCH.sawCamera=false;
  MATCH.sawPickup=false;MATCH.sawPlatoon=false;MATCH.sawFormation=false;MATCH.sawAttackMove=false;MATCH.sawScout=false;MATCH.sawRetreat=false;
  MATCH.territoryAck=false;MATCH.objectiveAck=false;MATCH.cloudAck=false;MATCH.usedAbility=false;
  MATCH.startCombat=combatCount();MATCH.pickup=null;MATCH.scoutIdx=-1;MATCH.scoutStart=null;
  MATCH.cameraBase={yaw:yawTarget,pitch:pitchTarget,span:orthoSpan};
  MATCH.lastAbCool=(typeof abCool!=='undefined')?abCool.slice():[0,0,0,0];
  if(TUT.trainingMode&&!TUT.active){ TUT.forceNext=false; beginRun(); }
}
function detectMatchEdge(){
  var dropping=(typeof carrier!=='undefined')&&carrier.active&&carrier.phase===0;
  if(dropping&&!prevDropping) onNewMatchBegin();
  prevDropping=dropping;
}

function tutTick(){
  try{
    detectMatchEdge();
    trackMatchSignals();
    trainingSafety();
    if(TUT.active) evalSteps();
    checkReactive();
    pump();
    /* A carrier/menu transition can hide KEEL while its speech queue is still

       holding the current line. TUT.active is the authoritative mission state,

       so never leave an active training objective visually absent just because

       the voice pump is between messages. */
    if(TUT.active&&restingText&&performance.now()/1000>=keelMapSpeakingUntil) showWrap(false);
    syncFocus();
    updateTrainingEntry();
    /* A carrier transition, a modal, or opening an interface briefly changes

       `running` in some mobile builds. Treating that as an abort was the

       reason a tap in training could silently throw the player to the menu.

       Training now ends only through Skip, completion, or the explicit return

       to main-menu transaction below. */
  }catch(e){ if(window.console) console.error('tutorial tick',e); }
}

/* ---------------------------------------------------------------------------

   TRAINING OPERATION — an explicit, safe mission. Normal PLAY never inherits

   these rules: every changed session field is snapshotted before launch and

   restored on completion, skip, or an early return to the main menu.

   --------------------------------------------------------------------------- */
var TRAINING_REWARD=150;
function saveTrainingConfig(){
  trainingPrev={difficulty:difficulty,defenseFocus:defenseFocus,infestationOn:infestationOn,
    wcChoice:wcChoice,goalSel:goalSel,timeLimit:timeLimit,resPace:resPace,crateRate:crateRate,
    curMap:curMap,curTheme:curTheme,aiFactionSel:aiFactionSel,playerStartZone:playerStartZone,
    activeWarMode:typeof activeWarMode==='string'?activeWarMode:'standard',
    aiSlots:aiSlots.map(function(A){return {on:!!A.on,diff:A.diff|0,zone:A.zone,ally:!!A.ally,behavior:A.behavior||'balanced'};}),
    opmods:Object.assign({},META.opmods||{}),threatSel:META.threatSel};
}
function restoreTrainingConfig(){
  if(!trainingPrev) return;
  difficulty=trainingPrev.difficulty; defenseFocus=trainingPrev.defenseFocus;
  infestationOn=trainingPrev.infestationOn; wcChoice=trainingPrev.wcChoice;
  goalSel=trainingPrev.goalSel; timeLimit=trainingPrev.timeLimit;
  resPace=trainingPrev.resPace; crateRate=trainingPrev.crateRate;
  curMap=trainingPrev.curMap; curTheme=trainingPrev.curTheme;
  aiFactionSel=trainingPrev.aiFactionSel; playerStartZone=trainingPrev.playerStartZone;
  if(typeof activeWarMode!=='undefined') activeWarMode=trainingPrev.activeWarMode||'standard';
  for(var i=0;i<aiSlots.length;i++){
    var A=trainingPrev.aiSlots[i]; if(!A) continue;
    aiSlots[i].on=A.on; aiSlots[i].diff=A.diff; aiSlots[i].zone=A.zone;aiSlots[i].ally=!!A.ally;aiSlots[i].behavior=A.behavior||'balanced';
  }
  META.opmods=trainingPrev.opmods; META.threatSel=trainingPrev.threatSel;
  trainingPrev=null;
}
function cancelTrainingMission(){
  if(!TUT.trainingMode&&!trainingPrev) return;
  TUT.trainingMode=false; trainingLaunched=false; TUT.active=false; TUT.finishTimer=0;
  queue.length=0; restingText=''; restingTag=''; restingId=''; keelMapSpeakingUntil=0; clearFocus(); hideWrap();
  document.body.classList.remove('trainingOperation');
  restoreTrainingConfig();
  /* Recover old OTA saves that retained trainingMode after losing the borrowed

     config snapshot; an invisible training mode must never reach Standard. */
  if(typeof activeWarMode!=='undefined'&&activeWarMode==='training') activeWarMode='standard';
  TUT.basicMode=false;TUT.course='certification';
  syncTrainingChrome();
  updateTrainingEntry();
}
function trainingStepId(){
  var active=courseSteps();return TUT.active&&active[TUT.stepIdx]?active[TUT.stepIdx].id:'';
}
function ensureTrainingPickup(){
  if(MATCH.sawPickup||(MATCH.pickup&&crates.indexOf(MATCH.pickup)>=0))return;
  var H=playerBld('hq');if(!H||typeof spawnCrate!=='function')return;
  var C=spawnCrate(H.x+155,H.y-55,'supply');
  C.alt=0;C.seen=true;C.t=0;C.site=true;C.siteName='TRAINING CACHE';C.announced=true;
  MATCH.pickup=C;
  if(typeof mmPing==='function')mmPing(C.x,C.y);
}
function ensureTrainingScout(){
  if(MATCH.scoutIdx>=0&&ualive[MATCH.scoutIdx])return;
  var H=playerBld('hq'),ty=-1;if(!H)return;
  for(var i=0;i<TYPES.length;i++)if(TYPES[i]&&TYPES[i].scout){ty=i;break;}
  if(ty<0)return;
  var u=spawnUnit(ty,0,H.x+95,H.y+55);if(u<0)return;
  clearSel();usel[u]=1;MATCH.scoutIdx=u;MATCH.scoutStart={x:ux[u],y:uy[u]};
  updateSelInfo();cam.x=ux[u];cam.y=uy[u];clampCam();camUpdateMatrices();
  toast('⌾ TRAINING SCOUT DEPLOYED — move it into black fog');
}
function trainingSafety(){
  if(!TUT.trainingMode||typeof running==='undefined'||!running||typeof matchLive==='undefined'||!matchLive) return;
  /* No scripted ambush while the player is reading. The enemy still builds a

     visible base and army, but its attack clocks remain parked until training

     completes. A resource floor prevents one mistaken duplicate placement from

     making the required lesson unaffordable. */
  if(typeof AI!=='undefined'){
    AI.waveTimer=Math.max(AI.waveTimer||0,300); AI.harassTimer=Math.max(AI.harassTimer||0,300); AI.warned=false;
  }
  /* A FLOOR, not income - econFloorBanks says so in its name and keeps the

     Math.min(cap,Math.max(cur,floor)) arithmetic verbatim. */
  if(typeof econFloorBanks==='function') econFloorBanks(620,2200);

  if(typeof heroIdx==='number'&&heroIdx>=0&&ualive[heroIdx]) uhp[heroIdx]=Math.max(uhp[heroIdx],uhpm[heroIdx]*.72);
  var H=playerBld('hq'); if(H) H.hp=Math.max(H.hp,H.hpm*.75);
  var step=trainingStepId();
  if(step==='pickup')ensureTrainingPickup();
  if(step==='fog')ensureTrainingScout();
  if(step==='tech'&&typeof heroLvl==='number')heroLvl=Math.max(heroLvl,3);
}
function startTrainingMission(){
  /* Standard deploy (setupStart → newSkirmish → deployCarrier) must never

     auto-enter Training. activeWarMode defaults to 'standard' on the menu, so

     this guard is only the live Standard battle, not the War Room card. */
  if(typeof running!=='undefined'&&running&&typeof matchLive!=='undefined'&&matchLive
     &&typeof activeWarMode!=='undefined'&&activeWarMode==='standard'
     &&!(typeof META!=='undefined'&&META.onboarding&&META.onboarding.choice==='training')){
    toast('Training is a separate operation — return to the menu or choose Training'); return;
  }
  if(typeof running!=='undefined'&&running){ toast('Leave the current battle before starting training'); return; }
  if(!trainingPrev) saveTrainingConfig();
  difficulty=0; defenseFocus=0; infestationOn=false; wcChoice=0;
  goalSel='annihilate'; timeLimit=0; resPace=1; crateRate=1;
  curMap='vanguard'; curTheme='verdant'; aiFactionSel='legion'; playerStartZone='sw';
  aiSlots[0].on=true; aiSlots[0].diff=0; aiSlots[0].zone='ne';aiSlots[0].ally=false;aiSlots[0].behavior='balanced';
  for(var i=1;i<aiSlots.length;i++){aiSlots[i].on=false;aiSlots[i].ally=false;aiSlots[i].behavior='balanced';}
  META.opmods={}; META.threatSel=1;
  var M=tutMeta(); M.skipped=false;M.basicSkipped=false;
  /* The first run is always the rebuilt basic course. New-career provenance

     makes that intent explicit, while a later replay after basics becomes the

     optional full certification. */
  TUT.basicMode=newCareerTrainingRequested()||(!M.basicDone&&!M.done);TUT.course=courseName();
  TUT.trainingMode=true; TUT.forceNext=true; TUT.finishTimer=0; trainingLaunched=false;
  /* Training is a real dedicated mode, not a Standard match with tutorial

     chrome painted over it. The saved mode is restored by the same transaction

     as the fixed map/rules, so reward and menu state cannot misclassify it. */
  if(typeof activeWarMode!=='undefined') activeWarMode='training';
  buildDOM(); document.body.classList.add('trainingOperation');
  /* The menu diorama can also animate a phase-0 carrier. Reset the edge latch

     before newSkirmish so that decorative drop cannot consume the real

     Training Operation's start signal. */
  prevDropping=false;
  if(typeof initAudio==='function') initAudio();
  if(typeof hideFrontScreens==='function') hideFrontScreens();
  /* Training uses the same selected-map deployment dossier as every other

     battle route. Previously it showed the shell's stale prior battlefield

     because this one entry point never populated #loadScr. */
  if(typeof mfLoadScreenFill==='function') mfLoadScreenFill();
  var load=document.getElementById('loadScr'); if(load) load.style.display='flex';
  requestAnimationFrame(function(){requestAnimationFrame(function(){
    applyTheme(); newSkirmish();fogOn=true;updateFog();trainingLaunched=true;
    /* The carrier edge is also observed by the normal polling path, but a

       loaded/fast device can advance the drop state between two 350ms polls.

       Training launch is the authoritative transaction, so arm guidance here

       and let the edge detector's !active guard avoid a second reset. */
    if(!TUT.active){TUT.forceNext=false;beginRun();}
    if(load) load.style.display='none';
    /* The loader counted as a front screen during the first layout. Recompute

       now so the dedicated Tutorial exposes KEEL and its deployment HUD. */
    if(typeof stopAttract==='function') stopAttract();
    if(typeof mfFlowLayout==='function') mfFlowLayout();
    toast(TUT.basicMode
      ?'PLANETARY BASICS — protected drop; faction commissioning follows'
      :'TRAINING OPERATION — war table skipped; threat paused while KEEL guides you');
  });});
}
function finishTrainingMission(){
  TUT.finishTimer=0;
  if(!TUT.trainingMode) return;
  var wasBasic=TUT.basicMode,active=courseSteps(),M=tutMeta(),before={
    done:M.done,basicDone:M.basicDone,skipped:M.skipped,basicSkipped:M.basicSkipped,
    version:M.version,progress:M.progress,basicProgress:M.basicProgress,rewardedVersion:M.rewardedVersion
  },reward=(M.rewardedVersion|0)<GUIDE_VERSION?TRAINING_REWARD:0;
  M.skipped=false;M.version=GUIDE_VERSION;
  if(wasBasic){M.basicDone=true;M.basicSkipped=false;M.basicProgress=BASIC_STEPS.length;}
  else{M.done=true;M.basicDone=true;M.progress=STEPS.length;M.basicProgress=BASIC_STEPS.length;}
  if(reward){ metaGrantCores(reward,'training_reward','training:'+GUIDE_VERSION); M.rewardedVersion=GUIDE_VERSION; }
  TUT.trainingMode=false; trainingLaunched=false; TUT.active=false;
  document.body.classList.remove('trainingOperation');
  clearFocus(); restoreTrainingConfig();
  var saved=false;
  if(typeof metaSave==='function')try{saved=metaSave()!==false;}catch(e){saved=false;}
  if(!saved){
    M.done=before.done;M.basicDone=before.basicDone;M.skipped=before.skipped;M.basicSkipped=before.basicSkipped;
    M.version=before.version;M.progress=before.progress;M.basicProgress=before.basicProgress;M.rewardedVersion=before.rewardedVersion;
  }
  if(typeof renderMetaHead==='function') renderMetaHead();
  if(typeof gameEnded!=='undefined') gameEnded=true;
  if(typeof paused!=='undefined') paused=false;
  if(typeof running!=='undefined') running=false;
  var title=document.getElementById('goTitle'); if(title){title.textContent='TRAINING COMPLETE';title.style.color='#9fffc4';}
  var out=document.getElementById('goOutcome'); if(out) out.textContent=(wasBasic?'PLANETARY BASICS':'FIELD CERTIFICATION')+' · ALL '+active.length+' OBJECTIVES COMPLETE';
  var gs=document.getElementById('goStats');
  if(gs) gs.innerHTML='<div class="goStatGrid"><div><b>'+active.length+' / '+active.length+'</b><span>OBJECTIVES</span></div>'
    +'<div><b>'+((stats&&stats.built&&stats.built[0])|0)+'</b><span>STRUCTURES BUILT</span></div>'
    +'<div><b>'+((stats&&stats.t)|0)+'s</b><span>TRAINING TIME</span></div>'
    +'<div><b>READY</b><span>FIELD STATUS</span></div></div>';
  var rw=document.getElementById('goRewards');
  if(rw) rw.innerHTML='<section class="goSection"><h3>TRAINING PAYOUT</h3><div class="goPayout">'
    +'<div><b>+'+reward+'</b><span>⬡ CORES</span></div><div><b>COMPLETE</b><span>KEEL ORIENTATION</span></div></div>'
    +'<div class="goNotice good">'+(wasBasic
      ?'PLANETARY BASICS COMPLETE · FACTION COMMISSIONING AND COMMANDER 1 ARE NEXT'
      :'FIELD CERTIFICATION COMPLETE · REPLAY ANY TIME FROM SETTINGS OR OPERATIONS')+'</div></section>';
  if(typeof drawMatchChart==='function') drawMatchChart();
  var go=document.getElementById('gameOver'); if(go) go.style.display='flex';
  if(typeof sfx==='function'){ try{sfx('level');}catch(e){} }
  if(wasBasic&&saved){
    trainingSignal(TRAINING_COMPLETE_EVENT,{schema:'massfront.protected-training-complete.v1',
      course:'basic',objectives:active.length,speaker:'KEEL',affiliation:'uga',
      nextStep:'required-faction-selection',at:Date.now()});
  }else if(wasBasic&&!saved&&typeof toast==='function'){
    toast('Training finished, but career progress could not be saved');
  }
  TUT.basicMode=false;TUT.course='certification';
  syncTrainingChrome();
  updateTrainingEntry();
}

function needsTraining(){
  var M=tutMeta(); return !M.skipped&&!M.basicSkipped&&!M.basicDone&&!M.done;
}
function trainingUiState(){
  var M=tutMeta(),certified=!!M.done&&(M.version|0)>=GUIDE_VERSION,
      basicDone=!!M.basicDone||certified,done=basicDone,
      active=!!TUT.trainingMode&&!!TUT.active&&typeof running!=='undefined'&&!!running,
      course=active?courseName():(certified?'certification':'basic'),
      total=active?courseSteps().length:(certified?STEPS.length:BASIC_STEPS.length),
      progress=active?TUT.stepIdx:certified?STEPS.length:basicDone?BASIC_STEPS.length:
        Math.min(BASIC_STEPS.length,M.basicProgress|0),
      interrupted=!done&&!active&&(progress>0||!!M.skipped||!!M.basicSkipped),rewarded=(M.rewardedVersion|0)>=GUIDE_VERSION;
  return {done:done,basicDone:basicDone,certified:certified,active:active,course:course,total:total,
    interrupted:interrupted,progress:progress,rewarded:rewarded,
    state:active?(TUT.basicMode?'PLANETARY BASICS ACTIVE':'FIELD CERTIFICATION ACTIVE'):
      certified?'FIELD CERTIFIED · REPLAYABLE':basicDone?'BASICS COMPLETE · CERTIFICATION AVAILABLE':
      interrupted?'INCOMPLETE · RESTARTABLE':'RECOMMENDED · SHORT PROTECTED DROP',
    action:active?'▶ RESUME TRAINING':certified?'↻ REPLAY CERTIFICATION':basicDone?'▶ START FIELD CERTIFICATION':
      interrupted?'↻ RESTART BASICS':'▶ START PLANETARY BASICS'};
}
function updateTrainingEntry(){
  var S=trainingUiState();
  /* The training card's label and state line come from trainingUiState(), the

     same source the Operations card reads, so the two can never disagree. The

     old 350 ms poll rebuilt `#warGrid` unconditionally; if that landed between

     a card's pointerdown and pointerup, mfBindTap's press record disappeared

     and the card silently did nothing. Refresh only when its rendered state

     changed, preserving the live control and its accessibility semantics

     through ordinary taps. */
  var wr=document.getElementById('warScr'),grid=document.getElementById('warGrid');
  var warSig=[S.done,S.certified,S.active,S.course,S.total,S.interrupted,S.progress,S.rewarded,S.state,S.action].join(':');
  if(wr&&wr.style.display&&wr.style.display!=='none'&&typeof renderWarRoom==='function'
     &&(!grid||grid.dataset.mfTrainingSig!==warSig)) renderWarRoom();
  var op=document.getElementById('keelTrainingOp'),sig=[S.done,S.certified,S.active,S.course,S.total,S.interrupted,S.progress,S.rewarded].join(':');
  if(op&&op.dataset.stateSig!==sig) appendTrainingOperation();
}
function openTrainingBrief(){
  if(typeof renderOps==='function') renderOps();
  if(typeof showFrontScreen==='function') showFrontScreen('opsScr');
  var ops=document.getElementById('opsScr');
  if(ops&&typeof mfSetTabs==='function') mfSetTabs(ops,'weekly',false);
  var sc=ops&&ops.querySelector('.opsScroll'); if(sc) sc.scrollTop=0;
  if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
}
function resumeTrainingMission(){
  var S=trainingUiState();
  if(!S.active){ startTrainingMission(); return; }
  if(typeof hideFrontScreens==='function') hideFrontScreens();
  var po=document.getElementById('pauseOverlay'); if(po) po.style.display='none';
  if(typeof paused!=='undefined') paused=false;
  if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
}
function ensureTrainingEntry(){
  /* The old entry was an identity CARD inserted BEFORE #startBtn, so the first

     thing on the menu was a training advert and PLAY got pushed down the screen

     behind it. Training is one destination, not a banner: it is now a single

     dedicated button sitting directly UNDER PLAY, styled as a peer of it

     (.mbtn.alt) so the two read as one primary + one secondary action.

     openTrainingBrief() is still reachable from Operations and Settings. */
  /* Training is a WAR ROOM card now, not a menu button — the menu is back to a

     single primary control. Both of this function's older entries (the identity

     callout above PLAY, and the standalone #trainBtn below it) are torn down

     here so an OTA patch landing on either shell converges on the same menu. */
  var stale=document.getElementById('keelTrainingCallout'); if(stale) stale.remove();
  var oldBtn=document.getElementById('trainBtn'); if(oldBtn) oldBtn.remove();
  updateTrainingEntry();
}
function appendTrainingOperation(){
  /* Operations was deliberately consolidated to one deployable-operation pane.

     The old Threat pane no longer exists; targeting it silently removed KEEL's

     unique Training entry from both orientations. Keep an old-shell fallback

     while the current shell inserts Training beside the weekly operation. */
  var pane=document.getElementById('opsPane-weekly')||document.getElementById('opsPane-threat'); if(!pane) return;
  var old=document.getElementById('keelTrainingOp'); if(old) old.remove();
  var S=trainingUiState(),displayDone=!S.active&&S.done,progressPct=Math.round(S.progress/Math.max(1,S.total)*100),
      topicHtml=S.course==='basic'
        ?'<span>◇ CAMERA</span><span>⚓ LANDING</span><span>★ COMMANDER</span><span>⬡ ECONOMY</span><span>▣ PRODUCTION</span><span>➤ ORDERS</span><span>◎ EXTRACTION</span>'
        :'<span>⌁ LOGISTICS</span><span>⬢ COUNTERS</span><span>⛨ DEFENCE</span><span>Ⅳ PLATOONS</span><span>⌾ SCOUTING</span><span>⌬ TECH</span><span>✦ POWERS</span>',
      rewardText=S.rewarded?'FIRST-CLEAR REWARD CLAIMED':'FIRST CLEAR · +'+TRAINING_REWARD+' ⬡ CORES';
  var d=document.createElement('article'); d.id='keelTrainingOp';
  d.className='keelTrainingOp'+(displayDone?' done':'')+(S.active?' active':'');
  d.dataset.stateSig=[S.done,S.certified,S.active,S.course,S.total,S.interrupted,S.progress,S.rewarded].join(':');
  d.dataset.progress=String(S.progress);
  d.innerHTML='<header class="ktoHead"><span>'+S.state+'</span><b>'+rewardText+'</b></header>'
    +'<div class="ktoHero"><div class="ktoPortrait ktoUgaPortrait" role="img" aria-label="KEEL, UGA ship liaison">'
      +'<span class="ktoUgaSeal"><b>UGA</b><i>KEEL</i></span></div>'
      +'<div class="ktoIdentity"><small>UGA TRAINING OPERATION 01</small><b>'+(S.course==='basic'?'PLANETARY BASICS':'FIELD CERTIFICATION')+'</b>'
      +'<i>KEEL · UGA SHIP LIAISON</i>'
      +'<p>'+(S.course==='basic'?'<b>Short faction-neutral orientation.</b> Learn the essentials before faction and Commander 1 commissioning.':'<b>Optional advanced course.</b> Continue into counters, logistics, defence, formations, scouting and technology.')+'</p></div></div>'
    +'<div class="ktoTeach" aria-label="Training topics">'+topicHtml+'</div>'
    +'<div class="ktoProgress"><div><span>OBJECTIVE PROGRESS</span><b>'+S.progress+' / '+S.total+'</b></div>'
      +'<div class="ktoProgressTrack"><i style="width:'+progressPct+'%"></i></div>'
      +'<small>'+(S.active?'Current lesson: '+((courseSteps()[Math.min(TUT.stepIdx,courseSteps().length-1)]||{}).id||'field systems').toUpperCase():
                   S.done?'Orientation certified · replay does not repeat the first-clear reward':
                   S.interrupted?'Previous best retained · a new drop restarts at camera control':S.total+' guided objectives · progress is saved to this profile')+'</small></div>'
    +'<div class="ktoFacts"><div><b>'+S.total+'</b><span>OBJECTIVES</span></div><div><b>EASY</b><span>FIXED OPPONENT</span></div>'
      +'<div><b>SAFE</b><span>NO INFESTATION</span></div></div>'
    +'<button class="ktoAction" type="button">'+S.action+'</button>';
  pane.insertBefore(d,pane.firstChild);
  var b=d.querySelector('button');
  var act=S.active?resumeTrainingMission:startTrainingMission;
  if(typeof mfBindTap==='function') mfBindTap(b,act); else b.addEventListener('click',act);
}

/* ---------------------------------------------------------------------------

   SETTINGS — a "Tutorial" row and a voice toggle, appended to #setList after

   renderSettings() (src/game/meta.js) rebuilds it. That function replaces

   #setList's entire innerHTML on every call — including every time a player

   flips ANY setting — so appending once at boot would vanish on the first

   unrelated toggle. Wrapping the function, the same way commander.js wraps

   econTick() and meta.js wraps heroXP(), means our row survives every

   rebuild without ever editing meta.js. */
function tutSettingsAction(){
  if(trainingUiState().active){
    resumeTrainingMission();
    return;
  }
  var midMatch=typeof running!=='undefined'&&running;
  if(midMatch){
    toast('Training is a separate operation — return to the main menu to replay it');
  } else {
    startTrainingMission();
  }
  if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
  if(midMatch&&typeof renderSettings==='function') renderSettings();
}
function appendTutorialSettingsRow(){
  var list=document.getElementById('setList');
  if(!list) return;
  var M=tutMeta(),S=trainingUiState();
  var status=TUT.active? ('Training in progress — step '+(TUT.stepIdx+1)+' of '+courseSteps().length)
             : S.certified? 'Field certification completed'
             : S.basicDone? 'Planetary basics completed · advanced certification available'
             : M.skipped||M.basicSkipped? 'Skipped'
             : 'Not started yet';
  var label=S.active?'▶ RESUME':S.certified?'↺ REPLAY':S.basicDone?'▶ ADVANCED':(!M.skipped&&!M.basicSkipped&&!TUT.active)?'▶ START':'↺ RESTART';
  var row=document.createElement('div');
  row.className='sItem setRow';
  row.id='keelSetRow';
  row.setAttribute('role','button');
  row.setAttribute('tabindex','0');
  row.innerHTML='<div class="sTx"><b>🎓 Training Operation</b><div class="sDs">'+status+' — protected planetary mission with real-state objectives</div></div>'
    +'<div class="sBuy">'+label+'</div>';
  var act=function(ev){ if(ev){ ev.preventDefault(); } tutSettingsAction(); };
  row.addEventListener('pointerdown',act);
  row.addEventListener('keydown',function(ev){ if(ev.key==='Enter'||ev.key===' ') act(ev); });
  list.appendChild(row);

  var vOn=!!(typeof META!=='undefined'&&META.settings&&META.settings.tutorialVoice);
  var vrow=document.createElement('div');
  vrow.className='sItem setRow';
  vrow.setAttribute('role','button');
  vrow.setAttribute('tabindex','0');
  vrow.innerHTML='<div class="sTx"><b>🔊 KEEL Voice</b><div class="sDs">Speak tutorial lines aloud, if your device supports it</div></div>'
    +'<div class="sBuy togB'+(vOn?' onT':'')+'">'+(vOn?'ON':'OFF')+'</div>';
  var vact=function(ev){
    if(ev){ ev.preventDefault(); }
    if(typeof META==='undefined'||!META.settings) return;
    META.settings.tutorialVoice=!META.settings.tutorialVoice;
    if(typeof metaSave==='function') metaSave();
    if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
    if(typeof renderSettings==='function') renderSettings();
  };
  vrow.addEventListener('pointerdown',vact);
  vrow.addEventListener('keydown',function(ev){ if(ev.key==='Enter'||ev.key===' ') vact(ev); });
  list.appendChild(vrow);
}
if(typeof renderSettings==='function'){
  var _keelRenderSettings=renderSettings;
  renderSettings=function(){
    _keelRenderSettings();
    appendTutorialSettingsRow();
  };
}

/* ---------------------------------------------------------------------------

   BOOT

   --------------------------------------------------------------------------- */
function initTutorial(){
  if(window.__keelInit) return;
  window.__keelInit=true;
  buildDOM();
  ensureTrainingEntry();
  if(typeof renderOps==='function'&&!window.__keelOpsWrapped){
    window.__keelOpsWrapped=true;
    var _keelRenderOps=renderOps;
    renderOps=function(){ _keelRenderOps(); appendTrainingOperation(); };
    appendTrainingOperation();
  }
  if(typeof orderMove==='function'&&!window.__keelMoveWrapped){
    window.__keelMoveWrapped=true;
    var _keelOrderMove=orderMove;
    orderMove=function(wx,wy,patrol,retreat){
      var combat=hasSelectedCombatUnit(),formationDone=MATCH.sawFormation,attackMode=typeof moveMode==='undefined'||!moveMode,
          ok=_keelOrderMove(wx,wy,patrol,retreat);
      /* Retreat is the one order the player cannot reach from a button, so

         it is credited from any live selection rather than a combat-only

         one — the Commander breaking off counts as having learned it. */
      if(ok&&retreat&&!patrol) MATCH.sawRetreat=true;
      if(ok&&combat&&!patrol){
        MATCH.sawMove=true;
        /* The formation release itself also calls orderMove. Requiring a

           formation observed on an earlier poll ensures the next deliberate

           map tap teaches attack-move instead of silently auto-completing. */
        if(formationDone&&attackMode&&!retreat)MATCH.sawAttackMove=true;
      }
      return ok;
    };
  }
  if(typeof applyCrate==='function'&&!window.__keelPickupWrapped){
    window.__keelPickupWrapped=true;
    var _keelApplyCrate=applyCrate;
    applyCrate=function(k,x,y){
      var out=_keelApplyCrate(k,x,y);
      if(TUT.trainingMode)MATCH.sawPickup=true;
      return out;
    };
  }
  setInterval(tutTick,350);
}
window.initTutorial=initTutorial;
/* Main-menu and War Room navigation own application-level entry/exit

   transactions. This file is intentionally scoped in an IIFE, so expose the

   small public bridge explicitly: otherwise meta.js' defensive typeof checks

   see no Training handlers at all and the card silently does nothing. */
window.cancelTrainingMission=cancelTrainingMission;
window.trainingMissionActive=function(){return !!(TUT.trainingMode||trainingPrev);};
window.trainingUiState=trainingUiState;
window.resumeTrainingMission=resumeTrainingMission;
window.needsTraining=needsTraining;
/* commander.js legacyVoiceSpeak checks window.speakVoice before voPlay.

   __tutDebug alone is not enough — the global must exist for battle comms. */
window.speakVoice=speakVoice;
/* Inspection hook for automated verification — read-only, harmless to leave

   wired for real players (no different from any other console-reachable

   global in this codebase). */
window.__tutDebug=function(){ return {TUT:TUT,MATCH:MATCH,REACT:REACT,STEPS:STEPS,BASIC_STEPS:BASIC_STEPS,
  activeSteps:courseSteps(),trainingCompleteEvent:TRAINING_COMPLETE_EVENT,trainingSkipEvent:TRAINING_SKIP_EVENT,
  needsTraining:needsTraining(),startTraining:startTrainingMission,
  /* The voice plumbing lives inside this IIFE, so verifykeen.mjs cannot reach

     speak()/keenLineId()/pump() any other way. Read-only handles, no state. */
  speak:speak,pump:pump,queue:queue,voiceOn:voiceOn,keenLineId:keenLineId,
  speakVoice:speakVoice,beginRun:beginRun,
  keenKey:function(kind,id){ return 'vo_keen_'+keenLineId(kind,id); }}; };

})();