@the-inclusionist/engine 8.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (756) hide show
  1. package/README.md +89 -78
  2. package/app/css/style.css +584 -374
  3. package/app/public/vendor/fonts/bitter-var-ext.woff2 +0 -0
  4. package/app/public/vendor/fonts/bitter-var.woff2 +0 -0
  5. package/app/public/vendor/fonts/bodonimoda-var-ext.woff2 +0 -0
  6. package/app/public/vendor/fonts/bodonimoda-var.woff2 +0 -0
  7. package/app/public/vendor/fonts/cat-abril_fatface-400-ext.woff2 +0 -0
  8. package/app/public/vendor/fonts/cat-abril_fatface-400.woff2 +0 -0
  9. package/app/public/vendor/fonts/cat-adlam_display-400-ext.woff2 +0 -0
  10. package/app/public/vendor/fonts/cat-adlam_display-400.woff2 +0 -0
  11. package/app/public/vendor/fonts/cat-agu_display-400-ext.woff2 +0 -0
  12. package/app/public/vendor/fonts/cat-agu_display-400.woff2 +0 -0
  13. package/app/public/vendor/fonts/cat-alfa_slab_one-400-ext.woff2 +0 -0
  14. package/app/public/vendor/fonts/cat-alfa_slab_one-400.woff2 +0 -0
  15. package/app/public/vendor/fonts/cat-are_you_serious-400-ext.woff2 +0 -0
  16. package/app/public/vendor/fonts/cat-are_you_serious-400.woff2 +0 -0
  17. package/app/public/vendor/fonts/cat-asimovian-400-ext.woff2 +0 -0
  18. package/app/public/vendor/fonts/cat-asimovian-400.woff2 +0 -0
  19. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400-ext.woff2 +0 -0
  20. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400.woff2 +0 -0
  21. package/app/public/vendor/fonts/cat-bakbak_one-400-ext.woff2 +0 -0
  22. package/app/public/vendor/fonts/cat-bakbak_one-400.woff2 +0 -0
  23. package/app/public/vendor/fonts/cat-bangers-400-ext.woff2 +0 -0
  24. package/app/public/vendor/fonts/cat-bangers-400.woff2 +0 -0
  25. package/app/public/vendor/fonts/cat-barriecito-400-ext.woff2 +0 -0
  26. package/app/public/vendor/fonts/cat-barriecito-400.woff2 +0 -0
  27. package/app/public/vendor/fonts/cat-berkshire_swash-400-ext.woff2 +0 -0
  28. package/app/public/vendor/fonts/cat-berkshire_swash-400.woff2 +0 -0
  29. package/app/public/vendor/fonts/cat-bevan-400-ext.woff2 +0 -0
  30. package/app/public/vendor/fonts/cat-bevan-400.woff2 +0 -0
  31. package/app/public/vendor/fonts/cat-bitcount-400-ext.woff2 +0 -0
  32. package/app/public/vendor/fonts/cat-bitcount-400.woff2 +0 -0
  33. package/app/public/vendor/fonts/cat-bowlby_one_sc-400-ext.woff2 +0 -0
  34. package/app/public/vendor/fonts/cat-bowlby_one_sc-400.woff2 +0 -0
  35. package/app/public/vendor/fonts/cat-bungee_shade-400-ext.woff2 +0 -0
  36. package/app/public/vendor/fonts/cat-bungee_shade-400.woff2 +0 -0
  37. package/app/public/vendor/fonts/cat-charm-400-ext.woff2 +0 -0
  38. package/app/public/vendor/fonts/cat-charm-400.woff2 +0 -0
  39. package/app/public/vendor/fonts/cat-charm-700-ext.woff2 +0 -0
  40. package/app/public/vendor/fonts/cat-charm-700.woff2 +0 -0
  41. package/app/public/vendor/fonts/cat-cherry_bomb_one-400-ext.woff2 +0 -0
  42. package/app/public/vendor/fonts/cat-cherry_bomb_one-400.woff2 +0 -0
  43. package/app/public/vendor/fonts/cat-chokokutai-400-ext.woff2 +0 -0
  44. package/app/public/vendor/fonts/cat-chokokutai-400.woff2 +0 -0
  45. package/app/public/vendor/fonts/cat-chonburi-400-ext.woff2 +0 -0
  46. package/app/public/vendor/fonts/cat-chonburi-400.woff2 +0 -0
  47. package/app/public/vendor/fonts/cat-climate_crisis-400-ext.woff2 +0 -0
  48. package/app/public/vendor/fonts/cat-climate_crisis-400.woff2 +0 -0
  49. package/app/public/vendor/fonts/cat-cookie-400.woff2 +0 -0
  50. package/app/public/vendor/fonts/cat-dela_gothic_one-400-ext.woff2 +0 -0
  51. package/app/public/vendor/fonts/cat-dela_gothic_one-400.woff2 +0 -0
  52. package/app/public/vendor/fonts/cat-fascinate_inline-400-ext.woff2 +0 -0
  53. package/app/public/vendor/fonts/cat-fascinate_inline-400.woff2 +0 -0
  54. package/app/public/vendor/fonts/cat-ga_maamli-400-ext.woff2 +0 -0
  55. package/app/public/vendor/fonts/cat-ga_maamli-400.woff2 +0 -0
  56. package/app/public/vendor/fonts/cat-itim-400-ext.woff2 +0 -0
  57. package/app/public/vendor/fonts/cat-itim-400.woff2 +0 -0
  58. package/app/public/vendor/fonts/cat-jacquard_24-400-ext.woff2 +0 -0
  59. package/app/public/vendor/fonts/cat-jacquard_24-400.woff2 +0 -0
  60. package/app/public/vendor/fonts/cat-kablammo-400-ext.woff2 +0 -0
  61. package/app/public/vendor/fonts/cat-kablammo-400.woff2 +0 -0
  62. package/app/public/vendor/fonts/cat-kalam-400-ext.woff2 +0 -0
  63. package/app/public/vendor/fonts/cat-kalam-400.woff2 +0 -0
  64. package/app/public/vendor/fonts/cat-kalam-700-ext.woff2 +0 -0
  65. package/app/public/vendor/fonts/cat-kalam-700.woff2 +0 -0
  66. package/app/public/vendor/fonts/cat-katibeh-400-ext.woff2 +0 -0
  67. package/app/public/vendor/fonts/cat-katibeh-400.woff2 +0 -0
  68. package/app/public/vendor/fonts/cat-lemonada-400-ext.woff2 +0 -0
  69. package/app/public/vendor/fonts/cat-lemonada-400.woff2 +0 -0
  70. package/app/public/vendor/fonts/cat-lilita_one-400-ext.woff2 +0 -0
  71. package/app/public/vendor/fonts/cat-lilita_one-400.woff2 +0 -0
  72. package/app/public/vendor/fonts/cat-lobster-400-ext.woff2 +0 -0
  73. package/app/public/vendor/fonts/cat-lobster-400.woff2 +0 -0
  74. package/app/public/vendor/fonts/cat-luckiest_guy-400-ext.woff2 +0 -0
  75. package/app/public/vendor/fonts/cat-luckiest_guy-400.woff2 +0 -0
  76. package/app/public/vendor/fonts/cat-marhey-400-ext.woff2 +0 -0
  77. package/app/public/vendor/fonts/cat-marhey-400.woff2 +0 -0
  78. package/app/public/vendor/fonts/cat-modak-400-ext.woff2 +0 -0
  79. package/app/public/vendor/fonts/cat-modak-400.woff2 +0 -0
  80. package/app/public/vendor/fonts/cat-momo_signature-400-ext.woff2 +0 -0
  81. package/app/public/vendor/fonts/cat-momo_signature-400.woff2 +0 -0
  82. package/app/public/vendor/fonts/cat-monofett-400-ext.woff2 +0 -0
  83. package/app/public/vendor/fonts/cat-monofett-400.woff2 +0 -0
  84. package/app/public/vendor/fonts/cat-monomakh-400-ext.woff2 +0 -0
  85. package/app/public/vendor/fonts/cat-monomakh-400.woff2 +0 -0
  86. package/app/public/vendor/fonts/cat-monoton-400-ext.woff2 +0 -0
  87. package/app/public/vendor/fonts/cat-monoton-400.woff2 +0 -0
  88. package/app/public/vendor/fonts/cat-nerko_one-400-ext.woff2 +0 -0
  89. package/app/public/vendor/fonts/cat-nerko_one-400.woff2 +0 -0
  90. package/app/public/vendor/fonts/cat-noto_sans_adlam-400-ext.woff2 +0 -0
  91. package/app/public/vendor/fonts/cat-noto_sans_adlam-400.woff2 +0 -0
  92. package/app/public/vendor/fonts/cat-noto_sans_arabic-400-ext.woff2 +0 -0
  93. package/app/public/vendor/fonts/cat-noto_sans_arabic-400.woff2 +0 -0
  94. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400-ext.woff2 +0 -0
  95. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400.woff2 +0 -0
  96. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400-ext.woff2 +0 -0
  97. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400.woff2 +0 -0
  98. package/app/public/vendor/fonts/cat-noto_sans_jp-400-ext.woff2 +0 -0
  99. package/app/public/vendor/fonts/cat-noto_sans_jp-400.woff2 +0 -0
  100. package/app/public/vendor/fonts/cat-noto_sans_khmer-400-ext.woff2 +0 -0
  101. package/app/public/vendor/fonts/cat-noto_sans_khmer-400.woff2 +0 -0
  102. package/app/public/vendor/fonts/cat-noto_sans_kr-400-ext.woff2 +0 -0
  103. package/app/public/vendor/fonts/cat-noto_sans_kr-400.woff2 +0 -0
  104. package/app/public/vendor/fonts/cat-noto_sans_sc-400-ext.woff2 +0 -0
  105. package/app/public/vendor/fonts/cat-noto_sans_sc-400.woff2 +0 -0
  106. package/app/public/vendor/fonts/cat-noto_sans_thai-400-ext.woff2 +0 -0
  107. package/app/public/vendor/fonts/cat-noto_sans_thai-400.woff2 +0 -0
  108. package/app/public/vendor/fonts/cat-ole-400-ext.woff2 +0 -0
  109. package/app/public/vendor/fonts/cat-ole-400.woff2 +0 -0
  110. package/app/public/vendor/fonts/cat-pacifico-400-ext.woff2 +0 -0
  111. package/app/public/vendor/fonts/cat-pacifico-400.woff2 +0 -0
  112. package/app/public/vendor/fonts/cat-patrick_hand-400-ext.woff2 +0 -0
  113. package/app/public/vendor/fonts/cat-patrick_hand-400.woff2 +0 -0
  114. package/app/public/vendor/fonts/cat-petit_formal_script-400-ext.woff2 +0 -0
  115. package/app/public/vendor/fonts/cat-petit_formal_script-400.woff2 +0 -0
  116. package/app/public/vendor/fonts/cat-pixelify_sans-400-ext.woff2 +0 -0
  117. package/app/public/vendor/fonts/cat-pixelify_sans-400.woff2 +0 -0
  118. package/app/public/vendor/fonts/cat-plaster-400-ext.woff2 +0 -0
  119. package/app/public/vendor/fonts/cat-plaster-400.woff2 +0 -0
  120. package/app/public/vendor/fonts/cat-playwrite_ar_guides-400-0.woff2 +0 -0
  121. package/app/public/vendor/fonts/cat-playwrite_at-400-0.woff2 +0 -0
  122. package/app/public/vendor/fonts/cat-playwrite_at_guides-400-0.woff2 +0 -0
  123. package/app/public/vendor/fonts/cat-playwrite_au_nsw-400-0.woff2 +0 -0
  124. package/app/public/vendor/fonts/cat-playwrite_au_nsw_guides-400-0.woff2 +0 -0
  125. package/app/public/vendor/fonts/cat-playwrite_au_qld-400-0.woff2 +0 -0
  126. package/app/public/vendor/fonts/cat-playwrite_au_qld_guides-400-0.woff2 +0 -0
  127. package/app/public/vendor/fonts/cat-playwrite_au_sa-400-0.woff2 +0 -0
  128. package/app/public/vendor/fonts/cat-playwrite_au_sa_guides-400-0.woff2 +0 -0
  129. package/app/public/vendor/fonts/cat-playwrite_au_tas-400-0.woff2 +0 -0
  130. package/app/public/vendor/fonts/cat-playwrite_au_tas_guides-400-0.woff2 +0 -0
  131. package/app/public/vendor/fonts/cat-playwrite_au_vic-400-0.woff2 +0 -0
  132. package/app/public/vendor/fonts/cat-playwrite_au_vic_guides-400-0.woff2 +0 -0
  133. package/app/public/vendor/fonts/cat-playwrite_be_vlg-400-0.woff2 +0 -0
  134. package/app/public/vendor/fonts/cat-playwrite_be_vlg_guides-400-0.woff2 +0 -0
  135. package/app/public/vendor/fonts/cat-playwrite_be_wal-400-0.woff2 +0 -0
  136. package/app/public/vendor/fonts/cat-playwrite_be_wal_guides-400-0.woff2 +0 -0
  137. package/app/public/vendor/fonts/cat-playwrite_br_guides-400-0.woff2 +0 -0
  138. package/app/public/vendor/fonts/cat-playwrite_ca_guides-400-0.woff2 +0 -0
  139. package/app/public/vendor/fonts/cat-playwrite_cl_guides-400-0.woff2 +0 -0
  140. package/app/public/vendor/fonts/cat-playwrite_co_guides-400-0.woff2 +0 -0
  141. package/app/public/vendor/fonts/cat-playwrite_cu_guides-400-0.woff2 +0 -0
  142. package/app/public/vendor/fonts/cat-playwrite_cz-400-0.woff2 +0 -0
  143. package/app/public/vendor/fonts/cat-playwrite_cz_guides-400-0.woff2 +0 -0
  144. package/app/public/vendor/fonts/cat-playwrite_de_grund-400-0.woff2 +0 -0
  145. package/app/public/vendor/fonts/cat-playwrite_de_grund_guides-400-0.woff2 +0 -0
  146. package/app/public/vendor/fonts/cat-playwrite_de_la-400-0.woff2 +0 -0
  147. package/app/public/vendor/fonts/cat-playwrite_de_la_guides-400-0.woff2 +0 -0
  148. package/app/public/vendor/fonts/cat-playwrite_de_sas-400-0.woff2 +0 -0
  149. package/app/public/vendor/fonts/cat-playwrite_de_sas_guides-400-0.woff2 +0 -0
  150. package/app/public/vendor/fonts/cat-playwrite_de_va-400-0.woff2 +0 -0
  151. package/app/public/vendor/fonts/cat-playwrite_de_va_guides-400-0.woff2 +0 -0
  152. package/app/public/vendor/fonts/cat-playwrite_dk_loopet-400-0.woff2 +0 -0
  153. package/app/public/vendor/fonts/cat-playwrite_dk_loopet_guides-400-0.woff2 +0 -0
  154. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet-400-0.woff2 +0 -0
  155. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet_guides-400-0.woff2 +0 -0
  156. package/app/public/vendor/fonts/cat-playwrite_es_deco_guides-400-0.woff2 +0 -0
  157. package/app/public/vendor/fonts/cat-playwrite_es_guides-400-0.woff2 +0 -0
  158. package/app/public/vendor/fonts/cat-playwrite_fr_moderne-400-0.woff2 +0 -0
  159. package/app/public/vendor/fonts/cat-playwrite_fr_moderne_guides-400-0.woff2 +0 -0
  160. package/app/public/vendor/fonts/cat-playwrite_fr_trad-400-0.woff2 +0 -0
  161. package/app/public/vendor/fonts/cat-playwrite_fr_trad_guides-400-0.woff2 +0 -0
  162. package/app/public/vendor/fonts/cat-playwrite_gb_j_guides-400-0.woff2 +0 -0
  163. package/app/public/vendor/fonts/cat-playwrite_gb_s_guides-400-0.woff2 +0 -0
  164. package/app/public/vendor/fonts/cat-playwrite_hr-400-0.woff2 +0 -0
  165. package/app/public/vendor/fonts/cat-playwrite_hr_guides-400-0.woff2 +0 -0
  166. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva-400-0.woff2 +0 -0
  167. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva_guides-400-0.woff2 +0 -0
  168. package/app/public/vendor/fonts/cat-playwrite_hu-400-0.woff2 +0 -0
  169. package/app/public/vendor/fonts/cat-playwrite_hu_guides-400-0.woff2 +0 -0
  170. package/app/public/vendor/fonts/cat-playwrite_id-400-0.woff2 +0 -0
  171. package/app/public/vendor/fonts/cat-playwrite_id_guides-400-0.woff2 +0 -0
  172. package/app/public/vendor/fonts/cat-playwrite_ie-400-0.woff2 +0 -0
  173. package/app/public/vendor/fonts/cat-playwrite_ie_guides-400-0.woff2 +0 -0
  174. package/app/public/vendor/fonts/cat-playwrite_in-400-0.woff2 +0 -0
  175. package/app/public/vendor/fonts/cat-playwrite_in_guides-400-0.woff2 +0 -0
  176. package/app/public/vendor/fonts/cat-playwrite_is-400-0.woff2 +0 -0
  177. package/app/public/vendor/fonts/cat-playwrite_is_guides-400-0.woff2 +0 -0
  178. package/app/public/vendor/fonts/cat-playwrite_it_moderna-400-0.woff2 +0 -0
  179. package/app/public/vendor/fonts/cat-playwrite_it_moderna_guides-400-0.woff2 +0 -0
  180. package/app/public/vendor/fonts/cat-playwrite_it_trad-400-0.woff2 +0 -0
  181. package/app/public/vendor/fonts/cat-playwrite_it_trad_guides-400-0.woff2 +0 -0
  182. package/app/public/vendor/fonts/cat-playwrite_mx_guides-400-0.woff2 +0 -0
  183. package/app/public/vendor/fonts/cat-playwrite_ng_modern-400-0.woff2 +0 -0
  184. package/app/public/vendor/fonts/cat-playwrite_ng_modern_guides-400-0.woff2 +0 -0
  185. package/app/public/vendor/fonts/cat-playwrite_nl-400-0.woff2 +0 -0
  186. package/app/public/vendor/fonts/cat-playwrite_nl_guides-400-0.woff2 +0 -0
  187. package/app/public/vendor/fonts/cat-playwrite_no-400-0.woff2 +0 -0
  188. package/app/public/vendor/fonts/cat-playwrite_no_guides-400-0.woff2 +0 -0
  189. package/app/public/vendor/fonts/cat-playwrite_nz-400-0.woff2 +0 -0
  190. package/app/public/vendor/fonts/cat-playwrite_nz_basic-400-0.woff2 +0 -0
  191. package/app/public/vendor/fonts/cat-playwrite_nz_basic_guides-400-0.woff2 +0 -0
  192. package/app/public/vendor/fonts/cat-playwrite_nz_guides-400-0.woff2 +0 -0
  193. package/app/public/vendor/fonts/cat-playwrite_pe_guides-400-0.woff2 +0 -0
  194. package/app/public/vendor/fonts/cat-playwrite_pl-400-0.woff2 +0 -0
  195. package/app/public/vendor/fonts/cat-playwrite_pl_guides-400-0.woff2 +0 -0
  196. package/app/public/vendor/fonts/cat-playwrite_pt_guides-400-0.woff2 +0 -0
  197. package/app/public/vendor/fonts/cat-playwrite_ro-400-0.woff2 +0 -0
  198. package/app/public/vendor/fonts/cat-playwrite_ro_guides-400-0.woff2 +0 -0
  199. package/app/public/vendor/fonts/cat-playwrite_sk-400-0.woff2 +0 -0
  200. package/app/public/vendor/fonts/cat-playwrite_sk_guides-400-0.woff2 +0 -0
  201. package/app/public/vendor/fonts/cat-playwrite_tz-400-0.woff2 +0 -0
  202. package/app/public/vendor/fonts/cat-playwrite_tz_guides-400-0.woff2 +0 -0
  203. package/app/public/vendor/fonts/cat-playwrite_us_modern_guides-400-0.woff2 +0 -0
  204. package/app/public/vendor/fonts/cat-playwrite_us_trad_guides-400-0.woff2 +0 -0
  205. package/app/public/vendor/fonts/cat-playwrite_vn-400-0.woff2 +0 -0
  206. package/app/public/vendor/fonts/cat-playwrite_vn_guides-400-0.woff2 +0 -0
  207. package/app/public/vendor/fonts/cat-playwrite_za-400-0.woff2 +0 -0
  208. package/app/public/vendor/fonts/cat-playwrite_za_guides-400-0.woff2 +0 -0
  209. package/app/public/vendor/fonts/cat-potta_one-400-ext.woff2 +0 -0
  210. package/app/public/vendor/fonts/cat-potta_one-400.woff2 +0 -0
  211. package/app/public/vendor/fonts/cat-ranchers-400-ext.woff2 +0 -0
  212. package/app/public/vendor/fonts/cat-ranchers-400.woff2 +0 -0
  213. package/app/public/vendor/fonts/cat-ranga-400-ext.woff2 +0 -0
  214. package/app/public/vendor/fonts/cat-ranga-400.woff2 +0 -0
  215. package/app/public/vendor/fonts/cat-ranga-700-ext.woff2 +0 -0
  216. package/app/public/vendor/fonts/cat-ranga-700.woff2 +0 -0
  217. package/app/public/vendor/fonts/cat-risque-400-ext.woff2 +0 -0
  218. package/app/public/vendor/fonts/cat-risque-400.woff2 +0 -0
  219. package/app/public/vendor/fonts/cat-rowdies-400-ext.woff2 +0 -0
  220. package/app/public/vendor/fonts/cat-rowdies-400.woff2 +0 -0
  221. package/app/public/vendor/fonts/cat-rowdies-700-ext.woff2 +0 -0
  222. package/app/public/vendor/fonts/cat-rowdies-700.woff2 +0 -0
  223. package/app/public/vendor/fonts/cat-ruge_boogie-400-ext.woff2 +0 -0
  224. package/app/public/vendor/fonts/cat-ruge_boogie-400.woff2 +0 -0
  225. package/app/public/vendor/fonts/cat-ruslan_display-400-ext.woff2 +0 -0
  226. package/app/public/vendor/fonts/cat-ruslan_display-400.woff2 +0 -0
  227. package/app/public/vendor/fonts/cat-sacramento-400-ext.woff2 +0 -0
  228. package/app/public/vendor/fonts/cat-sacramento-400.woff2 +0 -0
  229. package/app/public/vendor/fonts/cat-sancreek-400-ext.woff2 +0 -0
  230. package/app/public/vendor/fonts/cat-sancreek-400.woff2 +0 -0
  231. package/app/public/vendor/fonts/cat-sankofa_display-400-ext.woff2 +0 -0
  232. package/app/public/vendor/fonts/cat-sankofa_display-400.woff2 +0 -0
  233. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400-ext.woff2 +0 -0
  234. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400.woff2 +0 -0
  235. package/app/public/vendor/fonts/cat-shantell_sans-400-ext.woff2 +0 -0
  236. package/app/public/vendor/fonts/cat-shantell_sans-400.woff2 +0 -0
  237. package/app/public/vendor/fonts/cat-smokum-400-ext.woff2 +0 -0
  238. package/app/public/vendor/fonts/cat-smokum-400.woff2 +0 -0
  239. package/app/public/vendor/fonts/cat-solitreo-400-ext.woff2 +0 -0
  240. package/app/public/vendor/fonts/cat-solitreo-400.woff2 +0 -0
  241. package/app/public/vendor/fonts/cat-spicy_rice-400-ext.woff2 +0 -0
  242. package/app/public/vendor/fonts/cat-spicy_rice-400.woff2 +0 -0
  243. package/app/public/vendor/fonts/cat-sriracha-400-ext.woff2 +0 -0
  244. package/app/public/vendor/fonts/cat-sriracha-400.woff2 +0 -0
  245. package/app/public/vendor/fonts/cat-srisakdi-400-ext.woff2 +0 -0
  246. package/app/public/vendor/fonts/cat-srisakdi-400.woff2 +0 -0
  247. package/app/public/vendor/fonts/cat-srisakdi-700-ext.woff2 +0 -0
  248. package/app/public/vendor/fonts/cat-srisakdi-700.woff2 +0 -0
  249. package/app/public/vendor/fonts/cat-stix_two_math-400-0.woff2 +0 -0
  250. package/app/public/vendor/fonts/cat-tillana-400-ext.woff2 +0 -0
  251. package/app/public/vendor/fonts/cat-tillana-400.woff2 +0 -0
  252. package/app/public/vendor/fonts/cat-tillana-700-ext.woff2 +0 -0
  253. package/app/public/vendor/fonts/cat-tillana-700.woff2 +0 -0
  254. package/app/public/vendor/fonts/cat-titan_one-400-ext.woff2 +0 -0
  255. package/app/public/vendor/fonts/cat-titan_one-400.woff2 +0 -0
  256. package/app/public/vendor/fonts/cat-yatra_one-400-ext.woff2 +0 -0
  257. package/app/public/vendor/fonts/cat-yatra_one-400.woff2 +0 -0
  258. package/app/public/vendor/fonts/cat-yomogi-400-ext.woff2 +0 -0
  259. package/app/public/vendor/fonts/cat-yomogi-400.woff2 +0 -0
  260. package/app/public/vendor/fonts/dmserifdisplay-400-ext.woff2 +0 -0
  261. package/app/public/vendor/fonts/dmserifdisplay-400.woff2 +0 -0
  262. package/app/public/vendor/fonts/domine-var-ext.woff2 +0 -0
  263. package/app/public/vendor/fonts/domine-var.woff2 +0 -0
  264. package/app/public/vendor/fonts/fraunces-var-ext.woff2 +0 -0
  265. package/app/public/vendor/fonts/fraunces-var.woff2 +0 -0
  266. package/app/public/vendor/fonts/fredoka-var-ext.woff2 +0 -0
  267. package/app/public/vendor/fonts/fredoka-var.woff2 +0 -0
  268. package/app/public/vendor/fonts/jakarta-var-ext.woff2 +0 -0
  269. package/app/public/vendor/fonts/jakarta-var.woff2 +0 -0
  270. package/app/public/vendor/fonts/lora-var-ext.woff2 +0 -0
  271. package/app/public/vendor/fonts/lora-var.woff2 +0 -0
  272. package/app/public/vendor/fonts/merriweather-var-ext.woff2 +0 -0
  273. package/app/public/vendor/fonts/merriweather-var.woff2 +0 -0
  274. package/app/public/vendor/fonts/notosans-var-ext.woff2 +0 -0
  275. package/app/public/vendor/fonts/notosans-var.woff2 +0 -0
  276. package/app/public/vendor/fonts/nunito-var-ext.woff2 +0 -0
  277. package/app/public/vendor/fonts/nunito-var.woff2 +0 -0
  278. package/app/public/vendor/fonts/playfair-var-ext.woff2 +0 -0
  279. package/app/public/vendor/fonts/playfair-var.woff2 +0 -0
  280. package/app/public/vendor/fonts/playwrite-cu.woff2 +0 -0
  281. package/app/public/vendor/fonts/playwrite-es-deco.woff2 +0 -0
  282. package/app/public/vendor/fonts/playwrite-es.woff2 +0 -0
  283. package/app/public/vendor/fonts/playwrite-gb-j.woff2 +0 -0
  284. package/app/public/vendor/fonts/playwrite-gb-s.woff2 +0 -0
  285. package/app/public/vendor/fonts/playwrite-pe.woff2 +0 -0
  286. package/app/public/vendor/fonts/playwrite-pt.woff2 +0 -0
  287. package/app/public/vendor/fonts/quicksand-var-ext.woff2 +0 -0
  288. package/app/public/vendor/fonts/quicksand-var.woff2 +0 -0
  289. package/app/public/vendor/fonts/robotoflex-var-ext.woff2 +0 -0
  290. package/app/public/vendor/fonts/robotoflex-var.woff2 +0 -0
  291. package/app/public/vendor/fonts/sora-var-ext.woff2 +0 -0
  292. package/app/public/vendor/fonts/sora-var.woff2 +0 -0
  293. package/app/public/vendor/fonts/spacegrotesk-var-ext.woff2 +0 -0
  294. package/app/public/vendor/fonts/spacegrotesk-var.woff2 +0 -0
  295. package/app/public/vendor/fonts/spectral-400-ext.woff2 +0 -0
  296. package/app/public/vendor/fonts/spectral-400.woff2 +0 -0
  297. package/app/public/vendor/fonts/spectral-700-ext.woff2 +0 -0
  298. package/app/public/vendor/fonts/spectral-700.woff2 +0 -0
  299. package/app/public/vendor/fonts/teachers-var-ext.woff2 +0 -0
  300. package/app/public/vendor/fonts/teachers-var.woff2 +0 -0
  301. package/app/public/vendor/fonts/ubuntu-400-ext.woff2 +0 -0
  302. package/app/public/vendor/fonts/ubuntu-400.woff2 +0 -0
  303. package/app/public/vendor/fonts/ubuntu-700-ext.woff2 +0 -0
  304. package/app/public/vendor/fonts/ubuntu-700.woff2 +0 -0
  305. package/app/public/vendor/fonts.css +1167 -51
  306. package/dist-pkg/boot/create-game.d.ts +467 -162
  307. package/dist-pkg/boot/create-game.js +3563 -426
  308. package/dist-pkg/core/a11y-sr.d.ts +25 -3
  309. package/dist-pkg/core/a11y-sr.js +40 -19
  310. package/dist-pkg/core/accessible-label.d.ts +17 -0
  311. package/dist-pkg/core/accessible-label.js +37 -0
  312. package/dist-pkg/core/accommodation-subjects.d.ts +26 -0
  313. package/dist-pkg/core/accommodation-subjects.js +32 -0
  314. package/dist-pkg/core/accommodations.d.ts +107 -0
  315. package/dist-pkg/core/accommodations.js +178 -0
  316. package/dist-pkg/core/actions.d.ts +75 -51
  317. package/dist-pkg/core/actions.js +98 -69
  318. package/dist-pkg/core/calm-mode.d.ts +31 -0
  319. package/dist-pkg/core/calm-mode.js +47 -0
  320. package/dist-pkg/core/camera-cycle.d.ts +15 -0
  321. package/dist-pkg/core/camera-cycle.js +7 -0
  322. package/dist-pkg/core/caption-duration.d.ts +15 -0
  323. package/dist-pkg/core/caption-duration.js +19 -0
  324. package/dist-pkg/core/cartridge-problems.d.ts +34 -0
  325. package/dist-pkg/core/cartridge-problems.js +53 -0
  326. package/dist-pkg/core/constants.d.ts +0 -54
  327. package/dist-pkg/core/constants.js +9 -76
  328. package/dist-pkg/core/contract.d.ts +143 -163
  329. package/dist-pkg/core/contract.js +223 -207
  330. package/dist-pkg/core/dom-query.d.ts +4 -4
  331. package/dist-pkg/core/dom-query.js +12 -20
  332. package/dist-pkg/core/entity.d.ts +62 -77
  333. package/dist-pkg/core/entity.js +18 -32
  334. package/dist-pkg/core/escape-html.d.ts +9 -10
  335. package/dist-pkg/core/escape-html.js +16 -20
  336. package/dist-pkg/core/flash-threshold.d.ts +17 -0
  337. package/dist-pkg/core/flash-threshold.js +113 -0
  338. package/dist-pkg/core/game-speed.d.ts +11 -0
  339. package/dist-pkg/core/game-speed.js +16 -0
  340. package/dist-pkg/core/genres.d.ts +20 -0
  341. package/dist-pkg/core/genres.js +44 -0
  342. package/dist-pkg/core/i18n.d.ts +87 -41
  343. package/dist-pkg/core/i18n.js +206 -153
  344. package/dist-pkg/core/loop.d.ts +17 -6
  345. package/dist-pkg/core/loop.js +26 -26
  346. package/dist-pkg/core/pause-icon-catalogue.d.ts +15 -0
  347. package/dist-pkg/core/pause-icon-catalogue.js +54 -0
  348. package/dist-pkg/core/ring.d.ts +17 -0
  349. package/dist-pkg/core/ring.js +29 -0
  350. package/dist-pkg/core/rng.d.ts +7 -12
  351. package/dist-pkg/core/rng.js +23 -53
  352. package/dist-pkg/core/route.d.ts +26 -26
  353. package/dist-pkg/core/route.js +126 -121
  354. package/dist-pkg/core/scenes.d.ts +37 -40
  355. package/dist-pkg/core/scenes.js +41 -43
  356. package/dist-pkg/core/screens.d.ts +4 -4
  357. package/dist-pkg/core/screens.js +11 -16
  358. package/dist-pkg/core/setting-defaults.d.ts +61 -0
  359. package/dist-pkg/core/setting-defaults.js +66 -0
  360. package/dist-pkg/core/speech-rate.d.ts +29 -0
  361. package/dist-pkg/core/speech-rate.js +47 -0
  362. package/dist-pkg/core/state.d.ts +118 -118
  363. package/dist-pkg/core/state.js +322 -297
  364. package/dist-pkg/core/visual-cycles.d.ts +30 -0
  365. package/dist-pkg/core/visual-cycles.js +52 -0
  366. package/dist-pkg/core/visual-state.d.ts +13 -0
  367. package/dist-pkg/core/visual-state.js +6 -0
  368. package/dist-pkg/educational/activities-registry.d.ts +1 -1
  369. package/dist-pkg/educational/activities-registry.js +9 -8
  370. package/dist-pkg/educational/adaptive-engine.d.ts +11 -11
  371. package/dist-pkg/educational/adaptive-engine.js +17 -17
  372. package/dist-pkg/educational/segment-bar.d.ts +12 -12
  373. package/dist-pkg/educational/segment-bar.js +9 -9
  374. package/dist-pkg/i18n/en.js +321 -215
  375. package/dist-pkg/i18n/es.js +320 -215
  376. package/dist-pkg/i18n/pt.js +342 -215
  377. package/dist-pkg/input/default-bindings.d.ts +37 -38
  378. package/dist-pkg/input/default-bindings.js +127 -144
  379. package/dist-pkg/input/devices.d.ts +7 -7
  380. package/dist-pkg/input/devices.js +17 -16
  381. package/dist-pkg/input/edges.d.ts +23 -32
  382. package/dist-pkg/input/edges.js +23 -28
  383. package/dist-pkg/input/empathy-filter.d.ts +22 -0
  384. package/dist-pkg/input/empathy-filter.js +34 -0
  385. package/dist-pkg/input/face-map.d.ts +96 -0
  386. package/dist-pkg/input/face-map.js +255 -0
  387. package/dist-pkg/input/face-signals.d.ts +40 -0
  388. package/dist-pkg/input/face-signals.js +48 -0
  389. package/dist-pkg/input/gamepad.d.ts +164 -145
  390. package/dist-pkg/input/gamepad.js +351 -559
  391. package/dist-pkg/input/gaze-cycle.d.ts +64 -0
  392. package/dist-pkg/input/gaze-cycle.js +133 -0
  393. package/dist-pkg/input/gaze-relative.d.ts +68 -0
  394. package/dist-pkg/input/gaze-relative.js +142 -0
  395. package/dist-pkg/input/hand-map.d.ts +28 -0
  396. package/dist-pkg/input/hand-map.js +142 -0
  397. package/dist-pkg/input/input-cooldown.d.ts +17 -0
  398. package/dist-pkg/input/input-cooldown.js +39 -0
  399. package/dist-pkg/input/keyboard-runtime.d.ts +18 -20
  400. package/dist-pkg/input/keyboard-runtime.js +23 -27
  401. package/dist-pkg/input/keyboard.d.ts +68 -62
  402. package/dist-pkg/input/keyboard.js +83 -105
  403. package/dist-pkg/input/keydown.d.ts +124 -132
  404. package/dist-pkg/input/keydown.js +283 -288
  405. package/dist-pkg/input/latch-edge.d.ts +25 -21
  406. package/dist-pkg/input/latch-edge.js +33 -35
  407. package/dist-pkg/input/latch-scope.d.ts +43 -47
  408. package/dist-pkg/input/latch-scope.js +66 -75
  409. package/dist-pkg/input/latch-store.d.ts +28 -29
  410. package/dist-pkg/input/latch-store.js +44 -47
  411. package/dist-pkg/input/latch-sync.d.ts +28 -29
  412. package/dist-pkg/input/latch-sync.js +25 -25
  413. package/dist-pkg/input/latch.d.ts +14 -17
  414. package/dist-pkg/input/latch.js +29 -37
  415. package/dist-pkg/input/pad-defaults.d.ts +13 -16
  416. package/dist-pkg/input/pad-defaults.js +21 -28
  417. package/dist-pkg/input/pad-reading.d.ts +76 -0
  418. package/dist-pkg/input/pad-reading.js +164 -0
  419. package/dist-pkg/input/pad-wizard.d.ts +96 -0
  420. package/dist-pkg/input/pad-wizard.js +205 -0
  421. package/dist-pkg/input/pointer-space.d.ts +19 -20
  422. package/dist-pkg/input/pointer-space.js +27 -32
  423. package/dist-pkg/input/pointer.d.ts +39 -40
  424. package/dist-pkg/input/pointer.js +42 -45
  425. package/dist-pkg/input/state.d.ts +101 -89
  426. package/dist-pkg/input/state.js +54 -134
  427. package/dist-pkg/input/switch-scan.d.ts +42 -0
  428. package/dist-pkg/input/switch-scan.js +54 -0
  429. package/dist-pkg/input/synthetic-source.d.ts +42 -0
  430. package/dist-pkg/input/synthetic-source.js +57 -0
  431. package/dist-pkg/input/touch-bindings.d.ts +127 -113
  432. package/dist-pkg/input/touch-bindings.js +158 -173
  433. package/dist-pkg/input/touch.d.ts +130 -70
  434. package/dist-pkg/input/touch.js +321 -123
  435. package/dist-pkg/input/transport-in-use.d.ts +107 -0
  436. package/dist-pkg/input/transport-in-use.js +130 -0
  437. package/dist-pkg/input/transports.d.ts +96 -114
  438. package/dist-pkg/input/transports.js +104 -106
  439. package/dist-pkg/input/virtual-controller.d.ts +46 -0
  440. package/dist-pkg/input/virtual-controller.js +41 -0
  441. package/dist-pkg/input/vocabulary-migration.d.ts +50 -49
  442. package/dist-pkg/input/vocabulary-migration.js +80 -79
  443. package/dist-pkg/input/voice-map.d.ts +43 -0
  444. package/dist-pkg/input/voice-map.js +125 -0
  445. package/dist-pkg/platform/audio-ambient.d.ts +13 -1
  446. package/dist-pkg/platform/audio-ambient.js +11 -13
  447. package/dist-pkg/platform/audio-earcons.d.ts +20 -19
  448. package/dist-pkg/platform/audio-earcons.js +51 -42
  449. package/dist-pkg/platform/audio-jingles.js +9 -10
  450. package/dist-pkg/platform/audio-mixer.d.ts +14 -2
  451. package/dist-pkg/platform/audio-mixer.js +36 -39
  452. package/dist-pkg/platform/audio-sonar.d.ts +96 -113
  453. package/dist-pkg/platform/audio-sonar.js +245 -238
  454. package/dist-pkg/platform/audio.d.ts +53 -19
  455. package/dist-pkg/platform/audio.js +161 -169
  456. package/dist-pkg/platform/flash-sampler.d.ts +19 -0
  457. package/dist-pkg/platform/flash-sampler.js +79 -0
  458. package/dist-pkg/platform/guide-intensity.d.ts +26 -27
  459. package/dist-pkg/platform/guide-intensity.js +50 -52
  460. package/dist-pkg/platform/heavy-catalogue.d.ts +31 -0
  461. package/dist-pkg/platform/heavy-catalogue.js +223 -0
  462. package/dist-pkg/platform/heavy-mirror.d.ts +26 -0
  463. package/dist-pkg/platform/heavy-mirror.js +76 -0
  464. package/dist-pkg/platform/heavy.d.ts +104 -0
  465. package/dist-pkg/platform/heavy.js +169 -0
  466. package/dist-pkg/platform/interruptible-speech.d.ts +16 -16
  467. package/dist-pkg/platform/interruptible-speech.js +50 -51
  468. package/dist-pkg/platform/kokoro-port.d.ts +27 -0
  469. package/dist-pkg/platform/kokoro-port.js +50 -0
  470. package/dist-pkg/platform/kokoro-runtime.d.ts +20 -0
  471. package/dist-pkg/platform/kokoro-runtime.js +52 -0
  472. package/dist-pkg/platform/kokoro.d.ts +70 -0
  473. package/dist-pkg/platform/kokoro.js +133 -0
  474. package/dist-pkg/platform/listener-scope.d.ts +12 -0
  475. package/dist-pkg/platform/listener-scope.js +81 -0
  476. package/dist-pkg/platform/locale-host.d.ts +10 -0
  477. package/dist-pkg/platform/locale-host.js +34 -0
  478. package/dist-pkg/platform/microphone.d.ts +51 -0
  479. package/dist-pkg/platform/microphone.js +118 -0
  480. package/dist-pkg/platform/onnx-runtime.d.ts +34 -0
  481. package/dist-pkg/platform/onnx-runtime.js +42 -0
  482. package/dist-pkg/platform/reading-in-worker.d.ts +40 -0
  483. package/dist-pkg/platform/reading-in-worker.js +87 -0
  484. package/dist-pkg/platform/reading-model.d.ts +73 -0
  485. package/dist-pkg/platform/reading-model.js +247 -0
  486. package/dist-pkg/platform/reading-runtime.d.ts +24 -0
  487. package/dist-pkg/platform/reading-runtime.js +159 -0
  488. package/dist-pkg/platform/reading-worker.d.ts +41 -0
  489. package/dist-pkg/platform/reading-worker.js +58 -0
  490. package/dist-pkg/platform/reading.d.ts +69 -0
  491. package/dist-pkg/platform/reading.js +128 -0
  492. package/dist-pkg/platform/speech-recognition.d.ts +51 -0
  493. package/dist-pkg/platform/speech-recognition.js +84 -0
  494. package/dist-pkg/platform/speech.d.ts +14 -1
  495. package/dist-pkg/platform/speech.js +13 -13
  496. package/dist-pkg/platform/storage-keys.d.ts +96 -0
  497. package/dist-pkg/platform/storage-keys.js +106 -0
  498. package/dist-pkg/platform/storage.d.ts +46 -111
  499. package/dist-pkg/platform/storage.js +104 -169
  500. package/dist-pkg/platform/tts.d.ts +51 -38
  501. package/dist-pkg/platform/tts.js +217 -119
  502. package/dist-pkg/platform/vision-loop.d.ts +23 -0
  503. package/dist-pkg/platform/vision-loop.js +56 -0
  504. package/dist-pkg/platform/vision.d.ts +132 -0
  505. package/dist-pkg/platform/vision.js +117 -0
  506. package/dist-pkg/platform/voice-listener.d.ts +36 -0
  507. package/dist-pkg/platform/voice-listener.js +88 -0
  508. package/dist-pkg/platform/voice-plan.d.ts +6 -102
  509. package/dist-pkg/platform/voice-plan.js +9 -151
  510. package/dist-pkg/platform/vosk-runtime.d.ts +71 -0
  511. package/dist-pkg/platform/vosk-runtime.js +93 -0
  512. package/dist-pkg/render/canvas.d.ts +13 -7
  513. package/dist-pkg/render/canvas.js +13 -13
  514. package/dist-pkg/render/crt.d.ts +46 -12
  515. package/dist-pkg/render/crt.js +82 -80
  516. package/dist-pkg/render/cvd-matrices.d.ts +18 -18
  517. package/dist-pkg/render/cvd-matrices.js +33 -38
  518. package/dist-pkg/render/hc-role-data.d.ts +7 -7
  519. package/dist-pkg/render/hc-role-data.js +14 -14
  520. package/dist-pkg/render/high-contrast.d.ts +66 -54
  521. package/dist-pkg/render/high-contrast.js +221 -216
  522. package/dist-pkg/render/low-vision-drawing.d.ts +5 -0
  523. package/dist-pkg/render/low-vision-drawing.js +46 -0
  524. package/dist-pkg/render/lq-filter.d.ts +32 -20
  525. package/dist-pkg/render/lq-filter.js +59 -65
  526. package/dist-pkg/render/port.d.ts +86 -88
  527. package/dist-pkg/render/port.js +17 -23
  528. package/dist-pkg/render/screen-pipeline.d.ts +42 -42
  529. package/dist-pkg/render/screen-pipeline.js +80 -87
  530. package/dist-pkg/render/sprite-fx.d.ts +2 -1
  531. package/dist-pkg/render/sprite-fx.js +16 -14
  532. package/dist-pkg/render/viewports.d.ts +20 -15
  533. package/dist-pkg/render/viewports.js +83 -114
  534. package/dist-pkg/render/viz-axes-labels.d.ts +47 -0
  535. package/dist-pkg/render/viz-axes-labels.js +88 -0
  536. package/dist-pkg/render/viz-axes.d.ts +80 -86
  537. package/dist-pkg/render/viz-axes.js +115 -116
  538. package/dist-pkg/render/viz-modes.d.ts +28 -29
  539. package/dist-pkg/render/viz-modes.js +40 -45
  540. package/dist-pkg/render/viz-refusal.d.ts +32 -0
  541. package/dist-pkg/render/viz-refusal.js +57 -0
  542. package/dist-pkg/render/viz-setters.d.ts +104 -66
  543. package/dist-pkg/render/viz-setters.js +182 -191
  544. package/dist-pkg/ui/aac-sets.d.ts +40 -0
  545. package/dist-pkg/ui/aac-sets.js +49 -0
  546. package/dist-pkg/ui/audio-choices.d.ts +63 -0
  547. package/dist-pkg/ui/audio-choices.js +94 -0
  548. package/dist-pkg/ui/audio-rows-that-apply.d.ts +12 -0
  549. package/dist-pkg/ui/audio-rows-that-apply.js +43 -0
  550. package/dist-pkg/ui/camera-control.d.ts +6 -0
  551. package/dist-pkg/ui/camera-control.js +14 -0
  552. package/dist-pkg/ui/changed-mark.d.ts +9 -8
  553. package/dist-pkg/ui/changed-mark.js +22 -53
  554. package/dist-pkg/ui/control-choices.d.ts +37 -0
  555. package/dist-pkg/ui/control-choices.js +84 -0
  556. package/dist-pkg/ui/debug-panel.d.ts +59 -43
  557. package/dist-pkg/ui/debug-panel.js +68 -69
  558. package/dist-pkg/ui/dom.d.ts +10 -14
  559. package/dist-pkg/ui/dom.js +10 -47
  560. package/dist-pkg/ui/drawing-problems.d.ts +25 -0
  561. package/dist-pkg/ui/drawing-problems.js +93 -0
  562. package/dist-pkg/ui/eye-control.d.ts +36 -0
  563. package/dist-pkg/ui/eye-control.js +178 -0
  564. package/dist-pkg/ui/face-control.d.ts +25 -0
  565. package/dist-pkg/ui/face-control.js +183 -0
  566. package/dist-pkg/ui/focus-trap.d.ts +39 -49
  567. package/dist-pkg/ui/focus-trap.js +56 -65
  568. package/dist-pkg/ui/fonts.d.ts +118 -63
  569. package/dist-pkg/ui/fonts.js +267 -111
  570. package/dist-pkg/ui/game-options.d.ts +42 -0
  571. package/dist-pkg/ui/game-options.js +138 -0
  572. package/dist-pkg/ui/gaze-overlay.d.ts +64 -0
  573. package/dist-pkg/ui/gaze-overlay.js +165 -0
  574. package/dist-pkg/ui/hand-control.d.ts +25 -0
  575. package/dist-pkg/ui/hand-control.js +143 -0
  576. package/dist-pkg/ui/help-panel.d.ts +76 -0
  577. package/dist-pkg/ui/help-panel.js +232 -0
  578. package/dist-pkg/ui/hud-bands.d.ts +45 -0
  579. package/dist-pkg/ui/hud-bands.js +108 -0
  580. package/dist-pkg/ui/hud.d.ts +73 -70
  581. package/dist-pkg/ui/hud.js +94 -98
  582. package/dist-pkg/ui/item-announcement.d.ts +13 -12
  583. package/dist-pkg/ui/item-announcement.js +25 -26
  584. package/dist-pkg/ui/latch-refusal.d.ts +22 -21
  585. package/dist-pkg/ui/latch-refusal.js +37 -37
  586. package/dist-pkg/ui/layout.d.ts +103 -28
  587. package/dist-pkg/ui/layout.js +114 -99
  588. package/dist-pkg/ui/locale-flags.d.ts +16 -0
  589. package/dist-pkg/ui/locale-flags.js +30 -0
  590. package/dist-pkg/ui/loop-crash.d.ts +17 -17
  591. package/dist-pkg/ui/loop-crash.js +33 -62
  592. package/dist-pkg/ui/menu-intent.d.ts +32 -0
  593. package/dist-pkg/ui/menu-intent.js +54 -0
  594. package/dist-pkg/ui/menu-items.d.ts +10 -0
  595. package/dist-pkg/ui/menu-items.js +22 -0
  596. package/dist-pkg/ui/menu-nav.d.ts +60 -90
  597. package/dist-pkg/ui/menu-nav.js +301 -274
  598. package/dist-pkg/ui/mobility-choices.d.ts +26 -0
  599. package/dist-pkg/ui/mobility-choices.js +41 -0
  600. package/dist-pkg/ui/motion-choices.d.ts +31 -0
  601. package/dist-pkg/ui/motion-choices.js +68 -0
  602. package/dist-pkg/ui/motion-scene.d.ts +24 -19
  603. package/dist-pkg/ui/motion-scene.js +29 -61
  604. package/dist-pkg/ui/mount-panel.d.ts +81 -0
  605. package/dist-pkg/ui/mount-panel.js +61 -0
  606. package/dist-pkg/ui/panel-shell.d.ts +65 -31
  607. package/dist-pkg/ui/panel-shell.js +92 -66
  608. package/dist-pkg/ui/panel-widgets.d.ts +116 -0
  609. package/dist-pkg/ui/panel-widgets.js +208 -0
  610. package/dist-pkg/ui/pause-buttons.d.ts +35 -0
  611. package/dist-pkg/ui/pause-buttons.js +66 -0
  612. package/dist-pkg/ui/pause-icons.d.ts +250 -343
  613. package/dist-pkg/ui/pause-icons.js +614 -605
  614. package/dist-pkg/ui/pause-markup.d.ts +86 -0
  615. package/dist-pkg/ui/pause-markup.js +125 -0
  616. package/dist-pkg/ui/reach-notice.d.ts +22 -23
  617. package/dist-pkg/ui/reach-notice.js +55 -58
  618. package/dist-pkg/ui/scan-overlay.d.ts +23 -0
  619. package/dist-pkg/ui/scan-overlay.js +42 -0
  620. package/dist-pkg/ui/settings-aac.d.ts +68 -0
  621. package/dist-pkg/ui/settings-aac.js +189 -0
  622. package/dist-pkg/ui/settings-audio.d.ts +91 -113
  623. package/dist-pkg/ui/settings-audio.js +407 -405
  624. package/dist-pkg/ui/settings-controls.d.ts +53 -92
  625. package/dist-pkg/ui/settings-controls.js +143 -215
  626. package/dist-pkg/ui/settings-empathy.d.ts +39 -30
  627. package/dist-pkg/ui/settings-empathy.js +53 -60
  628. package/dist-pkg/ui/settings-mobility.d.ts +195 -0
  629. package/dist-pkg/ui/settings-mobility.js +374 -0
  630. package/dist-pkg/ui/settings-motion.d.ts +91 -70
  631. package/dist-pkg/ui/settings-motion.js +287 -198
  632. package/dist-pkg/ui/settings-panel.d.ts +48 -45
  633. package/dist-pkg/ui/settings-panel.js +164 -93
  634. package/dist-pkg/ui/settings-typo.d.ts +28 -76
  635. package/dist-pkg/ui/settings-typo.js +145 -125
  636. package/dist-pkg/ui/settings-visual.d.ts +29 -87
  637. package/dist-pkg/ui/settings-visual.js +240 -193
  638. package/dist-pkg/ui/shell.d.ts +113 -134
  639. package/dist-pkg/ui/shell.js +143 -216
  640. package/dist-pkg/ui/simulation-list.d.ts +27 -0
  641. package/dist-pkg/ui/simulation-list.js +76 -0
  642. package/dist-pkg/ui/simulation-over-the-world.d.ts +23 -0
  643. package/dist-pkg/ui/simulation-over-the-world.js +131 -0
  644. package/dist-pkg/ui/simulation-refusal.d.ts +5 -31
  645. package/dist-pkg/ui/simulation-refusal.js +8 -56
  646. package/dist-pkg/ui/switchable-control.d.ts +11 -0
  647. package/dist-pkg/ui/switchable-control.js +19 -0
  648. package/dist-pkg/ui/title.d.ts +4 -4
  649. package/dist-pkg/ui/title.js +11 -13
  650. package/dist-pkg/ui/top-band.d.ts +24 -0
  651. package/dist-pkg/ui/top-band.js +100 -0
  652. package/dist-pkg/ui/typo-choices.d.ts +71 -0
  653. package/dist-pkg/ui/typo-choices.js +84 -0
  654. package/dist-pkg/ui/visual-axes-panel.d.ts +11 -47
  655. package/dist-pkg/ui/visual-axes-panel.js +12 -94
  656. package/dist-pkg/ui/visual-choices.d.ts +77 -0
  657. package/dist-pkg/ui/visual-choices.js +107 -0
  658. package/dist-pkg/ui/vlibras.d.ts +33 -10
  659. package/dist-pkg/ui/vlibras.js +58 -81
  660. package/dist-pkg/ui/voice-control.d.ts +46 -0
  661. package/dist-pkg/ui/voice-control.js +143 -0
  662. package/dist-pkg/ui/voice-settings.d.ts +96 -0
  663. package/dist-pkg/ui/voice-settings.js +302 -0
  664. package/dist-pkg/ui/where-the-child-is.d.ts +31 -0
  665. package/dist-pkg/ui/where-the-child-is.js +70 -0
  666. package/docs/CREDITS.md +198 -54
  667. package/docs/LICENSES.md +160 -145
  668. package/package.json +24 -25
  669. package/scripts/heavy-into-the-delivery.mjs +144 -0
  670. package/scripts/licences/Apache-2.0.txt +176 -0
  671. package/scripts/licences/GPL-3.0.txt +674 -0
  672. package/scripts/licences/MIT.txt +17 -0
  673. package/scripts/licences/third-party.mjs +287 -0
  674. package/scripts/licences/vosk-browser.NOTICE.txt +268 -0
  675. package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
  676. package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
  677. package/dist-pkg/core/anel.d.ts +0 -17
  678. package/dist-pkg/core/anel.js +0 -31
  679. package/dist-pkg/core/collision.d.ts +0 -21
  680. package/dist-pkg/core/collision.js +0 -62
  681. package/dist-pkg/core/layers.d.ts +0 -63
  682. package/dist-pkg/core/layers.js +0 -95
  683. package/dist-pkg/core/letter-grid.d.ts +0 -55
  684. package/dist-pkg/core/letter-grid.js +0 -116
  685. package/dist-pkg/core/password.d.ts +0 -34
  686. package/dist-pkg/core/password.js +0 -205
  687. package/dist-pkg/core/rotulo-acessivel.d.ts +0 -17
  688. package/dist-pkg/core/rotulo-acessivel.js +0 -41
  689. package/dist-pkg/core/run-state.d.ts +0 -115
  690. package/dist-pkg/core/run-state.js +0 -40
  691. package/dist-pkg/core/tiles.d.ts +0 -12
  692. package/dist-pkg/core/tiles.js +0 -61
  693. package/dist-pkg/core/world.d.ts +0 -4
  694. package/dist-pkg/core/world.js +0 -58
  695. package/dist-pkg/input/origem-sintetica.d.ts +0 -44
  696. package/dist-pkg/input/origem-sintetica.js +0 -60
  697. package/dist-pkg/input/transporte-em-uso.d.ts +0 -101
  698. package/dist-pkg/input/transporte-em-uso.js +0 -130
  699. package/dist-pkg/platform/audio-nav.d.ts +0 -51
  700. package/dist-pkg/platform/audio-nav.js +0 -99
  701. package/dist-pkg/platform/pesados-catalogo.d.ts +0 -14
  702. package/dist-pkg/platform/pesados-catalogo.js +0 -177
  703. package/dist-pkg/platform/pesados.d.ts +0 -31
  704. package/dist-pkg/platform/pesados.js +0 -75
  705. package/dist-pkg/platform/vozes-prontas.d.ts +0 -28
  706. package/dist-pkg/platform/vozes-prontas.js +0 -61
  707. package/dist-pkg/render/camera.d.ts +0 -58
  708. package/dist-pkg/render/camera.js +0 -105
  709. package/dist-pkg/render/cenario-data.d.ts +0 -127
  710. package/dist-pkg/render/cenario-data.js +0 -145
  711. package/dist-pkg/render/city-tex.d.ts +0 -63
  712. package/dist-pkg/render/city-tex.js +0 -228
  713. package/dist-pkg/render/city-tiles.d.ts +0 -11
  714. package/dist-pkg/render/city-tiles.js +0 -140
  715. package/dist-pkg/render/draw.d.ts +0 -149
  716. package/dist-pkg/render/draw.js +0 -231
  717. package/dist-pkg/render/fx.d.ts +0 -63
  718. package/dist-pkg/render/fx.js +0 -123
  719. package/dist-pkg/render/minimap.d.ts +0 -11
  720. package/dist-pkg/render/minimap.js +0 -94
  721. package/dist-pkg/render/parallax.d.ts +0 -85
  722. package/dist-pkg/render/parallax.js +0 -158
  723. package/dist-pkg/render/player-anim.d.ts +0 -71
  724. package/dist-pkg/render/player-anim.js +0 -152
  725. package/dist-pkg/render/recycling-tex.d.ts +0 -48
  726. package/dist-pkg/render/recycling-tex.js +0 -164
  727. package/dist-pkg/render/scene-city.d.ts +0 -77
  728. package/dist-pkg/render/scene-city.js +0 -181
  729. package/dist-pkg/render/scene-parallax.d.ts +0 -72
  730. package/dist-pkg/render/scene-parallax.js +0 -426
  731. package/dist-pkg/render/scene-sky.d.ts +0 -126
  732. package/dist-pkg/render/scene-sky.js +0 -295
  733. package/dist-pkg/render/set-cenario.d.ts +0 -54
  734. package/dist-pkg/render/set-cenario.js +0 -103
  735. package/dist-pkg/render/textures.d.ts +0 -75
  736. package/dist-pkg/render/textures.js +0 -240
  737. package/dist-pkg/render/title-scene.d.ts +0 -46
  738. package/dist-pkg/render/title-scene.js +0 -75
  739. package/dist-pkg/render/weather.d.ts +0 -96
  740. package/dist-pkg/render/weather.js +0 -171
  741. package/dist-pkg/render/wheelchair-sprites.d.ts +0 -32
  742. package/dist-pkg/render/wheelchair-sprites.js +0 -69
  743. package/dist-pkg/render/world-tex.d.ts +0 -18
  744. package/dist-pkg/render/world-tex.js +0 -87
  745. package/dist-pkg/ui/activities-menu.d.ts +0 -258
  746. package/dist-pkg/ui/activities-menu.js +0 -648
  747. package/dist-pkg/ui/caa-sets.d.ts +0 -39
  748. package/dist-pkg/ui/caa-sets.js +0 -65
  749. package/dist-pkg/ui/map-hub.d.ts +0 -56
  750. package/dist-pkg/ui/map-hub.js +0 -138
  751. package/dist-pkg/ui/settings-caa.d.ts +0 -45
  752. package/dist-pkg/ui/settings-caa.js +0 -137
  753. package/dist-pkg/ui/settings-motor.d.ts +0 -175
  754. package/dist-pkg/ui/settings-motor.js +0 -342
  755. package/dist-pkg/ui/webcam.d.ts +0 -7
  756. package/dist-pkg/ui/webcam.js +0 -93
@@ -1,117 +1,145 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- // ui/pause-icons — the PER-SCREEN pause menu (`.screen-pause`) and the accessibility icon bar (`.pi-btn`).
3
- // Extracted VERBATIM from game.js (Estágio 4): PAUSE_ICONS, buildScreenPause, calmMode/applyCalm, iconAct,
4
- // iconLabel, reflectIconBtn, reflectPauseIcons and hasPrivateOutput.
2
+ // ui/pause-icons — the PER-SCREEN pause card (`.screen-pause`) and the accessibility quick bar (`.pi-btn`).
5
3
  //
6
- // WHY THE ICON BAR IS ITS OWN SLICE: the ten `.pi-btn` buttons are the ONLY place in the game where a state
7
- // owned by another subsystem (blind mode, TTS, Libras, TEA/reduced-motion, toggle-keys, contrast, CVD) is both
8
- // mutated AND read back as an `aria-label`. That round-trip — "the label must tell the truth about the state" —
9
- // is the whole reason `iconLabel` exists, and it is what the pure functions below make testable in node.
4
+ // WHY THE QUICK BAR IS ITS OWN MODULE: its buttons are the ONLY place where a state owned by another subsystem (blind
5
+ // mode, TTS, Libras, calm mode, the input mode, contrast, colour correction, camera, voice, speed, language) is both
6
+ // changed AND read back as an `aria-label`. That round-trip — «the label must tell the truth about the state» — is why
7
+ // `computeIconLabel` exists, and it is what the pure functions below make testable in node.
10
8
  //
11
- // `iconAct` IS A DISPATCHER (its ctx list is long by nature: seven subsystems, one button each), so it is
12
- // implemented here as a TABLE (`ICON_ACTS`) rather than the original if/else chain. Same order, same
13
- // guards, same announcements — only the shape changed.
9
+ // Each icon's action is a row of a TABLE (`ICON_ACTS`), not a chain of `if`s: the list of subsystems is long by nature,
10
+ // one button each.
14
11
  //
15
- // WHAT STAYS IN game.js (injected):
16
- // · `pauseActor` — read by the gamepad (input/gamepad.ts ctx) and the keyboard router, and by
17
- // openHelp()/openOptions(); this module only WRITES it (`setPauseActor`).
18
- // · `pauseActs` — the `.pm-btn` action table; every entry calls a panel that still lives in game.js
19
- // (openTypo/openAudio/openMovement/motion.open/openVisual/empathy.open/printMode/
20
- // quitGame/openHelp/setPhase/applyLetra/setQuizLevel/joinPlayer/fitsN/vpScreens).
21
- // Injected LAZILY (`getPauseActs`) because it is a `const` declared ~1200 lines below
22
- // the init site — an eager reference would hit its temporal dead zone.
23
- // · `vpPause` — the array of built pause screens; game.js rebuilds it in buildGameHud() and reads it
24
- // in setPhase/navPause/printMode/pauseSelect/__incl. Injected as a getter.
25
- // · `rm`/`saveRM` — the reduced-motion flags object, co-owned with ui/settings-motion (same reference).
26
- // · `PM_BTNS`/`QL_NAME` — owned by ui/activities-menu; injected, never copied.
27
- import { t } from '../core/i18n.js';
28
- import { CONTRAST_LEVELS } from './settings-visual.js';
29
- import { CURTO_DO_TEMA, CURTO_DA_CORRECAO } from './visual-axes-panel.js';
30
- import { proximoTema, proximaCorrecao, temAltoContraste, PADRAO, } from '../render/viz-axes.js';
31
- import { anunciarItem } from './item-announcement.js';
32
- import { rotuloAcessivel } from '../core/rotulo-acessivel.js';
33
- import { passoNoAnel } from '../core/anel.js'; // da FOLHA, e não de ui/menu-nav: ver a nota lá
34
- // LIGAÇÃO VIVA (ESM): o índice pode ser desligado no menu, e o valor aqui acompanha sem assinatura.
35
- import { menuIndexOn, DEFAULTS, setModoCegoValue } from '../core/state.js';
36
- // ⚠️ IMPORT DIRETO DE `platform/storage`, e não uma peça a mais no `ctx`, e a escolha é sobre quem pode
37
- // esquecer: `initPauseIcons` é chamado pela raiz de composição de CADA jogo, e um `store` injetado é um
38
- // campo que um consumidor pode omitir — e omiti-lo faria o nível TEA voltar a não persistir, em silêncio,
39
- // exactamente no jogo que se esqueceu. É a mesma forma que `ui/fonts` usa, e `ui/` depender de `platform/`
40
- // não inverte camada nenhuma.
41
- import * as store from '../platform/storage.js';
42
- import { definirAlternanciaDeMarcha } from './settings-motor.js';
43
- import { recusaDaAlternancia } from './latch-refusal.js';
44
- import { PM_BTNS, PM_OPTIONS_BTNS } from './activities-menu.js';
45
- import { CHAVES_DE_CENA, ANIMACOES_DO_PERSONAGEM, lerCenaGuardada, guardarCena } from './motion-scene.js';
12
+ // INJECTED, not owned here:
13
+ // · `setPauseActor` — who opened the card; the panels open scoped to that player. This module only WRITES it.
14
+ // · `getPauseActs` — the `.pm-btn` action table (`ui/shell` or the host's own), asked when an item is pressed, so it
15
+ // can be wired after this module.
16
+ // · the pause screens, asked through a getter: the host rebuilds them when the number of screens changes.
17
+ // · `rm`/`saveRM` — the reduced-motion flags, shared with ui/settings-motion (same reference).
18
+ // · `matchMedia` — the browser's media query, for the reduced-motion default when nothing is stored (ADR-0232).
19
+ // · `PM_BTNS` and the other lists of buttons are `ui/pause-buttons`', imported, never copied.
20
+ import { flagOf, nextLocale, LANGUAGE_NAME } from './locale-flags.js';
21
+ /*
22
+ * 🔴 THE TWO VISUAL CYCLES MOVED HOUSE to `core/visual-cycles` (ADR-0221, issue #203). They lived in TWO modules — the list of
23
+ * levels in the panel, the steps and the names here — and the thing that costs most to maintain is the ASYMMETRY between them,
24
+ * which only reads with both in sight. See the header there.
25
+ */
26
+ import { SHORT_THEME, SHORT_CORRECTION } from './visual-axes-panel.js';
27
+ /*
28
+ * 🔴 CALM MODE MOVED HOUSE (ADR-0221, issue #203). It is not about ICONS: it is about what a child who cannot bear noise needs
29
+ * the engine to silence, and the icon is only one of the surfaces she asks through. It lives in `core/calm-mode`, a leaf
30
+ * module — zero imports, zero DOM — which is why the destructive clamp of level 1 can be measured without mounting anything.
31
+ */
32
+ import { CALM_NAMES, CALM_AUDIO_CATS, nextCalmMode, sanitiseTeaLevel, calmAudioPlan, calmMotionPlan } from '../core/calm-mode.js';
33
+ /*
34
+ * 🔴 THE MARKUP MOVED HOUSE to `ui/pause-markup` (ADR-0221, issue #203). Building the strings and wiring the elements the
35
+ * browser makes from them are two jobs that do not need each other; what is left in this file is the second one. See the
36
+ * header there for why the icon catalogue had to leave before the markup could.
37
+ */
38
+ import { quickBarMarkup, screenPauseMarkup, } from './pause-markup.js';
39
+ /*
40
+ * 🔴 THE ICON CATALOGUE MOVED HOUSE to `core/pause-icon-catalogue` (ADR-0221, issue #203): WHICH icons exist and in what order
41
+ * is DATA, and this file is about what they DO. See the header there for why the data had to leave first.
42
+ */
43
+ import { PAUSE_ICONS, pauseIcon } from '../core/pause-icon-catalogue.js';
44
+ import { nextTheme, nextCorrection, hasHighContrast, DEFAULT_VISUAL, } from '../render/viz-axes.js';
45
+ import { announceItem } from './item-announcement.js';
46
+ import { accessibleLabel } from '../core/accessible-label.js';
47
+ import { stepInRing } from '../core/ring.js'; // from the LEAF, not from ui/menu-nav: see the note there
48
+ // A LIVE BINDING (ESM): the index can be turned off in the menu, and the value here follows without a subscription.
49
+ // The stateless half (ADR-0232): the defaults, the reduced-motion question and the 📷 cycle are vocabulary, not the store.
50
+ import { DEFAULTS, defaultReducedMotion } from '../core/setting-defaults.js';
51
+ import { nextCameraControl } from '../core/camera-cycle.js';
52
+ /** The word for each position of the 📷 cycle (ADR-0215). */
53
+ const CAMERA_MODE_NAME = { off: 'state.off', hands: 'camera.hands', face: 'camera.face', eyes: 'camera.eyes' };
46
54
  /**
47
- * A LEGENDA de um ícone da barra de acessibilidade — uma função, e não três cópias da mesma expressão.
55
+ * The word for each position. Private again since 2026-09-21: it was exported for the motor panel's row, and the Dev took that
56
+ * row out («Tire a linha de acessibilidade motora») — the icon is the only surface of this setting now.
57
+ */
58
+ const INPUT_MODE_NAME = { standard: 'input.standard', sticky: 'input.sticky', scan: 'input.scan' };
59
+ /**
60
+ * Which of the three is showing. The SCAN WINS over the latch on purpose: with one button there is nothing to hold, so a stored
61
+ * latch would otherwise make the icon claim a position the child is not in.
62
+ */
63
+ export function inputModeOf(s) {
64
+ return s.switchScan ? 'scan' : s.toggleMove ? 'sticky' : 'standard';
65
+ }
66
+ /**
67
+ * The next position. Two things take one away, and neither is an error to report — a cycle simply never stops where nothing
68
+ * would happen (ADR-0155), and a word that promised what this game cannot do would teach a child that her setting is broken
69
+ * (ADR-0106 §5):
48
70
  *
49
- * Ela é escrita em TRÊS momentos que parecem diferentes e são o mesmo: o cursor direcional pousa no ícone
50
- * (`ui/menu-nav`), o dedo o aciona, e o mouse ou o foco passa por cima. Enquanto eram três linhas soltas, o
51
- * índice do ADR-0044 teria de ser acrescentado em três lugares — e a chance de um ficar para trás é a mesma
52
- * que este repositório já pagou dezesseis vezes com o `DomQuery`.
71
+ * · A GAME THAT HOLDS NO KEY has nothing for the latch to hold, so the icon offers standard and one button.
72
+ * · A DEVICE THAT SENDS ONE COMMAND AT A TIME always latches and cannot be asked not to (ADR-0113 clause 3, ADR-0211), so
73
+ * `standard` is unreachable there. 🔴 That used to grey the whole icon out, and with three positions that would have cost a
74
+ * child playing with her eyes the one-button scan as well — the lock is the latch's, not the scan's.
75
+ */
76
+ function inputModeOrder(holdsKeys, latchRequired = false) {
77
+ if (!holdsKeys)
78
+ return ['standard', 'scan'];
79
+ return latchRequired ? ['sticky', 'scan'] : ['standard', 'sticky', 'scan'];
80
+ }
81
+ export function nextInputMode(m, holdsKeys, latchRequired = false) {
82
+ const order = inputModeOrder(holdsKeys, latchRequired);
83
+ const i = order.indexOf(m);
84
+ return order[(i < 0 ? 0 : i + 1) % order.length];
85
+ }
86
+ // The rule for applying an input mode is written in the icon's own action, where it happens: the ☝️ is the only surface of
87
+ // this setting.
88
+ import { nextGameSpeed } from '../core/game-speed.js';
89
+ import { KEYS } from '../platform/storage-keys.js';
90
+ import { setMoveLatch } from './settings-mobility.js';
91
+ import { latchRefusal } from './latch-refusal.js';
92
+ import { PM_BTNS, PM_OPTIONS_BTNS, PM_GAME_BTNS } from './pause-buttons.js';
93
+ import { SCENE_KEYS, CHARACTER_ANIMATIONS, readStoredScene, storeScene } from './motion-scene.js';
94
+ /**
95
+ * Hover and focus on a bar's icons write its NAME in the bar's `.pause-icons-cap` and ask for its EXPLANATION.
53
96
  *
54
- * O `aria-label` já conta o ESTADO ("Alto contraste, ativado"): é ele que o `reflectIconBtn` reescreve a cada
55
- * mudança, e é por isso que a legenda o lê de volta em vez de recompor o texto por conta própria.
97
+ * Shared by the per-screen quick bars and the bar the engine mounts in `#title-icons`: two copies of this wiring is
98
+ * how one bar showed the name and the other did not — the Dev found the engine's bar silent on hover and on the
99
+ * cursor. Leaving an icon falls back to the CURSOR of the bar's mode when there is one (`.pi-sel`): it is the only
100
+ * thing saying where that cursor is.
56
101
  */
57
- export function legendaDoIcone(barra, el) {
58
- const icones = [...barra.querySelectorAll('.pi-btn')];
59
- // A regra "rótulo declarado vence" nasceu AQUI e valia só para os dez ícones. Virou `core/rotulo-acessivel`
60
- // e agora vale para o menu inicial e para a lista de pausa também — uma resposta para "como se chama este
61
- // controle", e não três.
62
- return anunciarItem({ rotulo: rotuloAcessivel(el), posicao: icones.indexOf(el) + 1, total: icones.length }, menuIndexOn);
102
+ export function wireBarCaption(bar, explain) {
103
+ const cap = bar.querySelector('.pause-icons-cap');
104
+ const captionFor = (b) => {
105
+ if (cap)
106
+ cap.textContent = accessibleLabel(b); // name and state only: «N de M» is spoken, never written (ADR-0167)
107
+ explain(b.dataset.pi ?? null);
108
+ };
109
+ const restoreCaption = () => {
110
+ const cursor = bar.querySelector('.pi-sel');
111
+ if (cursor) {
112
+ captionFor(cursor);
113
+ return;
114
+ }
115
+ if (cap)
116
+ cap.textContent = '';
117
+ explain(null);
118
+ };
119
+ bar.querySelectorAll('.pi-btn').forEach((b) => {
120
+ b.addEventListener('mouseenter', () => captionFor(b));
121
+ b.addEventListener('focus', () => captionFor(b));
122
+ b.addEventListener('mouseleave', restoreCaption);
123
+ b.addEventListener('blur', restoreCaption);
124
+ });
63
125
  }
64
- /** The accessibility shortcut bar at the top of every pause screen (and of the splash `#title-icons`).
65
- * Sound-bound icons (blind/TTS) require a private audio output; webcam/voice are still `soon`.
66
- * VERBATIM from game.js in order and behaviour; the names became i18n keys in the Fase-5 pass. */
67
- // `n` é a CHAVE i18n do nome do ícone (o emoji `e` não traduz — é o mesmo glifo em toda língua).
68
- export const PAUSE_ICONS = [
69
- { k: 'blind', e: '🦯', n: 'icon.blind' },
70
- { k: 'tts', e: '🗨️', n: 'icon.tts' },
71
- { k: 'libras', e: '🤟', n: 'icon.libras' },
72
- { k: 'tea', e: '🧩', n: 'icon.tea' },
73
- { k: 'altmove', e: '☝️', n: 'icon.altmove' }, // ☝️ e não 🦾: o gesto é UM DEDO tocando, que é o que a alternância pede (pedido do Dev)
74
- { k: 'contrast', e: '🌗', n: 'icon.contrast' },
75
- { k: 'cvd', e: '🚥', n: 'icon.cvd' },
76
- { k: 'face', e: '🧑', n: 'icon.face', soon: true },
77
- { k: 'eyes', e: '👀', n: 'icon.eyes', soon: true },
78
- { k: 'voice', e: '👄', n: 'icon.voice', soon: true },
79
- ];
80
- const ICON_BY_KEY = new Map(PAUSE_ICONS.map((ic) => [ic.k, ic]));
81
- export function pauseIcon(k) { return ICON_BY_KEY.get(k); }
82
- /** TEA cycle: 0 = normal · 1 = calmo (reduces) · 2 = silencioso (switches off). Never touches TTS/blind mode. */
83
- export const CALM_NAMES = ['calm.off', 'calm.quiet', 'calm.silent'];
84
126
  /**
85
- * O nível TEA guardado, saneado. Fora de 0..2 devolve o padrão — dado do navegador é dado de fora, e um
86
- * nível inventado escolheria `CALM_NAMES[3]`, que é `undefined`, e o anúncio ao leitor de tela sairia vazio.
127
+ * What an icon of the quick bar SAYS — one function, not three copies of the same expression. It is spoken at moments
128
+ * that look different and are the same (the cursor lands on the icon, a finger activates it), and the `aria-label` it
129
+ * reads already carries the STATE («Alto contraste, ativado»): `reflectIconBtn` rewrites it on every change, which is why
130
+ * this reads it back instead of rebuilding the text.
87
131
  */
88
- export function saneiaNivelTea(bruto) {
89
- return Number.isInteger(bruto) && bruto >= 0 && bruto < CALM_NAMES.length ? bruto : DEFAULTS.calmMode;
132
+ function iconCaption(t, barEl, el, menuIndexOn) {
133
+ const icons = [...barEl.querySelectorAll('.pi-btn')];
134
+ // «A declared label wins» is `core/accessible-label`'s rule, shared with the pause list and every menu: one answer to
135
+ // «what is this control called».
136
+ return announceItem(t, { label: accessibleLabel(el), position: icons.indexOf(el) + 1, total: icons.length }, menuIndexOn);
90
137
  }
91
- /** Lê o nível TEA do armazenamento. Chamado no `init`, nunca no import. */
92
- function lerNivelTea() {
93
- return saneiaNivelTea(store.getNum(store.KEYS.tea, DEFAULTS.calmMode));
138
+ /** Reads the calm level from storage, sanitised. Called in `init`, never on import. */
139
+ function readTeaLevel(store) {
140
+ return sanitiseTeaLevel(store.getNum(KEYS.tea, DEFAULTS.calmMode), DEFAULTS.calmMode);
94
141
  }
95
- /** The audio categories `applyCalm` governs. TTS/sonar/guarda/guia stay untouched — a calm player still needs them. */
96
- export const CALM_AUDIO_CATS = ['ambient', 'music', 'earcons', 'other', 'interact'];
97
- /** Colour-vision-deficiency cycle, in `player.viz` values. */
98
- export const CVD_SEQ = ['normal', 'fix-protan', 'fix-deuter', 'fix-tritan'];
99
- /** i18n keys of the CVD announcement names, indexed the same as CVD_SEQ.
100
- *
101
- * ⚠️ POSITION 0 IS `cvd.tricro` AND NOT `cvd.off`, AND THE TWO KEYS ARE NOT INTERCHANGEABLE.
102
- * This list names the four CHOICES of the cycle, so position 0 is a way of seeing — trichromatic
103
- * vision, the one that needs no correction — and it is said as one. `cvd.off` below is a FALLBACK
104
- * for `s.viz` values that are not corrections at all, and 13 of the 16 viz modes are exactly that:
105
- * the three simulations, the three contrast levels, the five low-vision modes and blind mode.
106
- * Announcing "trichromatic vision" there would have the software assert what the child sees, while
107
- * she is simulating not seeing it. The choice is named; the fallback is switched off. */
108
- export const CVD_NAMES = ['cvd.tricro', 'cvd.protan', 'cvd.deuter', 'cvd.tritan'];
109
- /** `player.viz` → i18n key of the label used by iconLabel (anything else falls back to the 'off' key). */
110
- export const CVD_LABELS = {
111
- 'fix-protan': 'cvd.protan', 'fix-deuter': 'cvd.deuter', 'fix-tritan': 'cvd.tritan',
112
- };
113
- /** A player has private output when nobody else is on the same sink. Single screen ⇒ always private.
114
- * Pure form of game.js's hasPrivateOutput (see the report: it had no other caller left). */
142
+ /** A player has private output when nobody else is on the same sink. Single screen ⇒ always private. */
115
143
  export function hasPrivateOutputIn(list, count, i) {
116
144
  if (count <= 1)
117
145
  return true;
@@ -120,234 +148,173 @@ export function hasPrivateOutputIn(list, count, i) {
120
148
  return false;
121
149
  return !list.some((q, j) => j !== i && q && q.audioSink === p.audioSink);
122
150
  }
123
- /** TEA cycle step. */
124
- export function nextCalmMode(cur) { return (cur + 1) % 3; }
125
- /** Next high-contrast level. An unlisted `viz` (e.g. a CVD filter) is treated as index 0 ⇒ jumps to 'hc-direto'. */
126
- export function nextContrast(cur) {
127
- let idx = CONTRAST_LEVELS.indexOf(cur);
128
- idx = idx < 0 ? 0 : idx;
129
- return CONTRAST_LEVELS[(idx + 1) % CONTRAST_LEVELS.length];
130
- }
131
- /** Next CVD filter. NOTE the asymmetry with nextContrast: an unlisted `viz` maps to index 1 ('fix-protan'),
132
- * not 0 — verbatim from game.js (`idx = idx<0 ? 1 : (idx+1)%seq.length`). */
133
- export function nextCvd(cur) {
134
- let idx = CVD_SEQ.indexOf(cur);
135
- idx = idx < 0 ? 1 : (idx + 1) % CVD_SEQ.length;
136
- return { idx, mode: CVD_SEQ[idx] };
137
- }
138
- /** What `applyCalm` does to ONE audio category, given the TEA level. Extracted so the (destructive) volume
139
- * clamp at level 1 is visible and testable — see the report. */
140
- export function calmAudioPlan(calmMode, vol) {
141
- if (calmMode === 0)
142
- return { on: true, vol };
143
- if (calmMode === 1)
144
- return { on: true, vol: Math.min(vol, 0.3) };
145
- return { on: false, vol };
146
- }
147
- /** What `applyCalm` does to the scene/character reduced-motion flags. */
148
- export function calmMotionPlan(calmMode) {
149
- return { sceneReduced: calmMode >= 1, charFrozen: calmMode === 2 };
150
- }
151
151
  /** The `aria-label` of one icon — it MUST reflect the current state, on/off or level. This is the whole
152
152
  * point of the function: a toggle that looks pressed but does not say so is invisible to a screen reader. */
153
- export function computeIconLabel(k, s) {
154
- const ic = ICON_BY_KEY.get(k);
153
+ export function computeIconLabel(t, k, s) {
154
+ const ic = pauseIcon(k);
155
155
  if (!ic)
156
156
  return '';
157
- // O estado vira SEMPRE um parâmetro (`{v}`), nunca uma concatenação: 'on'/'off' eram palavras inglesas
158
- // presas numa frase em português, e uma língua que anteponha o estado ao nome precisa do dicionário para
159
- // reordenar. `nomeDoIcone: estado` é a moldura; o estado é o conteúdo, e ele também é traduzido.
160
- if (ic.soon)
161
- return t('icon.soon', { nome: t(ic.n) });
162
- const rotulo = (v) => t('icon.state', { nome: t(ic.n), v: t(v) });
163
- if (k === 'blind')
164
- return rotulo(s.modoCego ? 'state.on' : 'state.off');
165
- if (k === 'tts')
166
- return rotulo(s.ttsOn ? 'state.on' : 'state.off');
167
- if (k === 'libras')
168
- return rotulo(s.librasOn ? 'state.on' : 'state.off');
169
- // TEA e daltonismo usam um nome CURTO aqui, diferente do nome do botão: o rótulo já diz o nível, e
170
- // repetir a lista de níveis do nome ("(calmo / silencioso)", "(protan/deutan/tritan)") a diria duas vezes.
171
- // Era assim antes da conversão, com o texto curto embutido — preservado, não reinventado.
172
- if (k === 'tea')
173
- return t('icon.state', { nome: t('icon.tea.short'), v: t(CALM_NAMES[s.calmMode]) });
174
- if (k === 'altmove')
175
- return rotulo(s.toggleMove ? 'state.on' : 'state.off');
176
- if (k === 'contrast')
177
- return rotulo(CURTO_DO_TEMA[s.visual.tema]);
178
- if (k === 'cvd')
179
- return t('icon.state', { nome: t('icon.cvd.short'), v: t(CURTO_DA_CORRECAO[s.visual.correcao]) });
180
- return t(ic.n);
157
+ const state = STATE_OF_ICON[k];
158
+ // The state is ALWAYS a parameter (`{v}`), never a concatenation: a language that puts the state before the name needs
159
+ // the dictionary to reorder it. «name: state» is the frame; the state is the content, and it is translated too.
160
+ return state ? t('icon.state', { nome: t(SHORT_NAME[k] ?? ic.n), v: state(t, s) }) : t(ic.n);
181
161
  }
182
- /** Pure form of reflectIconBtn's branching. A `soon` icon lands on all-false — it never claims to be on. */
162
+ const onOff = (t, on) => t(on ? 'state.on' : 'state.off');
163
+ /** What each icon with a state says after its name, already in the child's language. An icon not here has no state to say. */
164
+ const STATE_OF_ICON = {
165
+ blind: (t, s) => onOff(t, s.blindMode),
166
+ tts: (t, s) => onOff(t, s.ttsOn),
167
+ libras: (t, s) => onOff(t, s.librasOn),
168
+ tea: (t, s) => t(CALM_NAMES[s.calmMode]),
169
+ altmove: (t, s) => t(INPUT_MODE_NAME[inputModeOf(s)]),
170
+ contrast: (t, s) => t(SHORT_THEME[s.visual.tema]),
171
+ cvd: (t, s) => t(SHORT_CORRECTION[s.visual.correcao]),
172
+ camera: (t, s) => t(CAMERA_MODE_NAME[s.camera ?? 'off']),
173
+ voice: (t, s) => onOff(t, s.voice),
174
+ // a language's own name is not translated: «Español» reads the same in every interface
175
+ idioma: (_t, s) => LANGUAGE_NAME[(s.locale ?? 'pt')] ?? LANGUAGE_NAME.pt,
176
+ velocidade: (t, s) => t('icon.velocidade.valor', { pct: Math.round((s.speed ?? 1) * 100) }),
177
+ };
178
+ /**
179
+ * Calm mode and colour correction have a SHORT name key for the label, because the button's name once carried the list of
180
+ * levels («(calmo / silencioso)», «(protan/deutan/tritan)») and the label already says the level. ⚠️ In the three
181
+ * dictionaries the short key now equals the long one word for word: the distinction has no subject left, and merging the
182
+ * keys is a dictionary change.
183
+ */
184
+ const SHORT_NAME = { tea: 'icon.tea.short', cvd: 'icon.cvd.short' };
185
+ const ICON_VISUAL = Object.freeze({
186
+ blind: (s) => ({ on: s.blindMode, dis: !s.privateOutput }),
187
+ tts: (s) => ({ on: s.ttsOn, dis: !s.privateOutput || !!s.noVoice }),
188
+ libras: (s) => ({ on: s.librasOn }),
189
+ tea: (s) => ({ on: s.calmMode === 2, calm: s.calmMode === 1 }),
190
+ // ⚠️ AND IT IS NEVER GREYED OUT. `latchRequired` says the DEVICE in use sends one command at a time and the latch cannot be
191
+ // turned off (ADR-0113 clause 3) — which the CYCLE tells, since `standard` simply does not appear in it. Greying the icon
192
+ // would take the one-button scan away from the child playing with their eyes, who is the likeliest to need it.
193
+ altmove: (s) => ({ on: inputModeOf(s) !== 'standard' }),
194
+ contrast: (s) => ({ on: hasHighContrast(s.visual) }),
195
+ velocidade: (s) => ({ on: (s.speed ?? 1) < 1 }),
196
+ camera: (s) => ({ on: (s.camera ?? 'off') !== 'off' }),
197
+ voice: (s) => ({ on: !!s.voice }),
198
+ // ⚠️ THE TWO-TONE BACKGROUND IS THIS ICON'S «ON» SIGNAL, and it reads the correction AXIS — so it still says the same when
199
+ // the high-contrast theme is on too, and the child's correction does not vanish from the icon that exists to show it.
200
+ cvd: (s) => ({ cvd: s.visual.correcao !== 'tricro' ? `pi-cvd-${s.visual.correcao}` : '' }),
201
+ });
202
+ /** At rest: an icon the table does not name shows nothing. */
203
+ const ICON_VISUAL_AT_REST = Object.freeze({ on: false, dis: false, calm: false, cvd: '', active: false });
204
+ /** Pure form of reflectIconBtn's branching. */
183
205
  export function computeIconVisual(k, s) {
184
- let on = false, dis = false, calm = false, cvd = '';
185
- if (k === 'blind') {
186
- on = s.modoCego;
187
- dis = !s.privateOutput;
188
- }
189
- else if (k === 'tts') {
190
- on = s.ttsOn;
191
- dis = !s.privateOutput;
192
- }
193
- else if (k === 'libras') {
194
- on = s.librasOn;
195
- }
196
- else if (k === 'tea') {
197
- on = s.calmMode === 2;
198
- calm = s.calmMode === 1;
199
- }
200
- else if (k === 'altmove') {
201
- on = s.toggleMove;
202
- dis = !!s.alternanciaExigida;
203
- }
204
- else if (k === 'contrast') {
205
- on = temAltoContraste(s.visual);
206
- }
207
- else if (k === 'cvd') {
208
- // ⚠️ O FUNDO DE DUAS CORES É O SINAL DE LIGADO deste ícone, e agora ele lê o EIXO da correção — que
209
- // continua a dizer o mesmo quando o tema também está ligado, coisa que a chave única não conseguia: com
210
- // `hc-direto-7` no campo, a correção da criança desaparecia do ícone que existe para a mostrar.
211
- if (s.visual.correcao !== 'tricro')
212
- cvd = 'pi-cvd-' + s.visual.correcao;
213
- }
214
- return { on, dis, calm, cvd, active: on || calm || !!cvd };
206
+ const v = { ...ICON_VISUAL_AT_REST, ...ICON_VISUAL[k]?.(s) };
207
+ // 📌 `active` is DERIVED, declared by no rule: it is the `aria-pressed`, and no icon should be able to say it is pressed
208
+ // without showing why.
209
+ return { ...v, active: v.on || v.calm || !!v.cvd };
215
210
  }
216
211
  /** The CSS classes reflectIconBtn clears before applying a fresh visual — in the original order. */
217
212
  export const ICON_STATE_CLASSES = ['pi-calm', 'pi-cvd-protan', 'pi-cvd-deuter', 'pi-cvd-tritan'];
218
- // --- markup (pure string builders; the DOM shell below just assigns them) ---
219
- /** One `.pi-btn`. `soon` icons get `.pi-soon` and the "under construction" suffix baked into the aria-label.
220
- * The label is the RESTING one: reflectIconBtn overwrites it with the stateful label as soon as the bar is
221
- * reflected. `ic.n` is an i18n key, so it must be resolved here too — the markup is rendered once at build
222
- * time and would otherwise ship the raw key to a screen reader. */
223
- export function iconBtnMarkup(ic) {
224
- return '<button class="pi-btn' + (ic.soon ? ' pi-soon' : '') + '" type="button" data-pi="' + ic.k +
225
- '" aria-label="' + (ic.soon ? t('icon.soon', { nome: t(ic.n) }) : t(ic.n)) + '">' + ic.e + '</button>';
226
- }
227
- export function iconesQueAccionam(escritores) {
228
- // ⚠️ POR ÍCONE, e não um booleano para os dois — e foi uma MUTAÇÃO SOBREVIVENTE que o mostrou. Com uma
229
- // única bandeira, `&&` e `||` produziam o mesmo resultado nos casos que eu tinha escrito, porque todos
230
- // tiravam os DOIS escritores. O `&&` escondia um ícone que FUNCIONA quando só um escritor falta, e o `||`
231
- // mostrava um que NÃO funciona. Os dois erram, em direcções opostas, e a pergunta certa nunca foi «este
232
- // jogo tem escritores visuais» — é «este ÍCONE tem quem o accione».
233
- // 📌 E o `altmove` entra pela MESMA porta, que é o achado: «este jogo segura teclas?» é a mesma pergunta
234
- // que «este ícone tem quem o accione», feita a um campo do contrato em vez de a um escritor injectado.
235
- // Um terceiro ramo, e não uma regra nova.
236
- return PAUSE_ICONS.filter((ic) => (ic.k === 'contrast' ? escritores.tema
237
- : ic.k === 'cvd' ? escritores.correcao
238
- : ic.k === 'altmove' ? escritores.seguraTeclas
239
- : true));
240
- }
241
213
  /**
242
- * OS TRÊS ITENS QUE A ENGINE ACCIONA SOZINHA, e que por isso nunca dependem do `getPauseActs` de um jogo.
214
+ * WHO ACTIVATES EACH ICON — one question per icon, and an icon nothing locks is offered.
243
215
  *
244
- * 📏 Lidos do despacho, e não decididos aqui: `options`/`pmback` trocam qual lista está no cartão e
245
- * `acessibilidade` leva o cursor à barra rápida — os três são tratados neste módulo e voltam antes de a
246
- * tabela do jogo ser consultada.
216
+ * PER ICON, not one flag for the whole bar: with one flag, a single missing writer would either hide an icon that WORKS
217
+ * or show one that does NOT. The question is «does this ICON have something to activate it». A table, so adding an icon
218
+ * is adding a row.
247
219
  */
248
- export const ITENS_DA_ENGINE = new Set(['options', 'pmback', 'acessibilidade']);
220
+ const ICON_IS_ACTIONABLE = Object.freeze({
221
+ contrast: (w) => w.theme,
222
+ cvd: (w) => w.correction,
223
+ // «Does this game hold keys?» is the same question, asked of a contract field rather than an injected writer.
224
+ // AND A SECOND HALF (ADR-0218): «one button only» has a subject wherever the game declares a position to scan, so a game
225
+ // holding no key (the quiz demo) still has a ☝️. A game that declares nothing has none: a scan of nothing is the dead
226
+ // button of ADR-0106 §5 paid for in seconds.
227
+ altmove: (w) => w.holdsKeys() || (w.declaredPositions?.() ?? 0) > 0,
228
+ // The same question asked of the typography cycle (ADR-0149): the icon exists when someone knows how to walk it.
229
+ // `Boolean(...)` and not the raw field: it is optional, and the conversion is written rather than left to a `filter`.
230
+ tipografia: (w) => Boolean(w.typography),
231
+ // the hourglass exists where time runs by itself (ADR-0180): a turn game has nothing to slow
232
+ velocidade: (w) => Boolean(w.clock?.()),
233
+ camera: (w) => Boolean(w.camera),
234
+ // 👄 exists where there is a MICROPHONE to ask for, the same rule as the camera's (ADR-0106 §5)
235
+ voice: (w) => Boolean(w.microphone),
236
+ menu: (w) => Boolean(w.menus),
237
+ });
238
+ export function iconsThatAct(writers) {
239
+ // ABSENCE FROM THE TABLE MEANS YES: an icon no rule locks depends on nobody to work, and hiding it for lack of a row
240
+ // would take away from the child a way that exists.
241
+ return PAUSE_ICONS.filter((ic) => ICON_IS_ACTIONABLE[ic.k]?.(writers) ?? true);
242
+ }
249
243
  /**
250
- * OS ITENS DO MENU QUE ESTE JOGO CONSEGUE MESMO ACCIONAR (ADR-0106 §5).
251
- *
252
- * ⚠️ HOJE UM ITEM SEM ACÇÃO É UM BOTÃO MORTO, E EM SILÊNCIO. O despacho faz `const fn = acts[act]; if (fn)
253
- * fn();` — quem carrega num item que o jogo não implementou não recebe erro, não recebe anúncio, não recebe
254
- * nada. Para quem vê, parece que o clique falhou; para quem navega por leitor de tela, o menu leu-lhe um
255
- * item que não existe. É exactamente o que o §5 chama de pior do que a ausência: «uma barra que oferece um
256
- * caminho e depois o recusa ensina-lhe que o caminho não é para ela».
244
+ * THE ITEMS THE ENGINE ACTIVATES BY ITSELF, which therefore never depend on a game's `getPauseActs`. Read from the
245
+ * dispatch, not decided here: `options`, `opcoesdojogo` and `pmback` change which list is on the card, and
246
+ * `acessibilidade` takes the cursor to the quick bar — all four are handled in this module before the game's table is
247
+ * consulted. What belongs to the game is the CONTENT of the list `opcoesdojogo` opens.
257
248
  */
258
- export function itensQueAccionam(botoes, acts) {
259
- return botoes.filter((b) => ITENS_DA_ENGINE.has(b.act) || typeof acts[b.act] === 'function');
260
- }
249
+ const ENGINE_ITEMS = new Set(['options', 'opcoesdojogo', 'pmback', 'acessibilidade']);
261
250
  /**
262
- * A LISTA RAIZ, com uma regra a mais: `options` é uma PORTA, e uma porta para uma sala vazia também é um
263
- * botão morto.
264
- *
265
- * ⚠️ Esta é a parte que um filtro item-a-item não apanha. Se todos os painéis de ajuste forem filtrados —
266
- * um jogo que não monta nenhum —, o item `options` sobrevive (a engine acciona-o) e abre uma lista sem nada.
267
- * A criança atravessa uma porta e fica presa num submenu vazio, cuja única saída é o `pmback` que também
268
- * sumiu com ele.
251
+ * Why a pause item is locked, in the child's words (ADR-0161). A key per item where the reason is particular to it —
252
+ * «Número de jogadores»: the GAME decides how many (ADR-0147) — and one general reason for the rest. Resolved at every
253
+ * refresh, so it follows the language of the moment the card opens.
269
254
  */
270
- export function raizQueAcciona(raiz, opcoes, acts) {
271
- const opcoesVivas = itensQueAccionam(opcoes, acts).filter((b) => b.act !== 'pmback');
272
- const viva = itensQueAccionam(raiz, acts);
273
- return opcoesVivas.length > 0 ? viva : viva.filter((b) => b.act !== 'options');
274
- }
275
- /** The whole icon bar. Used by the pause screen AND by the splash `#title-icons` (which built the same string
276
- * by hand in game.js — that duplication dies with this export).
277
- * O parâmetro é ADITIVO e o padrão é a lista inteira: quem já chamava sem argumentos não muda de resultado. */
278
- export function iconsMarkup(icones = PAUSE_ICONS) {
279
- return icones.map(iconBtnMarkup).join('');
255
+ const OWN_REASONS = new Set(['ajuda', 'addplayer', 'opcoesdojogo']);
256
+ function itemReason(t, act) {
257
+ return t(OWN_REASONS.has(act) ? `pause.motivo.${act}` : 'pause.motivo');
280
258
  }
281
- /** One `.pm-btn`. Dynamic labels (`letra`/`nivel`) are rendered eagerly and carry NO `data-i18n`, so
282
- * i18n.applyDom() cannot overwrite them. */
283
259
  /**
284
- * ⚠️ O RÓTULO DINÂMICO ENTRA PRONTO (item 19), e a mudança conserta DUAS coisas de uma vez.
285
- *
286
- * A linha era `'📚 Nível ' + level + ' · ' + qlName[level]` — e ela tinha dois defeitos que só se enxergam
287
- * juntos:
288
- *
289
- * 1. FRONTEIRA. `level` vinha de `core/state.quizLevel` e `qlName` de uma tabela do jogo. Um menu de pausa
290
- * da ENGINE montava o rótulo de uma atividade de alfabetização — conteúdo pedagógico, não mecânica.
291
- * 2. IDIOMA. "Nível" é pt-BR CRU dentro de um módulo de engine. O gate do item 14 vigia o `main.js` e não
292
- * alcança `ui/`, então esta linha atravessou a i18n inteira sem ser vista. Num build em inglês, o menu
293
- * de pausa de uma criança dizia "📚 Nível 2 · …".
294
- *
295
- * Agora o jogo entrega a frase montada (`dynLabel`), e a engine só a coloca no botão. O jogo é quem sabe o
296
- * que é um nível, quem sabe o nome dele e quem sabe em que idioma dizê-lo.
260
+ * THE MENU ITEMS THIS GAME CAN ACTUALLY ACTIVATE. The others are not hidden: they stay on the card, LOCKED with their
261
+ * reason said (ADR-0161) — an item that does nothing when pressed, and says nothing, reads to a screen-reader user as an
262
+ * item that does not exist.
297
263
  */
298
- export function pmBtnMarkup(b, dynLabel, tr) {
299
- const dyn = b.letra || b.nivel;
300
- const lbl = dynLabel(b) ?? (dyn ? (b.lbl ?? '') : tr('pause.' + b.act));
301
- return '<button class="pm-btn' + (b.letra ? ' pm-letra' : '') + (b.nivel ? ' pm-nivel' : '') +
302
- '" role="menuitem" type="button" data-act="' + b.act + '"' +
303
- (dyn ? '' : (' data-i18n="pause.' + b.act + '"')) + '>' + lbl + '</button>';
264
+ function itemsThatAct(buttons, acts) {
265
+ return buttons.filter((b) => ENGINE_ITEMS.has(b.act) || typeof acts[b.act] === 'function');
304
266
  }
305
267
  /**
306
- * Os itens navegáveis de um cartão de pausa — os da lista VISÍVEL, e só eles.
307
- *
308
- * Uma constante porque TRÊS módulos a consultam (a navegação em `ui/menu-nav`, a seleção inicial em
309
- * `ui/shell` e a troca de submenu aqui). Enquanto fosse `.pm-btn` escrito três vezes, bastaria um deles
310
- * esquecer o `:not([hidden])` para o anel atravessar para a lista invisível — e a criança ouviria itens de um
311
- * menu que não está na tela.
268
+ * THE ROOT LIST, with one more rule: `options` is a DOOR, and a door to an empty room acts no more than a dead button.
269
+ * An item-by-item filter misses this: the engine activates `options` itself, so it would pass and open a list with
270
+ * nothing in it.
312
271
  */
313
- export const PM_ITENS_VISIVEIS = '.pause-menu:not([hidden]) .pm-btn';
314
- /** O innerHTML de UMA `.pause-menu`: a lista, e só ela. */
315
- export function pauseMenuHtml(bs, sub, dynLabel, tr) {
316
- return '<div class="pause-menu" role="menu" data-sub="' + sub + '"' + (sub === 'raiz' ? '' : ' hidden') + '>' +
317
- bs.map((b) => pmBtnMarkup(b, dynLabel, tr)).join('') + '</div>';
272
+ export function rootThatActs(rootEl, options, acts,
273
+ // An OPTIONAL fourth argument defaulting to the EMPTY list: a game that declares nothing of its own is exactly that
274
+ // case, so the default is also the right answer.
275
+ fromGame = []) {
276
+ const howManyAct = (bs) => itemsThatAct(bs, acts).filter((b) => b.act !== 'pmback').length;
277
+ let alive = itemsThatAct(rootEl, acts);
278
+ if (howManyAct(options) === 0)
279
+ alive = alive.filter((b) => b.act !== 'options');
280
+ // The same rule for the game's own door (ADR-0146): a game with nothing of its own gets no live «game options».
281
+ // ADR-0182: a door whose room the ENGINE draws from the cartridge's rows is live through its action, with no list behind it
282
+ if (howManyAct(fromGame) === 0 && typeof acts.opcoesdojogo !== 'function')
283
+ alive = alive.filter((b) => b.act !== 'opcoesdojogo');
284
+ return alive;
318
285
  }
319
286
  /**
320
- * Troca a lista visível de UM cartão de pausa, e põe o cursor no PRIMEIRO item da lista que entrou.
287
+ * The navigable items of a pause card — those of the VISIBLE list, and only those.
321
288
  *
322
- * Livre (e não um método do `init`) de propósito: `ui/menu-nav` precisa dela para o "não" voltar da lista de
323
- * opções à raiz, e não tem acesso às tabelas de botões. Como as duas listas já existem no markup, a troca é
324
- * só DOM — nada a re-renderizar, nada a injetar.
289
+ * One constant because THREE modules ask (navigation in `ui/menu-nav`, the first selection in `ui/shell` and the list
290
+ * switch here): if one of them forgot `:not([hidden])`, the ring would step into the invisible list and the child would
291
+ * hear items of a menu that is not on screen. `:not([hidden])` on the ITEM too: a hidden item must not be reached nor
292
+ * counted in «N de M» (a locked one is not hidden — it is reached, and says why).
325
293
  */
326
- export function mostrarSubmenuDaPausa(sp, sub) {
327
- sp.querySelectorAll('.pause-menu').forEach((m) => { m.hidden = m.dataset.sub !== sub; });
328
- const primeiro = sp.querySelector(PM_ITENS_VISIVEIS);
329
- sp.querySelectorAll('.pm-sel,.pi-sel').forEach((b) => b.classList.remove('pm-sel', 'pi-sel'));
330
- if (primeiro)
331
- primeiro.classList.add('pm-sel');
332
- return primeiro;
333
- }
294
+ export const PM_VISIBLE_ITEMS = '.pause-menu:not([hidden]) .pm-btn:not([hidden])';
334
295
  /**
335
- * A BARRA RÁPIDA DE ACESSIBILIDADE — dez alternadores e a legenda que os explica (ADR-0044, item 7).
336
- *
337
- * Saiu do cartão de pausa e passou a viver no HUD. O motivo é de uso, não de arrumação: é DURANTE a partida
338
- * que uma criança precisa mudar um ajuste que está a atrapalhando, e não depois de pausar. E o motivo
339
- * secundário é estrutural — sem ela, o cartão deixa de ter duas zonas e vira uma LISTA, o que é o que
340
- * finalmente autoriza o anel (a XAG 106 permite laço para menu linear e o proíbe para grade).
296
+ * THE DOORS INSIDE THE CARD, and which list each opens. They do nothing to the game — they change which list is on
297
+ * screen — so they live here and not in the action table, which lives in `ui/shell` and does not know the card.
298
+ * `pmback` always returns to the ROOT, from either list. A table and not a chain of `if`s: a fourth list is one more
299
+ * row, not one more branch.
300
+ */
301
+ const DOOR_TO_LIST = Object.freeze({ options: 'opcoes', opcoesdojogo: 'jogo', pmback: 'raiz' });
302
+ /**
303
+ * Switches the visible list of ONE pause card, and puts the cursor on the FIRST item of the list that came in.
341
304
  *
342
- * A LEGENDA VIAJA JUNTO. Ela é a dica que substitui, para quem não vê, o `title` que só o mouse revela;
343
- * deixá-la no cartão tornaria a barra do HUD muda.
305
+ * Free-standing (not a method of `init`) on purpose: `ui/menu-nav` needs it for «no» to go back to the root, and it has no
306
+ * access to the button tables. Every list already exists in the markup, so the switch is DOM only.
344
307
  */
345
- export function quickBarMarkup(icones = PAUSE_ICONS) {
346
- return '<div class="pause-icons" role="group" aria-label="' + t('pause.iconBarAria') + '">' + iconsMarkup(icones) +
347
- '</div><p class="pause-icons-cap" aria-live="polite"></p>';
308
+ export function showPauseOptions(sp, sub) {
309
+ sp.querySelectorAll('.pause-menu').forEach((m) => { m.hidden = m.dataset.sub !== sub; });
310
+ const first = sp.querySelector(PM_VISIBLE_ITEMS);
311
+ sp.querySelectorAll('.pm-sel,.pi-sel').forEach((b) => b.classList.remove('pm-sel', 'pi-sel'));
312
+ if (first)
313
+ first.classList.add('pm-sel');
314
+ return first;
348
315
  }
349
- export function acaoNaBarra(k, temStart) {
350
- if (temStart || k.no)
316
+ export function barAction(k, hasStart) {
317
+ if (hasStart || k.no)
351
318
  return 'sair';
352
319
  if (k.yes)
353
320
  return 'ativar';
@@ -355,130 +322,101 @@ export function acaoNaBarra(k, temStart) {
355
322
  return 'andar';
356
323
  return 'nada';
357
324
  }
358
- /** The full innerHTML of a `.screen-pause`. Pure — every input is a parameter. */
359
- export function screenPauseMarkup(o) {
360
- // O NOME ACESSÍVEL DO DIÁLOGO passa pelo dicionário. Era texto cru, e MEDIDO num jogo em inglês o efeito
361
- // era este: o título visível dizia "Paused" e o nome do diálogo, "Menu de pausa do jogador 1". Quem enxerga
362
- // lia em inglês; quem escuta recebia o menu anunciado em português — a mesma assimetria do item 4 do
363
- // ADR-0044, um nível acima. E `aria-label`, não `aria-labelledby`: o `<h2>` é rótulo VISUAL, e é por isso
364
- // que escondê-lo num quadro apertado não tira o nome do diálogo de quem escuta.
365
- return '<div class="pause-card" role="dialog" aria-modal="true" aria-label="' + t('pause.cardAria', { n: o.player + 1 }) + '">' +
366
- '<h2><span data-i18n="pause.title">' + o.t('pause.title') + '</span>' + (o.numPlayers > 1 ? ' · Jogador ' + (o.player + 1) : '') + '</h2>' +
367
- pauseMenuHtml(o.pmButtons, 'raiz', o.dynLabel, o.t) +
368
- pauseMenuHtml(o.optionsButtons, 'opcoes', o.dynLabel, o.t) +
369
- '<p class="pause-legend"></p></div>';
370
- }
371
325
  // ---------------------------------------------------------------------------------------------
372
326
  // DOM-facing shell
373
327
  // ---------------------------------------------------------------------------------------------
374
328
  export function initPauseIcons(ctx) {
375
- // ⚠️ O NÍVEL TEA PASSOU A PERSISTIR EM 2026-09-07 (issue #61). Este comentário dizia «deliberately NOT
376
- // persisted — verbatim: game.js never wrote it to storage», e o **verbatim** é o que o desqualificava como
377
- // decisão: foi PRESERVADO na extração do monólito, não escolhido. O ADR-0028 diz que todo menu persiste, e
378
- // este é um controlo de menu que vive na barra rápida.
379
- //
380
- // O custo de não persistir era da criança que mais precisa dele: quem usa o modo SILENCIOSO voltava a
381
- // pô-lo a cada sessão — e é para quem o barulho inesperado custa mais. Um ajuste que se esquece não é um
382
- // ajuste, é uma tarefa diária.
329
+ const { t } = ctx.translator;
330
+ // THE CALM LEVEL PERSISTS (issue #61; ADR-0028: every menu setting persists). The child who uses the SILENT mode is the
331
+ // one for whom unexpected noise costs most, and a setting that is forgotten is a daily chore, not a setting.
383
332
  //
384
- // ⚠️ LÊ NO INIT, NUNCA NO IMPORT: a regra vale para todo o projeto e aqui tem custo concreto — um teste que
385
- // importasse este módulo passaria a depender do `localStorage` do ambiente, e um nível herdado de outro
386
- // caso é uma falha que aparece longe da causa.
387
- let calmMode = lerNivelTea();
333
+ // READ IN INIT, NEVER ON IMPORT: a test importing this module would otherwise depend on the environment's storage, and a
334
+ // level inherited from another case is a failure that shows up far from its cause.
335
+ let calmMode = readTeaLevel(ctx.store);
388
336
  const P = () => ctx.getPlayers();
389
337
  /*
390
- * ⚠️ A ALTERNÂNCIA DE MARCHA PASSOU A TER PADRÃO DA ENGINE (ADR-0106 §4, etapa 1b). Quem injecta continua a
391
- * mandar; quem não injecta deixa de ficar sem ela — que era o caso dos cinco jogos sem barra.
392
- *
393
- * O `store` vem do import directo deste módulo e não do `ctx`, porque é assim que este ficheiro já persiste
394
- * o resto: uma chave injectada é um campo que um consumidor pode omitir, e omiti-la faria escrever num nome
395
- * torto.
338
+ * THE MOVEMENT LATCH HAS AN ENGINE DEFAULT (ADR-0106 §4): a host that injects one still rules; a host that does not is
339
+ * no longer left without it. The `store` is the page's, from the ctx (ADR-0232).
396
340
  */
397
341
  const setToggleMove = ctx.setToggleMove
398
- ?? ((i, on) => definirAlternanciaDeMarcha({
399
- players: P(), store, srSay: ctx.srSay, getNumPlayers: ctx.getNumPlayers,
400
- // ⚠️ ATRAVESSA, e não se resolve aqui: o ícone e o painel têm de escrever a MESMA coisa. Resolver
401
- // o aparelho num deles e não no outro é como duas superfícies da mesma engine passam a discordar.
402
- transporteEmUso: ctx.transporteEmUso,
342
+ ?? ((i, on) => setMoveLatch({
343
+ t, players: P(), store: ctx.store, srSay: ctx.srSay, getNumPlayers: ctx.getNumPlayers,
344
+ // Passed through, not resolved here: the icon and the panel must write the SAME thing, and resolving the
345
+ // device in one of them only is how two surfaces of the same engine come to disagree.
346
+ transportInUse: ctx.transportInUse,
403
347
  }, i, on));
404
348
  /*
405
- * ⚠️ O MODO CEGO IDEM, e aqui o padrão é literalmente o que o `core/state` já decidiu que um setter faz:
406
- * «grava, persiste, avisa» — e nada mais. Os efeitos de jogo (refazer os extras do nível) são REACÇÃO, e
407
- * quem reage assina `on('modoCego', …)`. O anúncio não se perde para quem não injecta: este ícone já diz
408
- * `sr.icon.blindOn`/`Off` por si, logo abaixo.
349
+ * BLIND MODE LIKEWISE: the default is exactly what `core/state` decided a setter does — write, persist, notify — and
350
+ * nothing more. Game effects are REACTIONS, subscribed with `on('blindMode', …)`. The announcement is not lost for a host
351
+ * that injects nothing: this icon says `sr.icon.blindOn`/`Off` itself, below.
409
352
  */
410
- const setModoCego = ctx.setModoCego ?? setModoCegoValue;
411
- /** O documento onde se constroi. Resolvido a cada uso, e por globalThis — em node o identificador
412
- * document nem existe, e um ?? sobre ele lançaria ReferenceError em vez de cair no padrão. */
413
- const docDaMontagem = () => ctx.doc ?? globalThis.document;
353
+ const writeBlindMode = ctx.setBlindMode ?? ((on) => { ctx.settings.setBlindModeValue(on); });
414
354
  /*
415
- * ⚠️ O CONTRASTE E A COR SÓ APARECEM SE HOUVER QUEM OS ESCREVA (ADR-0106 §5).
355
+ * CONTRAST AND COLOUR ONLY APPEAR IF SOMEONE CAN WRITE THEM (ADR-0106 §5): the theme and the correction are written by
356
+ * the host (`setPlayerTheme`/`setPlayerCorrection`), and without a writer the engine does not pretend.
416
357
  *
417
- * 📏 Medido em 2026-09-08, e é o que separa este caso dos outros seis campos «acidentais»: eles eram estado
418
- * que um cartucho calhou de guardar, e a engine pôde reclamá-los. Estes precisam de um
419
- * `render/viz-setters`, cujo contexto pede **34 campos** do grafo de render de UM jogo — `parallaxLayers`,
420
- * `decoSprites`, `getPowerups`, `rebuildCoins`, `worldSprite`. O `createGame` não monta isso, e um quiz não
421
- * tem nada disso para montar. A engine não pode dar um padrão aqui; o que ela pode é não FINGIR.
422
- *
423
- * Decidido uma vez, no arranque, e não a cada montagem de barra: o conjunto de escritores de um consumidor
424
- * não muda a meio de uma partida, e recalcular por tela faria as telas discordarem entre si.
358
+ * Decided once, at start, not per bar: a host's set of writers does not change mid-game, and recomputing per screen
359
+ * would let the screens disagree.
425
360
  */
426
- const iconesDoJogo = iconesQueAccionam({
427
- tema: Boolean(ctx.setTemaDoJogador),
428
- correcao: Boolean(ctx.setCorrecaoDoJogador),
429
- // 📌 Sem `Boolean(...)`: os dois de cima perguntam «existe escritor?» a um campo opcional; este é uma
430
- // RESPOSTA que o jogo deu, e envolvê-la faria um `undefined` de um ctx mal montado virar `false` —
431
- // esconder o controle em silêncio, que é metade do defeito que este campo existe para não cometer.
432
- seguraTeclas: ctx.seguraTeclas,
361
+ const gameIcons = iconsThatAct({
362
+ theme: Boolean(ctx.setPlayerTheme),
363
+ correction: Boolean(ctx.setPlayerCorrection),
364
+ // No `Boolean(...)`: the two above ask an optional field «is there a writer?»; this is an ANSWER the game gave, and
365
+ // wrapping it would turn a badly wired ctx's `undefined` into `false` — hiding the control silently.
366
+ holdsKeys: ctx.holdsKeys,
367
+ // the same question the scan asks (ADR-0218): how many positions this game would give it to offer
368
+ declaredPositions: ctx.declaredPositions,
369
+ typography: Boolean(ctx.cycleTypography),
370
+ // mounted when the root can answer the clock question; shown or hidden per cartridge in `reflectIconBtn` (ADR-0142)
371
+ clock: () => Boolean(ctx.clock),
372
+ camera: Boolean(ctx.camera),
373
+ microphone: Boolean(ctx.microphone),
374
+ menus: Boolean(ctx.openMenus),
433
375
  });
434
376
  /*
435
- * ⚠️ RESOLVIDOS UMA VEZ, no arranque, pela mesma razão do `settings-motion`: o `rm` é mutado in-place e
436
- * partilhado por REFERÊNCIA com quem desenha a cena, e resolvê-lo a cada uso criaria um objecto novo por
437
- * chamada — o interruptor deixaria de alcançar o desenho, sem erro nenhum.
438
- *
439
- * 📌 O `getPauseActs` fica FUNÇÃO e não valor, porque a laziness dele é a razão de ele existir assim: no
440
- * cartucho a tabela é um `const` declarado ~1200 linhas abaixo, e lê-la aqui cairia na zona morta temporal.
377
+ * RESOLVED ONCE, at start, as in `settings-motion`: `rm` is mutated in place and shared by REFERENCE with whoever draws
378
+ * the scene, and resolving it on each use would make a new object per call — the switch would stop reaching the drawing,
379
+ * with no error at all.
441
380
  */
442
- const rm = ctx.rm ?? lerCenaGuardada();
443
- const rmKeys = ctx.rmKeys ?? CHAVES_DE_CENA;
444
- const rmChar = ctx.rmChar ?? ANIMACOES_DO_PERSONAGEM;
445
- const saveRM = ctx.saveRM ?? (() => guardarCena(rm));
381
+ const rm = ctx.rm ?? readStoredScene(ctx.store, defaultReducedMotion(ctx.matchMedia));
382
+ const rmKeys = ctx.rmKeys ?? SCENE_KEYS;
383
+ const rmChar = ctx.rmChar ?? CHARACTER_ANIMATIONS;
384
+ const saveRM = ctx.saveRM ?? (() => storeScene(ctx.store, rm));
446
385
  /*
447
- * ⚠️ ESTES TRÊS SÃO LIDOS A CADA CHAMADA, e não resolvidos uma vez como o `rm` acima. A diferença é
448
- * deliberada e um teste apanhou-me a errá-la: congelar `ctx.getPauseActs` no arranque partiu um caso que
449
- * TROCA a tabela depois do `init` — e trocar depois é legítimo, porque a laziness deste campo existe
450
- * precisamente por a tabela chegar tarde. Um padrão não pode custar a ligação tardia que o campo tem.
386
+ * THESE THREE ARE READ ON EACH CALL, not resolved once like `rm` above: a host may replace the action table after `init`
387
+ * (it can arrive late), and a default must not cost that late binding. `rm` is the opposite — what matters there is the
388
+ * object's IDENTITY, shared by reference with whoever draws the scene.
451
389
  *
452
- * O `rm` é o contrário e por isso fica congelado: ali o que importa é a IDENTIDADE do objecto, partilhada
453
- * por referência com quem desenha a cena.
454
- *
455
- * A anotação de tipo é necessária: sem ela o padrão `() => ({})` infere `{}`, que não aceita indexação por
456
- * string, e o compilador passaria a recusar `acts[act]` — o despacho inteiro.
390
+ * The type annotation is needed: without it the default `() => ({})` infers `{}`, which does not accept string indexing,
391
+ * and the compiler would refuse `acts[act]` — the whole dispatch.
457
392
  */
458
393
  const dynLabel = (b) => (ctx.dynLabel ? ctx.dynLabel(b) : null);
459
394
  const getPauseActs = () => (ctx.getPauseActs ? ctx.getPauseActs() : {});
460
395
  const setPauseActor = (i) => { if (ctx.setPauseActor)
461
396
  ctx.setPauseActor(i); };
462
397
  function hasPrivateOutput(i) { return hasPrivateOutputIn(P(), ctx.getNumPlayers(), i); }
463
- /** A recusa da alternância para este jogador agora, ou `null`. Recalculada: o aparelho em uso muda. */
464
- function recusaAgora(i) { return ctx.transporteEmUso ? recusaDaAlternancia(ctx.transporteEmUso(i)) : null; }
398
+ /** The latch refusal for this player now, or `null`. Recomputed: the device in use changes. */
399
+ function refusalNow(i) { return ctx.transportInUse ? latchRefusal(ctx.transportInUse(i)) : null; }
465
400
  function iconState(i) {
466
401
  const p = P()[i] || {};
467
402
  const cat = ctx.getAudioCat();
468
403
  return {
469
- modoCego: ctx.getModoCego(),
404
+ blindMode: ctx.getBlindMode(),
470
405
  ttsOn: !!(cat && cat.tts && cat.tts.on),
471
406
  librasOn: ctx.isLibrasOn(),
472
407
  calmMode,
473
408
  toggleMove: !!p.toggleMove,
474
- // ⚠️ `DEFAULTS.viz` E NÃO `''` (issue #61). A cadeia vazia funcionava por ACIDENTE: não casa
475
- // `hc-direto` nem `fix-*`, então os dois ícones ficavam apagados pelo motivo certo por engano. O padrão
476
- // passou a ter nome em `core/state`, e `render/viz-modes` já declarava esse modo com `kind:'normal'` —
477
- // o que não faz nada. Dizer o padrão em vez de o deduzir é o que torna a marca do ADR-0029 possível
478
- // aqui, porque ela lê `DEFAULTS` e mais nada.
479
- visual: p.visual ?? PADRAO,
409
+ switchScan: ctx.settings.switchScan,
410
+ voice: ctx.settings.voiceControl,
411
+ // The NAMED default, not an empty value that only happened to match nothing: ADR-0029's changed-mark reads the
412
+ // default and nothing else.
413
+ visual: p.visual ?? DEFAULT_VISUAL,
414
+ speed: ctx.settings.gameSpeed,
415
+ camera: ctx.settings.cameraControl,
416
+ locale: ctx.translator.locale(),
480
417
  privateOutput: hasPrivateOutput(i),
481
- alternanciaExigida: recusaAgora(i) !== null,
418
+ latchRequired: refusalNow(i) !== null,
419
+ noVoice: !!ctx.noVoice?.(),
482
420
  };
483
421
  }
484
422
  // --- TEA ---------------------------------------------------------------------------------
@@ -505,14 +443,15 @@ export function initPauseIcons(ctx) {
505
443
  }
506
444
  // --- icon actions (the dispatcher, as a table) ---------------------------------------------
507
445
  const ICON_ACTS = {
446
+ menu: (i) => { ctx.openMenus?.(i); },
508
447
  blind: () => {
509
- setModoCego(!ctx.getModoCego());
510
- ctx.srSay(t(ctx.getModoCego() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
448
+ writeBlindMode(!ctx.getBlindMode());
449
+ ctx.srSay(t(ctx.getBlindMode() ? 'sr.icon.blindOn' : 'sr.icon.blindOff'));
511
450
  },
512
451
  tts: () => {
513
452
  const cat = ctx.getAudioCat();
514
453
  if (!cat || !cat.tts)
515
- return; // a guarda que `iconLabel` e `applyCalm` já tinham e esta ação não
454
+ return; // the same guard `iconLabel` and `applyCalm` have
516
455
  cat.tts.on = !cat.tts.on;
517
456
  ctx.setCatGain('tts');
518
457
  if (ctx.reflectTtsPanelEnabled)
@@ -525,66 +464,127 @@ export function initPauseIcons(ctx) {
525
464
  },
526
465
  tea: () => {
527
466
  calmMode = nextCalmMode(calmMode);
528
- store.set(store.KEYS.tea, calmMode); // ADR-0028: todo menu persiste. Ver a nota no `let` acima.
467
+ ctx.store.set(KEYS.tea, calmMode); // ADR-0028: every menu setting persists. See the note at the `let` above.
529
468
  applyCalm();
530
469
  ctx.srSay(t('sr.icon.tea', { v: t(CALM_NAMES[calmMode]) }));
531
470
  },
471
+ /*
472
+ * ☝️ CYCLES THREE READINGS OF A PRESS (ADR-0218): standard · no holding needed · one button only.
473
+ *
474
+ * 📌 ENTERING THE SCAN LEAVES THE LATCH WHERE IT IS, and leaving it turns the latch off. The scan wins in `inputModeOf`, so
475
+ * the position shown is never ambiguous; and because the latch writer announces by itself, it is called ONLY where it
476
+ * changes something — otherwise the child would hear «no holding needed, off» when what ended was the scan.
477
+ */
532
478
  altmove: (i) => {
533
479
  // verbatim: `players[i].toggleMove` with no `||{}` guard (unlike contrast/cvd below).
534
- setToggleMove(i, !P()[i].toggleMove);
480
+ const latched = !!P()[i].toggleMove;
481
+ const next = nextInputMode(inputModeOf({ toggleMove: latched, switchScan: ctx.settings.switchScan }), ctx.holdsKeys(), refusalNow(i) !== null);
482
+ ctx.settings.setSwitchScanValue(next === 'scan');
483
+ // 📌 ENTERING THE SCAN LEAVES THE LATCH WHERE SHE PUT IT — the scan wins in `inputModeOf`, so the position shown is never
484
+ // ambiguous and coming back out returns her to the choice she had made. Leaving it writes the position she walked to.
485
+ // ⚠️ And the latch writer ANNOUNCES BY ITSELF, so it is called only where it changes something: otherwise the child would
486
+ // hear «não precisa segurar, desligado» when what ended was the scan.
487
+ if (next !== 'scan' && latched !== (next === 'sticky')) {
488
+ // ⚠️ THE REFUSAL, over the WRITE, which is the only thing it ever meant. The cycle above already skips `standard` on a
489
+ // device that always latches, so this should be unreachable — and it stays because `iconAct` is exported and the two
490
+ // rules could drift apart, which is the same reason the guards below give for not being belt and braces.
491
+ const refusal = refusalNow(i);
492
+ if (refusal) {
493
+ ctx.srAlert(t(refusal.key));
494
+ return;
495
+ }
496
+ setToggleMove(i, next === 'sticky');
497
+ return;
498
+ }
499
+ ctx.srSay(t('sr.icon.inputMode', { v: t(INPUT_MODE_NAME[next]) }));
535
500
  },
536
- // ⚠️ OS DOIS ÍCONES DEIXARAM DE SE APAGAR UM AO OUTRO (#104). Eles SEMPRE ciclaram dentro do seu eixo —
537
- // `nextContrast` e `nextCvd` existem separados desde sempre —, mas escreviam os dois no mesmo campo, e
538
- // por isso mexer num zerava o outro. O snapshot dizia isso como se fosse desenho: «they overwrite each
539
- // other; that is by design». Agora cada um escreve no seu eixo e o outro fica onde estava.
540
- // ⚠️ AS DUAS GUARDAS NÃO SÃO CINTO E SUSPENSÓRIOS. Sem escritor, o ícone nem sequer é montado
541
- // (`iconesDoJogo`), então este ramo não deveria ser alcançável pela barra — mas `iconAct` é EXPORTADO e
542
- // qualquer consumidor pode chamá-lo por chave. Sem a guarda, essa chamada rebentaria; com ela, não faz
543
- // nada e não anuncia — que é o mesmo que dizer a verdade: este jogo não tem por onde.
501
+ // Each of the two icons writes its OWN axis (#104), so changing one leaves the other where it was.
502
+ // ⚠️ THE TWO GUARDS ARE NOT BELT AND BRACES. Without a writer the icon is not even mounted (`gameIcons`), so the bar
503
+ // cannot reach this branch — but `iconAct` is EXPORTED and any host can call it by key. Without the guard that call
504
+ // would throw; with it, nothing happens and nothing is announced — which is the truth: this game has no way to do it.
544
505
  contrast: (i) => {
545
- if (!ctx.setTemaDoJogador)
506
+ if (!ctx.setPlayerTheme)
507
+ return;
508
+ const v = nextTheme((P()[i] || {}).visual ?? DEFAULT_VISUAL);
509
+ ctx.setPlayerTheme(i, v.tema);
510
+ ctx.srSay(t('sr.visual.contrast', { v: t(SHORT_THEME[v.tema]) }));
511
+ },
512
+ /*
513
+ * THE TYPOGRAPHY CYCLE (ADR-0149 §1): one press changes the letter CASE and the FACE at once. The guard is the one
514
+ * written at `contrast` above, for the same reason.
515
+ *
516
+ * It ANNOUNCES THE FACE, NOT THE POSITION: «position 3 of 5» tells nobody anything; the face's name is what the child
517
+ * recognises — the same reason ADR-0074 forbids `action2` from reaching a person.
518
+ */
519
+ tipografia: () => {
520
+ if (!ctx.cycleTypography)
546
521
  return;
547
- const v = proximoTema((P()[i] || {}).visual ?? PADRAO);
548
- ctx.setTemaDoJogador(i, v.tema);
549
- ctx.srSay(t('sr.visual.contrast', { v: t(CURTO_DO_TEMA[v.tema]) }));
522
+ const face = ctx.cycleTypography();
523
+ if (face)
524
+ ctx.srSay(t('sr.typo.font', { fam: face }));
525
+ },
526
+ // PLAYING THROUGH THE WEBCAM (ADR-0215): off → hands → face → eyes → off; stored in one key, so one mode at a time.
527
+ camera: () => {
528
+ const v = nextCameraControl(ctx.settings.cameraControl);
529
+ ctx.settings.setCameraControlValue(v);
530
+ ctx.srSay(t('sr.icon.camera', { v: t(CAMERA_MODE_NAME[v]) }));
531
+ },
532
+ // PLAYING BY SPEAKING (ADR-0189, issue #184): on or off, one stored key. What cannot start puts it back to off and says why,
533
+ // which is the control's own job (`ui/voice-control`) — this only writes the child's answer.
534
+ voice: () => {
535
+ const v = !ctx.settings.voiceControl;
536
+ ctx.settings.setVoiceControlValue(v);
537
+ ctx.srSay(t('sr.icon.voice', { v: t(v ? 'state.on' : 'state.off') }));
538
+ },
539
+ // THE LANGUAGE (the Dev, 2026-09-16): the next flag; `setLocale` stores it and every surface redraws on `i18n:change`. Said in the NEW
540
+ // language, once it has loaded.
541
+ idioma: () => {
542
+ const v = nextLocale(ctx.translator.locale());
543
+ void ctx.translator.setLocale(v).then(() => ctx.srSay(t('sr.icon.idioma', { v: LANGUAGE_NAME[v] })));
544
+ },
545
+ // THE GAME SPEED (ADR-0180): one step down, wrapping at 50%; stored, and felt on the next frame of `startLoop`.
546
+ velocidade: () => {
547
+ const v = nextGameSpeed(ctx.settings.gameSpeed);
548
+ ctx.settings.setGameSpeedValue(v);
549
+ ctx.srSay(t('sr.icon.velocidade', { pct: Math.round(v * 100) }));
550
550
  },
551
551
  cvd: (i) => {
552
- if (!ctx.setCorrecaoDoJogador)
552
+ if (!ctx.setPlayerCorrection)
553
553
  return;
554
- const v = proximaCorrecao((P()[i] || {}).visual ?? PADRAO);
555
- ctx.setCorrecaoDoJogador(i, v.correcao);
556
- ctx.srSay(t('sr.icon.cvd', { v: t(CURTO_DA_CORRECAO[v.correcao]) }));
554
+ const v = nextCorrection((P()[i] || {}).visual ?? DEFAULT_VISUAL);
555
+ ctx.setPlayerCorrection(i, v.correcao);
556
+ ctx.srSay(t('sr.icon.cvd', { v: t(SHORT_CORRECTION[v.correcao]) }));
557
557
  },
558
558
  };
559
559
  function iconAct(k, i) {
560
- const ic = ICON_BY_KEY.get(k);
561
- if (ic && ic.soon) {
562
- ctx.srAlert(t('sr.icon.underConstruction', { nome: t(ic.n) }));
560
+ // locked like the panel's row (ADR-0185): the same reason, said, and nothing turned on
561
+ if (k === 'tts' && ctx.noVoice?.()) {
562
+ ctx.srAlert(t('audio.semVoz'));
563
563
  return;
564
564
  }
565
565
  if ((k === 'blind' || k === 'tts') && !hasPrivateOutput(i)) {
566
566
  ctx.srAlert(t('sr.icon.needsPrivateOutput'));
567
567
  return;
568
568
  }
569
- // ⚠️ A MESMA RECUSA DO PAINEL, na mesma forma que as duas acima: DIZER e voltar. Aceitar o clique e
570
- // ignorá-lo é a outra metade do que o ADR-0076 proíbe, e aqui há um agravante — este ícone e o
571
- // `#opt-altmove` escrevem o MESMO valor, logo um a aceitar enquanto o outro recusa daria à criança dois
572
- // botões que discordam sobre o mesmo ajuste.
573
- if (k === 'altmove') {
574
- const recusa = recusaAgora(i);
575
- if (recusa) {
576
- ctx.srAlert(t(recusa.chave));
577
- return;
578
- }
579
- }
569
+ // No blanket refusal for `altmove` here (ADR-0218): refusing the whole icon on a device that always latches would also
570
+ // lock «one button only» away from the child playing with their eyes. The lock lives in the CYCLE, which skips
571
+ // `standard` on that device, and the refusal lives inside the action, over the latch write only.
580
572
  const act = ICON_ACTS[k];
581
573
  if (act)
582
574
  act(i);
583
575
  }
584
576
  // --- reflection --------------------------------------------------------------------------
585
- function iconLabel(k, i) { return computeIconLabel(k, iconState(i)); }
577
+ function iconLabel(k, i) { return computeIconLabel(t, k, iconState(i)); }
586
578
  function reflectIconBtn(b, i) {
587
579
  const k = b.dataset.pi || '';
580
+ // the hourglass follows the CURRENT cartridge's clock (ADR-0180): a turn game mounted later hides it, a clock game shows it
581
+ if (k === 'velocidade')
582
+ b.hidden = !ctx.clock?.();
583
+ if (k === 'idioma') {
584
+ const flag = flagOf(ctx.translator.locale());
585
+ if (b.innerHTML !== flag)
586
+ b.innerHTML = flag;
587
+ }
588
588
  const st = iconState(i);
589
589
  const v = computeIconVisual(k, st);
590
590
  b.classList.remove(...ICON_STATE_CLASSES);
@@ -595,26 +595,25 @@ export function initPauseIcons(ctx) {
595
595
  b.classList.toggle('pi-on', v.on);
596
596
  b.classList.toggle('pi-dis', v.dis);
597
597
  /*
598
- * 🔴 A ISSUE #128, E ELA É DE UMA LINHA: `pi-dis` é CLASSE CSS. A criança que enxerga vê o ícone
599
- * apagado; a que navega por leitor de tela não recebe nada — o botão anuncia-se accionável e não
600
- * responde. `aria-disabled` espelha o mesmo facto para quem ouve.
601
- *
602
- * ⚠️ E `aria-disabled` e NÃO `disabled`: o segundo tira o botão da ordem de tabulação, e quem navega
603
- * por teclado deixaria de o alcançar — logo deixaria de poder ouvir POR QUE ele não responde. É a
604
- * mesma escolha que o `#opt-altmove` faz no painel, pela mesma razão.
598
+ * `pi-dis` is a CSS class (issue #128): a child who sees gets a greyed icon, a child on a screen reader gets nothing — the
599
+ * button announces itself actionable and does not respond. `aria-disabled` mirrors the same fact for whoever listens.
600
+ * `aria-disabled` and NOT `disabled`: the latter takes the button out of the tab order, and a keyboard user could no
601
+ * longer reach it — nor hear WHY it does not respond.
605
602
  */
606
603
  if (v.dis)
607
604
  b.setAttribute('aria-disabled', 'true');
608
605
  else
609
606
  b.removeAttribute('aria-disabled');
610
- b.setAttribute('aria-pressed', String(v.active));
611
- // ⚠️ TODO ÍCONE RECEBE RÓTULO, `soon` INCLUÍDO — e o guarda que aqui estava dizia por que não: «`soon`
612
- // buttons keep the label the markup gave them (same string)». A segunda metade continua certa (não há
613
- // estado a reportar), mas «mesma string» era verdade só enquanto a marcação e o reflexo corressem no
614
- // MESMO IDIOMA — e desde que a engine passou a montar a barra (ADR-0106 etapa 2) deixam de correr.
615
- // O `initI18n` carrega en/es de forma assíncrona; a marcação nasce em pt e só o reflexo a corrige.
616
- // 📌 Medido num navegador: cinco ícones em inglês e três ainda em «(em construção)», na mesma barra.
617
- b.setAttribute('aria-label', computeIconLabel(k, st));
607
+ // the ☰ opens something and holds no state: a pressed/unpressed button would announce a toggle
608
+ if (k === 'menu')
609
+ b.removeAttribute('aria-pressed');
610
+ else
611
+ b.setAttribute('aria-pressed', String(v.active));
612
+ // ⚠️ EVERY icon is relabelled here, including the ones that carry no state — because the markup and this reflection do not
613
+ // run in the same language. `initI18n` loads en/es asynchronously, so the markup is born in pt and only the reflection
614
+ // corrects it. 📌 Measured in a browser back when a guard skipped some of them: five icons in English and three still in
615
+ // Portuguese, on the same bar.
616
+ b.setAttribute('aria-label', computeIconLabel(t, k, st));
618
617
  }
619
618
  function reflectIconsIn(root, i) {
620
619
  if (!root)
@@ -622,111 +621,136 @@ export function initPauseIcons(ctx) {
622
621
  root.querySelectorAll('.pi-btn').forEach((b) => reflectIconBtn(b, i));
623
622
  }
624
623
  /**
625
- * Reflete os ícones de TODAS as telas. Varre as BARRAS e não mais os cartões de pausa: desde o item 7 do
626
- * ADR-0044 os ícones vivem no HUD, e um cartão de pausa não contém `.pi-btn` nenhum.
624
+ * THE CARDS THIS INSTANCE BUILT. Private: whoever built them keeps them, which asks nothing of any host — a ctx field
625
+ * would be one more thing every game had to remember to pass.
627
626
  */
628
- /**
629
- * OS CARTÕES QUE ESTA INSTÂNCIA CONSTRUIU. Privado, e é a resposta a um problema que eu próprio criei.
630
- *
631
- * ⚠️ O `PauseIconsCtx` tinha um `getPauseScreens` e eu removi-o hoje, com razão: tinha ZERO leitores. Agora
632
- * este módulo precisa de alcançar os cartões — e a saída certa NÃO é repor o campo. Quem os construiu foi
633
- * ele; guardar o que construiu não pede nada a consumidor nenhum, e um campo de ctx é mais uma coisa que
634
- * cada um dos 300 jogos teria de se lembrar de passar.
627
+ const cards = [];
628
+ /*
629
+ * `refreshPauseItems` LOCKS THE ITEMS THIS GAME CANNOT ACTIVATE — recomputed, not decided at start. The action table can
630
+ * arrive after the card is built (`getPauseActs` is asked when needed), so deciding at build time would lose for good
631
+ * every item whose action exists only later. The host calls `reflectPauseIcons()` when the pause opens and when a
632
+ * cartridge is mounted, so the card is right at the moment the child sees it.
635
633
  */
636
- const cartoes = [];
637
634
  /**
638
- * ESCONDE OS ITENS QUE ESTE JOGO NÃO CONSEGUE ACCIONAR — recalculado, e não decidido no arranque.
635
+ * THE CARD'S NAME — what is SEEN and what is HEARD — repainted in the current language.
639
636
  *
640
- * ⚠️ ESTA FUNÇÃO EXISTE POR UM DEFEITO DE TEMPO. O cartão era FILTRADO no `buildScreenPause`, e o
641
- * `getPauseActs` é um getter precisamente porque a tabela CHEGA TARDE — «`pauseActs` is a `const` declared
642
- * far below the init site», diz o próprio campo. Um consumidor que siga esse padrão documentado montava um
643
- * cartão sem os itens cuja acção só existiu depois do boot, e nunca mais os recuperava.
637
+ * The markup is born before an asynchronously loaded dictionary arrives, and an `aria-label` set once cannot be corrected
638
+ * later by `applyDom`, which only reaches `[data-i18n]` and `[data-i18n-aria]`. Nor would `data-i18n-aria` do: `applyDom`
639
+ * calls `t(k)` without parameters, and this key carries the seat number — the child would hear «Player {n} pause menu»,
640
+ * braces included. So it is repainted on each `reflectPauseIcons()`, i.e. when the pause opens.
644
641
  *
645
- * 📌 A avaliação passou para o ÚLTIMO instante possível: o `ui/shell` chama `reflectPauseIcons()` quando a
646
- * fase vira `pause-menu`, ou seja quando a pausa ABRE. O §5 continua respeitado — a criança nunca vê um
647
- * item que não acciona —, e agora também vê os que passaram a accionar.
642
+ * The one who lost was the one who LISTENS (ADR-0044 item 4): to a sighted child the card was entirely in the right
643
+ * language, and nothing looked wrong.
648
644
  */
649
- function refrescarItensDaPausa() {
645
+ function renameCard(cardEl, i) {
646
+ const card = cardEl.querySelector('.pause-card');
647
+ if (card)
648
+ card.setAttribute('aria-label', t('pause.cardAria', { n: i + 1 }));
649
+ const seat = cardEl.querySelector('h2 .pause-seat');
650
+ if (seat)
651
+ seat.textContent = ctx.getNumPlayers() > 1 ? t('pause.cardSeat', { n: i + 1 }) : '';
652
+ }
653
+ function refreshPauseItems() {
650
654
  const acts = getPauseActs();
651
- const raiz = ctx.pmButtons ?? PM_BTNS;
652
- const opcoes = ctx.optionsButtons ?? PM_OPTIONS_BTNS;
653
- const vivos = new Set([
654
- ...raizQueAcciona(raiz, opcoes, acts).map((b) => b.act),
655
- ...itensQueAccionam(opcoes, acts).map((b) => b.act),
655
+ const rootEl = ctx.pmButtons ?? PM_BTNS;
656
+ const options = ctx.optionsButtons ?? PM_OPTIONS_BTNS;
657
+ const fromGame = ctx.gameButtons ?? PM_GAME_BTNS;
658
+ const actingItems = new Set([
659
+ ...rootThatActs(rootEl, options, acts, fromGame).map((b) => b.act),
660
+ ...itemsThatAct(options, acts).map((b) => b.act),
661
+ ...itemsThatAct(fromGame, acts).map((b) => b.act),
656
662
  ]);
657
- // ⚠️ `filter(Boolean)`: os cartões são indexados por JOGADOR, e montar só a tela 2 deixa um buraco no
658
- // índice 0. Um `for…of` sobre array esparso entrega `undefined`, e foi o que rebentou à primeira.
659
- for (const cartao of cartoes.filter(Boolean)) {
660
- for (const btn of cartao.querySelectorAll('.pm-btn')) {
661
- // ⚠️ `hidden` e não `remove()`: reaparecer tem de ser possível, porque a tabela pode crescer outra vez
662
- // (um jogo que só liga «sair» depois da primeira fase). Remover seria decidir uma vez de novo.
663
- btn.hidden = !vivos.has(btn.dataset.act ?? '');
663
+ // An explicit index, not `filter(Boolean)`: the cards are indexed by PLAYER, building only screen 2 leaves a hole at
664
+ // index 0, and closing the hole would renumber the seats in the card's name. The `undefined` guard is written by hand.
665
+ for (let i = 0; i < cards.length; i++) {
666
+ const cardEl = cards[i];
667
+ if (!cardEl)
668
+ continue;
669
+ renameCard(cardEl, i);
670
+ for (const btn of cardEl.querySelectorAll('.pm-btn')) {
671
+ // LOCKED WITH ITS REASON, NOT HIDDEN (ADR-0161, superseding ADR-0106 §5 here): a card that changed shape from game
672
+ // to game hid from the child that an option exists. The cursor still stops on the item and it still counts;
673
+ // reaching or pressing it says why. And not `remove()`: the table can grow later.
674
+ const act = btn.dataset.act ?? '';
675
+ if (actingItems.has(act)) {
676
+ btn.removeAttribute('aria-disabled');
677
+ delete btn.dataset.motivo;
678
+ }
679
+ else {
680
+ btn.setAttribute('aria-disabled', 'true');
681
+ btn.dataset.motivo = itemReason(t, act);
682
+ }
664
683
  }
665
684
  }
666
685
  }
667
686
  function reflectPauseIcons() {
668
687
  ctx.getA11yBars().forEach((bar, i) => reflectIconsIn(bar, i));
669
- refrescarItensDaPausa();
688
+ refreshPauseItems();
670
689
  }
671
690
  // --- the pause screen ----------------------------------------------------------------------
672
691
  /**
673
- * Troca a lista visível E ANUNCIA o primeiro item da lista que entrou.
674
- *
675
- * O anúncio não é enfeite: quem não enxerga acabou de mudar de menu e o cursor pulou para outro lugar. Sem
676
- * a fala, a única pista de que a tela mudou seria o silêncio. O índice "N de M" vem junto (item 3), e é ele
677
- * que diz de quantos itens é a lista nova.
692
+ * Switches the visible list AND ANNOUNCES the first item of the list that came in. Not decoration: a child who cannot see
693
+ * has just changed menus and the cursor jumped; without speech the only clue would be silence. The «N de M» index comes
694
+ * with it (ADR-0044 item 3) and says how many items the new list has.
678
695
  */
679
- function anunciarLista(sp, sub) {
680
- const primeiro = mostrarSubmenuDaPausa(sp, sub);
681
- if (!primeiro)
696
+ function announceList(sp, sub) {
697
+ const first = showPauseOptions(sp, sub);
698
+ if (!first)
682
699
  return;
683
- const itens = [...sp.querySelectorAll(PM_ITENS_VISIVEIS)];
684
- ctx.srSay(anunciarItem({ rotulo: primeiro.textContent || '', posicao: 1, total: itens.length }, menuIndexOn));
700
+ const items = [...sp.querySelectorAll(PM_VISIBLE_ITEMS)];
701
+ ctx.srSay(announceItem(t, { label: first.textContent || '', position: 1, total: items.length }, ctx.settings.menuIndexOn));
685
702
  }
686
- /* ===================== O MODO `accessibility` (ADR-0044, item 7) ===================== */
703
+ /* ===================== THE `accessibility` MODE (ADR-0044 item 7) ===================== */
687
704
  /**
688
- * Quem está com o direcional dirigindo a BARRA em vez do personagem.
689
- *
690
- * Vida de RODADA (ADR-0038): mora no closure desta instância, não é persistido, e some com a partida. Um
691
- * modo de entrada que sobrevivesse ao reinício seria a armadilha voltando pela porta dos fundos — a criança
692
- * abriria o jogo no dia seguinte e o personagem não andaria.
705
+ * Who has the d-pad steering the BAR instead of the character. ROUND lifetime (ADR-0038): it lives in this instance's
706
+ * closure, is not persisted, and goes with the round — an input mode that survived a restart would be the trap coming
707
+ * back: the child would open the game the next day and the character would not move.
693
708
  */
694
- const naBarra = new Set();
695
- /** O cursor da barra da tela `i`, ou o primeiro ícone quando ainda não há cursor. */
696
- function iconeSelecionado(bar) {
709
+ const onBar = new Set();
710
+ /** Screen `i`'s bar cursor, or the first icon when there is no cursor yet. */
711
+ function selectedIcon(bar) {
697
712
  return bar.querySelector('.pi-sel') || bar.querySelector('.pi-btn');
698
713
  }
699
- /** Põe o cursor num ícone, escreve a legenda e ANUNCIA — a legenda é o canal de quem não vê o ícone. */
700
- function selecionarIcone(bar, el) {
714
+ /** Puts the cursor on an icon, writes the caption and ANNOUNCES — speech is the channel of whoever cannot see the icon. */
715
+ function selectIcon(i, bar, el) {
701
716
  bar.querySelectorAll('.pi-sel').forEach((x) => x.classList.remove('pi-sel'));
702
717
  el.classList.add('pi-sel');
703
718
  const cap = bar.querySelector('.pause-icons-cap');
704
719
  if (cap)
705
- cap.textContent = legendaDoIcone(bar, el);
706
- ctx.srSay(legendaDoIcone(bar, el));
720
+ cap.textContent = accessibleLabel(el);
721
+ ctx.srSay(iconCaption(t, bar, el, ctx.settings.menuIndexOn)); // the spoken one carries the place (ADR-0167)
722
+ ctx.explainIcon?.(i, el.dataset.pi ?? null);
707
723
  }
708
724
  /**
709
- * ENTRA no modo: o direcional passa a dirigir a barra da tela `i`, e o jogo VOLTA a rodar.
725
+ * ENTERS the mode: the d-pad now steers screen `i`'s bar.
726
+ *
727
+ * It does NOT resume the game (ADR-0155): START is the QUICK PAUSE, the bar is used with the game FROZEN, and resuming on
728
+ * entry would unfreeze the world the instant the child asked it to stop. Whoever enters decides the phase.
710
729
  *
711
- * O anúncio diz como SAIR, e diz na hora de entrar. É a linha que desarma a armadilha que o próprio
712
- * ADR-0044 anotou como consequência negativa desta decisão: quem não enxerga aperta a direção, o personagem
713
- * não anda, e sem esta frase não há nada na tela que explique — porque a tela não é o canal dessa criança.
730
+ * The announcement says how to LEAVE, at the moment of entering — the line that disarms the trap ADR-0044 recorded as a
731
+ * negative consequence: a child who cannot see presses a direction, the character does not move, and nothing on screen
732
+ * explains why, because the screen is not that child's channel.
714
733
  */
715
- function entrarNaBarra(i) {
734
+ function enterBarMode(i) {
716
735
  const bar = ctx.getA11yBars()[i];
717
- const primeiro = bar && iconeSelecionado(bar);
718
- if (!bar || !primeiro)
736
+ const first = bar && selectedIcon(bar);
737
+ if (!bar || !first)
719
738
  return;
720
- naBarra.add(i);
721
- const acts = getPauseActs();
722
- if (acts.resume)
723
- acts.resume(); // volta à tela normal: o modo é para usar DURANTE a partida
739
+ onBar.add(i);
724
740
  ctx.srSay(t('sr.a11y.barEnter'));
725
- selecionarIcone(bar, primeiro);
741
+ selectIcon(i, bar, first);
726
742
  }
727
- /** SAI do modo e devolve o direcional ao personagem. Anuncia, porque a devolução também é informação. */
728
- function sairDaBarra(i) {
729
- if (!naBarra.delete(i))
743
+ /**
744
+ * LEAVES the mode and gives the d-pad back to the character. It announces, because giving it back is information too.
745
+ *
746
+ * `silent` is for leaving the bar for ANOTHER screen, not for the game — SELECT, which swaps the quick pause for the card
747
+ * (ADR-0155): «back to the game» said there would be a lie, with the card opening on top.
748
+ *
749
+ * `onLeaveBar` runs on EVERY exit — Back, START or whoever calls this — because the root must unfreeze the game by any
750
+ * door. An exit only one path knew would leave the child on the other back at the character in a stopped world.
751
+ */
752
+ function leaveBarMode(i, silent = false) {
753
+ if (!onBar.delete(i))
730
754
  return;
731
755
  const bar = ctx.getA11yBars()[i];
732
756
  if (bar) {
@@ -735,113 +759,112 @@ export function initPauseIcons(ctx) {
735
759
  if (cap)
736
760
  cap.textContent = '';
737
761
  }
738
- ctx.srSay(t('sr.a11y.barExit'));
762
+ ctx.explainIcon?.(i, null);
763
+ if (!silent)
764
+ ctx.srSay(t('sr.a11y.barExit'));
765
+ ctx.onLeaveBar?.(i, silent);
739
766
  }
740
- /** A tela `i` está com o direcional na barra? É o que o roteamento de entrada pergunta a cada quadro. */
741
- const naBarraDe = (i) => naBarra.has(i);
767
+ /** Is screen `i`'s d-pad on the bar? What input routing asks every frame. */
768
+ const isOnBar = (i) => onBar.has(i);
742
769
  /**
743
- * UM PASSO dentro do modo. `temStart` é a borda do botão que abre a pausa — a segunda saída.
770
+ * ONE STEP inside the mode. `hasStart` is the edge of the button that opens the pause — the second way out.
744
771
  *
745
- * A barra é uma fileira, então as QUATRO direções andam nela: para quem navega sem ver, "cima" numa lista
746
- * de uma linha só não pode ser um beco. E anda em ANEL, como todo menu do jogo desde o item 1.
772
+ * The bar is one row, so all FOUR directions move along it: for someone navigating without sight, «up» in a one-row list
773
+ * must not be a dead end. And it moves in a RING, like every menu of the game (ADR-0044 item 1).
747
774
  */
748
- function navBar(i, k, temStart = false) {
749
- if (!naBarra.has(i))
775
+ function navBar(i, k, hasStart = false) {
776
+ if (!onBar.has(i))
750
777
  return;
751
778
  const bar = ctx.getA11yBars()[i];
752
779
  if (!bar)
753
780
  return;
754
- const acao = acaoNaBarra(k, temStart);
755
- if (acao === 'sair') {
756
- sairDaBarra(i);
781
+ const action = barAction(k, hasStart);
782
+ if (action === 'sair') {
783
+ leaveBarMode(i);
757
784
  return;
758
785
  }
759
- const icones = [...bar.querySelectorAll('.pi-btn')];
760
- if (!icones.length)
786
+ const icons = [...bar.querySelectorAll('.pi-btn')];
787
+ if (!icons.length)
761
788
  return;
762
- const cur = iconeSelecionado(bar);
763
- const idx = cur ? icones.indexOf(cur) : 0;
764
- if (acao === 'ativar') {
765
- setPauseActor(i);
766
- if (cur)
767
- cur.click();
789
+ // never null here: the bar has an icon, and `selectedIcon` falls back to the first one
790
+ const cur = selectedIcon(bar);
791
+ // the click goes through the bar's own listener, which records who pressed it (`setPauseActor`)
792
+ if (action === 'ativar') {
793
+ cur.click();
768
794
  return;
769
795
  }
770
- if (acao === 'andar') {
771
- const d = (k.down || k.right) ? 1 : -1;
772
- selecionarIcone(bar, icones[passoNoAnel(icones.length, idx, d)]);
773
- }
796
+ if (action === 'andar')
797
+ selectIcon(i, bar, icons[stepInRing(icons.length, icons.indexOf(cur), (k.down || k.right) ? 1 : -1)]);
774
798
  }
775
799
  function buildScreenPause(i) {
776
- const sp = docDaMontagem().createElement('div');
800
+ const sp = ctx.doc.createElement('div');
777
801
  sp.className = 'screen-pause';
778
802
  sp.hidden = true;
779
803
  sp.dataset.player = String(i);
780
804
  /*
781
- * ⚠️ MONTA A LISTA INTEIRA E ESCONDE DEPOIS — e a versão anterior desta linha FILTRAVA aqui, o que estava
782
- * errado por uma razão de TEMPO. O comentário que estava neste sítio dizia «montar é o primeiro instante
783
- * em que a resposta existe»; não é. O `getPauseActs` é um getter precisamente porque a tabela chega
784
- * TARDE — «`pauseActs` is a `const` declared far below the init site», diz o próprio campo —, e o
785
- * `createGame` monta durante o próprio `createGame(...)`. Filtrar aqui apagava para sempre todo item cuja
786
- * acção só passou a existir depois do boot.
787
- *
788
- * Quem decide o que se VÊ é o `refrescarItensDaPausa`, a cada `reflectPauseIcons()` — que o `ui/shell`
789
- * dispara quando a fase vira `pause-menu`, ou seja quando a pausa ABRE. O §5 continua respeitado (a
790
- * criança nunca vê um item que não acciona) e agora também vê os que passaram a accionar.
805
+ * BUILDS THE WHOLE LIST, and `refreshPauseItems` decides which items act — here and on every `reflectPauseIcons()`.
806
+ * Filtering at build time would lose for good every item whose action exists only after the boot (see above).
791
807
  */
792
808
  sp.innerHTML = screenPauseMarkup({
793
809
  player: i,
794
810
  numPlayers: ctx.getNumPlayers(),
795
811
  pmButtons: ctx.pmButtons ?? PM_BTNS,
796
812
  optionsButtons: ctx.optionsButtons ?? PM_OPTIONS_BTNS,
813
+ gameButtons: ctx.gameButtons ?? PM_GAME_BTNS,
797
814
  dynLabel: dynLabel, t,
798
815
  });
799
- cartoes[i] = sp;
800
- refrescarItensDaPausa(); // o §5 vale já na montagem, e não só na primeira abertura
816
+ cards[i] = sp;
817
+ refreshPauseItems(); // the lock holds from the build, not only from the first opening
801
818
  sp.addEventListener('click', (e) => {
802
- const target = e.target;
803
- const b = target && target.closest('.pm-btn');
804
- if (b) {
805
- setPauseActor(i);
806
- const act = b.dataset.act || '';
807
- // NAVEGAÇÃO DENTRO DO CARTÃO fica aqui, e não na tabela de ações: `options` e `pmback` não fazem nada
808
- // ao jogo — trocam qual lista está na tela. A tabela vive em `ui/shell`, que não conhece este `sp`.
809
- if (act === 'options' || act === 'pmback') {
810
- anunciarLista(sp, act === 'options' ? 'opcoes' : 'raiz');
811
- return;
812
- }
813
- // `acessibilidade` leva o cursor à BARRA RÁPIDA. Enquanto ela mora dentro do cartão, "entrar no modo"
814
- // é pôr o cursor nela — e a saída continua sendo a saída da pausa, que é a mesma de sempre. Quando o
815
- // item 7 levar a barra para o HUD, esta linha o segue; o que o item SIGNIFICA não muda.
816
- if (act === 'acessibilidade') {
817
- entrarNaBarra(i);
818
- return;
819
- }
820
- const acts = getPauseActs();
821
- const fn = acts[act];
822
- if (fn)
823
- fn();
824
- return;
825
- }
819
+ const b = e.target?.closest('.pm-btn');
820
+ if (b)
821
+ pressPauseItem(i, sp, b);
826
822
  });
827
823
  return sp;
828
824
  }
825
+ /** One item of screen `i`'s pause card pressed — a locked one, a door between its lists, the quick bar, or an action. */
826
+ function pressPauseItem(i, sp, b) {
827
+ // LOCKED (ADR-0161): pressing it SAYS the reason and does nothing — neither door nor action.
828
+ if (b.getAttribute('aria-disabled') === 'true') {
829
+ const reason = b.dataset.motivo ?? '';
830
+ ctx.srSay(reason);
831
+ ctx.explainItem?.(reason);
832
+ return;
833
+ }
834
+ setPauseActor(i);
835
+ const act = b.dataset.act || '';
836
+ // «Opções do jogo» with the cartridge's rows opens the engine's panel (ADR-0182), not the list a host may pass
837
+ const doorWithPanel = act === 'opcoesdojogo' && typeof getPauseActs().opcoesdojogo === 'function';
838
+ if (DOOR_TO_LIST[act] && !doorWithPanel) {
839
+ announceList(sp, DOOR_TO_LIST[act]);
840
+ return;
841
+ }
842
+ // `acessibilidade` takes the cursor to the QUICK BAR. The item is not in the engine's root (ADR-0151); only a list a
843
+ // GAME passes reaches it, and for that list the card closes and the bar is used — explicitly, since `enterBar` does
844
+ // not resume by itself (ADR-0155).
845
+ if (act === 'acessibilidade') {
846
+ getPauseActs().resume?.();
847
+ enterBarMode(i);
848
+ return;
849
+ }
850
+ const fn = getPauseActs()[act];
851
+ if (fn)
852
+ fn();
853
+ }
829
854
  /**
830
- * A BARRA RÁPIDA de uma tela: os dez alternadores, a legenda, e a fiação dos dois.
855
+ * One screen's QUICK BAR: the toggles, the caption, and their wiring.
831
856
  *
832
- * Ela é IRMÃ da `.screen-exp` e não filha, e isso é a decisão do #82 aplicada: a barra é CONTROLE, não
833
- * experiência. O modo empatia degrada a experiência de propósito — simulação é criar dificuldade onde a
834
- * facilidade não existe —, e degradar o que existe para DAR acesso seria o contrário do que ele serve.
857
+ * A SIBLING of `.screen-exp`, not a child (#82): the bar is CONTROL, not experience. Empathy mode degrades the experience
858
+ * on purpose — a simulation creates difficulty — and degrading what exists to GIVE access would be the opposite.
835
859
  */
836
860
  function buildQuickBar(i) {
837
- const bar = docDaMontagem().createElement('div');
861
+ const bar = ctx.doc.createElement('div');
838
862
  bar.className = 'screen-a11y';
839
863
  bar.dataset.player = String(i);
840
- bar.innerHTML = quickBarMarkup(iconesDoJogo);
841
- // FORA DA ORDEM DE TABULAÇÃO durante a partida (ADR-0044, item 7). Dez paradas entre a criança e o jogo
842
- // seria o preço de deixá-los lá — e o alcance por teclado não se perde: ele passa a ser o modo
843
- // `accessibility`, que se abre pela pausa. A barra do TÍTULO não é afetada: lá não se está jogando, e o
844
- // `tabindex` é posto AQUI, no elemento, e não no markup que as duas compartilham.
864
+ bar.innerHTML = quickBarMarkup(ctx.translator, gameIcons);
865
+ // OUT OF THE TAB ORDER during play (ADR-0044 item 7): a stop per icon between the child and the game would be the price
866
+ // of leaving them in, and keyboard reach is not lost — it becomes the `accessibility` mode, opened from the pause. The
867
+ // bar the root mounts in `#title-icons` is unaffected: the `tabindex` is set HERE, on the element, not in the shared markup.
845
868
  bar.querySelectorAll('.pi-btn').forEach((b) => { b.tabIndex = -1; });
846
869
  const cap = bar.querySelector('.pause-icons-cap');
847
870
  bar.addEventListener('click', (e) => {
@@ -852,39 +875,25 @@ export function initPauseIcons(ctx) {
852
875
  iconAct(ib.dataset.pi || '', i);
853
876
  reflectPauseIcons(); // must run BEFORE reading the label back — that is what makes the caption honest
854
877
  if (cap)
855
- cap.textContent = legendaDoIcone(bar, ib);
856
- });
857
- // Legenda = o `aria-label` do botão, para que passar o mouse ou focar diga a MESMA verdade que um leitor
858
- // de tela anunciaria. Uma fonte só para quem vê e para quem escuta.
859
- //
860
- // E ELA SOME AO SAIR. Antes ficava: a última explicação apontada permanecia por cima do jogo até alguém
861
- // apontar outra. Numa barra que agora vive na TELA DE JOGO isso é uma faixa de texto parada em cima da
862
- // partida — o Dev viu e disse o que é: explicação só enquanto o mouse estiver no botão.
863
- //
864
- // A EXCEÇÃO É O CURSOR DO MODO `accessibility`: quando ele está pousado num ícone, a legenda é a única
865
- // coisa que diz onde ele está, e apagá-la ao mexer o mouse cegaria o modo. Daí a pergunta pelo `.pi-sel`.
866
- const limpar = () => { if (cap && !bar.querySelector('.pi-sel'))
867
- cap.textContent = ''; };
868
- bar.querySelectorAll('.pi-btn').forEach((b) => {
869
- const show = () => { if (cap)
870
- cap.textContent = legendaDoIcone(bar, b); };
871
- b.addEventListener('mouseenter', show);
872
- b.addEventListener('focus', show);
873
- b.addEventListener('mouseleave', limpar);
874
- b.addEventListener('blur', limpar);
878
+ cap.textContent = accessibleLabel(ib);
875
879
  });
880
+ // Caption = the button's `aria-label`, so hovering or focusing says the SAME truth a screen reader would announce: one
881
+ // source for whoever sees and whoever listens. It GOES AWAY on leaving — a bar on the play screen must not leave a strip
882
+ // of text over the game (the Dev: an explanation only while the pointer is on the button). The exception is the
883
+ // `accessibility` mode's CURSOR: when it sits on an icon the caption is the only thing saying where it is, hence the
884
+ // `.pi-sel` check in `wireBarCaption`.
885
+ wireBarCaption(bar, (k) => ctx.explainIcon?.(i, k));
876
886
  return bar;
877
887
  }
878
888
  return {
879
- buildScreenPause, buildQuickBar, entrarNaBarra, sairDaBarra, naBarraDe, navBar,
880
- iconesMontados: iconesDoJogo,
889
+ buildScreenPause, buildQuickBar, enterBar: enterBarMode, leaveBar: leaveBarMode, onBar: isOnBar, navBar,
890
+ mountedIcons: gameIcons,
881
891
  iconAct, iconLabel, reflectIconBtn, reflectIconsIn, reflectPauseIcons,
882
- // ⚠️ O `setCalmMode` PERSISTE TAMBÉM, e sanea. Ele é a outra porta para o mesmo valor — se só o ciclo do
883
- // ícone gravasse, um nível posto por aqui sobreviveria à sessão e não ao fecho da aba, que é a metade
884
- // pior do defeito: o ajuste parece ter pegado e some depois.
892
+ // `setCalmMode` PERSISTS TOO, and sanitises: it is the other door to the same value, and a level set here that only
893
+ // lasted the session would look applied and then vanish.
885
894
  applyCalm,
886
895
  getCalmMode: () => calmMode,
887
- setCalmMode: (n) => { calmMode = saneiaNivelTea(n); store.set(store.KEYS.tea, calmMode); },
896
+ setCalmMode: (n) => { calmMode = sanitiseTeaLevel(n, DEFAULTS.calmMode); ctx.store.set(KEYS.tea, calmMode); },
888
897
  iconState,
889
898
  };
890
899
  }