@the-inclusionist/engine 7.0.1 → 8.0.0-rc.1

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 (116) hide show
  1. package/README.md +5 -3
  2. package/app/css/style.css +22 -5
  3. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  4. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  5. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  7. package/app/public/vendor/fonts.css +43 -2
  8. package/dist-pkg/boot/create-game.d.ts +76 -2
  9. package/dist-pkg/boot/create-game.js +271 -13
  10. package/dist-pkg/core/constants.d.ts +0 -28
  11. package/dist-pkg/core/constants.js +34 -17
  12. package/dist-pkg/core/contract.d.ts +39 -0
  13. package/dist-pkg/core/contract.js +36 -0
  14. package/dist-pkg/core/entity.d.ts +47 -4
  15. package/dist-pkg/core/layers.d.ts +18 -0
  16. package/dist-pkg/core/layers.js +18 -0
  17. package/dist-pkg/core/rng.js +14 -4
  18. package/dist-pkg/core/route.d.ts +42 -0
  19. package/dist-pkg/core/route.js +158 -0
  20. package/dist-pkg/core/state.d.ts +10 -1
  21. package/dist-pkg/core/state.js +13 -0
  22. package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
  23. package/dist-pkg/educational/adaptive-engine.js +117 -0
  24. package/dist-pkg/educational/segment-bar.d.ts +96 -0
  25. package/dist-pkg/educational/segment-bar.js +89 -0
  26. package/dist-pkg/i18n/en.js +33 -0
  27. package/dist-pkg/i18n/es.js +33 -0
  28. package/dist-pkg/i18n/pt.js +46 -0
  29. package/dist-pkg/input/default-bindings.d.ts +40 -0
  30. package/dist-pkg/input/default-bindings.js +143 -9
  31. package/dist-pkg/input/gamepad.d.ts +18 -11
  32. package/dist-pkg/input/gamepad.js +75 -9
  33. package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
  34. package/dist-pkg/input/keyboard-runtime.js +24 -5
  35. package/dist-pkg/input/keyboard.d.ts +10 -0
  36. package/dist-pkg/input/keyboard.js +41 -11
  37. package/dist-pkg/input/keydown.d.ts +27 -2
  38. package/dist-pkg/input/keydown.js +20 -6
  39. package/dist-pkg/input/latch-scope.d.ts +70 -0
  40. package/dist-pkg/input/latch-scope.js +110 -0
  41. package/dist-pkg/input/latch-store.d.ts +45 -0
  42. package/dist-pkg/input/latch-store.js +74 -0
  43. package/dist-pkg/input/origem-sintetica.d.ts +44 -0
  44. package/dist-pkg/input/origem-sintetica.js +60 -0
  45. package/dist-pkg/input/pointer.d.ts +61 -0
  46. package/dist-pkg/input/pointer.js +71 -0
  47. package/dist-pkg/input/state.d.ts +86 -1
  48. package/dist-pkg/input/state.js +128 -1
  49. package/dist-pkg/input/touch-bindings.d.ts +11 -2
  50. package/dist-pkg/input/touch-bindings.js +9 -3
  51. package/dist-pkg/input/touch.js +9 -1
  52. package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
  53. package/dist-pkg/input/transporte-em-uso.js +130 -0
  54. package/dist-pkg/input/transports.d.ts +88 -2
  55. package/dist-pkg/input/transports.js +60 -5
  56. package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
  57. package/dist-pkg/platform/audio-earcons.d.ts +29 -2
  58. package/dist-pkg/platform/audio-earcons.js +10 -0
  59. package/dist-pkg/platform/audio-nav.d.ts +1 -1
  60. package/dist-pkg/platform/audio-sonar.d.ts +149 -9
  61. package/dist-pkg/platform/audio-sonar.js +232 -21
  62. package/dist-pkg/platform/guide-intensity.d.ts +40 -0
  63. package/dist-pkg/platform/guide-intensity.js +73 -0
  64. package/dist-pkg/platform/storage.d.ts +30 -0
  65. package/dist-pkg/platform/storage.js +35 -0
  66. package/dist-pkg/platform/tts.js +16 -1
  67. package/dist-pkg/platform/voice-plan.d.ts +90 -0
  68. package/dist-pkg/platform/voice-plan.js +137 -0
  69. package/dist-pkg/render/draw.d.ts +1 -1
  70. package/dist-pkg/render/draw.js +15 -5
  71. package/dist-pkg/render/viz-axes.d.ts +17 -0
  72. package/dist-pkg/render/viz-axes.js +19 -0
  73. package/dist-pkg/render/viz-setters.d.ts +34 -2
  74. package/dist-pkg/render/viz-setters.js +189 -33
  75. package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
  76. package/dist-pkg/render/wheelchair-sprites.js +10 -4
  77. package/dist-pkg/ui/dom.js +20 -2
  78. package/dist-pkg/ui/fonts.d.ts +47 -1
  79. package/dist-pkg/ui/fonts.js +47 -9
  80. package/dist-pkg/ui/latch-refusal.d.ts +32 -0
  81. package/dist-pkg/ui/latch-refusal.js +60 -0
  82. package/dist-pkg/ui/layout.d.ts +32 -0
  83. package/dist-pkg/ui/layout.js +64 -1
  84. package/dist-pkg/ui/motion-scene.d.ts +41 -0
  85. package/dist-pkg/ui/motion-scene.js +76 -0
  86. package/dist-pkg/ui/panel-shell.d.ts +53 -0
  87. package/dist-pkg/ui/panel-shell.js +103 -0
  88. package/dist-pkg/ui/pause-icons.d.ts +134 -20
  89. package/dist-pkg/ui/pause-icons.js +307 -45
  90. package/dist-pkg/ui/reach-notice.js +8 -0
  91. package/dist-pkg/ui/settings-audio.d.ts +14 -1
  92. package/dist-pkg/ui/settings-audio.js +43 -3
  93. package/dist-pkg/ui/settings-controls.d.ts +66 -6
  94. package/dist-pkg/ui/settings-controls.js +179 -17
  95. package/dist-pkg/ui/settings-empathy.d.ts +15 -0
  96. package/dist-pkg/ui/settings-empathy.js +2 -0
  97. package/dist-pkg/ui/settings-motion.d.ts +25 -19
  98. package/dist-pkg/ui/settings-motion.js +32 -19
  99. package/dist-pkg/ui/settings-motor.d.ts +81 -3
  100. package/dist-pkg/ui/settings-motor.js +118 -9
  101. package/dist-pkg/ui/settings-typo.d.ts +32 -2
  102. package/dist-pkg/ui/settings-typo.js +64 -18
  103. package/dist-pkg/ui/settings-visual.d.ts +21 -1
  104. package/dist-pkg/ui/settings-visual.js +48 -4
  105. package/dist-pkg/ui/shell.d.ts +13 -3
  106. package/dist-pkg/ui/shell.js +3 -4
  107. package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
  108. package/dist-pkg/ui/simulation-refusal.js +57 -0
  109. package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
  110. package/dist-pkg/ui/visual-axes-panel.js +95 -0
  111. package/dist-pkg/ui/webcam.js +6 -1
  112. package/docs/CREDITS.md +18 -0
  113. package/docs/LICENSES.md +12 -0
  114. package/package.json +24 -4
  115. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  116. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
@@ -1,4 +1,36 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * A RÉGUA DO ALVO DE TOQUE, INDEXADA PELA ALTURA DO VIEWPORT (ADR-0095, decisão do Dev).
4
+ *
5
+ * ⚠️ O ALVO DEIXOU DE SER UM NÚMERO E PASSOU A SER UMA FUNÇÃO DA TELA, e o motivo é um custo que o gate de
6
+ * `pausa-44px` já tinha MEDIDO e deixado por resolver: a 640×360 o cartão de pausa não cabe e a lista ROLA.
7
+ * Remedido em 06/09, porque o cartão mudou desde então: 391 px de conteúdo para 349 visíveis, ou seja 42 px
8
+ * de excesso (o comentário antigo dizia 413/353). Um alvo de 44 px que exige rolagem para ser alcançado
9
+ * pode custar mais dedo do que um de 24 px que está à vista.
10
+ *
11
+ * Os três degraus são os do Dev, e os dois extremos são as duas normas — não números de gosto:
12
+ *
13
+ * altura ≥ 720 44 px WCAG 2.2 · 2.5.5 Target Size (Enhanced) — AAA
14
+ * altura ≥ 540 34 px o degrau do meio
15
+ * altura < 540 24 px WCAG 2.2 · 2.5.8 Target Size (Minimum) — AA
16
+ *
17
+ * ⚠️ ISTO É «MARCAR HONESTAMENTE ONDE SÓ DÁ AA», que é regra escrita do projeto — e não uma renúncia
18
+ * silenciosa. O que se perde em 360 está registrado com número no ADR-0095: a 96 px/pol, 24 CSS px são
19
+ * 6,4 mm, abaixo do alvo de polegar de 9,6 mm que o painel de toque deste jogo cita. É por isso que o
20
+ * ESPAÇAMENTO entre alvos passa a ser o que protege o dedo onde o tamanho não pode — a mesma saída que a
21
+ * própria 2.5.8 dá na sua exceção de spacing.
22
+ */
23
+ export declare const REGUA_DE_ALVO: readonly {
24
+ readonly altura: number;
25
+ readonly alvo: number;
26
+ }[];
27
+ /**
28
+ * O menor alvo de toque aceitável num viewport desta altura, em CSS px.
29
+ *
30
+ * ⚠️ NUNCA DEVOLVE MENOS DE 24: abaixo disso não é «AA num aparelho pequeno», é furar o piso da WCAG. Uma
31
+ * tela mais baixa que 360 não compra o direito de encolher mais — compra o direito de mostrar menos itens.
32
+ */
33
+ export declare function alvoMinimoDeToque(alturaCss: number): number;
2
34
  /** Liga a contagem de jogadores. Chamado uma vez pela raiz, antes do primeiro `layout()`. */
3
35
  export declare function initLayout(deps: {
4
36
  numJogadores: () => number;
@@ -11,6 +11,45 @@
11
11
  import { $ } from './dom.js';
12
12
  import { screenBaseSize } from '../core/screens.js';
13
13
  import { crtScanVars } from '../render/crt.js';
14
+ /**
15
+ * A RÉGUA DO ALVO DE TOQUE, INDEXADA PELA ALTURA DO VIEWPORT (ADR-0095, decisão do Dev).
16
+ *
17
+ * ⚠️ O ALVO DEIXOU DE SER UM NÚMERO E PASSOU A SER UMA FUNÇÃO DA TELA, e o motivo é um custo que o gate de
18
+ * `pausa-44px` já tinha MEDIDO e deixado por resolver: a 640×360 o cartão de pausa não cabe e a lista ROLA.
19
+ * Remedido em 06/09, porque o cartão mudou desde então: 391 px de conteúdo para 349 visíveis, ou seja 42 px
20
+ * de excesso (o comentário antigo dizia 413/353). Um alvo de 44 px que exige rolagem para ser alcançado
21
+ * pode custar mais dedo do que um de 24 px que está à vista.
22
+ *
23
+ * Os três degraus são os do Dev, e os dois extremos são as duas normas — não números de gosto:
24
+ *
25
+ * altura ≥ 720 44 px WCAG 2.2 · 2.5.5 Target Size (Enhanced) — AAA
26
+ * altura ≥ 540 34 px o degrau do meio
27
+ * altura < 540 24 px WCAG 2.2 · 2.5.8 Target Size (Minimum) — AA
28
+ *
29
+ * ⚠️ ISTO É «MARCAR HONESTAMENTE ONDE SÓ DÁ AA», que é regra escrita do projeto — e não uma renúncia
30
+ * silenciosa. O que se perde em 360 está registrado com número no ADR-0095: a 96 px/pol, 24 CSS px são
31
+ * 6,4 mm, abaixo do alvo de polegar de 9,6 mm que o painel de toque deste jogo cita. É por isso que o
32
+ * ESPAÇAMENTO entre alvos passa a ser o que protege o dedo onde o tamanho não pode — a mesma saída que a
33
+ * própria 2.5.8 dá na sua exceção de spacing.
34
+ */
35
+ export const REGUA_DE_ALVO = Object.freeze([
36
+ { altura: 720, alvo: 44 },
37
+ { altura: 540, alvo: 34 },
38
+ { altura: 0, alvo: 24 },
39
+ ]);
40
+ /**
41
+ * O menor alvo de toque aceitável num viewport desta altura, em CSS px.
42
+ *
43
+ * ⚠️ NUNCA DEVOLVE MENOS DE 24: abaixo disso não é «AA num aparelho pequeno», é furar o piso da WCAG. Uma
44
+ * tela mais baixa que 360 não compra o direito de encolher mais — compra o direito de mostrar menos itens.
45
+ */
46
+ export function alvoMinimoDeToque(alturaCss) {
47
+ const h = Number.isFinite(alturaCss) ? alturaCss : 0;
48
+ for (const degrau of REGUA_DE_ALVO)
49
+ if (h >= degrau.altura)
50
+ return degrau.alvo;
51
+ return 24;
52
+ }
14
53
  // A CONTAGEM DE JOGADORES entra por injeção desde 2026-08-26. Era `numPlayers`, um `let` de `core/state`
15
54
  // importado como binding vivo — e um `let` de módulo é compartilhado por qualquer segundo jogo que a
16
55
  // mesma página carregue (D13 do `demos`, ADR-0038). O que entra aqui é o GETTER da rodada que a raiz
@@ -18,8 +57,23 @@ import { crtScanVars } from '../render/crt.js';
18
57
  let _numJogadores = () => 1;
19
58
  /** Liga a contagem de jogadores. Chamado uma vez pela raiz, antes do primeiro `layout()`. */
20
59
  export function initLayout(deps) { _numJogadores = deps.numJogadores; }
60
+ /**
61
+ * A CASCA QUE DÁ O ESPAÇO DISPONÍVEL — por id OU por classe, e as duas formas valem o mesmo.
62
+ *
63
+ * ⚠️ ERA SÓ `#stage-wrap`, E ISSO DEIXOU A ENGINE SEM ESCALA NO PRÓPRIO HOST. O `app/index.html` do jogo
64
+ * trazia `<div id="stage-wrap">`; quando o cartucho saiu (issue #111) sobrou o `app/quiz.html`, que tem
65
+ * `<div class="stage-wrap">`. A procura por id falhava, `layout()` fazia early-return, e **nada reportava
66
+ * nada**: um `return` silencioso é indistinguível de «não havia o que fazer». O comentário do próprio
67
+ * `quiz.html` dizia «mesmo id que ui/layout escala» — quem o escreveu acreditava que corria.
68
+ *
69
+ * O consumidor não erra ao usar a classe: um documento pode ter várias telas, e um id é único. Aceitar as
70
+ * duas é o que torna a engine consumível por quem não copiou o markup dela.
71
+ */
72
+ function cascaDoPalco() {
73
+ return $('#stage-wrap') ?? $('.stage-wrap');
74
+ }
21
75
  export function layout() {
22
- const wrap = $('#stage-wrap');
76
+ const wrap = cascaDoPalco();
23
77
  if (!wrap)
24
78
  return;
25
79
  wrap.style.paddingRight = '0px';
@@ -47,6 +101,15 @@ export function layout() {
47
101
  gr.style.setProperty('--hud-fs', Math.max(9, Math.round(180 * k * 0.052)) + 'px');
48
102
  gr.style.setProperty('--ui-fs', (8 * k) + 'px'); // base LÓGICA 8px × k (16px em k=2)
49
103
  gr.style.setProperty('--tap', (22 * k) + 'px'); // toque 22px × k (44px em k=2, piso WCAG)
104
+ // ⚠️ O PISO DA RÉGUA (ADR-0095), e ele é OUTRA COISA que o `--tap`. O `--tap` é o tamanho PREFERIDO e
105
+ // cresce com a escala do canvas; `--alvo-min` é o CHÃO por altura de tela — 24 px abaixo de 540, 34 a
106
+ // partir de 540, 44 a partir de 720. Um botão isolado usa o preferido; um item de LISTA, que tem de
107
+ // caber inteiro na tela, usa o chão.
108
+ //
109
+ // ⚠️ A ALTURA É A DO ESPAÇO DISPONÍVEL, e não a da sub-tela de um jogador: o dedo toca o aparelho, não
110
+ // o viewport lógico. Em quatro telas divididas cada uma tem 180 px de alto, e encolher o alvo por causa
111
+ // disso seria ler o número errado — o aparelho continua o mesmo.
112
+ gr.style.setProperty('--alvo-min', alvoMinimoDeToque(availH) + 'px');
50
113
  }
51
114
  crtScanVars(); // scanlines re-alinham quando a escala k muda
52
115
  if (/[?&]debug=true/.test(location.search))
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { PlayerView } from '../core/entity.js';
3
+ /** As quatro animações de CENA, como vocabulário fechado. */
4
+ export type MotionSceneKey = 'parallax' | 'decor' | 'items' | 'particles';
5
+ /** As três do PERSONAGEM, que são campos do jogador. */
6
+ export type MotionCharProp = 'rmWalk' | 'rmBreath' | 'rmFlavor';
7
+ /** Uma animação do personagem e a chave i18n do seu rótulo. */
8
+ export interface MotionCharDef {
9
+ readonly prop: MotionCharProp;
10
+ readonly lbl: string;
11
+ }
12
+ /** ⚠️ `PlayerView` e não `Record`: um `Record` aceita qualquer objecto com essas chaves, jogador ou não. */
13
+ export type MotionPlayer = PlayerView<'rmWalk' | 'rmBreath' | 'rmFlavor'>;
14
+ /** Os quatro interruptores de cena. Objecto VIVO — ver a nota sobre partilha por referência no topo. */
15
+ export type MotionSceneFlags = Record<MotionSceneKey, boolean>;
16
+ /** As quatro animações de CENA. São a união inteira, e o compilador prova-o logo abaixo. */
17
+ export declare const CHAVES_DE_CENA: readonly ["parallax", "decor", "items", "particles"];
18
+ /** As três animações do PERSONAGEM, com as chaves que o `RM_LABEL` desta mesma camada já traduz. */
19
+ export declare const ANIMACOES_DO_PERSONAGEM: readonly [{
20
+ readonly prop: "rmWalk";
21
+ readonly lbl: "rm.walk";
22
+ }, {
23
+ readonly prop: "rmBreath";
24
+ readonly lbl: "rm.breath";
25
+ }, {
26
+ readonly prop: "rmFlavor";
27
+ readonly lbl: "rm.flavor";
28
+ }];
29
+ /** Os quatro interruptores no padrão do sistema — `prefers-reduced-motion`, por `defaultReducedMotion()`. */
30
+ export declare function padraoDeCena(): MotionSceneFlags;
31
+ /**
32
+ * O estado guardado, ou o padrão do sistema quando não há nada guardado.
33
+ *
34
+ * ⚠️ CADA CHAVE É LIDA UMA A UMA, e não `{...guardado}`. O que está no armazenamento veio do navegador de uma
35
+ * criança e pode estar truncado ou de uma versão anterior: espalhar o objecto traria chaves a mais e deixaria
36
+ * chaves a menos por preencher, e uma chave em falta lê-se como `undefined` — que é «não reduzido» para quem
37
+ * pediu redução. O laço garante exactamente as quatro.
38
+ */
39
+ export declare function lerCenaGuardada(): MotionSceneFlags;
40
+ /** Guarda os quatro interruptores. Chamada depois de cada mudança, como o `saveRM` do cartucho fazia. */
41
+ export declare function guardarCena(rm: MotionSceneFlags): void;
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/motion-scene — O MOVIMENTO REDUZIDO DE CENA VOLTA PARA A ENGINE (ADR-0106 §4, etapa 1).
3
+ //
4
+ // ========================= O QUE ESTAVA ERRADO, MEDIDO E NÃO SUPOSTO =========================
5
+ // O `PauseIconsCtx` e o `SettingsMotionCtx` pediam ao JOGO quatro coisas — `rm`, `rmKeys`, `rmChar`,
6
+ // `saveRM` — e o ADR-0106 chamou-lhes «do jogo POR ACIDENTE». A medição de 2026-09-08 no `game-platformer`
7
+ // mostra que a palavra é exacta, porque nenhuma das quatro contém uma escolha do jogo:
8
+ //
9
+ // · `RM_KEYS` era `['parallax','decor','items','particles']` escrito à mão — que é a união `MotionSceneKey`
10
+ // INTEIRA, declarada na engine. Não é «quais destes este jogo tem»: são os quatro, sempre;
11
+ // · `RM_CHAR` era as três propriedades de `MotionCharProp` com as chaves i18n `rm.walk`/`rm.breath`/
12
+ // `rm.flavor` — e o `RM_LABEL` da engine já traduz essas mesmas chaves;
13
+ // · `rm` era lido de `store.KEYS.reducedMotion` (chave da engine) com `defaultReducedMotion()` (padrão da
14
+ // engine);
15
+ // · `saveRM` era `store.setJSON` para a mesma chave da engine.
16
+ //
17
+ // ⚠️ ERA UMA CÓPIA, NÃO UMA DECISÃO. E o custo não é elegância: **cinco jogos não têm nada disto**, porque
18
+ // cada cartucho tinha de se lembrar de escrever as quatro linhas. Uma criança que precisa de parar o
19
+ // movimento da cena abre esses cinco e não tem por onde.
20
+ //
21
+ // ⚠️ O QUE CONTINUA A SER DO JOGO, e por natureza: o EFEITO. Quem lê `rm.decor` para congelar as nuvens é o
22
+ // jogo — a engine possui o interruptor, não o que ele apaga. É a mesma divisão do ADR-0106: o valor é da
23
+ // engine, o efeito colateral é do cartucho.
24
+ //
25
+ // ⚠️ E O OBJECTO É PARTILHADO POR REFERÊNCIA, de propósito. No cartucho ele entra em oito módulos
26
+ // (`weather`, `life`, `fx`, …) que leem `rm.decor`/`rm.particles` a cada quadro. Devolver uma cópia faria
27
+ // cada leitor ver um valor congelado no arranque, e o interruptor deixaria de fazer nada — em silêncio.
28
+ // ⚠️ O VOCABULÁRIO DESCEU PARA CÁ, e foi um gate que o mandou. Escrito ao contrário — os tipos no
29
+ // `settings-motion` e este módulo a importá-los —, o `tests/lotes-passo5` reprovou por CICLO: o painel importa
30
+ // os valores daqui e este importava os tipos de lá. O analisador conta o `import type` como aresta, e tem
31
+ // razão para o que mede (ordem de extração). A saída certa não era calar o gate: o vocabulário pertence a quem
32
+ // possui os VALORES, e o painel é consumidor dele. O `settings-motion` mantém os nomes publicados por alias,
33
+ // então nenhuma linha de importação de nenhum consumidor muda.
34
+ import * as store from '../platform/storage.js';
35
+ import { defaultReducedMotion } from '../core/state.js';
36
+ /** As quatro animações de CENA. São a união inteira, e o compilador prova-o logo abaixo. */
37
+ export const CHAVES_DE_CENA = ['parallax', 'decor', 'items', 'particles'];
38
+ const _COBRE_A_UNIAO = true;
39
+ void _COBRE_A_UNIAO;
40
+ /** As três animações do PERSONAGEM, com as chaves que o `RM_LABEL` desta mesma camada já traduz. */
41
+ export const ANIMACOES_DO_PERSONAGEM = Object.freeze([
42
+ { prop: 'rmWalk', lbl: 'rm.walk' },
43
+ { prop: 'rmBreath', lbl: 'rm.breath' },
44
+ { prop: 'rmFlavor', lbl: 'rm.flavor' },
45
+ ]);
46
+ const _COBRE_O_PERSONAGEM = true;
47
+ void _COBRE_O_PERSONAGEM;
48
+ /** Os quatro interruptores no padrão do sistema — `prefers-reduced-motion`, por `defaultReducedMotion()`. */
49
+ export function padraoDeCena() {
50
+ const o = {};
51
+ const padrao = defaultReducedMotion();
52
+ for (const k of CHAVES_DE_CENA)
53
+ o[k] = padrao;
54
+ return o;
55
+ }
56
+ /**
57
+ * O estado guardado, ou o padrão do sistema quando não há nada guardado.
58
+ *
59
+ * ⚠️ CADA CHAVE É LIDA UMA A UMA, e não `{...guardado}`. O que está no armazenamento veio do navegador de uma
60
+ * criança e pode estar truncado ou de uma versão anterior: espalhar o objecto traria chaves a mais e deixaria
61
+ * chaves a menos por preencher, e uma chave em falta lê-se como `undefined` — que é «não reduzido» para quem
62
+ * pediu redução. O laço garante exactamente as quatro.
63
+ */
64
+ export function lerCenaGuardada() {
65
+ const guardado = store.getJSON(store.KEYS.reducedMotion, null);
66
+ if (!guardado || typeof guardado !== 'object')
67
+ return padraoDeCena();
68
+ const o = {};
69
+ for (const k of CHAVES_DE_CENA)
70
+ o[k] = !!guardado[k];
71
+ return o;
72
+ }
73
+ /** Guarda os quatro interruptores. Chamada depois de cada mudança, como o `saveRM` do cartucho fazia. */
74
+ export function guardarCena(rm) {
75
+ store.setJSON(store.KEYS.reducedMotion, rm);
76
+ }
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** As três coisas do `document` de que a casca precisa. Mesma forma de `ui/loop-crash`. */
3
+ export interface PanelShellCtx {
4
+ /** `document.querySelector`, injetado — a casca nunca alcança o `document` global. */
5
+ procurar: (sel: string) => HTMLElement | null;
6
+ /** `document.createElement`, injetado. */
7
+ criar: (tag: string) => HTMLElement;
8
+ }
9
+ export interface PanelShellSpec {
10
+ /** O id do painel: `typo`, `audio`, `visual`… Gera `#X`, `#X-title`, `#X-list`, `#X-reset`, `#X-close`. */
11
+ id: string;
12
+ /** O título, JÁ TRADUZIDO. Vai por `textContent`. */
13
+ titulo: string;
14
+ /** O `aria-label` da lista, já traduzido — o nome do grupo que a criança ouve ao entrar nele. */
15
+ rotuloDaLista: string;
16
+ /** Os rótulos dos dois botões, já traduzidos. */
17
+ rotuloReset: string;
18
+ rotuloFechar: string;
19
+ /**
20
+ * A introdução do painel, já traduzida. Vira o texto de REPOUSO do rodapé, via `data-explain-idle`.
21
+ *
22
+ * ⚠️ É O ÚNICO CAMINHO QUE ESTA CASCA OFERECE PARA UMA INTRODUÇÃO, e é o ponto da issue #62: não há por
23
+ * onde passar um parágrafo de prosa para o topo do cartão. Ausente = o painel não tem introdução, que é
24
+ * uma resposta legítima e não uma omissão.
25
+ */
26
+ introducao?: string;
27
+ }
28
+ /** O que a casca devolve: o nó e os ids que ela criou, para o painel não os adivinhar. */
29
+ export interface PanelShell {
30
+ overlay: HTMLElement;
31
+ card: HTMLElement;
32
+ lista: HTMLElement;
33
+ reset: HTMLElement;
34
+ fechar: HTMLElement;
35
+ /** Os cinco selectores que este painel passa a garantir. É o contrato, agora dito em vez de descoberto. */
36
+ ids: {
37
+ overlay: string;
38
+ title: string;
39
+ lista: string;
40
+ reset: string;
41
+ fechar: string;
42
+ };
43
+ }
44
+ /** Os ids que um painel de `id` ocupa. Exportado porque um gate e um consumidor precisam de os nomear. */
45
+ export declare function idsDaCasca(id: string): PanelShell['ids'];
46
+ /**
47
+ * Monta (ou reaproveita) a casca do painel `spec.id` e devolve as suas partes.
48
+ *
49
+ * IDEMPOTENTE: se já existir um `#id`, ele é reutilizado e o conteúdo do cartão é reconstruído. Um painel que
50
+ * a raiz monte duas vezes não pode acabar com dois véus — e a raiz monta mais do que uma vez, porque a
51
+ * contagem de jogadores muda a grade de telas.
52
+ */
53
+ export declare function montarCasca(ctx: PanelShellCtx, spec: PanelShellSpec): PanelShell;
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/panel-shell — A CASCA DE UM PAINEL DE AJUSTES, construída em vez de exigida.
3
+ //
4
+ // ========================= O ACHADO QUE ISTO CONSERTA =========================
5
+ // O segundo consumidor (a etapa C da issue #63) mediu-o e escreveu-o no achado 6:
6
+ //
7
+ // «O CONTRATO DE MARKUP É INVISÍVEL. O ctx do painel pede `$` e `store`; o que ele REALMENTE exige é que
8
+ // o documento do consumidor contenha `#typo`, `#typo-list`, `#typo-preview`, `#typo-close` e
9
+ // `#typo-reset`. Nada no tipo diz isso — descobre-se por tentativa, e o modo de falhar é o pior
10
+ // possível: o painel abre VAZIO, sem erro.»
11
+ //
12
+ // Cada `ui/settings-*.ts` preenche o INTERIOR do seu painel; o EXTERIOR — o véu, o cartão, o título, o
13
+ // rodapé de ações e o botão de restaurar — vinha do `app/index.html`, que saiu com o cartucho (#111). Desde
14
+ // então a engine EXIGE cinco ids por painel e não os declara em lado nenhum.
15
+ //
16
+ // ⚠️ E A REGRA DE MENU DO `CLAUDE.md` §4 DEPENDIA DE ALGUÉM SE LEMBRAR DELA. A introdução de um painel vai no
17
+ // `data-explain-idle` do cartão — nunca num `<p>` de prosa no topo —, porque um menu que explica item a item
18
+ // obriga a criança a LER TUDO para achar o que procura. A issue #62 mandava editar seis blocos de markup para
19
+ // isso; o markup saiu, e a regra ficou sem alvo. Aqui ela deixa de ser lembrete e passa a ser construção: a
20
+ // casca não tem por onde receber um `<p>` no topo.
21
+ //
22
+ // ========================= A FORMA, QUE FOI MEDIDA E NÃO INVENTADA =========================
23
+ // É a do painel de tipografia do `app/quiz.html`, que é o único que sobrou e o que o segundo consumidor
24
+ // exercitou de facto:
25
+ //
26
+ // <div id="X" class="overlay" hidden>
27
+ // <div class="overlay__card" role="dialog" aria-modal="true" aria-labelledby="X-title" [data-explain-idle]>
28
+ // <h2 id="X-title">…</h2>
29
+ // <div id="X-list" class="ctrl-list" role="group" aria-label="…"></div> ← o interior, do settings-*
30
+ // <div class="overlay__actions">
31
+ // <button id="X-reset">…</button> <button id="X-close">…</button>
32
+ // </div>
33
+ // </div>
34
+ // </div>
35
+ //
36
+ // ⚠️ O RODAPÉ `.opt-explain` NÃO É CRIADO AQUI, e isso é deliberado: `ui/settings-panel.fillExplain` cria-o
37
+ // quando move a primeira dica para lá. Criá-lo vazio aqui daria uma região `aria-live` que anuncia nada, e
38
+ // duas mãos a criar o mesmo nó é como ele acabaria duplicado.
39
+ //
40
+ // Sem `innerHTML`: tudo por `criar` + `textContent`, no molde de `ui/loop-crash` e `ui/focus-trap`. O título
41
+ // e o rótulo da lista vêm do CHAMADOR já resolvidos — este módulo não traduz, para poder ser exercitado sem
42
+ // dicionário.
43
+ //
44
+ // ⚠️ E NÃO IMPORTA NADA. Uma casca que não usa `innerHTML` também não precisa de escapar texto: `textContent`
45
+ // escapa por construção. Chegou a haver aqui um `escaparHtml` importado «por conveniência» — que é como um
46
+ // módulo-folha deixa de o ser, e como um leitor futuro passa a procurar a interpolação que não existe.
47
+ /** Os ids que um painel de `id` ocupa. Exportado porque um gate e um consumidor precisam de os nomear. */
48
+ export function idsDaCasca(id) {
49
+ return { overlay: id, title: `${id}-title`, lista: `${id}-list`, reset: `${id}-reset`, fechar: `${id}-close` };
50
+ }
51
+ /**
52
+ * Monta (ou reaproveita) a casca do painel `spec.id` e devolve as suas partes.
53
+ *
54
+ * IDEMPOTENTE: se já existir um `#id`, ele é reutilizado e o conteúdo do cartão é reconstruído. Um painel que
55
+ * a raiz monte duas vezes não pode acabar com dois véus — e a raiz monta mais do que uma vez, porque a
56
+ * contagem de jogadores muda a grade de telas.
57
+ */
58
+ export function montarCasca(ctx, spec) {
59
+ const ids = idsDaCasca(spec.id);
60
+ const overlay = ctx.procurar('#' + ids.overlay) ?? ctx.criar('div');
61
+ overlay.id = ids.overlay;
62
+ overlay.className = 'overlay';
63
+ overlay.hidden = true;
64
+ while (overlay.firstChild)
65
+ overlay.removeChild(overlay.firstChild);
66
+ const card = ctx.criar('div');
67
+ card.className = 'overlay__card';
68
+ card.setAttribute('role', 'dialog');
69
+ card.setAttribute('aria-modal', 'true');
70
+ card.setAttribute('aria-labelledby', ids.title);
71
+ // A introdução do painel é o texto de REPOUSO do rodapé (CLAUDE.md §4), nunca um `<p>` no topo.
72
+ if (spec.introducao)
73
+ card.setAttribute('data-explain-idle', spec.introducao);
74
+ const h2 = ctx.criar('h2');
75
+ h2.id = ids.title;
76
+ h2.textContent = spec.titulo;
77
+ card.appendChild(h2);
78
+ const lista = ctx.criar('div');
79
+ lista.id = ids.lista;
80
+ lista.className = 'ctrl-list';
81
+ lista.setAttribute('role', 'group');
82
+ lista.setAttribute('aria-label', spec.rotuloDaLista);
83
+ card.appendChild(lista);
84
+ const acoes = ctx.criar('div');
85
+ acoes.className = 'overlay__actions';
86
+ const reset = botao(ctx, ids.reset, spec.rotuloReset, 'mode-btn');
87
+ const fechar = botao(ctx, ids.fechar, spec.rotuloFechar, 'mode-btn is-on');
88
+ acoes.appendChild(reset);
89
+ acoes.appendChild(fechar);
90
+ card.appendChild(acoes);
91
+ overlay.appendChild(card);
92
+ return { overlay, card, lista, reset, fechar, ids };
93
+ }
94
+ function botao(ctx, id, rotulo, classe) {
95
+ const b = ctx.criar('button');
96
+ b.id = id;
97
+ b.className = classe;
98
+ b.setAttribute('type', 'button');
99
+ // `textContent` e não `innerHTML`: um rótulo traduzido é dado de fora como qualquer outro, e um dicionário
100
+ // de consumidor pode trazer o que quiser dentro dele.
101
+ b.textContent = rotulo;
102
+ return b;
103
+ }
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  import type { PlayerView } from '../core/entity.js';
3
3
  import type { NavKeys } from '../input/edges.js';
4
+ import { type Tema, type Correcao, type VisualState } from '../render/viz-axes.js';
4
5
  import type { MotionSceneFlags, MotionSceneKey, MotionCharDef } from './settings-motion.js';
5
6
  import type { AudioCatState } from './settings-audio.js';
6
7
  /**
@@ -32,6 +33,11 @@ export declare const PAUSE_ICONS: readonly PauseIcon[];
32
33
  export declare function pauseIcon(k: string): PauseIcon | undefined;
33
34
  /** TEA cycle: 0 = normal · 1 = calmo (reduces) · 2 = silencioso (switches off). Never touches TTS/blind mode. */
34
35
  export declare const CALM_NAMES: readonly string[];
36
+ /**
37
+ * O nível TEA guardado, saneado. Fora de 0..2 devolve o padrão — dado do navegador é dado de fora, e um
38
+ * nível inventado escolheria `CALM_NAMES[3]`, que é `undefined`, e o anúncio ao leitor de tela sairia vazio.
39
+ */
40
+ export declare function saneiaNivelTea(bruto: number): number;
35
41
  /** The audio categories `applyCalm` governs. TTS/sonar/guarda/guia stay untouched — a calm player still needs them. */
36
42
  export declare const CALM_AUDIO_CATS: readonly string[];
37
43
  /** Colour-vision-deficiency cycle, in `player.viz` values. */
@@ -62,7 +68,7 @@ export declare const CVD_LABELS: Readonly<Record<string, string>>;
62
68
  * tipar `core/state.players` como `Player[]`: o compilador recusou converter um `Player` — que não tem
63
69
  * assinatura de índice — para ela, e essa recusa é a informação.
64
70
  */
65
- export type PausePlayer = PlayerView<'viz' | 'toggleMove' | 'audioSink' | 'rmWalk' | 'rmBreath' | 'rmFlavor'>;
71
+ export type PausePlayer = PlayerView<'visual' | 'toggleMove' | 'walkDir' | 'audioSink' | 'rmWalk' | 'rmBreath' | 'rmFlavor'>;
66
72
  /** One `.pm-btn` descriptor — the shape of game.js's PM_BTNS (owned by ui/activities-menu). */
67
73
  export interface PauseMenuButton {
68
74
  act: string;
@@ -81,10 +87,32 @@ export interface IconStateSnapshot {
81
87
  librasOn: boolean;
82
88
  calmMode: number;
83
89
  toggleMove: boolean;
84
- /** `player.viz` — shared by the contrast and CVD icons (they overwrite each other; that is by design). */
85
- viz: string;
90
+ /**
91
+ * O estado visual em DOIS EIXOS (#104).
92
+ *
93
+ * ⚠️ ERA `viz: string`, E O COMENTÁRIO DIZIA: «shared by the contrast and CVD icons (they overwrite each
94
+ * other; **that is by design**)». Não era desenho — era o campo único a impor-se, e a frase é o defeito
95
+ * escrito como se fosse decisão. Os dois ícones sempre ciclaram DENTRO do seu eixo (`nextContrast` e
96
+ * `nextCvd` existem desde sempre, separados); só não tinham onde guardar o resultado sem apagar o vizinho.
97
+ */
98
+ visual: VisualState;
86
99
  /** False disables the blind/TTS icons: those need an audio output nobody else is listening to. */
87
100
  privateOutput: boolean;
101
+ /**
102
+ * O aparelho em uso EXIGE a alternância? (ADR-0113 cláusula 3.)
103
+ *
104
+ * ⚠️ Em olhos, rosto, gestos e fala ela é o que faz a entrada funcionar, logo não há escolha a oferecer.
105
+ * O ícone fica desabilitado COM MOTIVO, como o painel — e não pode divergir dele: as duas superfícies
106
+ * escrevem o mesmo valor, e uma que aceitasse o clique enquanto a outra recusa deixaria a criança com
107
+ * dois botões que discordam sobre o mesmo ajuste.
108
+ *
109
+ * 🔴 OPCIONAL PORQUE O GATE DA FORMA ME APANHOU: eu escrevi-o obrigatório, e o
110
+ * `tests/superficie-publica` reprovou com «ENTROU como obrigatório» — que é uma MUDANÇA QUEBRANTE do
111
+ * pacote, porque quem constrói este snapshot passa a ter de o preencher. Foi para isto que o gate da
112
+ * FORMA (`2f2582f`) foi escrito, e é a primeira vez que ele apanha um campo A ENTRAR e não a sair.
113
+ * Ausente significa «ninguém me disse», que degrada para «não exijo» — o comportamento de hoje.
114
+ */
115
+ alternanciaExigida?: boolean;
88
116
  }
89
117
  /** A player has private output when nobody else is on the same sink. Single screen ⇒ always private.
90
118
  * Pure form of game.js's hasPrivateOutput (see the report: it had no other caller left). */
@@ -135,9 +163,61 @@ export declare const ICON_STATE_CLASSES: readonly string[];
135
163
  * reflected. `ic.n` is an i18n key, so it must be resolved here too — the markup is rendered once at build
136
164
  * time and would otherwise ship the raw key to a screen reader. */
137
165
  export declare function iconBtnMarkup(ic: PauseIcon): string;
166
+ /**
167
+ * OS ÍCONES QUE ESTE JOGO CONSEGUE MESMO ACCIONAR (ADR-0106 §5).
168
+ *
169
+ * ⚠️ NENHUMA ETAPA PODE ENTREGAR BOTÃO MORTO, e o registo diz porquê com todas as letras: «uma barra que
170
+ * oferece a uma criança um caminho e depois o recusa é pior do que uma barra que ela vê que não está lá,
171
+ * porque a primeira ensina-lhe que o caminho não é para ela».
172
+ *
173
+ * ⚠️ E O CONTRASTE E A COR SÃO O CASO REAL, medido em 2026-09-08 e diferente dos outros seis campos
174
+ * «acidentais»: eles não são estado que um cartucho calhou de guardar — precisam de um `render/viz-setters`,
175
+ * cujo contexto tem **34 campos** do grafo de render de UM jogo (`parallaxLayers`, `decoSprites`,
176
+ * `getPowerups`, `rebuildCoins`, `worldSprite`…). O `createGame` não monta isso, e um quiz não tem nada
177
+ * disso para montar. Então aqui a engine não pode oferecer um padrão — o que ela pode é **não fingir**.
178
+ *
179
+ * ⚠️ Isto NÃO é o mesmo que `soon`. `soon` é «ainda não construímos»; isto é «este jogo não tem por onde», e
180
+ * um botão que anuncia «em breve» diria a coisa errada.
181
+ */
182
+ export interface EscritoresVisuais {
183
+ /** Há quem escreva o TEMA (o alto contraste)? Sem ele, o ícone `contrast` não é montado. */
184
+ readonly tema: boolean;
185
+ /** Há quem escreva a CORREÇÃO de cor? Sem ela, o ícone `cvd` não é montado. */
186
+ readonly correcao: boolean;
187
+ }
188
+ export declare function iconesQueAccionam(escritores: EscritoresVisuais): readonly PauseIcon[];
189
+ /**
190
+ * OS TRÊS ITENS QUE A ENGINE ACCIONA SOZINHA, e que por isso nunca dependem do `getPauseActs` de um jogo.
191
+ *
192
+ * 📏 Lidos do despacho, e não decididos aqui: `options`/`pmback` trocam qual lista está no cartão e
193
+ * `acessibilidade` leva o cursor à barra rápida — os três são tratados neste módulo e voltam antes de a
194
+ * tabela do jogo ser consultada.
195
+ */
196
+ export declare const ITENS_DA_ENGINE: ReadonlySet<string>;
197
+ /**
198
+ * OS ITENS DO MENU QUE ESTE JOGO CONSEGUE MESMO ACCIONAR (ADR-0106 §5).
199
+ *
200
+ * ⚠️ HOJE UM ITEM SEM ACÇÃO É UM BOTÃO MORTO, E EM SILÊNCIO. O despacho faz `const fn = acts[act]; if (fn)
201
+ * fn();` — quem carrega num item que o jogo não implementou não recebe erro, não recebe anúncio, não recebe
202
+ * nada. Para quem vê, parece que o clique falhou; para quem navega por leitor de tela, o menu leu-lhe um
203
+ * item que não existe. É exactamente o que o §5 chama de pior do que a ausência: «uma barra que oferece um
204
+ * caminho e depois o recusa ensina-lhe que o caminho não é para ela».
205
+ */
206
+ export declare function itensQueAccionam(botoes: readonly PauseMenuButton[], acts: Record<string, (() => void) | undefined>): readonly PauseMenuButton[];
207
+ /**
208
+ * A LISTA RAIZ, com uma regra a mais: `options` é uma PORTA, e uma porta para uma sala vazia também é um
209
+ * botão morto.
210
+ *
211
+ * ⚠️ Esta é a parte que um filtro item-a-item não apanha. Se todos os painéis de ajuste forem filtrados —
212
+ * um jogo que não monta nenhum —, o item `options` sobrevive (a engine acciona-o) e abre uma lista sem nada.
213
+ * A criança atravessa uma porta e fica presa num submenu vazio, cuja única saída é o `pmback` que também
214
+ * sumiu com ele.
215
+ */
216
+ export declare function raizQueAcciona(raiz: readonly PauseMenuButton[], opcoes: readonly PauseMenuButton[], acts: Record<string, (() => void) | undefined>): readonly PauseMenuButton[];
138
217
  /** The whole icon bar. Used by the pause screen AND by the splash `#title-icons` (which built the same string
139
- * by hand in game.js — that duplication dies with this export). */
140
- export declare function iconsMarkup(): string;
218
+ * by hand in game.js — that duplication dies with this export).
219
+ * O parâmetro é ADITIVO e o padrão é a lista inteira: quem já chamava sem argumentos não muda de resultado. */
220
+ export declare function iconsMarkup(icones?: readonly PauseIcon[]): string;
141
221
  /** One `.pm-btn`. Dynamic labels (`letra`/`nivel`) are rendered eagerly and carry NO `data-i18n`, so
142
222
  * i18n.applyDom() cannot overwrite them. */
143
223
  /**
@@ -194,7 +274,7 @@ export declare function mostrarSubmenuDaPausa(sp: HTMLElement, sub: PauseSub): H
194
274
  * A LEGENDA VIAJA JUNTO. Ela é a dica que substitui, para quem não vê, o `title` que só o mouse revela;
195
275
  * deixá-la no cartão tornaria a barra do HUD muda.
196
276
  */
197
- export declare function quickBarMarkup(): string;
277
+ export declare function quickBarMarkup(icones?: readonly PauseIcon[]): string;
198
278
  /**
199
279
  * O QUE UMA INTENÇÃO SIGNIFICA DENTRO DO MODO `accessibility` (ADR-0044, item 7).
200
280
  *
@@ -228,6 +308,21 @@ export interface ScreenPauseMarkupOpts {
228
308
  /** The full innerHTML of a `.screen-pause`. Pure — every input is a parameter. */
229
309
  export declare function screenPauseMarkup(o: ScreenPauseMarkupOpts): string;
230
310
  export interface PauseIconsCtx {
311
+ /**
312
+ * O DOCUMENTO onde a pausa e a barra são CONSTRUÍDAS. Ausente, vale o global.
313
+ *
314
+ * ⚠️ ISTO É O ACHADO 15 OUTRA VEZ, e o `boot/create-game` já o descreve no próprio cabeçalho: «`initI18n()`
315
+ * chamava `applyDom(document)`, o GLOBAL, por baixo de quem a chamasse. Num navegador dá no mesmo e por
316
+ * isso sobreviveu; num teste de lógica pura é a diferença entre bootar e não bootar, e num futuro com dois
317
+ * documentos (uma engine em iframe, um editor ao lado do jogo) seria a diferença entre traduzir o documento
318
+ * certo e o outro.»
319
+ *
320
+ * 📏 Medido em 2026-09-08: as duas metades que constroem DOM (`buildScreenPause`, `buildQuickBar`) faziam
321
+ * `document.createElement` no global. Enquanto a raiz de composição de cada jogo era um `main.ts` a correr
322
+ * num navegador, dava no mesmo — e foi por isso que sobreviveu. Deixa de dar assim que a ENGINE monta,
323
+ * porque o `createGame` recebe o documento por `host.doc` e pode estar a montar noutro.
324
+ */
325
+ doc?: Document;
231
326
  /** Quantos jogadores/telas. Estado de RODADA (ADR-0038): vem da instância que a raiz possui.
232
327
  * Era `numPlayers`, um `let` de `core/state` importado como binding vivo — e um `let` de módulo
233
328
  * é compartilhado por qualquer segundo jogo que a mesma página carregue (D13 do `demos`). */
@@ -248,9 +343,9 @@ export interface PauseIconsCtx {
248
343
  */
249
344
  getA11yBars: () => readonly HTMLElement[];
250
345
  /** PM_OPTIONS_BTNS — o submenu de opções. Mesma dona, mesmo motivo: ninguém tem duas cópias de uma lista. */
251
- optionsButtons: readonly PauseMenuButton[];
346
+ optionsButtons?: readonly PauseMenuButton[];
252
347
  /** PM_BTNS — the `.pm-btn` list. Owned by ui/activities-menu; injected, never copied. */
253
- pmButtons: readonly PauseMenuButton[];
348
+ pmButtons?: readonly PauseMenuButton[];
254
349
  /** QL_NAME — literacy-level names, for the (dormant) `nivel` button. Same owner as pmButtons. */
255
350
  /**
256
351
  * O RÓTULO de um botão dinâmico, pronto — ou `null` quando aquele botão não tem um (item 19).
@@ -259,16 +354,15 @@ export interface PauseIconsCtx {
259
354
  * frase. Um menu de pausa da ENGINE não sabe o que é nível de alfabetização, nem em que idioma dizê-lo.
260
355
  * Função e não valor, porque o rótulo muda em execução — de nível E de idioma.
261
356
  */
262
- dynLabel: (b: PauseMenuButton) => string | null;
357
+ /** O rótulo dinâmico, pronto — ou `null`. Ausente: nenhum botão deste jogo tem rótulo dinâmico. */
358
+ dynLabel?: (b: PauseMenuButton) => string | null;
263
359
  /** The `.pm-btn` action table. LAZY: `pauseActs` is a `const` declared far below the init site in game.js. */
264
- getPauseActs: () => Record<string, (() => void) | undefined>;
360
+ getPauseActs?: () => Record<string, (() => void) | undefined>;
265
361
  /** Records which player opened the menu. `pauseActor` itself stays in game.js — the gamepad, the keyboard
266
362
  * router, openHelp() and openOptions() all read it there. */
267
- setPauseActor: (i: number) => void;
268
- /** The live `vpPause` array (game.js rebuilds it on every buildGameHud). Getter, not the array. */
269
- getPauseScreens: () => readonly Element[];
363
+ setPauseActor?: (i: number) => void;
270
364
  getModoCego: () => boolean;
271
- setModoCego: (on: boolean) => void;
365
+ setModoCego?: (on: boolean) => void;
272
366
  /**
273
367
  * O mixer por categoria. NULO até `initAudioMixer()` — `platform/audio` o declara
274
368
  * `Record<string, CatState> | null` porque o import dele é PURO (não lê localStorage), e quem inicializa
@@ -286,18 +380,38 @@ export interface PauseIconsCtx {
286
380
  reflectTtsPanelEnabled: boolean;
287
381
  isLibrasOn: () => boolean;
288
382
  toggleLibras: () => void;
289
- rm: MotionSceneFlags;
290
- rmKeys: readonly MotionSceneKey[];
291
- rmChar: readonly MotionCharDef[];
292
- saveRM: () => void;
293
- setToggleMove: (i: number, on: boolean) => void;
294
- setPlayerViz: (i: number, mode: string) => void;
383
+ rm?: MotionSceneFlags;
384
+ rmKeys?: readonly MotionSceneKey[];
385
+ rmChar?: readonly MotionCharDef[];
386
+ saveRM?: () => void;
387
+ setToggleMove?: (i: number, on: boolean) => void;
388
+ /**
389
+ * QUAL APARELHO ESTE JOGADOR ESTÁ A USAR (ADR-0113).
390
+ *
391
+ * 📌 O ícone `altmove` desta barra é a OUTRA superfície que escreve a alternância — a mesma razão pela
392
+ * qual o escritor voltou para a engine (ADR-0106 §4). Sem este campo, ele escreveria só a chave antiga
393
+ * enquanto o painel escreve as duas, e as duas superfícies divergiriam em silêncio.
394
+ */
395
+ transporteEmUso?: (jogador: number) => string;
396
+ setPlayerViz?: (i: number, mode: string) => void;
397
+ /** Os escritores POR EIXO (#104): mexer no tema não apaga a correção, e vice-versa. */
398
+ setTemaDoJogador?: (i: number, tema: Tema) => void;
399
+ setCorrecaoDoJogador?: (i: number, correcao: Correcao) => void;
295
400
  }
296
401
  export interface PauseIconsApi {
297
402
  /** Builds one `.screen-pause` (hidden), wired for click + hover/focus caption. Caller appends it. */
298
403
  buildScreenPause: (i: number) => HTMLElement;
299
404
  /** Monta a BARRA RÁPIDA (`.screen-a11y`) da tela `i`, já fiada. Chamada por ui/hud.ts, uma por tela. */
300
405
  buildQuickBar: (i: number) => HTMLElement;
406
+ /**
407
+ * OS ÍCONES QUE ESTA INSTÂNCIA MONTA — já filtrados pelo §5 do ADR-0106.
408
+ *
409
+ * ⚠️ Existe para que quem monta a barra do TÍTULO não repita o filtro. A barra do título não pode usar
410
+ * `buildQuickBar` (ele põe `tabIndex = -1`, e o próprio comentário lá diz porquê: no título não se está a
411
+ * jogar), então ela chama `iconsMarkup` directamente — e sem este acessor teria de recalcular quais ícones
412
+ * accionam, que é uma segunda cópia da mesma decisão.
413
+ */
414
+ iconesMontados: readonly PauseIcon[];
301
415
  /** ENTRA no modo `accessibility` da tela `i` — é o que o item `acessibilidade` da pausa faz. */
302
416
  entrarNaBarra: (i: number) => void;
303
417
  /** SAI do modo e devolve o direcional ao personagem. */