File size: 106,004 Bytes
b51e03a
3597f5a
c0d5f19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597f5a
 
 
 
 
 
 
 
c0d5f19
 
 
 
3597f5a
 
 
 
 
 
 
 
 
c0d5f19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd8e34c
 
c0d5f19
cd8e34c
c0d5f19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd8e34c
c0d5f19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b51e03a
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b51e03a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597f5a
 
 
 
 
 
 
 
 
 
 
b51e03a
 
 
 
3597f5a
 
 
 
 
 
 
 
 
b51e03a
 
 
 
 
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b51e03a
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
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
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
;
;
/* ============================================================================
   BROOD SWARM β€” UNIT MODELS 30..32
   ----------------------------------------------------------------------------
   Load after src/engine/models.js. Everything here is built from the ORGANIC
   kit that already lives there β€” carapace(), broodLeg(), boneSeg(),
   chitinBlade(), tendril(), tubercles(), eyeCluster() β€” because the Brood's
   whole readability rests on the roster looking like ONE species. A Sovereign
   assembled from a different vocabulary than the Ravager it spawned beside is
   not a hero, it is a second faction.

   The kit covered slots 0..27. These are the three creatures that were still
   falling through to the base roster and rendering as somebody else's
   geometry: the faction's hero, its caster, and its miner.

     30  The Brood Sovereign   hero  hp 7400  dmg 110  rng 34   melee
     31  Brood Tidecaster      sup   hp 390   dmg 24   rng 142  caster
     32  Prospector            sup   hp 190   dmg 0    rng 0    UNARMED miner

   +X is forward, +Y is up, Z is lateral, feet at y=0. Organisms have no
   turret, so every builder returns tur:null.

   ----------------------------------------------------------------------------
   MATERIALS ARE THE RIG.
   The vertex stage keys its animation off the MATERIAL ID, not off a bone
   count or a name: MAT.LEAF (BIO_LEG/BIO_MEM) gets the two-oscillator spring
   bend, MAT.CHITIN gets the breathing pulse, MAT.SERVO gets the machine walk
   cycle. Nothing here may ever wear SERVO β€” a Brood creature stepping through
   a servo gait is a mech wearing a shell.

   The catch is that models.js only patches the material onto the PRIMITIVE
   builders (box/cyl/sphere/wedge/extrude/ring/bevelBox/inset/tube/greeble).
   sculpt(), quad() and therefore everything built out of boneSeg() β€”
   chitinBlade, tendril, broodLeg's segments, every helper below β€” inherit
   whatever material the last primitive call happened to leave on the builder.
   Build a carapace right after a lamp-coloured eye and the whole body animates
   as a lamp; build one after a leg and the body flexes like a limb. That is
   invisible in a screenshot and only shows up as a creature whose shell
   breathes wrong.

   So the surface is declared EXPLICITLY before every stretch of unpatched
   geometry, via the three helpers below. It costs one call and removes the
   entire class of bug.
   ============================================================================ */

/* Shell: hard chitin. Breathes, takes the subsurface term, does not flex. */
function brdShell(m){ return m.mat(MAT.CHITIN).team(0); }
/* Limb: everything that hangs, reaches, trails or whips β€” legs, scythes,
   mandibles, spines, antennae. Boned geometry is driven by its hinge chain;
   unboned geometry gets the spring follow-through. Both need this id. */
function brdLimb(m){ return m.mat(MAT.LEAF).team(0); }
/* Livery: faction-tinted living tissue, the organic equivalent of TEAM_A.
   Without a few of these patches a creature renders untinted and the player
   cannot tell whose army just walked over the ridge. */
function brdLivery(m){ return m.mat(MAT.CHITIN).team(1); }

/* Local palette. Being absent from COL_MAT does NOT keep the primitive patch
   off the material we just declared: matDetect() consults COL_MAT first and
   then falls through to a luminance heuristic that always returns something.
   Anything here handed to a PATCHED primitive is silently reclassified β€” SOFT
   lands on hull, GULLET on hull.dark β€” which is what the slot 32 contract at
   the foot of this file has to undo on the feeding rasp. Only the two that are
   read inside a sculpt paint callback (SEAM, LUMEN) never reach a primitive
   and are genuinely inert. */
const BRD_GULLET=C(38,32,26);        // wet dark inside the feeding mouth
const BRD_SEAM  =C(22,24,18);        // near-black crease between shell plates
const BRD_SOFT  =C(216,198,152);     // pale soft grub tissue β€” no armour value
const BRD_LUMEN =C(214,190,236);     // lit synaptic tissue, reads under livery
/* Combat gore triples [wet, dark, highlight]. Mucus green matches
   MAT.BROOD_SLIME / infestation INF_MUCUS; amber is acid organs; violet is
   the horde FX palette (186,82,245). Not human crimson. organicfx.js reads
   this so a later livery retint cannot silently ship the wrong fluid. */
const BRD_ICHOR=Object.freeze([
  Object.freeze([[115,177,77],[62,88,30],[185,255,72]]),
  Object.freeze([[214,168,40],[78,48,12],[255,220,110]]),
  Object.freeze([[186,82,245],[48,18,64],[220,150,255]])
]);

/* Deterministic 2D value noise, seeded, for the sculpts below. carapace() has
   its own private copy; duplicating four lines is cheaper than exporting a
   fifth global from models.js just to share it. */
function brdNoise(seed){
  const sd=(seed||3)>>>0;
  const h=(a,b)=>{ let n=(a*374761393+b*668265263+sd)>>>0;
    n=(n^(n>>>13))*1274126177>>>0; return ((n^(n>>>16))>>>0)/4294967296; };
  return (a,b)=>{
    const ia=Math.floor(a), ib=Math.floor(b), fa=a-ia, fb=b-ib;
    const sa=fa*fa*(3-2*fa), sb=fb*fb*(3-2*fb);
    return (h(ia,ib)*(1-sa)+h(ia+1,ib)*sa)*(1-sb)+(h(ia,ib+1)*(1-sa)+h(ia+1,ib+1)*sa)*sb;
  };
}

/* ---------------------------------------------------------------------------
   DORSAL CREST β€” the raised armoured shield the Sovereign carries over its
   shoulders, and the smaller collar it wears at the throat.

   This is its own sculpt rather than a third carapace() because carapace masks
   all of its relief with cos(u) β€” the +Z flank β€” so its ridges and keel run
   down ONE SIDE of the body, not along the spine. That is fine on a body seen
   from a three-quarter camera, but a crest is read entirely from above: its
   ribs have to fan across the top or the whole part collapses into a smooth
   lens. Here the rib mask keys off the lateral coordinate and the vertical one
   independently, so the plates fan out from the spine the way a crab's do.

   A lens section, not an open shell: the underside is flattened to 16% of the
   rise and buries itself in the carapace, which keeps the part watertight
   (no back faces to cull) for six extra quads.
   --------------------------------------------------------------------------- */
function brdCrest(m,x,y,z,len,wid,rise,ribs,col,seam,seed){
  const nz=brdNoise(seed||17), lo=seam||BRD_SEAM;
  const plan=v=>Math.pow(Math.sin(Math.PI*Math.pow(v,0.74)),0.52);
  /* Lateral index across the shield, 0..ribs. Derived from cos so the rib
     count is the same on both flanks and the seam falls on the spine. */
  /* THREE grooves, not seven, and eighteen samples around the section.
     u sweeps rim -> crown -> rim across the top half, so with segU=18 the top
     gets nine samples for `ribs` periods. The first version asked for seven
     ribs at segU=14 β€” barely one sample per groove β€” and a smoothstepped
     sawtooth sampled at its own frequency does not alias into a soft ridge, it
     aliases into NOTHING: the shield rendered as a bare smooth dome and every
     other decision about it was invisible. Three grooves is what fits, and
     three is also what actually reads at 40 pixels. */
  const lat=ct=>(ct*0.5+0.5)*ribs;
  m.sculpt(x,y,z,18,8,(u,v)=>{
    const th=u*TAU, ct=Math.cos(th), st=Math.sin(th);
    const w=plan(v);
    const f=lat(ct)%1, rib=f*f*(3-2*f);
    /* Ribs stand proudest on the crown and die out at the rim, which is what
       makes the edge read as a thin plate rather than a corrugated bowl. */
    const crown=Math.max(0,st);
    let r=1+0.26*(rib-0.5)*2*crown+0.055*(nz(u*15,v*11)-0.5)*2;
    r+=0.12*Math.pow(crown,3);                        // spine ridge down the middle
    /* Scalloped trailing edge β€” a rank of shallow bays cut into the rim is
       the single cheapest thing that stops a shield looking machined. */
    r*=1-0.09*Math.pow(v,2.6)*Math.cos(lat(ct)*TAU);
    return [(v-0.5)*len, st>0?st*rise*w*r:st*rise*0.16*w, ct*wid*w*r];
  },(u,v)=>{
    const th=u*TAU, ct=Math.cos(th), st=Math.sin(th);
    const f=lat(ct)%1, d=Math.min(f,1-f)*2;
    let k=Math.pow(d,0.44); k=k*k*(3-2*k);
    k*=0.58+0.42*Math.max(0,st);                       // underside stays dark
    k*=0.82+0.18*nz(u*27,v*19);                        // pore stipple
    k=0.16+0.84*k;
    return [lo[0]+(col[0]-lo[0])*k, lo[1]+(col[1]-lo[1])*k, lo[2]+(col[2]-lo[2])*k];
  },false,false);
  return m;
}

/* ---------------------------------------------------------------------------
   SCYTHE ARM β€” the Sovereign's forward weapon.
   Two hinged links and an opposed pair of curved blades at the wrist. It is a
   RAPTORIAL arm, not a leg: the elbow rides high and outboard and the wrist
   drops forward and inboard, so the pair of them frame the head instead of
   splaying like limbs five and six. Only two joints, because the bone budget
   is 80 and eight seven-segment legs already spend 56 of them.
   --------------------------------------------------------------------------- */
function brdScythe(m,x,y,z,side,reach,th,phase){
  const S=side, R=reach;
  /* Every offset scales with reach, and the shoulder starts a long way
     OUTBOARD. The first version socketed the arm at z=Β±0.6 β€” inside the
     thorax β€” so both arms ran through the body and out through the head, and
     the one feature that separates a Sovereign from a big Ravager rendered as
     a couple of stray blades near the jaw. The chain now goes out, up, then
     forward and back in, so the two claws close in front of the face. */
  const P=[
    [x,          y,          z+S*R*0.42],                // shoulder, on the flank
    [x+R*0.42,   y+R*0.14,   z+S*R*0.64],                // elbow, raised and outboard
    [x+R*0.98,   y-R*0.28,   z+S*R*0.32],                // wrist, forward and drawn in
  ];
  const j0=m.joint(P[0],null,[0,1,0],phase,0.15*S,0);         // sweep about vertical
  const j1=m.joint(P[1],j0,[S,0,0],phase+0.62,0.22,0);        // elbow flex
  brdLimb(m);
  m.bone(j0); boneSeg(m,P[0],P[1],th*1.30,th*1.02,6,BIO_LEG);
  m.bone(j1); boneSeg(m,P[1],P[2],th*1.00,th*0.72,6,BIO_LEG);
  /* Knuckles ride the bone BELOW the joint so they stay welded to the segment
     they drive and never tear open as the arm folds. */
  m.bone(j1); m.sphere(P[1][0],P[1][1],P[1][2],th*1.18,5,BIO_LEG,0.88,false);
  m.bone(j1); m.sphere(P[2][0],P[2][1],P[2][2],th*0.92,5,BIO_LEG,0.90,false);
  /* Opposed blades: a long upper hook and a shorter lower one, curving toward
     each other. Two blades read as a grasping claw; one reads as a sword. */
  m.bone(j1);
  chitinBlade(m,P[2][0],P[2][1]+th*0.30,P[2][2],R*0.62,th*0.88,-S*0.40,S*0.12,CLAW,0.22);
  chitinBlade(m,P[2][0],P[2][1]-th*0.55,P[2][2],R*0.48,th*0.62,-S*0.30,S*0.06,CLAW,-0.26);
  /* Forearm spur β€” the elbow blade every mantid carries, and the part that
     makes the arm read as armed even folded. */
  chitinBlade(m,P[1][0],P[1][1],P[1][2],R*0.30,th*0.44,S*0.24,Math.PI*0.80,CLAW,0.44);
  m.bone(-1);
  return m;
}

/* ---------------------------------------------------------------------------
   SENSORY BULB β€” the Tidecaster's coordinating organ.
   Pear-shaped and lobed, heavy at the base where it meets the stalk, painted
   with the veins that run between its lobes. Built as livery, because this
   organ IS the unit's faction read: it is the largest single thing on the
   silhouette and it is the part that glows.
   --------------------------------------------------------------------------- */
function brdBulb(m,x,y,z,r,lobes,col,seed){
  const nz=brdNoise(seed||29);
  m.sculpt(x,y,z,12,9,(u,v)=>{
    const th=u*TAU, ph=v*Math.PI, st=Math.sin(ph), ct=Math.cos(ph);
    /* Lobes fade out at both poles so the surface stays closed and smooth
       where the sculpt collapses to a point. */
    const lobe=1+0.15*Math.cos(th*lobes)*Math.pow(st,1.5);
    const pear=1+0.26*Math.pow(v,2.4);                 // hangs heavy at the crown
    const R=r*lobe*pear*(1+0.035*(nz(u*19,v*13)-0.5)*2);
    return [Math.cos(th)*st*R*0.88, -ct*R, Math.sin(th)*st*R];
  },(u,v)=>{
    const th=u*TAU, st=Math.sin(v*Math.PI);
    /* Bright along the lobe crests, dark in the grooves β€” the vein pattern.
       Value break, not hue: the team wash multiplies this, so a flat colour
       here would flatten the whole organ into one plastic ball. */
    let k=0.5+0.5*Math.cos(th*lobes);
    k=k*k*(3-2*k);
    k=0.34+0.66*k*(0.55+0.45*Math.pow(st,0.7));
    return [BRD_LUMEN[0]*k+col[0]*(1-k), BRD_LUMEN[1]*k+col[1]*(1-k), BRD_LUMEN[2]*k+col[2]*(1-k)];
  },false,false);
  return m;
}

/* ---------------------------------------------------------------------------
   PROLEG β€” the grub's stubby walking foot.
   A grub does not walk on seven-jointed arachnid legs; it hauls itself on many
   short soft prolegs, and modelling eight of those costs a third of what six
   broodLeg()s would. Three links, a soft pad at the end, and deliberately NO
   claw: this animal is unarmed down to its feet.
   --------------------------------------------------------------------------- */
function brdProleg(m,x,y,z,side,reach,drop,th,phase){
  const S=side, R=reach, D=drop;
  const P=[
    [x,          y,          z+S*0.22*R],
    [x+R*0.12,   y-D*0.30,   z+S*0.72*R],
    [x-R*0.06,   y-D*0.74,   z+S*0.96*R],
    [x-R*0.22,   y-D,        z+S*1.02*R],
  ];
  const RD=[th*1.30,th*1.08,th*0.76,th*0.40];
  let parent=null; const bone=[];
  for(let k=0;k<3;k++){
    const ax=k?[S,0,0]:[0,1,0];
    parent=m.joint(P[k],parent,ax,phase+k*0.36,(k?0.17:0.21*S),0);
    bone.push(parent);
  }
  brdLimb(m);
  for(let k=0;k<3;k++){ m.bone(bone[k]); boneSeg(m,P[k],P[k+1],RD[k],RD[k+1],4,BIO_LEG); }
  m.bone(bone[2]);
  m.sphere(P[3][0],P[3][1]+th*0.22,P[3][2],th*0.66,4,BIO_LEG,0.68,true);   // soft pad
  m.bone(-1);
  return m;
}

/* ---------------------------------------------------------------------------
   FEEDING MOUTH β€” a blunt radial rasp on the +X face.
   The Prospector is UNARMED, and an unarmed unit that looks armed is a lie the
   player will act on: they will walk it into a fight because it has jaws. So
   nothing here protrudes past the lip. A soft everted ring, a genuinely hollow
   dark bore, and a circle of blunt grinding nubs set INSIDE the mouth where
   they can chew ore and nothing else.
   --------------------------------------------------------------------------- */
function brdRasp(m,x,y,z,r,col){
  brdShell(m);
  cylX(m,x,y,z,r*0.55,r*0.86,r*1.02,8,col,false);          // everted soft lip
  tubeX(m,x+r*0.50,y,z,r*0.42,r*0.94,r*0.52,8,BRD_GULLET); // real bore, not a decal
  for(let k=0;k<6;k++){
    const a=k/6*TAU;
    m.sphere(x+r*0.30,y+Math.sin(a)*r*0.66,z+Math.cos(a)*r*0.66,r*0.24,4,BRD_SOFT,0.62,true);
  }
  return m;
}

/* Spiracle β€” a breathing vent through the shell. Hollow, so it reads as a hole
   in a living thing rather than a stud glued to one. */
function brdSpiracle(m,x,y,z,r,col){
  brdShell(m);
  m.tube(x,y,z,r,r*0.52,r*0.86,6,col||CHIT_D);
  return m;
}

/* ============================================================================
   30 β€” THE BROOD SOVEREIGN
   ----------------------------------------------------------------------------
   hp 7400, dmg 110, rng 34. The faction's Commander: the one creature a Brood
   player looks at, and a melee animal at a range shorter than a Ravager's
   body. It has to read at 40px as a HILL with claws in front of it.

   Three things carry that and nothing else does:
     1. A raised armoured crest over the shoulders, wider than the body. From
        the game's overhead camera the plan silhouette is what the player sees,
        and this is the only part that is wider than everything around it.
     2. Two scythe arms held forward of the head. The Alpha Ravager's jaws open
        sideways; the Sovereign's arms reach, which is the difference between a
        big animal and a commanding one.
     3. Height. It stands on eight seven-jointed limbs with the body slung at
        6.4 and the knees breaking above 10 β€” the arachnid read, where the
        animal hangs inside its own legs instead of sitting on them.
   ============================================================================ */
function mdlBrdSovereign(){
  const m=MB();
  const HIP=6.4, DROP=6.4;
  /* ONE continuous skin from the jaw to the tail. The waist, the segment
     plates, the tubercle field and the keel are displacement INSIDE that
     surface, so nothing intersects and the animal reads as grown rather than
     assembled. Two masses: a deep thorax forward of the waist carrying the
     crest and the arms, a tapering armoured abdomen behind it. */
  brdShell(m);
  carapace(m,-1.6,7.0,0,20.4,7.0,4.5,CHITIN,
    {segs:7,ridge:0.28,bump:0.125,keel:0.20,nose:0.46,tail:0.16,
     waist:0.36,waistAt:0.56,waistW:0.062,seed:30071,u:16,v:20});
  /* Head capsule proud of the front of that skin, in the darker shell so the
     face separates from the body mass at distance. */
  brdShell(m);
  carapace(m,10.2,6.6,0,6.4,4.0,3.2,CHIT_D,
    {segs:3,ridge:0.20,bump:0.16,keel:0.14,nose:0.36,tail:0.44,seed:3011,u:11,v:8});
  eyeCluster(m,11.9,7.2,0,1.30,3,HOT);

  /* THE CREST. Seven fanned plates on a lens, sitting down into the thorax. */
  brdShell(m);
  /* WIDER THAN THE BODY, deliberately. At wid 5.8 against a body of 7.0 the
     shield sat inside the flank and the two masses fused into one smooth hump
     in plan β€” which is the only view the game camera gives. Overhanging the
     body is what puts a hard silhouette break between them, and the plan
     silhouette is the whole reason this part exists. */
  brdCrest(m,2.2,10.8,0,11.4,7.4,3.4,3,CHITIN,BRD_SEAM,30071);
  /* Gorget at the throat, in livery. Placed forward and high on purpose: this
     is the faction patch that survives the overhead camera, where flank sacs
     are foreshortened into nothing. */
  brdLivery(m);
  brdCrest(m,7.2,8.5,0,4.2,4.6,1.5,3,BIO_TEAM,C(96,78,120),777);
  m.team(0);

  /* Eight limbs, staggered in reach and knee height so they fan instead of
     marching in parallel, and phased in an alternating tripod so diagonal
     pairs swing in antiphase. */
  for(let k=0;k<4;k++) for(const sd of [-1,1]){
    brdLimb(m);
    /* hip = where this animal's shell actually is at that station, so the limb
       emerges from the body instead of floating beside it. See broodLeg. */
    broodLeg(m, 4.6-k*3.1, HIP, 0, sd, 5.7+k*0.24, DROP, 4.15-k*0.19, 1.02, null,
             ((k+(sd<0?1:0))&1)*Math.PI, [5.25,4.05,5.30,4.75][k]);
  }
  /* The arms, in antiphase with each other so the animal never looks like it
     is clapping. */
  for(const sd of [-1,1]) brdScythe(m,6.6,8.3,0,sd,7.4,1.06,sd>0?0:Math.PI);

  brdLimb(m);
  for(const sd of [-1,1]){
    /* Mandibles under the head β€” opposed, curved, and small enough that the
       arms stay the thing the eye goes to. */
    chitinBlade(m,11.6,5.7,sd*1.9,4.4,0.60,-sd*0.44,sd*0.24,CLAW,0.06);
    chitinBlade(m,11.4,6.6,sd*1.0,3.0,0.38,-sd*0.32,sd*0.12,CLAW,0.10);
    /* Sensory tendrils. Unboned would spring; these are hinged, so they lag
       the head and settle after it. */
    tendril(m,12.2,9.0,sd*1.5,8.2,0.34,sd,3.6,BIO_LEG);
    tendril(m,9.6,9.7,sd*2.6,6.4,0.28,sd,3.0,BIO_LEG);
    /* Shoulder horns β€” the widest point of the animal in plan. */
    chitinBlade(m,6.2,9.0,sd*3.8,5.6,0.58,sd*0.30,Math.PI*0.80,CLAW,0.46);
    /* Spikes raking back off the crest rim. */
    for(let k=0;k<3;k++)
      chitinBlade(m,5.0-k*2.6,10.7-k*0.10,sd*(5.4+k*0.42),3.8-k*0.35,0.46,sd*0.34,
                  Math.PI*0.86,CLAW,0.52);
  }
  /* Crown of spines down the abdomen, tallest just behind the crest β€” the
     hero read at 40px, and the only thing that stops the tail being a lump. */
  brdLimb(m);
  for(let k=0;k<7;k++)
    chitinBlade(m,-3.5-k*1.24,9.9-k*0.34,0,3.4+Math.sin(k/6*Math.PI)*2.2,0.46,0.10,
                Math.PI*0.97,CLAW,1.02);

  /* Brood-lumen lobes: lit tissue slung along the abdomen flanks. Livery, so
     they carry the faction colour through the translucency term. */
  for(const sd of [-1,1]){
    m.sphere(-7.4,8.6,sd*3.0,2.15,7,BIO_TEAM,0.76,false);
    m.sphere(-4.0,9.4,sd*3.1,1.50,7,BIO_TEAM,0.76,false);
  }
  /* Spiracles along the abdomen, and the tubercle field over the tail. */
  for(const sd of [-1,1]){ brdSpiracle(m,-5.8,9.6,sd*2.0,0.80,CHIT_D);
                           brdSpiracle(m,-9.0,8.6,sd*1.5,0.66,CHIT_D); }
  brdShell(m);
  tubercles(m,-6.6,9.0,0,4.2,3.4,14,1.05,CHIT_D,30071);
  return {hull:m.build(),tur:null,s:1.15};
}

/* ============================================================================
   31 β€” BROOD TIDECASTER
   ----------------------------------------------------------------------------
   hp 390, dmg 24, rng 142. It forms where 28 broodmates gather and organises
   the mass into one targeted tide, which means it is not a fighter at all β€”
   it is a COORDINATING ORGAN that grew legs, and it should look like it cost
   the swarm something to make.

   So the mass is inverted relative to every other creature here: a small light
   body on long thin legs, carrying a raised synaptic bulb that is bigger than
   the body itself. The ring of fused lobes around the bulb's base is the 28
   creatures that went into it, and the trailing tendrils are what it reaches
   the rest of the swarm with. Its only weapon is a slender ventral sting,
   deliberately thin: at dmg 24 it must not read as a threat next to a Ravager.
   ============================================================================ */
function mdlBrdTidecaster(){
  const m=MB();
  const HIP=3.6, DROP=3.6;
  /* Slight, narrow, and low β€” everything the Sovereign is not. */
  brdShell(m);
  carapace(m,-0.6,4.2,0,9.4,3.0,2.0,CHITIN,
    {segs:6,ridge:0.18,bump:0.10,keel:0.16,nose:0.30,tail:0.34,
     waist:0.32,waistAt:0.50,waistW:0.080,seed:3101,u:12,v:12});
  brdShell(m);
  carapace(m,4.4,4.1,0,2.8,1.7,1.4,CHIT_D,
    {segs:2,ridge:0.16,bump:0.14,keel:0.12,nose:0.38,tail:0.40,seed:311,u:8,v:6});
  eyeCluster(m,5.2,4.3,0,0.52,2,LAMP);

  /* Six long spindly legs. Thin enough (th 0.44) that broodLeg skips its knee
     spur β€” a caster with blades on its knees is a fighter. */
  for(let k=0;k<3;k++) for(const sd of [-1,1]){
    brdLimb(m);
    broodLeg(m, 1.6-k*2.3, HIP, 0, sd, 4.3+k*0.22, DROP, 3.05-k*0.16, 0.44, null,
             ((k+(sd<0?1:0))&1)*Math.PI, [2.10,1.60,2.05][k]);
  }

  /* THE STALK. Three hinged links rising back off the thorax, so the bulb
     sways a beat behind the body instead of being welded to it. Every part
     above this rides the top link. */
  /* Height is capped deliberately. The bulb wants to be the biggest thing on
     the model, but a 390hp support unit that stands as tall as the 7400hp
     Sovereign reads as the more important creature, and the player picks
     targets off silhouette. Crown tops out around 11 against the Sovereign's
     15 before either is scaled. */
  const NP=[[-0.9,5.0,0],[-1.3,6.5,0],[-1.7,7.7,0],[-1.9,8.5,0]];
  let par=null; const nb=[];
  for(let k=0;k<3;k++){ par=m.joint(NP[k],par,[0,0,1],k*0.48,0.055+k*0.030,0); nb.push(par); }
  brdLimb(m);
  for(let k=0;k<3;k++){
    m.bone(nb[k]);
    boneSeg(m,NP[k],NP[k+1],0.82-k*0.13,0.69-k*0.13,5,BIO_LEG);
  }
  for(let k=1;k<3;k++){ m.bone(nb[k]); m.sphere(NP[k][0],NP[k][1],NP[k][2],0.76-k*0.10,4,BIO_LEG,0.88,false); }

  /* The organ itself, riding the top link. */
  m.bone(nb[2]);
  brdLivery(m);
  brdBulb(m,-2.0,9.5,0,2.30,6,BIO_TEAM,3101);
  m.team(0);
  m.bone(nb[2]);
  m.sphere(-2.0,11.3,0,0.78,6,LAMP,0.90,false);              // synaptic node
  /* The 28. A ring of fused lobes around the bulb's base β€” the broodmates the
     Tidecaster was assembled out of, still readable as separate heads. */
  m.bone(nb[2]);
  brdLivery(m);
  /* Set OUTSIDE the bulb's own radius. At 1.45 out from a bulb of 2.30 the
     whole ring was swallowed by the surface it was supposed to fringe, and the
     one detail that says "this was 28 creatures" rendered as nothing. */
  for(let k=0;k<6;k++){
    const a=k/6*TAU;
    m.sphere(-2.0+Math.cos(a)*2.30,8.05,Math.sin(a)*2.55,0.72,5,BIO_TEAM,0.86,true);
  }
  m.team(0);
  /* Sensory crown raking up and out off the ring. */
  m.bone(nb[2]);
  brdLimb(m);
  for(let k=0;k<6;k++){
    const a=k/6*TAU, q=k/5-0.5;
    chitinBlade(m,-2.0+Math.cos(a)*2.15,8.4,Math.sin(a)*2.40,
                3.0-Math.abs(q)*1.2,0.22,Math.sin(a)*0.34,Math.PI*(0.60+q*0.55),CLAW,1.00);
  }
  m.bone(-1);

  /* Trailing tendrils β€” what it actually reaches the swarm with. Long, thin,
     and the last thing on the model to come to rest. */
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,-2.6,5.4,sd*1.1,7.6,0.24,sd,2.4,BIO_LEG);
    tendril(m,-3.8,4.6,sd*0.6,6.0,0.19,sd,1.6,BIO_LEG);
  }
  /* Ventral sting and its gland. Slender on purpose: rng 142 at dmg 24 is a
     needle, not a jaw. */
  brdLivery(m);
  m.sphere(-4.6,4.0,0,1.05,6,BIO_TEAM,0.84,false);
  m.team(0);
  brdLimb(m);
  chitinBlade(m,-5.2,3.6,0,3.2,0.28,0,Math.PI*0.98,CLAW,-0.34);
  /* Flank sacs so the livery reads from the side as well as from above. */
  for(const sd of [-1,1]) m.sphere(0.6,5.2,sd*1.5,0.78,6,BIO_TEAM,0.90,false);
  for(const sd of [-1,1]) brdSpiracle(m,-1.6,5.4,sd*1.2,0.44,CHIT_D);
  brdShell(m);
  tubercles(m,-2.4,5.2,0,2.0,1.4,5,0.46,CHIT_D,3101);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ============================================================================
   32 β€” PROSPECTOR
   ----------------------------------------------------------------------------
   hp 190, dmg 0, rng 0. A mobile ore miner that chews ore where it lies, and
   the Brood's version of it is a GRUB.

   It is unarmed, and that is the whole brief. No claws, no jaws, no spines, no
   blade of any kind appears in this builder β€” not on the mouth, not on the
   back, not even on the feet, where every other creature in the kit ends in a
   CLAW-coloured point. An unarmed unit that looks armed gets walked into a
   fight, and the player is right to blame the model.

   What replaces the weapons is the other half of the job: a blunt feeding
   rasp at the front and a swollen carrying abdomen at the back, pinched apart
   by a waist so the load reads as LOAD. Soft, low-value seams and a pale skin
   keep it from looking armoured β€” the value break that makes the Sovereign's
   shell read as chitin is deliberately turned down here, because this animal
   is not wearing any.
   ============================================================================ */
function mdlBrdGrub(){
  const m=MB();
  const HIP=1.35, DROP=1.35;
  /* One soft body. Nine shallow segment rolls instead of six deep plates, a
     late waist, and almost no rear taper, so the mass piles up behind the
     pinch as an ore-swollen abdomen. */
  brdShell(m);
  carapace(m,-0.6,2.05,0,10.6,3.4,2.5,CHITIN,
    {segs:9,ridge:0.14,bump:0.085,keel:0.08,nose:0.06,tail:0.34,
     waist:0.24,waistAt:0.66,waistW:0.085,seed:3207,u:13,v:14,
     seam:[0.26,0.24,0.18]});
  /* Head: small, blunt, no capsule ridge. */
  brdShell(m);
  /* nose (the -X end) is left almost untapered so the head's rear plugs into
     the body's front taper. At 0.30 both ends narrowed toward the same gap and
     the head floated a full unit clear of the animal. */
  carapace(m,4.7,1.95,0,3.6,2.1,1.7,CHIT_D,
    {segs:2,ridge:0.10,bump:0.10,keel:0.06,nose:0.08,tail:0.30,seed:327,u:9,v:7,
     seam:[0.24,0.22,0.17]});
  /* Eyespots, not an eye cluster. A grub barely sees; it feels its way. */
  for(const sd of [-1,1]) m.sphere(5.45,2.52,sd*0.66,0.26,4,LAMP,0.86,false);
  /* Set INTO the snout, not hung off the end of it. Every carapace() tapers
     to a needle at both ends whatever nose/tail say, so a mouth placed at the
     nominal front edge floats clear of the head with daylight between them. */
  brdRasp(m,5.75,1.88,0,1.30,BRD_SOFT);

  /* Eight short prolegs. Many small feet under a heavy body is the grub read,
     and it costs a third of what six arachnid limbs would. */
  for(let k=0;k<4;k++) for(const sd of [-1,1])
    brdProleg(m, 2.6-k*1.9, HIP, 0, sd, 1.72+k*0.05, DROP, 0.34,
              ((k+(sd<0?1:0))&1)*Math.PI);

  /* Ore sacs β€” swollen, translucent, and the faction patch. Sitting in the
     dorsal midline behind the waist, which is exactly where a full load would
     push the skin out. */
  m.sphere(-3.3,3.45,0,1.38,6,BIO_TEAM,0.82,false);
  m.sphere(-1.5,3.55,0,1.02,6,BIO_TEAM,0.84,false);
  m.sphere(-4.9,3.22,0,0.92,6,BIO_TEAM,0.80,false);
  /* Short feeler antennae β€” the only appendages it has, and they trail rather
     than reach. */
  brdLimb(m);
  for(const sd of [-1,1]) tendril(m,4.9,3.10,sd*0.60,2.6,0.13,sd,0.7,BIO_LEG);
  for(const sd of [-1,1]) brdSpiracle(m,-2.6,3.05,sd*1.35,0.34,CHIT_D);
  brdShell(m);
  tubercles(m,-2.8,3.05,0,2.4,1.5,6,0.40,CHIT_D,3207);
  return {hull:m.build(),tur:null,s:0.92};
}

/* ============================================================================
   THE TWELVE β€” SPLITTING mdlHordeSpitter AND mdlHordeBombardier
   ----------------------------------------------------------------------------
   FAC_KIT.horde mapped 28 roster slots onto nine builders, each called with NO
   arguments, so twelve of those slots were not "similar", they were the SAME
   VERTEX BUFFER. Six units shared mdlHordeSpitter and six shared
   mdlHordeBombardier β€” a tier-1 line brawler and a tier-3 siege caster
   rendering as one animal.

   WHAT SEPARATES THEM IS PROPORTION, NOT DECORATION.
   The camera is overhead and a unit is ~40px. At that size a player reads
   three things and nothing else: the PLAN outline (long/short, wide/narrow),
   the HEIGHT of the mass over the ground, and ONE dominant feature breaking
   the outline. Greebles do not survive; a body that is twice as long as its
   neighbour does. So each of the twelve below picks a different corner of that
   space, and the detail exists only to explain the shape it is already making:

     slot  animal        plan            stance   dominant feature
      1    Gorger        short + WIDE    low      frontal brow shield
      2    Ramparthorn   long + wide     TALL     paired shoulder pauldrons
      7    Skysting      long + NARROW   mid      dorsal rack, raked up
     10    Flakspine     SHORT + narrow  TALL     vertical neck mast
     20    Bloomsac      round + FAT     LOW      bloated sac + burst ring
     21    Emberthroat   long + narrow   mid      dorsal vane fan, drooping gullet
      3    Mortarback    SHORT + deep    hunched  one up-and-back mortar throat
      6    Lancespine    VERY LONG + low low      straight forward harpoon
     16    Siegemound    wide + squat    LOW      near-vertical chimney
     22    Railfang      long + narrow   mid      braced forward lance
     26    Basilisk      long + wide     tall     crowned head, single great eye
     27    Harbinger     short + narrow  STILTS   hanging spore bell + curtain

   Cross-checked against src/game/sim.js TYPES: every choice above is something
   the unit's own numbers already claim. The Vulture (10) cannot shoot ground at
   all and reaches 172px, so it is a mast; the Bombard (16) fires 400px at speed
   14, so it is a mound that barely moves; the Longbow (6) is a 110hp single-
   target sniper, so it is all length and no armour.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   MAW β€” the one weapon organ, parameterised.
   Every Brood gun in this file is this surface with different numbers, which
   is the point: a swarm whose artillery and whose brawler grew DIFFERENT
   weapons is two factions. A throat, built as a surface of revolution along
   its own axis and then pitched and yawed into place, with:
     * a taper, so it is an organ and not a pipe,
     * annular sphincter rings that die out toward the tip,
     * a lobed section, so the silhouette is never a circle,
     * a bell that only opens over the last quarter, and
     * a rim that folds back INWARD over the last tenth.
   That last one matters: leaving the end open renders the inside of the tube,
   and with backface culling on that is a hole straight through the animal. The
   fold closes the surface and still reads as a dark wet socket, because the
   paint callback drives the same region toward the seam colour.
   --------------------------------------------------------------------------- */
function brdMaw(m,x,y,z,o){
  o=o||{};
  const len=o.len==null?4.0:o.len, r0=o.r0==null?0.95:o.r0, r1=o.r1==null?0.55:o.r1;
  const flare=o.flare==null?0.85:o.flare, lobes=o.lobes==null?5:o.lobes;
  const rings=o.rings==null?4:o.rings, taper=o.taper==null?0.85:o.taper;
  const socket=o.socket===false?0:1;
  const pitch=o.pitch||0, yaw=o.yaw||0, col=o.col||CHIT_D;
  const seam=o.seam||BRD_SEAM, nz=brdNoise(o.seed||53);
  const cp=Math.cos(pitch), sp=Math.sin(pitch), cw=Math.cos(yaw), sw=Math.sin(yaw);
  const CM=(typeof COL_MAT!=='undefined'&&COL_MAT.get(col));
  if(CM!=null) m.mat(CM);
  m.sculpt(x,y,z,o.u||9,o.v||9,(u,v)=>{
    const th=u*TAU;
    let r=r0+(r1-r0)*Math.pow(v,taper);
    const b=Math.max(0,(v-0.74)/0.26);
    r*=1+flare*b*b;                                   // bell, last quarter only
    const f=(v*rings)%1, s=f*f*(3-2*f);
    r*=1+0.11*(s-0.5)*2*(1-Math.pow(v,2.6));          // sphincter rings
    r*=1+0.085*Math.cos(th*lobes);                    // never a circle in section
    r*=1+0.05*(nz(u*17,v*13)-0.5)*2;
    if(socket&&v>0.90) r*=1-((v-0.90)/0.10)*0.94;     // rim folds in: socket, not hole
    const a=len*v, by=Math.cos(th)*r, cz=Math.sin(th)*r;
    const px=a*cp-by*sp, py=a*sp+by*cp;               // pitch about +Z
    return [px*cw-cz*sw, py, px*sw+cz*cw];            // then yaw about +Y
  },(u,v)=>{
    const th=u*TAU, f=(v*rings)%1, d=Math.min(f,1-f)*2;
    let k=Math.pow(d,0.50); k=k*k*(3-2*k);
    k*=0.62+0.38*(0.5+0.5*Math.cos(th*lobes));
    k*=0.80+0.20*nz(u*23,v*29);
    k=0.20+0.80*k;
    k*=1-0.60*Math.pow(Math.max(0,(v-0.78)/0.22),1.3);   // aperture goes wet-dark
    return [seam[0]+(col[0]-seam[0])*k, seam[1]+(col[1]-seam[1])*k,
            seam[2]+(col[2]-seam[2])*k];
  },true,true);
  return m;
}

/* ---------------------------------------------------------------------------
   FIN β€” a dorsal vane. A flattened lens standing on the spine, swept back by
   `rake`, with a scalloped free edge and a root that is three times the
   thickness of the rim. It is a MEMBRANE stretched on ribs, so it is painted
   bright at the root and dark at the rim with radial rib strokes across it β€”
   the same value-break logic carapace() uses, keyed to the rib angle instead
   of the segment index.
   --------------------------------------------------------------------------- */
function brdFin(m,x,y,z,len,rise,th,rake,col,seam,seed){
  const nz=brdNoise(seed||71), lo=seam||BRD_SEAM;
  const CM=(typeof COL_MAT!=='undefined'&&COL_MAT.get(col));
  if(CM!=null) m.mat(CM);
  m.sculpt(x,y,z,9,6,(u,v)=>{
    const ang=u*TAU, ph=v*Math.PI, st=Math.sin(ph), ct=Math.cos(ph);
    const ex=Math.cos(ang)*st, ey=(1-ct)*0.5, ez=Math.sin(ang)*st;
    const scal=1-0.11*Math.cos(ang*5)*Math.pow(ey,1.6);        // scalloped rim
    const tk=th*(0.34+0.66*Math.pow(1-ey,1.5))*(1+0.18*(nz(u*21,v*17)-0.5)*2);
    return [ex*len*0.5*scal+ey*rise*rake, ey*rise*scal, ez*tk];
  },(u,v)=>{
    const ang=u*TAU, ey=(1-Math.cos(v*Math.PI))*0.5;
    let k=0.5+0.5*Math.cos(ang*5);  k=k*k*(3-2*k);             // rib strokes
    k=0.30+0.70*k;
    k*=1-0.55*Math.pow(ey,1.2);                                // rim goes translucent-dark
    k*=0.84+0.16*nz(u*29,v*23);
    return [lo[0]+(col[0]-lo[0])*k, lo[1]+(col[1]-lo[1])*k, lo[2]+(col[2]-lo[2])*k];
  },false,false);
  return m;
}

/* ---------------------------------------------------------------------------
   STALK β€” a hinged neck/mast. Returns the id of the TOP bone so the caller can
   hang the organ it carries on it; anything welded to the body instead would
   stay rigid while the mast swayed, and the join would visibly tear.
   --------------------------------------------------------------------------- */
function brdStalk(m,P,r0,r1,phase,axis){
  const n=P.length-1, ax=axis||[0,0,1];
  let par=null; const bone=[];
  for(let k=0;k<n;k++){ par=m.joint(P[k],par,ax,phase+k*0.46,0.05+k*0.028,0); bone.push(par); }
  brdLimb(m);
  for(let k=0;k<n;k++){
    m.bone(bone[k]);
    boneSeg(m,P[k],P[k+1],r0+(r1-r0)*(k/n),r0+(r1-r0)*((k+1)/n),5,BIO_LEG);
  }
  for(let k=1;k<n;k++){
    m.bone(bone[k]);
    m.sphere(P[k][0],P[k][1],P[k][2],(r0+(r1-r0)*(k/n))*1.16,4,BIO_LEG,0.88,false);
  }
  m.bone(-1);
  return bone[n-1];
}

/* A rank of walking limbs, staggered and phased into an alternating tripod.
   Every builder below did this by hand; twelve copies of a five-line loop is
   twelve chances to get the antiphase term wrong. `hips` is per-pair, because
   the shell's half-width changes down the body and broodLeg() needs the real
   number or the limb floats off the flank (see broodLeg's own note). */
function brdRank(m,x0,dx,hipY,drop,n,reach,knee,th,hips){
  for(let k=0;k<n;k++) for(const sd of [-1,1]){
    brdLimb(m);
    broodLeg(m, x0-k*dx, hipY, 0, sd, reach+k*0.20, drop, knee-k*0.16, th, null,
             ((k+(sd<0?1:0))&1)*Math.PI, hips[Math.min(k,hips.length-1)]);
  }
  return m;
}

/* WING β€” a membrane lobe lying in the XZ plane, which is the plan the player
   actually reads. Flyers and swimmers cannot share one extruded polygon the
   way mdlHordeFlyer did: a delta, a fat oval and a swept needle are three
   different silhouettes, and that difference is the whole split. `span` is
   lateral reach, `chord` is fore-aft, `sweep` shifts the tip aft. Tessellation
   is the triangle-floor: without legs a flyer has to spend its budget here. */
function brdWing(m,x,y,z,side,span,chord,th,sweep,col,seam,seed){
  const nz=brdNoise(seed||41), lo=seam||BRD_SEAM, S=side;
  const CM=(typeof COL_MAT!=='undefined'&&COL_MAT.get(col));
  if(CM!=null) m.mat(CM);
  m.sculpt(x,y,z,14,10,(u,v)=>{
    const ang=u*TAU, ph=v*Math.PI, st=Math.sin(ph), ct=Math.cos(ph);
    const ex=Math.cos(ang)*st, ez=Math.abs(Math.sin(ang)*st);
    const scal=1-0.13*Math.cos(ang*5)*Math.pow(ez,1.45);
    const tk=th*(0.20+0.80*Math.pow(1-ez,1.55))*(1+0.14*(nz(u*19,v*13)-0.5)*2);
    return [ex*chord*0.5*scal+ez*span*sweep, ct*tk, S*ez*span*scal];
  },(u,v)=>{
    const ang=u*TAU, ez=Math.abs(Math.sin(ang)*Math.sin(v*Math.PI));
    let k=0.5+0.5*Math.cos(ang*5); k=k*k*(3-2*k);
    k=0.28+0.72*k;
    k*=1-0.58*Math.pow(ez,1.15);
    k*=0.84+0.16*nz(u*29,v*23);
    return [lo[0]+(col[0]-lo[0])*k, lo[1]+(col[1]-lo[1])*k, lo[2]+(col[2]-lo[2])*k];
  },false,false);
  return m;
}

/* ===================== 1 β€” GORGER (was mdlHordeSpitter) =====================
   Rhino slot: tier 1, 130hp, 16dmg at 88px. The army's line brawler and the
   thing a player buys twenty of. So: SHORT and WIDE, carried LOW, with the
   armour concentrated dead ahead where it walks into fire. The brow shield is
   wider than the body and is the whole silhouette from above β€” a blunt wedge,
   which is the one plan shape nothing else in the twelve makes.
   =========================================================================== */
function mdlBrdGorger(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.5,2.55,0,10.8,4.8,2.30,CHITIN,
    {segs:5,ridge:0.22,bump:0.115,keel:0.14,nose:0.50,tail:0.26,
     waist:0.24,waistAt:0.58,waistW:0.095,seed:10101,u:13,v:13});
  brdShell(m);
  carapace(m,4.7,2.40,0,3.8,3.0,1.55,CHIT_D,
    {segs:3,ridge:0.18,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:1011,u:10,v:7});
  eyeCluster(m,5.6,2.65,0,0.60,2,HOT);
  /* THE BROW. Low and forward, overhanging the head β€” at rise 1.4 it never
     makes the animal tall, it makes it broad, which is the read the Rhino slot
     needs against the Ramparthorn standing next to it. */
  brdShell(m);
  brdCrest(m,3.2,3.55,0,6.2,5.6,1.40,3,CHITIN,BRD_SEAM,10101);
  brdLivery(m);
  brdCrest(m,5.2,3.05,0,2.6,3.2,0.80,3,BIO_TEAM,C(96,78,120),1011);
  m.team(0);
  /* Short thick throat set into the face β€” a spitter at 88px is a gob, not a
     barrel, so it is stubby, wide-mouthed and barely clears the brow. */
  brdMaw(m,5.9,2.25,0,{len:3.2,r0:0.86,r1:0.62,flare:0.95,lobes:5,rings:3,
                       pitch:-0.05,col:CHIT_D,seed:1011,u:9,v:8});
  brdRank(m,2.0,2.4,2.35,2.35,3,2.95,2.05,0.56,[2.55,2.15,2.35]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,5.0,1.95,sd*1.5,2.6,0.40,-sd*0.34,sd*0.26,CLAW,0.02);   // opposed jaws
    chitinBlade(m,2.6,3.30,sd*2.5,3.0,0.36,sd*0.30,Math.PI*0.80,CLAW,0.34);
    tendril(m,5.2,3.30,sd*0.9,3.0,0.16,sd,1.3,BIO_LEG);
    m.sphere(-2.6,3.30,sd*1.9,0.92,6,BIO_TEAM,0.82,false);
    brdSpiracle(m,-0.8,3.45,sd*1.6,0.46,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-2.4,3.10,0,2.4,1.8,6,0.48,CHIT_D,10101);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ================== 2 β€” RAMPARTHORN (was mdlHordeSpitter) ==================
   Goliath slot: tier 2, 450hp, 42dmg, splash 10. The heavy assault chassis,
   and the only unit in this group that should read as ARMOUR. It stands a
   full body-height above the Gorger on longer limbs, and carries two crests
   angled off the flanks like pauldrons rather than one flat brow β€” so from
   above its plan is a broad H, not a wedge.
   =========================================================================== */
function mdlBrdRamparthorn(){
  const m=MB();
  brdShell(m);
  carapace(m,-1.0,4.9,0,14.0,5.4,3.30,CHITIN,
    {segs:6,ridge:0.27,bump:0.12,keel:0.26,nose:0.52,tail:0.20,
     waist:0.32,waistAt:0.54,waistW:0.072,seed:20202,u:14,v:16});
  brdShell(m);
  carapace(m,6.6,4.6,0,4.4,3.0,2.10,CHIT_D,
    {segs:3,ridge:0.20,bump:0.15,keel:0.16,nose:0.36,tail:0.44,seed:2022,u:10,v:8});
  eyeCluster(m,7.9,5.0,0,0.82,3,HOT);
  /* PAULDRONS. Two crests set out on the flanks and rolled outboard, so the
     shoulder line breaks the plan silhouette twice instead of once. This is
     the difference between "big brawler" and "heavy" at 40px. */
  for(const sd of [-1,1]){
    brdShell(m);
    brdCrest(m,1.6,7.4,sd*3.1,7.6,3.4,2.40,3,CHITIN,BRD_SEAM,20202+sd);
    brdLimb(m);
    for(let k=0;k<3;k++)
      chitinBlade(m,3.6-k*2.4,7.9,sd*(4.0+k*0.30),3.4-k*0.30,0.44,sd*0.32,
                  Math.PI*0.84,CLAW,0.50);
  }
  brdLivery(m);
  brdCrest(m,5.4,6.2,0,3.6,3.4,1.20,3,BIO_TEAM,C(96,78,120),2022);
  m.team(0);
  /* Twin throats, splayed. Two weapons is the tier-2 read, and splaying them
     keeps the head from becoming one lump. */
  for(const sd of [-1,1])
    brdMaw(m,7.4,4.30,sd*1.15,{len:4.6,r0:0.80,r1:0.54,flare:0.80,lobes:5,rings:4,
                               pitch:-0.03,yaw:sd*0.16,col:CHIT_D,seed:2022+sd,u:9,v:9});
  brdRank(m,3.2,2.9,4.70,4.70,3,4.35,3.35,0.86,[3.15,2.55,2.95]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,7.2,3.9,sd*1.9,3.8,0.52,-sd*0.40,sd*0.24,CLAW,0.04);
    tendril(m,7.8,6.2,sd*1.2,4.6,0.22,sd,2.1,BIO_LEG);
    m.sphere(-4.6,6.0,sd*2.3,1.35,7,BIO_TEAM,0.80,false);
    brdSpiracle(m,-2.2,6.3,sd*1.9,0.60,CHIT_D);
  }
  for(let k=0;k<4;k++)
    chitinBlade(m,-2.6-k*1.9,6.6-k*0.22,0,2.6-k*0.24,0.38,0.10,Math.PI*0.96,CLAW,0.86);
  brdShell(m);
  tubercles(m,-4.2,6.2,0,3.2,2.2,8,0.62,CHIT_D,20202);
  return {hull:m.build(),tur:null,s:1.06};
}

/* ==================== 7 β€” SKYSTING (was mdlHordeSpitter) ===================
   Hornet slot: tier 2, 175px, splash 24, and the only unit in this group that
   can engage AIRCRAFT as well as ground. So the weapon does not point forward
   at all β€” it is a rack of five slim spine-throats raked UP and back off the
   spine, fanned in plan. Narrow body underneath, because everything this unit
   has is in that rack.
   =========================================================================== */
function mdlBrdSkysting(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.6,3.25,0,11.2,3.25,2.05,CHITIN,
    {segs:6,ridge:0.20,bump:0.10,keel:0.20,nose:0.42,tail:0.30,
     waist:0.30,waistAt:0.52,waistW:0.078,seed:70707,u:12,v:14});
  brdShell(m);
  carapace(m,5.2,3.15,0,3.2,1.9,1.45,CHIT_D,
    {segs:2,ridge:0.16,bump:0.14,keel:0.12,nose:0.34,tail:0.42,seed:707,u:9,v:6});
  eyeCluster(m,6.1,3.35,0,0.52,2,HOT);
  /* THE RACK. Five throats, fanned in yaw and staggered in pitch so the tips
     describe an arc instead of a comb β€” a comb reads as machinery. The two
     outermost are shortest, which puts the tallest point on the centreline. */
  for(let k=0;k<5;k++){
    const q=k/4-0.5;
    brdMaw(m,-1.4+Math.abs(q)*1.2,4.35,q*3.4,
      {len:5.4-Math.abs(q)*2.0,r0:0.44,r1:0.20,flare:0.30,lobes:4,rings:5,
       taper:0.62,pitch:0.86-Math.abs(q)*0.10,yaw:q*0.62,col:CHITIN,
       seed:700+k,u:7,v:8});
  }
  brdLivery(m);
  for(const sd of [-1,1]) m.sphere(-2.4,4.15,sd*1.5,0.90,6,BIO_TEAM,0.84,false);
  brdCrest(m,3.2,4.10,0,3.4,2.4,0.95,3,BIO_TEAM,C(96,78,120),707);
  m.team(0);
  brdRank(m,2.2,2.5,3.10,3.10,3,3.35,2.30,0.54,[1.90,1.55,1.80]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,6.0,4.10,sd*0.9,4.4,0.18,sd,2.0,BIO_LEG);
    chitinBlade(m,5.6,2.75,sd*1.1,2.2,0.26,-sd*0.28,sd*0.20,CLAW,0.04);
    brdSpiracle(m,-3.8,4.05,sd*1.2,0.42,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-4.2,3.90,0,2.2,1.2,5,0.42,CHIT_D,70707);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ==================== 10 β€” FLAKSPINE (was mdlHordeSpitter) =================
   Vulture slot: tier 1, 172px, and it CANNOT SHOOT GROUND AT ALL. That is the
   most specific fact about any unit in this group and the silhouette should
   say it before the player reads a card: a short stubby body on four splayed
   legs carrying a near-vertical neck mast with a flak bell on top. It is the
   tallest thing in the group at less than half the Ramparthorn's body length.
   =========================================================================== */
function mdlBrdFlakspine(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.4,4.35,0,7.6,3.15,2.20,CHITIN,
    {segs:4,ridge:0.20,bump:0.11,keel:0.16,nose:0.40,tail:0.34,
     waist:0.22,waistAt:0.56,waistW:0.10,seed:10010,u:16,v:16});
  brdShell(m);
  carapace(m,3.3,4.25,0,2.4,1.7,1.30,CHIT_D,
    {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.40,seed:100,u:12,v:8});
  eyeCluster(m,4.0,4.45,0,0.46,2,HOT);
  /* THE MAST. Four hinged links rising off the thorax, so the bell sways a
     beat behind the body β€” and everything above rides the top bone, or the
     join tears open the first time it leans. */
  const NP=[[-0.9,5.5,0],[-0.9,7.2,0],[-0.7,8.7,0],[-0.5,9.9,0]];
  const top=brdStalk(m,NP,0.62,0.42,0.20,[0,0,1]);
  m.bone(top);
  brdMaw(m,-0.5,9.9,0,{len:3.6,r0:0.66,r1:0.40,flare:1.35,lobes:6,rings:3,
                       pitch:1.30,col:CHIT_D,seed:1001,u:10,v:8});
  m.bone(top);
  brdLimb(m);
  for(let k=0;k<4;k++){
    const a=k/4*TAU+0.4;
    chitinBlade(m,-0.5+Math.cos(a)*0.75,10.6,Math.sin(a)*0.75,
                2.4,0.22,Math.sin(a)*0.30,a,CLAW,0.92);           // ranging spines
  }
  m.bone(top);
  brdLivery(m);
  m.sphere(-0.9,8.9,0,0.78,6,BIO_TEAM,0.86,false);
  m.bone(-1);
  m.team(0);
  /* FOUR legs, splayed wide and long β€” a tripod-tall stance, and the only
     four-legged animal in the twelve. */
  brdRank(m,1.5,3.0,4.20,4.20,2,4.30,3.30,0.60,[1.95,1.70]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,3.8,5.1,sd*0.8,4.0,0.17,sd,1.9,BIO_LEG);
    m.sphere(-2.2,5.1,sd*1.3,0.72,6,BIO_TEAM,0.84,false);
    brdSpiracle(m,-1.2,5.3,sd*1.1,0.38,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-2.4,4.95,0,1.6,1.1,4,0.38,CHIT_D,10010);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ===================== 20 β€” BLOOMSAC (was mdlHordeSpitter) =================
   Reaper slot: tier 2, splash 52 β€” the largest area of effect on any unit in
   either group, ground only. It is a delivery system for one enormous burst,
   so the animal is mostly ABDOMEN: a bloated livery sac slung low between
   short legs, ringed with the spines that will go outward when it fires.
   Round in plan, low to the ground, and the only silhouette here with no
   forward weapon at all.
   =========================================================================== */
function mdlBrdBloomsac(){
  const m=MB();
  brdShell(m);
  carapace(m,0.6,2.65,0,9.6,4.30,2.60,CHITIN,
    {segs:4,ridge:0.20,bump:0.12,keel:0.10,nose:0.44,tail:0.44,
     waist:0.40,waistAt:0.40,waistW:0.075,seed:20020,u:14,v:14});
  brdShell(m);
  carapace(m,4.6,2.45,0,2.8,2.1,1.40,CHIT_D,
    {segs:2,ridge:0.16,bump:0.15,keel:0.08,nose:0.34,tail:0.42,seed:2002,u:12,v:8});
  eyeCluster(m,5.3,2.65,0,0.50,2,HOT);
  /* THE SAC. Bigger than the thorax carrying it, pear-heavy, and painted from
     the same vein function the Tidecaster's organ uses so the two read as the
     same tissue. Livery, because at this size it IS the faction patch. */
  brdLivery(m);
  brdBulb(m,-3.7,5.35,0,3.35,7,BIO_TEAM,20020);
  m.team(0);
  /* The burst ring β€” eight spines set OUTSIDE the sac's radius and raked flat,
     so in plan they make a starburst nothing else in the twelve makes. */
  brdLimb(m);
  for(let k=0;k<8;k++){
    const a=k/8*TAU;
    chitinBlade(m,-3.7+Math.cos(a)*3.05,3.15,Math.sin(a)*3.35,
                2.8,0.30,Math.sin(a)*0.26,a,CLAW,0.26);
  }
  brdRank(m,2.4,2.3,2.45,2.45,3,2.75,1.95,0.52,[2.30,2.05,2.15]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,4.9,2.10,sd*1.2,2.2,0.32,-sd*0.32,sd*0.24,CLAW,0.02);
    tendril(m,5.1,3.20,sd*0.8,2.6,0.15,sd,1.1,BIO_LEG);
    brdSpiracle(m,1.6,3.55,sd*1.5,0.44,CHIT_D);
  }
  brdShell(m);
  tubercles(m,1.4,3.30,0,2.0,1.6,5,0.44,CHIT_D,20020);
  return {hull:m.build(),tur:null,s:1.05};
}

/* ==================== 21 β€” EMBERTHROAT (was mdlHordeSpitter) ===============
   Cinder slot: tier 2, incendiary, splash 46 at 96px. A thing that carries
   burning chemistry has to shed heat, so the spine wears a fan of five
   membrane vanes β€” tall, thin, translucent at the rim β€” and the weapon hangs
   UNDER the head as a drooping gullet rather than sitting on it. Long narrow
   body, vertical fan: the plan is a fish, and no other silhouette here is.
   =========================================================================== */
function mdlBrdEmberthroat(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.6,3.05,0,11.6,3.60,2.35,CHITIN,
    {segs:7,ridge:0.22,bump:0.105,keel:0.12,nose:0.46,tail:0.28,
     waist:0.28,waistAt:0.54,waistW:0.080,seed:21021,u:13,v:15});
  brdShell(m);
  carapace(m,5.4,2.95,0,3.4,2.1,1.55,CHIT_D,
    {segs:2,ridge:0.16,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:2101,u:9,v:7});
  eyeCluster(m,6.3,3.20,0,0.54,2,HOT);
  /* THE FAN. Five vanes, tallest over the shoulders, raked back. Membrane, so
     they take the translucency term instead of shading as plate. */
  for(let k=0;k<5;k++){
    const q=k/4;
    brdFin(m,2.6-k*2.15,4.05,0,3.4-q*0.9,3.4-Math.abs(q-0.25)*2.2,0.28,
           -0.34,BIO_MEM,C(46,44,30),2100+k);
  }
  /* THE GULLET. Under the jaw, drooping, wide-belled: a flamethrower's read. */
  brdMaw(m,6.2,2.35,0,{len:4.2,r0:0.92,r1:0.70,flare:1.15,lobes:6,rings:3,
                       pitch:-0.52,col:CHIT_D,seed:2101,u:10,v:8});
  brdLivery(m);
  for(const sd of [-1,1]) m.sphere(-1.4,3.95,sd*1.9,1.05,6,BIO_TEAM,0.84,false);
  m.team(0);
  brdRank(m,2.4,2.6,2.90,2.90,3,3.30,2.25,0.56,[2.05,1.70,1.95]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,6.2,3.55,sd*1.2,2.4,0.28,-sd*0.30,sd*0.20,CLAW,0.16);
    tendril(m,6.4,3.90,sd*0.8,3.6,0.17,sd,1.6,BIO_LEG);
    brdSpiracle(m,-3.4,3.80,sd*1.3,0.42,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-4.0,3.65,0,2.2,1.4,5,0.42,CHIT_D,21021);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ================= 3 β€” MORTARBACK (was mdlHordeBombardier) =================
   Thumper slot: tier 2 artillery, 265px, and a MINIMUM range of 80 β€” it
   cannot defend itself up close, and the model should look like it is aiming
   over things. Short deep body with the back arched high, one heavy throat
   pitched up and rearward off the hump. Shortest body of the six bombardiers.
   =========================================================================== */
function mdlBrdMortarback(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.5,3.20,0,9.0,4.05,3.20,CHITIN,
    {segs:5,ridge:0.25,bump:0.115,keel:0.34,nose:0.46,tail:0.32,
     waist:0.26,waistAt:0.60,waistW:0.085,seed:30303,u:12,v:13});
  brdShell(m);
  carapace(m,4.2,2.85,0,3.0,2.1,1.50,CHIT_D,
    {segs:2,ridge:0.16,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:303,u:9,v:6});
  eyeCluster(m,5.0,3.05,0,0.50,2,HOT);
  /* THE HUMP. A crest laid ALONG the back rather than across the shoulders,
     which is what turns the profile into an arch. */
  brdShell(m);
  brdCrest(m,-1.2,5.15,0,7.2,3.1,1.55,3,CHITIN,BRD_SEAM,30303);
  /* The mortar. Pitched 55 degrees and yawed a full half-turn so it fires back
     over the animal's own abdomen β€” the one weapon here that does not point
     where the head does. */
  brdMaw(m,0.4,5.70,0,{len:5.6,r0:1.15,r1:0.86,flare:0.62,lobes:6,rings:4,
                       pitch:0.96,yaw:Math.PI,col:CHIT_D,seed:303,u:10,v:9});
  brdLivery(m);
  brdCrest(m,3.4,4.30,0,2.8,2.6,0.90,3,BIO_TEAM,C(96,78,120),3033);
  m.team(0);
  brdRank(m,2.2,2.4,3.05,3.05,3,3.30,2.35,0.62,[2.35,1.95,2.20]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,4.8,2.45,sd*1.2,2.0,0.28,-sd*0.30,sd*0.22,CLAW,0.02);
    tendril(m,5.0,3.70,sd*0.8,3.0,0.16,sd,1.4,BIO_LEG);
    m.sphere(-3.2,3.95,sd*1.8,1.00,6,BIO_TEAM,0.82,false);
    brdSpiracle(m,-1.6,4.15,sd*1.6,0.44,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-3.0,4.00,0,2.2,1.6,6,0.46,CHIT_D,30303);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ================= 6 β€” LANCESPINE (was mdlHordeBombardier) =================
   Longbow slot: 110hp, 95dmg, 205px, NO splash. The most fragile thing in
   either group and a pure single-target shot, so it is all reach and no
   armour: the longest, lowest, narrowest body of the twelve, with one rigid
   harpoon running forward almost as far again as the body behind it. Length
   is the entire read; there is deliberately nothing tall on it.
   =========================================================================== */
function mdlBrdLancespine(){
  const m=MB();
  brdShell(m);
  carapace(m,-1.4,2.30,0,13.6,2.65,1.75,CHITIN,
    {segs:8,ridge:0.18,bump:0.095,keel:0.10,nose:0.36,tail:0.30,
     waist:0.26,waistAt:0.50,waistW:0.070,seed:60606,u:14,v:16});
  brdShell(m);
  carapace(m,5.9,2.30,0,3.2,1.65,1.25,CHIT_D,
    {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.32,tail:0.40,seed:606,u:12,v:8});
  eyeCluster(m,6.7,2.45,0,0.44,2,HOT);
  /* THE HARPOON. Straight, unbelled, tapering to a needle over nine units.
     `socket:false` because this one is not a throat β€” nothing opens at the
     end of it, and a rolled rim would blunt the only line on the model. */
  brdMaw(m,6.6,2.30,0,{len:9.0,r0:0.52,r1:0.055,flare:0,lobes:4,rings:7,
                       taper:0.50,socket:false,pitch:0.015,col:CLAW,
                       seam:C(56,54,44),seed:606,u:7,v:10});
  brdLimb(m);
  for(const sd of [-1,1]){
    /* Two launch rails bracing the harpoon's root β€” the part that says the
       spine is fired rather than bitten with. */
    chitinBlade(m,6.4,2.30,sd*0.78,4.2,0.20,-sd*0.05,sd*0.045,CLAW,0.02);
    tendril(m,6.9,3.05,sd*0.7,3.4,0.15,sd,1.2,BIO_LEG);
    m.sphere(-4.4,3.05,sd*1.2,0.80,6,BIO_TEAM,0.84,false);
    brdSpiracle(m,-1.8,3.10,sd*1.1,0.36,CHIT_D);
  }
  brdRank(m,2.6,3.0,2.20,2.20,3,3.55,2.25,0.46,[1.60,1.35,1.50]);
  brdShell(m);
  tubercles(m,-4.8,2.95,0,2.6,1.0,5,0.34,CHIT_D,60606);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ================= 16 β€” SIEGEMOUND (was mdlHordeBombardier) ================
   Bombard slot: 400px range at speed 14 β€” the slowest and longest-ranged unit
   in the game's ground roster. It should look like it barely walks: a squat
   broad mound carried LOW on eight short thick legs, with a near-vertical
   chimney throat as the only tall thing on it. Widest plan of the twelve.
   =========================================================================== */
function mdlBrdSiegemound(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.6,2.90,0,12.4,6.30,3.30,CHITIN,
    {segs:5,ridge:0.28,bump:0.13,keel:0.20,nose:0.50,tail:0.34,
     waist:0.20,waistAt:0.62,waistW:0.10,seed:16016,u:14,v:13});
  brdShell(m);
  carapace(m,5.8,2.70,0,3.4,2.5,1.60,CHIT_D,
    {segs:2,ridge:0.18,bump:0.15,keel:0.10,nose:0.34,tail:0.42,seed:1601,u:9,v:6});
  eyeCluster(m,6.6,2.90,0,0.54,2,HOT);
  /* THE CHIMNEY. Nine units of near-vertical throat with a heavy bell, set
     back over the animal's centre of mass. Everything else on this model is
     deliberately short so that this is the only vertical line. */
  brdMaw(m,-1.0,4.60,0,{len:8.6,r0:1.70,r1:1.05,flare:0.75,lobes:7,rings:5,
                        pitch:1.40,col:CHIT_D,seed:1601,u:11,v:10});
  /* A buttress collar where the chimney leaves the shell β€” a nine-unit organ
     growing straight out of a smooth back reads as bolted on. */
  brdShell(m);
  brdCrest(m,-1.0,4.15,0,5.4,4.6,1.15,3,CHITIN,BRD_SEAM,16016);
  brdLivery(m);
  brdCrest(m,4.0,3.90,0,3.2,3.4,0.95,3,BIO_TEAM,C(96,78,120),1601);
  m.team(0);
  /* EIGHT short thick legs. A mound needs many small supports, not four long
     ones; the count is also what stops it reading as a building. */
  brdRank(m,3.4,2.3,2.70,2.70,4,2.85,1.95,0.66,[3.30,2.85,2.60,2.90]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,6.2,2.35,sd*1.4,2.2,0.34,-sd*0.30,sd*0.24,CLAW,0.02);
    tendril(m,6.6,3.55,sd*0.9,2.8,0.16,sd,1.2,BIO_LEG);
    m.sphere(-5.0,3.85,sd*2.0,1.15,6,BIO_TEAM,0.80,false);
    brdSpiracle(m,2.0,3.95,sd*2.1,0.52,CHIT_D);
    for(let k=0;k<2;k++)
      chitinBlade(m,1.4-k*2.6,3.95,sd*(3.0+k*0.2),2.6,0.34,sd*0.28,Math.PI*0.84,CLAW,0.30);
  }
  brdShell(m);
  tubercles(m,1.8,3.70,0,3.0,2.4,8,0.54,CHIT_D,16016);
  return {hull:m.build(),tur:null,s:1.10};
}

/* ================== 22 β€” RAILFANG (was mdlHordeBombardier) =================
   Lancer slot: 150 damage at 230px on a 900-unit projectile β€” a hypervelocity
   anti-armour shot. The organ that throws it is braced: a long forward lance
   with four struts fanning back from its root into the shoulders, and the
   whole body raked nose-down along the shot line. Slim, forward-leaning, and
   the only silhouette in the twelve with a visible frame around its weapon.
   =========================================================================== */
function mdlBrdRailfang(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.8,3.35,0,11.8,2.95,2.05,CHITIN,
    {segs:6,ridge:0.20,bump:0.10,keel:0.22,nose:0.44,tail:0.28,
     waist:0.30,waistAt:0.52,waistW:0.072,seed:22022,u:12,v:14});
  brdShell(m);
  carapace(m,5.4,3.05,0,3.2,1.9,1.40,CHIT_D,
    {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.42,seed:2202,u:9,v:6});
  eyeCluster(m,6.2,3.25,0,0.48,2,HOT);
  /* THE LANCE. Long, slim, barely belled, raked slightly down along the shot
     line so the model leans into its own shot. */
  brdMaw(m,5.8,3.35,0,{len:7.4,r0:0.66,r1:0.30,flare:0.35,lobes:5,rings:6,
                       taper:0.66,pitch:-0.10,col:CHIT_D,seed:2202,u:9,v:10});
  /* THE BRACE. Four struts from the lance root back into the shoulders. They
     are the difference between "a spike" and "a weapon that recoils". */
  brdLimb(m);
  for(let k=0;k<4;k++){
    const a=k/4*TAU+Math.PI/4;
    chitinBlade(m,5.6+Math.cos(a)*0.0,3.35+Math.sin(a)*0.90,Math.cos(a)*1.05,
                4.4,0.24,0,Math.PI*(a>Math.PI/2&&a<Math.PI*1.5?1.02:0.98),CLAW,
                -Math.sin(a)*0.30);
  }
  brdLivery(m);
  brdCrest(m,1.2,4.55,0,4.6,2.6,1.05,3,BIO_TEAM,C(96,78,120),2202);
  m.team(0);
  brdRank(m,2.6,2.7,3.20,3.20,3,3.55,2.45,0.58,[1.85,1.55,1.75]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,6.2,4.10,sd*0.8,3.8,0.17,sd,1.7,BIO_LEG);
    m.sphere(-4.0,4.15,sd*1.4,0.88,6,BIO_TEAM,0.84,false);
    brdSpiracle(m,-2.0,4.25,sd*1.2,0.40,CHIT_D);
  }
  for(let k=0;k<3;k++)
    chitinBlade(m,-2.0-k*1.9,4.35-k*0.16,0,2.2-k*0.22,0.30,0.10,Math.PI*0.96,CLAW,0.80);
  brdShell(m);
  tubercles(m,-3.6,4.05,0,2.4,1.2,5,0.38,CHIT_D,22022);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ================== 26 β€” BASILISK (was mdlHordeBombardier) =================
   Tier 3, 1100hp, 120 damage at 190px β€” the heaviest thing either group
   fields. It is the only unit in the twelve that gets a HEAD as its dominant
   feature: a crowned skull-plate wider than the body, with one great eye at
   its centre instead of a cluster. Eight limbs, tall stance, long body.
   =========================================================================== */
function mdlBrdBasilisk(){
  const m=MB();
  brdShell(m);
  carapace(m,-1.2,4.65,0,16.4,5.90,3.60,CHITIN,
    {segs:7,ridge:0.28,bump:0.125,keel:0.30,nose:0.50,tail:0.20,
     waist:0.34,waistAt:0.56,waistW:0.068,seed:26026,u:15,v:18});
  brdShell(m);
  carapace(m,8.0,4.40,0,5.4,3.5,2.30,CHIT_D,
    {segs:3,ridge:0.20,bump:0.16,keel:0.14,nose:0.36,tail:0.44,seed:2602,u:11,v:8});
  /* THE CROWN. Wider than the head AND wider than the body, so the plan
     silhouette is a spade β€” a shape nothing else in the twelve makes. */
  brdShell(m);
  brdCrest(m,7.2,6.15,0,6.6,5.4,1.90,3,CHITIN,BRD_SEAM,26026);
  /* ONE eye, not a cluster. At tier 3 the face has to be a face. */
  m.sphere(9.9,4.85,0,1.30,8,HOT,0.92,false);
  brdLimb(m);
  for(let k=0;k<5;k++){
    const q=k/4-0.5;
    chitinBlade(m,7.6-Math.abs(q)*1.0,7.05,q*4.4,3.6-Math.abs(q)*1.1,0.40,q*0.36,
                Math.PI*(0.5+q*0.9),CLAW,0.96);                   // crown horns
  }
  /* Twin flank throats set BACK under the crown, so the head stays the read. */
  for(const sd of [-1,1])
    brdMaw(m,8.6,3.70,sd*1.85,{len:4.4,r0:0.80,r1:0.52,flare:0.85,lobes:5,rings:4,
                               pitch:-0.10,yaw:sd*0.22,col:CHIT_D,seed:2602+sd,u:9,v:8});
  brdLivery(m);
  brdCrest(m,3.0,6.35,0,4.6,3.8,1.30,3,BIO_TEAM,C(96,78,120),2602);
  m.team(0);
  brdRank(m,4.4,2.9,4.50,4.50,4,4.55,3.45,0.90,[3.55,2.95,3.40,3.10]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,9.2,3.35,sd*2.1,4.4,0.56,-sd*0.42,sd*0.26,CLAW,0.04);
    tendril(m,9.6,5.85,sd*1.4,5.6,0.24,sd,2.5,BIO_LEG);
    m.sphere(-5.4,6.00,sd*2.5,1.45,7,BIO_TEAM,0.80,false);
    brdSpiracle(m,-2.4,6.25,sd*2.0,0.62,CHIT_D);
    chitinBlade(m,1.6,6.10,sd*3.2,4.4,0.46,sd*0.30,Math.PI*0.84,CLAW,0.48);
  }
  for(let k=0;k<5;k++)
    chitinBlade(m,-2.4-k*1.9,6.55-k*0.24,0,2.8+Math.sin(k/4*Math.PI)*1.2,0.40,0.10,
                Math.PI*0.96,CLAW,0.90);
  brdShell(m);
  tubercles(m,-5.0,6.10,0,3.6,2.4,10,0.66,CHIT_D,26026);
  return {hull:m.build(),tur:null,s:1.10};
}

/* ================= 27 β€” HARBINGER (was mdlHordeBombardier) =================
   Tier 3, 210px, ground only, 760hp on a 24-size chassis β€” a lightly built
   long-range area caster. It is the inverse of the Siegemound: almost no body,
   carried absurdly high on six very long limbs, with a spore bell HANGING
   under a forward-reaching stalk and a curtain of tendrils trailing beneath
   it. Tallest of the twelve, and the only one whose mass is above its legs'
   knees rather than slung between them.
   =========================================================================== */
function mdlBrdHarbinger(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.6,6.40,0,10.2,3.30,2.30,CHITIN,
    {segs:6,ridge:0.20,bump:0.10,keel:0.24,nose:0.44,tail:0.30,
     waist:0.32,waistAt:0.52,waistW:0.075,seed:27027,u:12,v:14});
  brdShell(m);
  carapace(m,4.6,6.30,0,2.8,1.8,1.35,CHIT_D,
    {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.42,seed:2702,u:9,v:6});
  eyeCluster(m,5.3,6.50,0,0.50,2,LAMP);
  /* THE GANTRY. A stalk reaching forward and up off the shoulders; the bell
     hangs from its far end, so the organ swings out beyond the head instead of
     sitting on the back. Everything below the top bone rides it. */
  const NP=[[0.4,7.30,0],[2.4,8.60,0],[4.4,9.30,0],[6.0,9.40,0]];
  const top=brdStalk(m,NP,0.60,0.40,0.30,[0,0,1]);
  m.bone(top);
  brdLivery(m);
  brdBulb(m,6.0,8.90,0,2.05,6,BIO_TEAM,27027);          // hangs DOWN off the stalk
  m.team(0);
  m.bone(top);
  m.sphere(6.0,9.65,0,0.62,6,LAMP,0.90,false);
  m.bone(top);
  brdLimb(m);
  for(let k=0;k<6;k++){
    const a=k/6*TAU;
    tendril(m,6.0+Math.cos(a)*1.55,6.90,Math.sin(a)*1.70,4.6,0.16,
            Math.sin(a)>0?1:-1,-1.5,BIO_LEG);            // the curtain
  }
  m.bone(-1);
  /* Six very long limbs. The body rides at 6.4 while the knees break above
     10 β€” the animal hangs inside its own legs, and that stilt read is the
     whole silhouette from any angle. */
  brdRank(m,2.0,2.9,6.30,6.30,3,5.30,4.30,0.52,[1.95,1.60,1.85]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,-4.8,6.90,sd*0.9,6.4,0.18,sd,1.4,BIO_LEG);
    m.sphere(-2.6,7.35,sd*1.4,0.92,6,BIO_TEAM,0.84,false);
    brdSpiracle(m,-0.6,7.45,sd*1.2,0.40,CHIT_D);
  }
  for(let k=0;k<3;k++)
    chitinBlade(m,-1.6-k*1.9,7.55-k*0.18,0,2.4-k*0.24,0.30,0.10,Math.PI*0.96,CLAW,0.86);
  brdShell(m);
  tubercles(m,-3.4,7.20,0,2.2,1.2,5,0.38,CHIT_D,27027);
  return {hull:m.build(),tur:null,s:1.12};
}

/* ============================================================================
   THE FOURTEEN β€” SPLITTING THE FIVE REMAINING SHARED BUILDERS
   ----------------------------------------------------------------------------
   After the Spitter/Bombardier split, FAC_KIT.horde still mapped fourteen
   slots onto five builders called with NO arguments:

     mdlHordeBeast      0, 9
     mdlHordeLeviathan  4, 8, 18
     mdlHordeFlyer      5, 17, 25
     mdlHordeSupport    11, 19, 23, 24
     mdlHordeSwimmer    14, 15

   Same failure mode as the twelve: a 40hp skirmisher and a 210hp flamethrower
   were one vertex buffer; a shield, a builder, a sonic gun and a medic were
   one vertex buffer; three aircraft were one pair of wings.

   Same rule: proportion, not decoration. Plan / height / one dominant feature.
   Unarmed slots (11, 19, 24) get no maw β€” a builder that looks armed is a lie
   the player will walk into a fight. Air slots return air:1 so the renderer
   lifts them. Swimmers have no walking legs.

     slot  animal        was          plan            stance   dominant feature
      0    Skitterling   Beast        SHORT + NARROW  low      dart mandibles
      9    Brandmaw      Beast        SHORT + WIDE    low      open furnace mouth
      4    Crownbeast    Leviathan    long + WIDE     TALL     ring of sensory stalks
      8    Worldshell    Leviathan    VERY LONG+WIDE  TALL     stacked dorsal plates
     18    Furnaceback   Leviathan    wide + squat    LOW      twin flank heat sacs
      5    Stingwing     Flyer        compact DELTA   air      triangular wings + sting
     17    Sacfly        Flyer        round + FAT     air      hanging bomb sac
     25    Needlewren    Flyer        long + NARROW   air      swept needle wings
     11    Bastioncrab   Support      SHORT + WIDE    low      disc shield, NO weapon
     19    Weaver        Support      mid + arms      mid      reaching spinnerets
     23    Drumback      Support      round           mid      paired tympanic plates
     24    Ichorleech    Support      NARROW          low      palps + flank sacs
     14    Razorfinn     Swimmer      long + NARROW   LOW      one tall dorsal keel
     15    Keelback      Swimmer      VERY LONG+WIDE  LOW      row of chimney spines
   ============================================================================ */

/* ===================== 0 β€” SKITTERLING (was mdlHordeBeast) =================
   Striker slot: tier 1, 40hp, 5.4dmg at 62px, spd 38. Fastest ground unit and
   the first thing that dies. SHORT and NARROW, carried LOW, with the only
   mass that breaks the plan being a pair of dart mandibles past the head β€”
   a needle, which is the one plan nothing else in this fourteen makes.
   =========================================================================== */
function mdlBrdSkitterling(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.2,2.05,0,7.4,2.35,1.55,CHITIN,
    {segs:5,ridge:0.18,bump:0.10,keel:0.12,nose:0.38,tail:0.30,
     waist:0.22,waistAt:0.52,waistW:0.090,seed:101,u:16,v:16});
  brdShell(m);
  carapace(m,3.4,1.95,0,2.2,1.45,1.05,CHIT_D,
    {segs:2,ridge:0.14,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:10,u:12,v:8});
  eyeCluster(m,4.0,2.15,0,0.38,2,HOT);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,4.2,1.70,sd*0.55,3.2,0.22,-sd*0.18,sd*0.10,CLAW,0.02);  // dart mandibles
    tendril(m,4.1,2.55,sd*0.55,2.4,0.12,sd,1.6,BIO_LEG);
    brdSpiracle(m,-1.6,2.55,sd*0.85,0.28,CHIT_D);
  }
  brdLivery(m);
  brdCrest(m,1.6,2.55,0,2.4,1.8,0.55,3,BIO_TEAM,C(96,78,120),101);
  m.team(0);
  brdRank(m,1.6,1.7,1.95,1.95,3,2.15,1.45,0.38,[1.25,1.05,1.15]);
  brdShell(m);
  tubercles(m,-2.0,2.40,0,1.4,0.9,4,0.28,CHIT_D,101);
  return {hull:m.build(),tur:null,s:0.92};
}

/* ====================== 9 β€” BRANDMAW (was mdlHordeBeast) ===================
   Pyro slot: tier 1, 210hp, 11dmg at 58px, splash 18, melee-typed incendiary.
   SHORT and WIDE, LOW, with one huge open furnace mouth filling the front of
   the plan. Emberthroat (21) already owns the vane-fan + drooping gullet;
   this is the squat ground-level maw that slot is not.
   =========================================================================== */
function mdlBrdBrandmaw(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.4,2.45,0,8.6,4.70,2.25,CHITIN,
    {segs:4,ridge:0.22,bump:0.12,keel:0.10,nose:0.48,tail:0.34,
     waist:0.20,waistAt:0.58,waistW:0.10,seed:909,u:16,v:14});
  brdShell(m);
  carapace(m,3.8,2.30,0,2.6,2.4,1.35,CHIT_D,
    {segs:2,ridge:0.16,bump:0.14,keel:0.08,nose:0.36,tail:0.40,seed:90,u:12,v:8});
  eyeCluster(m,4.4,2.55,0,0.48,2,HOT);
  brdMaw(m,4.6,2.05,0,{len:4.0,r0:1.18,r1:0.92,flare:1.35,lobes:6,rings:3,
                       pitch:-0.12,col:CHIT_D,seed:90,u:12,v:10});
  brdLivery(m);
  for(const sd of [-1,1]) m.sphere(-1.8,3.15,sd*1.7,0.95,6,BIO_TEAM,0.84,false);
  m.team(0);
  brdRank(m,1.8,2.2,2.30,2.30,3,2.70,1.90,0.54,[2.45,2.15,2.25]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,4.4,1.85,sd*1.5,2.0,0.32,-sd*0.28,sd*0.22,CLAW,0.04);
    tendril(m,4.6,3.05,sd*1.0,2.6,0.15,sd,1.1,BIO_LEG);
    brdSpiracle(m,-0.6,3.25,sd*1.7,0.40,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-2.4,3.05,0,2.0,1.6,5,0.42,CHIT_D,909);
  return {hull:m.build(),tur:null,s:1.0};
}

/* ==================== 4 β€” CROWNBEAST (was mdlHordeLeviathan) ===============
   Commander chassis fallback: size 32, 5200hp. Brood's live hero is the
   Sovereign at slot 30; this is what type 4 becomes if the horde kit ever
   fields it. Long + WIDE + TALL, with a RING of sensory stalks on the thorax
   β€” a crown, not scythes (those are the Sovereign) and not stacked plates
   (those are the Worldshell).
   =========================================================================== */
function mdlBrdCrownbeast(){
  const m=MB();
  brdShell(m);
  carapace(m,-1.2,5.10,0,16.4,6.20,3.70,CHITIN,
    {segs:6,ridge:0.26,bump:0.12,keel:0.22,nose:0.48,tail:0.22,
     waist:0.30,waistAt:0.54,waistW:0.072,seed:404,u:14,v:16});
  brdShell(m);
  carapace(m,7.0,4.80,0,4.2,3.0,2.05,CHIT_D,
    {segs:3,ridge:0.18,bump:0.14,keel:0.14,nose:0.36,tail:0.42,seed:40,u:10,v:8});
  eyeCluster(m,8.2,5.15,0,0.78,3,HOT);
  /* THE CROWN. Five short stalks in a ring, each capped with a livery bulb.
     From above that is a pentagon of lights sitting on a wide thorax β€” the
     command read, and the only plan in this group with a hole in the middle. */
  for(let k=0;k<5;k++){
    const a=k/5*TAU+0.2, cx=Math.cos(a)*1.7, cz=Math.sin(a)*1.7;
    const NP=[[0.4+cx,6.6,cz],[0.4+cx*1.15,8.0,cz*1.15]];
    const top=brdStalk(m,NP,0.42,0.28,0.18+k*0.4,[0,0,1]);
    m.bone(top);
    brdLivery(m);
    brdBulb(m,NP[1][0],NP[1][1]+0.15,NP[1][2],0.72,5,BIO_TEAM,404+k);
    m.bone(-1);
    m.team(0);
  }
  brdMaw(m,8.0,4.40,0,{len:4.4,r0:0.86,r1:0.52,flare:0.70,lobes:5,rings:4,
                       pitch:-0.04,col:CHIT_D,seed:40,u:9,v:8});
  brdRank(m,3.2,3.1,4.90,4.90,4,4.40,3.40,0.62,[3.20,2.70,2.90,3.10]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,8.4,6.10,sd*1.1,4.8,0.18,sd,1.8,BIO_LEG);
    m.sphere(-4.2,6.35,sd*2.4,1.20,6,BIO_TEAM,0.82,false);
    brdSpiracle(m,-1.2,6.55,sd*2.2,0.50,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-5.0,6.20,0,3.2,2.2,8,0.58,CHIT_D,404);
  return {hull:m.build(),tur:null,s:1.12};
}

/* ==================== 8 β€” WORLDSHELL (was mdlHordeLeviathan) ===============
   TITAN slot: tier 3, 14000hp, size 46, spd 15, beam. The heaviest thing the
   kit can field. VERY LONG and VERY WIDE, carried TALL on four thick pairs,
   with three overlapping dorsal plates that make the plan a fortress oval.
   The head is small on purpose: this animal is a walking hill, not a face.
   =========================================================================== */
function mdlBrdWorldshell(){
  const m=MB();
  brdShell(m);
  carapace(m,-1.6,6.60,0,22.0,8.40,5.10,CHITIN,
    {segs:8,ridge:0.30,bump:0.13,keel:0.28,nose:0.50,tail:0.18,
     waist:0.26,waistAt:0.56,waistW:0.065,seed:808,u:16,v:18});
  brdShell(m);
  carapace(m,9.4,6.10,0,4.6,3.2,2.20,CHIT_D,
    {segs:3,ridge:0.18,bump:0.14,keel:0.14,nose:0.34,tail:0.44,seed:80,u:10,v:8});
  eyeCluster(m,10.6,6.50,0,0.70,2,HOT);
  brdShell(m);
  brdCrest(m,2.4,10.4,0,10.4,6.6,2.60,3,CHITIN,BRD_SEAM,808);
  brdCrest(m,-4.2,9.6,0,8.8,7.2,2.20,3,CHITIN,BRD_SEAM,818);
  brdCrest(m,-9.0,8.6,0,6.4,5.8,1.70,3,CHIT_D,BRD_SEAM,828);
  brdMaw(m,10.4,5.70,0,{len:5.6,r0:0.92,r1:0.48,flare:0.40,lobes:4,rings:5,
                        pitch:0.04,col:CHIT_D,seed:80,u:9,v:9});
  brdLivery(m);
  brdCrest(m,6.6,8.4,0,4.2,3.6,1.20,3,BIO_TEAM,C(96,78,120),80);
  m.team(0);
  brdRank(m,4.4,3.6,6.40,6.40,4,5.20,4.10,0.78,[4.20,3.60,3.80,4.00]);
  brdLimb(m);
  for(const sd of [-1,1]){
    chitinBlade(m,10.0,5.40,sd*1.4,3.2,0.44,-sd*0.26,sd*0.18,CLAW,0.06);
    tendril(m,10.6,7.40,sd*1.2,5.6,0.20,sd,1.6,BIO_LEG);
    m.sphere(-6.4,8.20,sd*3.2,1.55,6,BIO_TEAM,0.82,false);
    brdSpiracle(m,-2.0,8.55,sd*3.0,0.58,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-7.2,8.10,0,4.2,3.0,10,0.72,CHIT_D,808);
  return {hull:m.build(),tur:null,s:1.18};
}

/* =================== 18 β€” FURNACEBACK (was mdlHordeLeviathan) ==============
   Scorcher slot: tier 2, incendiary, 640hp, 13dmg at 80px, splash 38. Wide
   and squat, LOW, with twin heat sacs sitting OUTBOARD of the thorax so the
   plan is a body with two bulbs β€” not Brandmaw's one mouth, not Emberthroat's
   dorsal fan.
   =========================================================================== */
function mdlBrdFurnaceback(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.6,3.15,0,12.4,5.60,2.85,CHITIN,
    {segs:5,ridge:0.22,bump:0.12,keel:0.14,nose:0.46,tail:0.30,
     waist:0.24,waistAt:0.56,waistW:0.080,seed:1818,u:13,v:14});
  brdShell(m);
  carapace(m,5.6,2.95,0,3.4,2.4,1.55,CHIT_D,
    {segs:2,ridge:0.16,bump:0.14,keel:0.10,nose:0.34,tail:0.42,seed:181,u:9,v:7});
  eyeCluster(m,6.5,3.20,0,0.52,2,HOT);
  brdLivery(m);
  for(const sd of [-1,1]) brdBulb(m,-1.2,4.55,sd*3.15,1.85,6,BIO_TEAM,1818+sd);
  m.team(0);
  for(const sd of [-1,1])
    brdMaw(m,6.2,2.45,sd*0.95,{len:3.8,r0:0.78,r1:0.58,flare:1.10,lobes:6,rings:3,
                               pitch:-0.42,yaw:sd*0.18,col:CHIT_D,seed:181+sd,u:9,v:8});
  brdRank(m,2.4,2.7,3.00,3.00,3,3.20,2.20,0.58,[2.90,2.45,2.70]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,6.4,3.90,sd*1.0,3.2,0.16,sd,1.3,BIO_LEG);
    brdSpiracle(m,1.4,4.05,sd*2.0,0.44,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-4.0,3.90,0,2.6,1.8,6,0.48,CHIT_D,1818);
  return {hull:m.build(),tur:null,s:1.06};
}

/* ====================== 5 β€” STINGWING (was mdlHordeFlyer) ==================
   Wasp slot: tier 1 air, 135hp, 13dmg at 78px, spd 74. Compact DELTA β€” the
   only triangular plan in the kit. Short sting on the nose, two swept
   forewings, tiny hind lobes. No legs; air:1 so the renderer lifts it.
   =========================================================================== */
function mdlBrdStingwing(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.4,1.15,0,8.2,2.55,1.70,CHITIN,
    {segs:5,ridge:0.18,bump:0.10,keel:0.16,nose:0.36,tail:0.28,
     waist:0.20,waistAt:0.50,waistW:0.090,seed:505,u:16,v:14});
  brdShell(m);
  carapace(m,3.6,1.05,0,2.4,1.5,1.05,CHIT_D,
    {segs:2,ridge:0.14,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:50,u:10,v:7});
  eyeCluster(m,4.2,1.25,0,0.40,2,HOT);
  brdMaw(m,4.4,0.95,0,{len:3.0,r0:0.42,r1:0.18,flare:0.25,lobes:4,rings:4,
                       pitch:0.02,col:CHIT_D,seed:50,u:8,v:8});
  brdLimb(m);
  for(const sd of [-1,1]){
    brdWing(m,-0.2,1.05,0,sd,7.6,5.4,0.38,0.38,BIO_MEM,C(46,44,30),505+sd);
    brdWing(m,-2.8,0.85,0,sd,4.6,3.6,0.28,0.22,BIO_MEM,C(46,44,30),515+sd);
    for(let k=0;k<4;k++)
      chitinBlade(m,-0.6-k*0.7,1.15,sd*(1.4+k*1.45),2.6-k*0.3,0.16,sd*0.08,sd*1.05,CLAW,0.04);
    tendril(m,4.2,1.55,sd*0.5,2.8,0.12,sd,1.8,BIO_LEG);
    m.sphere(-1.6,1.75,sd*1.0,0.55,6,BIO_TEAM,0.86,false);
  }
  brdLivery(m);
  brdCrest(m,1.4,1.85,0,2.6,1.6,0.50,3,BIO_TEAM,C(96,78,120),50);
  m.team(0);
  brdShell(m);
  tubercles(m,-2.4,1.65,0,1.6,0.9,4,0.30,CHIT_D,505);
  return {hull:m.build(),tur:null,s:0.96,air:1};
}

/* ======================= 17 β€” SACFLY (was mdlHordeFlyer) ===================
   Raptor slot: tier 1 air, 280hp, 85dmg at 52px, splash 32. A bomber. Round
   and FAT in plan, with a gravid sac hanging UNDER the abdomen β€” the only
   flyer whose mass is below the wing line. Broad rounded wings, almost no
   sweep: a disc, not a dart.
   =========================================================================== */
function mdlBrdSacfly(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.2,1.35,0,10.2,4.20,2.40,CHITIN,
    {segs:4,ridge:0.18,bump:0.11,keel:0.10,nose:0.44,tail:0.40,
     waist:0.28,waistAt:0.42,waistW:0.085,seed:1717,u:16,v:14});
  brdShell(m);
  carapace(m,4.6,1.20,0,2.8,2.0,1.25,CHIT_D,
    {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.34,tail:0.42,seed:171,u:10,v:7});
  eyeCluster(m,5.3,1.40,0,0.46,2,HOT);
  brdLivery(m);
  brdBulb(m,-3.2,0.15,0,2.45,7,BIO_TEAM,1717);            // hangs DOWN
  m.team(0);
  brdLimb(m);
  for(const sd of [-1,1]){
    brdWing(m,-0.4,1.20,0,sd,9.6,6.2,0.42,0.10,BIO_MEM,C(46,44,30),1717+sd);
    brdWing(m,-3.2,1.00,0,sd,6.4,4.4,0.32,0.06,BIO_MEM,C(46,44,30),1727+sd);
    chitinBlade(m,5.0,1.00,sd*1.0,2.0,0.24,-sd*0.22,sd*0.16,CLAW,0.02);
    tendril(m,-3.2,-1.6,sd*1.4,2.8,0.14,sd,-0.8,BIO_LEG);
    m.sphere(-1.2,2.15,sd*1.5,0.72,6,BIO_TEAM,0.84,false);
  }
  brdMaw(m,5.4,1.05,0,{len:2.6,r0:0.55,r1:0.32,flare:0.50,lobes:5,rings:3,
                       pitch:-0.18,col:CHIT_D,seed:171,u:8,v:7});
  brdShell(m);
  tubercles(m,1.2,2.05,0,2.0,1.4,5,0.38,CHIT_D,1717);
  return {hull:m.build(),tur:null,s:1.04,air:1};
}

/* ===================== 25 β€” NEEDLEWREN (was mdlHordeFlyer) =================
   Kestrel slot: tier 1 air scout, 120hp, 14dmg at 150px, spd 96. Long and
   NARROW, the fastest thing in the sky. Swept needle wings whose span is
   three times the body width, plus a pair of antennae β€” a dragonfly, which
   is the one flyer plan that is not a triangle and not a disc.
   =========================================================================== */
function mdlBrdNeedlewren(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.6,1.05,0,9.6,1.90,1.35,CHITIN,
    {segs:6,ridge:0.16,bump:0.09,keel:0.18,nose:0.32,tail:0.24,
     waist:0.26,waistAt:0.50,waistW:0.075,seed:2525,u:16,v:14});
  brdShell(m);
  carapace(m,4.2,0.95,0,2.0,1.15,0.90,CHIT_D,
    {segs:2,ridge:0.12,bump:0.11,keel:0.08,nose:0.30,tail:0.40,seed:252,u:9,v:6});
  eyeCluster(m,4.7,1.15,0,0.34,2,LAMP);
  brdMaw(m,5.0,0.90,0,{len:2.4,r0:0.28,r1:0.12,flare:0.18,lobes:4,rings:4,
                       pitch:0.06,col:CHIT_D,seed:252,u:7,v:7});
  brdLimb(m);
  for(const sd of [-1,1]){
    brdWing(m,0.6,0.95,0,sd,9.2,3.4,0.26,0.72,BIO_MEM,C(46,44,30),2525+sd);
    brdWing(m,-2.8,0.80,0,sd,7.4,2.8,0.22,0.55,BIO_MEM,C(46,44,30),2535+sd);
    tendril(m,4.8,1.35,sd*0.35,4.2,0.10,sd,2.4,BIO_LEG);   // antennae
    m.sphere(-1.8,1.45,sd*0.7,0.42,6,BIO_TEAM,0.88,false);
    brdSpiracle(m,-3.2,1.40,sd*0.55,0.22,CHIT_D);
  }
  brdLivery(m);
  brdCrest(m,1.2,1.55,0,2.2,1.2,0.40,3,BIO_TEAM,C(96,78,120),252);
  m.team(0);
  brdShell(m);
  tubercles(m,-3.0,1.35,0,1.4,0.6,4,0.24,CHIT_D,2525);
  return {hull:m.build(),tur:null,s:0.94,air:1};
}

/* ==================== 11 β€” BASTIONCRAB (was mdlHordeSupport) ===============
   Bulwark slot: tier 2, 950hp, UNARMED, size 21. A walking shield. SHORT and
   VERY WIDE, LOW, with one disc crest wider than the body and nothing that
   looks like a gun. An unarmed unit that looks armed is a lie the player
   will walk into a fight.
   =========================================================================== */
function mdlBrdBastioncrab(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.2,2.55,0,8.4,5.40,2.35,CHITIN,
    {segs:4,ridge:0.22,bump:0.12,keel:0.10,nose:0.48,tail:0.40,
     waist:0.18,waistAt:0.55,waistW:0.10,seed:1111,u:12,v:12});
  brdShell(m);
  carapace(m,3.6,2.40,0,2.4,2.2,1.30,CHIT_D,
    {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.34,tail:0.42,seed:111,u:8,v:6});
  eyeCluster(m,4.2,2.60,0,0.42,2,HOT);
  /* THE DISC. Wider than the body in both axes, low enough that it never
     makes the animal tall β€” from above this IS the silhouette. */
  brdShell(m);
  brdCrest(m,1.4,3.85,0,8.8,8.2,1.70,3,CHITIN,BRD_SEAM,1111);
  brdLivery(m);
  brdCrest(m,3.8,3.25,0,3.2,3.6,0.70,3,BIO_TEAM,C(96,78,120),111);
  m.team(0);
  brdRank(m,2.0,2.3,2.40,2.40,3,2.85,1.95,0.56,[2.70,2.40,2.55]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,4.0,3.15,sd*1.0,2.4,0.14,sd,1.0,BIO_LEG);
    m.sphere(-2.2,3.25,sd*1.9,0.85,6,BIO_TEAM,0.84,false);
    brdSpiracle(m,-0.4,3.45,sd*1.8,0.42,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-2.0,3.20,0,2.2,1.8,6,0.44,CHIT_D,1111);
  return {hull:m.build(),tur:null,s:1.04};
}

/* ====================== 19 β€” WEAVER (was mdlHordeSupport) ==================
   Constructor slot: tier 1 builder, 220hp, UNARMED, spd 44. Mid-length with
   two long spinneret arms reaching past the head and a silk bulb on the
   back. The rasp is blunt and recessed β€” same contract as the Grub: nothing
   here protrudes like a weapon.
   =========================================================================== */
function mdlBrdWeaver(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.4,2.85,0,9.6,3.20,2.10,CHITIN,
    {segs:5,ridge:0.18,bump:0.10,keel:0.14,nose:0.40,tail:0.32,
     waist:0.24,waistAt:0.52,waistW:0.085,seed:1919,u:15,v:15});
  brdShell(m);
  carapace(m,4.4,2.70,0,2.6,1.8,1.20,CHIT_D,
    {segs:2,ridge:0.14,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:191,u:12,v:8});
  eyeCluster(m,5.1,2.90,0,0.44,2,HOT);
  brdRasp(m,5.4,2.55,0,0.85,BRD_SOFT);
  brdLivery(m);
  brdBulb(m,-3.4,4.35,0,1.55,6,BIO_TEAM,1919);
  m.team(0);
  /* Spinneret arms β€” long, forward, NO blades. Two reaching tendrils are
     the builder read at 40px; a claw here would look armed. */
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,4.2,3.05,sd*1.35,5.2,0.22,sd,0.6,BIO_LEG);
    tendril(m,4.6,2.70,sd*0.85,4.4,0.16,sd,0.2,BIO_LEG);
  }
  brdRank(m,2.0,2.4,2.70,2.70,3,3.00,2.10,0.50,[1.75,1.45,1.65]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,5.0,3.50,sd*0.7,3.2,0.14,sd,1.5,BIO_LEG);
    brdSpiracle(m,-1.0,3.55,sd*1.2,0.36,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-2.6,3.45,0,1.8,1.2,5,0.36,CHIT_D,1919);
  return {hull:m.build(),tur:null,s:0.98};
}

/* ===================== 23 β€” DRUMBACK (was mdlHordeSupport) =================
   Resonator slot: tier 2, sonic, 340hp, 34dmg at 130px, splash 18. Round in
   plan, with a paired tympanic plate standing off each flank β€” two vertical
   discs that make an H from above. That is the sonic read, and nothing else
   in the fourteen makes it.
   =========================================================================== */
function mdlBrdDrumback(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.4,3.05,0,10.2,4.40,2.55,CHITIN,
    {segs:5,ridge:0.20,bump:0.11,keel:0.12,nose:0.44,tail:0.34,
     waist:0.22,waistAt:0.54,waistW:0.085,seed:2323,u:13,v:13});
  brdShell(m);
  carapace(m,4.6,2.90,0,2.8,2.0,1.40,CHIT_D,
    {segs:2,ridge:0.14,bump:0.13,keel:0.08,nose:0.34,tail:0.42,seed:232,u:9,v:6});
  eyeCluster(m,5.3,3.10,0,0.48,2,HOT);
  /* THE DRUMS. Two crests stood on the flanks and rolled outboard, so the
     plan silhouette breaks twice β€” an H, not a wedge and not a disc. */
  for(const sd of [-1,1]){
    brdShell(m);
    brdCrest(m,-0.4,4.55,sd*2.85,5.6,3.2,2.10,3,CHITIN,BRD_SEAM,2323+sd);
  }
  brdMaw(m,5.4,2.70,0,{len:3.4,r0:0.72,r1:0.50,flare:0.85,lobes:6,rings:3,
                       pitch:0.08,col:CHIT_D,seed:232,u:9,v:8});
  brdLivery(m);
  brdCrest(m,3.2,4.10,0,2.8,2.4,0.80,3,BIO_TEAM,C(96,78,120),232);
  m.team(0);
  brdRank(m,2.2,2.5,2.90,2.90,3,3.15,2.20,0.54,[2.30,1.95,2.15]);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,5.2,3.70,sd*0.9,3.0,0.15,sd,1.3,BIO_LEG);
    m.sphere(-2.8,3.85,sd*1.7,0.90,6,BIO_TEAM,0.84,false);
    brdSpiracle(m,-0.8,4.05,sd*1.6,0.40,CHIT_D);
  }
  brdShell(m);
  tubercles(m,-3.2,3.75,0,2.2,1.6,5,0.42,CHIT_D,2323);
  return {hull:m.build(),tur:null,s:1.02};
}

/* ==================== 24 β€” ICHORLEECH (was mdlHordeSupport) ================
   Warden slot: tier 2 medic, 420hp, UNARMED. NARROW body, LOW, with a pair
   of palps reaching past the head and a rank of small healing sacs along
   each flank. Slim + palps: the only support plan that is neither a shield,
   nor reaching arms, nor an H.
   =========================================================================== */
function mdlBrdIchorleech(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.4,2.55,0,10.0,2.70,1.95,CHITIN,
    {segs:6,ridge:0.16,bump:0.10,keel:0.14,nose:0.36,tail:0.28,
     waist:0.26,waistAt:0.50,waistW:0.080,seed:2424,u:13,v:13});
  brdShell(m);
  carapace(m,4.6,2.40,0,2.4,1.6,1.10,CHIT_D,
    {segs:2,ridge:0.12,bump:0.12,keel:0.08,nose:0.32,tail:0.40,seed:242,u:8,v:6});
  eyeCluster(m,5.2,2.60,0,0.40,2,LAMP);
  brdRasp(m,5.5,2.30,0,0.70,BRD_SOFT);
  brdLivery(m);
  for(const sd of [-1,1]){
    for(let k=0;k<4;k++)
      brdBulb(m,1.6-k*1.7,3.35,sd*1.55,0.62,5,BIO_TEAM,2424+k+sd);
  }
  m.team(0);
  brdLimb(m);
  for(const sd of [-1,1]){
    tendril(m,5.0,2.70,sd*0.55,4.6,0.16,sd,0.4,BIO_LEG);   // palps
    tendril(m,5.2,2.90,sd*0.35,3.8,0.12,sd,0.8,BIO_LEG);
  }
  brdRank(m,2.0,2.3,2.40,2.40,3,2.80,1.95,0.46,[1.45,1.20,1.35]);
  brdLimb(m);
  for(const sd of [-1,1]) brdSpiracle(m,-1.4,3.15,sd*1.0,0.32,CHIT_D);
  brdShell(m);
  tubercles(m,-3.0,3.05,0,1.6,0.9,4,0.30,CHIT_D,2424);
  return {hull:m.build(),tur:null,s:0.96};
}

/* ==================== 14 β€” RAZORFINN (was mdlHordeSwimmer) =================
   Corvette slot: tier 1 naval, 320hp, 24dmg at 115px, spd 42. Long and
   NARROW, LOW in the water, with one tall dorsal keel. No walking legs β€” a
   knife, which is the one naval plan the Dreadnought is not.
   =========================================================================== */
function mdlBrdRazorfinn(){
  const m=MB();
  brdShell(m);
  carapace(m,-0.8,1.25,0,14.4,3.05,1.75,CHITIN,
    {segs:7,ridge:0.20,bump:0.10,keel:0.22,nose:0.34,tail:0.22,
     waist:0.18,waistAt:0.52,waistW:0.080,seed:1414,u:16,v:12});
  brdShell(m);
  carapace(m,6.4,1.15,0,3.0,1.8,1.15,CHIT_D,
    {segs:2,ridge:0.14,bump:0.12,keel:0.10,nose:0.30,tail:0.40,seed:141,u:10,v:7});
  eyeCluster(m,7.2,1.40,0,0.44,2,HOT);
  brdFin(m,-0.6,2.15,0,8.4,4.6,0.42,-0.22,CHITIN,BRD_SEAM,1414);
  brdMaw(m,7.4,1.05,0,{len:3.6,r0:0.58,r1:0.32,flare:0.45,lobes:4,rings:4,
                       pitch:0.04,col:CHIT_D,seed:141,u:8,v:8});
  brdLimb(m);
  for(const sd of [-1,1]){
    brdWing(m,-0.4,0.85,0,sd,4.8,6.4,0.28,0.18,BIO_MEM,C(46,44,30),1414+sd);
    tendril(m,7.0,1.65,sd*0.7,3.2,0.14,sd,1.2,BIO_LEG);
    m.sphere(-3.6,1.85,sd*1.1,0.62,6,BIO_TEAM,0.84,false);
    brdSpiracle(m,-1.2,2.00,sd*1.0,0.34,CHIT_D);
  }
  brdLivery(m);
  brdCrest(m,3.2,2.05,0,2.8,1.8,0.55,3,BIO_TEAM,C(96,78,120),141);
  m.team(0);
  brdShell(m);
  tubercles(m,-4.4,1.85,0,2.4,1.2,6,0.36,CHIT_D,1414);
  return {hull:m.build(),tur:null,s:1.02};
}

/* ==================== 15 β€” KEELBACK (was mdlHordeSwimmer) ==================
   Dreadnought slot: tier 2 naval, 1300hp, 88dmg at 290px, splash 34, spd 20.
   VERY LONG and WIDE, LOW, with a row of five chimney throats along the
   spine β€” a barge that fires, which is the one naval plan the knife is not.
   =========================================================================== */
function mdlBrdKeelback(){
  const m=MB();
  brdShell(m);
  carapace(m,-1.2,1.45,0,20.4,5.60,2.45,CHITIN,
    {segs:8,ridge:0.24,bump:0.11,keel:0.18,nose:0.42,tail:0.20,
     waist:0.16,waistAt:0.54,waistW:0.070,seed:1515,u:18,v:14});
  brdShell(m);
  carapace(m,8.8,1.30,0,3.6,2.4,1.40,CHIT_D,
    {segs:2,ridge:0.14,bump:0.13,keel:0.10,nose:0.32,tail:0.42,seed:151,u:10,v:7});
  eyeCluster(m,9.8,1.55,0,0.52,2,HOT);
  for(let k=0;k<5;k++)
    brdMaw(m,4.4-k*2.6,2.85,0,{len:3.8,r0:0.62-k*0.04,r1:0.36,flare:0.55,lobes:5,rings:3,
                               pitch:1.12,col:CHIT_D,seed:151+k,u:8,v:8});
  brdLimb(m);
  for(const sd of [-1,1]){
    brdWing(m,-0.6,0.95,0,sd,6.8,9.2,0.36,0.08,BIO_MEM,C(46,44,30),1515+sd);
    brdWing(m,-6.4,0.80,0,sd,5.2,6.4,0.28,0.04,BIO_MEM,C(46,44,30),1525+sd);
    tendril(m,9.6,1.90,sd*0.9,3.6,0.16,sd,1.1,BIO_LEG);
    m.sphere(-5.6,2.25,sd*1.9,0.95,6,BIO_TEAM,0.82,false);
    brdSpiracle(m,-2.0,2.45,sd*1.8,0.42,CHIT_D);
  }
  brdLivery(m);
  brdCrest(m,6.4,2.55,0,3.4,2.6,0.70,3,BIO_TEAM,C(96,78,120),151);
  m.team(0);
  brdShell(m);
  tubercles(m,-6.8,2.15,0,3.4,2.0,8,0.50,CHIT_D,1515);
  return {hull:m.build(),tur:null,s:1.10};
}

/* The exports. Wire into FAC_KIT.horde; nothing here takes over a global.

   SURFACE CONTRACTS. Keys are RAW pre-remap ids β€” the id the builder actually
   emitted β€” and for the Brood that set is small and mostly untouchable.

   MAT.CHITIN and MAT.LEAF are the organic equivalent of MAT.SERVO. mesh.js
   substitutes them into the shader as CHITIN_CONST and BIOLEG_CONST, where the
   vertex stage reads them for the breathing pulse and the spring bend and the
   fragment stage reads them AGAIN for `surfaceOrganic`, the subsurface term.
   Remap either and the animation is stranded on the geometry that carries it:
   a Sovereign is CHITIN from jaw to tail, so one line would stop it breathing
   and turn its shell mechanical in the same pass. Body, crest, limbs and every
   blade are therefore off the table, which leaves the emissive id β€” and, on
   the Prospector, the two hull ids its mouth leaked. */
const BRD_BESPOKE_PACKS=Object.freeze({
  12:Object.freeze({id:'brood-ravager-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({
      /* Chaff, and MAT.LAMP is not only the eyes here: models.js builds the
         first leg while the lamp material eyeCluster() left current is still
         set, so seven segments of it render at full emissive. Filament glow on
         dull tissue is the right answer for an eye AND for a leg. */
      [MAT.LAMP]:MAT.BROOD_VEIN
    })
  }),
  13:Object.freeze({id:'brood-alpha-ravager-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({
      /* The Alpha's exposed brood core shares the eyes' id, and SLIME is the
         one tile whose emissive is wet blobs rather than thin strokes. Against
         the drone's BROOD_VEIN that is the only separation this contract can
         draw β€” every other id the two builders emit is a shader marker. */
      [MAT.LAMP]:MAT.BROOD_SLIME
    })
  }),
  30:Object.freeze({id:'brood-sovereign-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({
      /* Eyes at r 1.30 over three rows: the largest light organ in the kit,
         and the hero should look wet rather than switched on. */
      [MAT.LAMP]:MAT.BROOD_SLIME
    })
  }),
  31:Object.freeze({id:'brood-tidecaster-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({
      /* The synaptic node capping the stalk. VEIN, not SLIME: its emissive is
         linear filaments, which carries on the vein pattern brdBulb() already
         paints into the organ underneath instead of arguing with it. */
      [MAT.LAMP]:MAT.BROOD_VEIN
    })
  }),
  32:Object.freeze({id:'brood-grub-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({
      /* The rasp is the only part of the kit painted from the local palette,
         and those colours are deliberately absent from COL_MAT β€” so matDetect
         fell through to its luminance heuristic and handed the one unarmed
         animal in the game a hull-plate lip and a hull-dark machined bore.
         BRD_SOFT's pale skin is BROOD_MEMBRANE. The gullet takes BROOD_VEIN
         because that tile is a flat fill: the normal map is derived from tile
         luminance, so a flat tile keeps the bore smooth and the hole a hole. */
      [MAT.PLATE]:MAT.BROOD_MEMBRANE,
      [MAT.GREEBLE]:MAT.BROOD_VEIN
    })
  }),

  /* ------------------------------------------------------------------------
     THE TWELVE. Twenty-three Brood slots had no per-asset contract at all β€”
     brdOrganicSurfacePass falls back to IDENTITY for this kit (there is no
     faction-wide remap table), so those units were running the shared atlas
     unmodified. These twelve now declare a SEMANTIC contract.

     Their dedicated geometry is real, but their own BaseAO/NRE/mask triplets
     have not been baked yet β€” except the Gorger (slot 1), which is the first
     live in-engine Brood bake, analogous to the Nova Rhino. Do not advertise
     map names that do not exist: `?assetskin=1` otherwise makes avoidable
     image requests before safely falling back to the shared atlas. `maps:null`
     keeps that fallback deliberate while retaining every organic material-role
     correction below. The Gorger pack names a published triplet and an
     `assetSkin` token so the query cannot silently unwrap the rest of the kit.

     Two ids are worth claiming and the rest are off the table:

       MAT.LAMP  β€” eyes, lumen nodes, and (as on slot 12) any stretch of
                   geometry that inherited the eye material. SLIME for wet
                   blobby organs, VEIN for thin filament glow. Chosen per
                   animal by what the emissive part actually is.

       MAT.TRIM  β€” every claw, mandible, spine and horn. CLAW resolves through
                   COL_MAT to 'hull.trim' (models.js:123), which is a MACHINED
                   METAL TRIM tile: the Brood's blades have been rendering on
                   the same surface as a tank's edge chrome. BROOD_CHITIN is
                   the faction's own shell tile and is what a grown blade is
                   made of. This is the single highest-value remap available
                   to this kit and it costs one line per asset.

     MAT.CHITIN and MAT.LEAF stay put on all twelve: they are the vertex-stage
     markers for the breathing pulse and the limb spring (see the note above),
     so remapping either would strand the animation on the geometry carrying
     it. MAT.BROOD_MEMBRANE (BIO_MEM, the Emberthroat's vanes) is already the
     right tile and needs no override.
     ------------------------------------------------------------------------ */
  1:Object.freeze({id:'brood-gorger-v2', source:'authored', maps:'brood-gorger-v2',
    /* Live mdlBrdGorger hull rebaked in-engine. No turret. Opt-in via
       ?assetskin=1 or ?assetskin=gorger β€” not a global Brood material switch. */
    assetSkin:'gorger',
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  2:Object.freeze({id:'brood-ramparthorn-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  /* Launcher throats, not a face: the rack is what glows, and a rack of five
     apertures wants the wet read rather than filaments. */
  7:Object.freeze({id:'brood-skysting-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  10:Object.freeze({id:'brood-flakspine-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  /* The burst sac is the unit. SLIME is the one tile whose emissive is wet
     blobs, which is exactly what a gravid abdomen about to rupture is. */
  20:Object.freeze({id:'brood-bloomsac-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  21:Object.freeze({id:'brood-emberthroat-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  3:Object.freeze({id:'brood-mortarback-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  /* The harpoon is painted CLAW along its whole nine units, so on this slot
     the TRIM remap is not trim at all β€” it is the unit's primary surface. */
  6:Object.freeze({id:'brood-lancespine-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  16:Object.freeze({id:'brood-siegemound-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  22:Object.freeze({id:'brood-railfang-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  /* One great eye at r 1.30 β€” the largest single light organ outside the
     Sovereign, and it should look wet rather than switched on. */
  26:Object.freeze({id:'brood-basilisk-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  27:Object.freeze({id:'brood-harbinger-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),

  /* THE FOURTEEN. Same contract as the twelve: dedicated geometry is real,
     unique BaseAO/NRE/mask triplets are not baked yet, so maps:null. Weaver
     and Ichorleech reuse the Grub's rasp, so they inherit its PLATE/GREEBLE
     remap or the unarmed mouth becomes machined hull again. */
  0:Object.freeze({id:'brood-skitterling-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  9:Object.freeze({id:'brood-brandmaw-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  4:Object.freeze({id:'brood-crownbeast-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  8:Object.freeze({id:'brood-worldshell-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  18:Object.freeze({id:'brood-furnaceback-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  5:Object.freeze({id:'brood-stingwing-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  /* The bomb sac is the unit. SLIME is the wet-blob tile. */
  17:Object.freeze({id:'brood-sacfly-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  25:Object.freeze({id:'brood-needlewren-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  11:Object.freeze({id:'brood-bastioncrab-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  19:Object.freeze({id:'brood-weaver-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN,
                            [MAT.PLATE]:MAT.BROOD_MEMBRANE, [MAT.GREEBLE]:MAT.BROOD_VEIN})}),
  23:Object.freeze({id:'brood-drumback-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  24:Object.freeze({id:'brood-ichorleech-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_SLIME, [MAT.TRIM]:MAT.BROOD_CHITIN,
                            [MAT.PLATE]:MAT.BROOD_MEMBRANE, [MAT.GREEBLE]:MAT.BROOD_VEIN})}),
  14:Object.freeze({id:'brood-razorfinn-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})}),
  15:Object.freeze({id:'brood-keelback-v2', source:'semantic-bake', maps:null,
    surfaces:Object.freeze({[MAT.LAMP]:MAT.BROOD_VEIN, [MAT.TRIM]:MAT.BROOD_CHITIN})})
});

function brdOrganicSurfacePass(geo,pack){
  if(!geo||!geo.v)return geo;
  const v=geo.v;
  for(let o=11;o<v.length;o+=VFLOATS){
    const raw=v[o],sgn=raw<0?-1:1,packed=Math.abs(raw);
    const whole=Math.floor(packed),src=whole-1;
    const dst=pack&&pack.surfaces&&pack.surfaces[src]!==undefined?pack.surfaces[src]:src;
    if(dst!==undefined)v[o]=sgn*((dst+1)+(packed-whole));
  }
  return geo;
}

const BRD_FACTORY_CACHE=new Map();
function brdBroodFactory(fn,slot){
  const key=slot+':'+fn.name;
  if(BRD_FACTORY_CACHE.has(key))return BRD_FACTORY_CACHE.get(key);
  const wrapped=function(){
    const g=fn();
    const pack=BRD_BESPOKE_PACKS[slot]||null;
    if(g && typeof g==='object'){
      if(g.hull)brdOrganicSurfacePass(g.hull,pack);
      if(g.tur)brdOrganicSurfacePass(g.tur,pack);
      if(g.v)brdOrganicSurfacePass(g,pack);
    }
    return g;
  };
  Object.defineProperty(wrapped,'name',{value:'brdPurple'+slot+'_'+fn.name});
  /* WHAT DIES HERE HAS TO BLEED.
     sim.js asks unitModelOrganic(kit,type) what a corpse is made of, and that
     answer is `ORGANIC_MDL.has(FAC_KIT[kit][type])` β€” the function actually
     registered in the kit, which for every Brood slot is THIS WRAPPER, never
     the builder inside it. models.js can only ever have listed the builders,
     so wrapping a slot silently reclassified the animal as machinery and its
     death dropped scrap instead of biomass.
     That was already true of slots 12 and 13 before this line existed; adding
     twelve more wrapped slots would have made it true of half the roster. The
     wrapper is a Brood creature by construction, so it registers itself. */
  if(typeof ORGANIC_MDL!=='undefined'&&ORGANIC_MDL&&ORGANIC_MDL.add) ORGANIC_MDL.add(wrapped);
  BRD_FACTORY_CACHE.set(key,wrapped);
  return wrapped;
}

/* The registry. Merged over FAC_KIT.horde by mergeFactionUnitKits(), so THIS
   is the live mapping for every slot named here β€” the entries left in
   FAC_KIT.horde are a dead fallback that only fires if this file fails to
   load at all.

   The twelve below are the Spitter and Bombardier splits. The fourteen after
   them are the remaining shared Beast / Leviathan / Flyer / Support / Swimmer
   slots. Before this file, FAC_KIT.horde mapped 28 roster slots onto nine
   builders called with no arguments. */
const UNIT_MDL_BROOD={
  12:brdBroodFactory(typeof mdlRavager==='function'?mdlRavager:mdlBroodmother,12),
  13:brdBroodFactory(typeof mdlAlphaRavager==='function'?mdlAlphaRavager:mdlBroodmother,13),
  30:brdBroodFactory(mdlBrdSovereign,30),
  31:brdBroodFactory(mdlBrdTidecaster,31),
  32:brdBroodFactory(mdlBrdGrub,32),
  /* was mdlHordeSpitter x6 */
  1:brdBroodFactory(mdlBrdGorger,1),
  2:brdBroodFactory(mdlBrdRamparthorn,2),
  7:brdBroodFactory(mdlBrdSkysting,7),
  10:brdBroodFactory(mdlBrdFlakspine,10),
  20:brdBroodFactory(mdlBrdBloomsac,20),
  21:brdBroodFactory(mdlBrdEmberthroat,21),
  /* was mdlHordeBombardier x6 */
  3:brdBroodFactory(mdlBrdMortarback,3),
  6:brdBroodFactory(mdlBrdLancespine,6),
  16:brdBroodFactory(mdlBrdSiegemound,16),
  22:brdBroodFactory(mdlBrdRailfang,22),
  26:brdBroodFactory(mdlBrdBasilisk,26),
  27:brdBroodFactory(mdlBrdHarbinger,27),
  /* was mdlHordeBeast x2 */
  0:brdBroodFactory(mdlBrdSkitterling,0),
  9:brdBroodFactory(mdlBrdBrandmaw,9),
  /* was mdlHordeLeviathan x3 */
  4:brdBroodFactory(mdlBrdCrownbeast,4),
  8:brdBroodFactory(mdlBrdWorldshell,8),
  18:brdBroodFactory(mdlBrdFurnaceback,18),
  /* was mdlHordeFlyer x3 */
  5:brdBroodFactory(mdlBrdStingwing,5),
  17:brdBroodFactory(mdlBrdSacfly,17),
  25:brdBroodFactory(mdlBrdNeedlewren,25),
  /* was mdlHordeSupport x4 */
  11:brdBroodFactory(mdlBrdBastioncrab,11),
  19:brdBroodFactory(mdlBrdWeaver,19),
  23:brdBroodFactory(mdlBrdDrumback,23),
  24:brdBroodFactory(mdlBrdIchorleech,24),
  /* was mdlHordeSwimmer x2 */
  14:brdBroodFactory(mdlBrdRazorfinn,14),
  15:brdBroodFactory(mdlBrdKeelback,15)
};

/* Hull skins. initFactionKits already calls mfAssetSkin on hull when the pack
   names a triplet AND mfAssetSkinEnabled() is on. mesh.js only tests
   ?assetskin=1; the Nova file extends that for `rhino`. This file must not
   edit either owner, so we take over after both return. ?assetskin=gorger
   is the pack token mfPackMaps already honours. Gorger has no turret. */
if(typeof mfAssetSkinEnabled==='function'){
  const _brdSkinOn=mfAssetSkinEnabled;
  mfAssetSkinEnabled=function(){
    try{ if((new URLSearchParams(location.search).get('assetskin')||'')==='gorger') return true; }catch(e){}
    return _brdSkinOn();
  };
}