@r1z4x/tezgah 1.9.2 → 1.11.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 (426) hide show
  1. package/CHANGELOG.md +83 -1
  2. package/MANIFEST +382 -144
  3. package/README.de.md +1 -1
  4. package/README.es.md +1 -1
  5. package/README.fr.md +1 -1
  6. package/README.md +1 -1
  7. package/README.tr.md +1 -1
  8. package/bin/codegen +5 -2
  9. package/bin/tezgah-design +288 -1
  10. package/bin/tezgah-docs +1 -1
  11. package/bin/tezgah-doctor +39 -2
  12. package/bin/tezgah-import-ai-research +26 -12
  13. package/bin/tezgah-ocr-review +296 -0
  14. package/bin/tezgah-research +216 -40
  15. package/bin/tezgah-review-quote +185 -0
  16. package/bin/tezgah-setup +136 -33
  17. package/bin/tezgah-skill +1 -1
  18. package/bin/tezgah-status +201 -3
  19. package/bin/tezgah-task +203 -3
  20. package/commands/adhd.md +2 -2
  21. package/commands/ponytail.md +4 -4
  22. package/docs/architecture.md +2 -2
  23. package/docs/citations-baseline.json +2 -2
  24. package/docs/clarity-baseline.json +2 -2
  25. package/docs/contract.md +87 -42
  26. package/docs/environment.md +2 -0
  27. package/docs/evidence.md +28 -9
  28. package/docs/feature-audit.md +2 -2
  29. package/docs/gate.md +54 -31
  30. package/docs/glossary.md +5 -2
  31. package/docs/hosts.md +17 -3
  32. package/docs/index.json +34 -8
  33. package/docs/judge.md +4 -2
  34. package/docs/layers.md +3 -3
  35. package/docs/models.md +31 -9
  36. package/docs/operations.md +126 -1
  37. package/docs/research.md +786 -5
  38. package/docs/skills.md +137 -13
  39. package/docs/status-line.md +115 -1
  40. package/hooks/hooks.json +1 -1
  41. package/hooks/lang/en.json +10 -0
  42. package/hooks/lang/tr.json +11 -1
  43. package/hooks/tezgah_agents.py +38 -22
  44. package/hooks/tezgah_apps.py +18 -0
  45. package/hooks/tezgah_components.py +4 -0
  46. package/hooks/tezgah_context.py +189 -50
  47. package/hooks/tezgah_extension.py +337 -0
  48. package/hooks/tezgah_gate.py +188 -56
  49. package/hooks/tezgah_integrity.py +652 -105
  50. package/hooks/tezgah_judge.py +6 -4
  51. package/hooks/tezgah_models.py +59 -19
  52. package/hooks/tezgah_orca.py +1 -1
  53. package/hooks/tezgah_paths.py +102 -6
  54. package/hooks/tezgah_policy.py +94 -45
  55. package/hooks/tezgah_rank.py +9 -6
  56. package/hooks/tezgah_replay.py +4 -4
  57. package/hooks/tezgah_research.py +1260 -65
  58. package/hooks/tezgah_research_capture.py +1544 -0
  59. package/hooks/tezgah_research_evidence.py +842 -0
  60. package/hooks/tezgah_research_stats.py +258 -0
  61. package/hooks/tezgah_skill_pick.py +408 -106
  62. package/hooks/tezgah_stopspec.py +8 -1
  63. package/hooks/tezgah_task.py +12 -4
  64. package/hooks/tezgah_taste_ledger.py +1 -1
  65. package/hosts/cursor/hook.py +29 -2
  66. package/hosts/opencode/plugins/tezgah.js +307 -46
  67. package/hosts/opencode/tui/cli/tui.tsx +100 -0
  68. package/hosts/opencode/tui/tezgah-tui.tsx +2 -1
  69. package/output-styles/tezgah.md +10 -10
  70. package/package.json +1 -1
  71. package/skills/ai-research/SKILL.md +21 -14
  72. package/skills/ai-research/SOURCE +7 -5
  73. package/skills/ai-research/index/1-frame.md +6 -6
  74. package/skills/ai-research/index/2-data.md +9 -9
  75. package/skills/ai-research/index/3-train.md +36 -36
  76. package/skills/ai-research/index/4-measure.md +13 -13
  77. package/skills/ai-research/index/5-run.md +11 -11
  78. package/skills/ai-research/index/6-write.md +23 -23
  79. package/skills/ai-research/library.json +104 -104
  80. package/skills/analyze-app/SKILL.md +17 -12
  81. package/skills/design-contract/SKILL.md +42 -7
  82. package/skills/design-contract/data-views.md +2 -2
  83. package/skills/design-library/EVALS.md +1 -1
  84. package/skills/design-library/INDEX.md +39 -39
  85. package/skills/design-library/SKILL.md +19 -13
  86. package/skills/design-library/SOURCE +45 -43
  87. package/skills/feature-audit/SKILL.md +20 -14
  88. package/skills/hallmark/LICENSE +21 -0
  89. package/skills/hallmark/SKILL.md +154 -0
  90. package/skills/hallmark/SOURCE +207 -0
  91. package/skills/hallmark/agents/openai.yaml +13 -0
  92. package/skills/hallmark/references/anatomy.md +71 -0
  93. package/skills/hallmark/references/brand-truth.md +40 -0
  94. package/skills/hallmark/references/comp.md +104 -0
  95. package/skills/hallmark/references/craft.md +116 -0
  96. package/skills/hallmark/references/dark-mode.md +49 -0
  97. package/skills/hallmark/references/data-viz.md +48 -0
  98. package/skills/hallmark/references/design-md.md +53 -0
  99. package/skills/hallmark/references/direction.md +52 -0
  100. package/skills/hallmark/references/export-formats.md +80 -0
  101. package/skills/hallmark/references/flow.md +131 -0
  102. package/skills/hallmark/references/fonts.md +58 -0
  103. package/skills/hallmark/references/gates.md +243 -0
  104. package/skills/hallmark/references/genres/atmospheric.md +24 -0
  105. package/skills/hallmark/references/genres/editorial.md +24 -0
  106. package/skills/hallmark/references/genres/modern-minimal.md +24 -0
  107. package/skills/hallmark/references/genres/playful.md +24 -0
  108. package/skills/hallmark/references/imagery.md +99 -0
  109. package/skills/hallmark/references/interaction-and-states.md +89 -0
  110. package/skills/hallmark/references/layout.md +104 -0
  111. package/skills/hallmark/references/macrostructures/01-bento-grid.md +21 -0
  112. package/skills/hallmark/references/macrostructures/02-long-document.md +35 -0
  113. package/skills/hallmark/references/macrostructures/03-marquee-hero.md +21 -0
  114. package/skills/hallmark/references/macrostructures/04-stat-led.md +21 -0
  115. package/skills/hallmark/references/macrostructures/05-workbench.md +21 -0
  116. package/skills/hallmark/references/macrostructures/06-conversational-faq.md +28 -0
  117. package/skills/hallmark/references/macrostructures/07-manifesto.md +28 -0
  118. package/skills/hallmark/references/macrostructures/08-photographic.md +21 -0
  119. package/skills/hallmark/references/macrostructures/09-quote-led.md +21 -0
  120. package/skills/hallmark/references/macrostructures/10-specimen.md +21 -0
  121. package/skills/hallmark/references/macrostructures/11-catalogue.md +21 -0
  122. package/skills/hallmark/references/macrostructures/12-letter.md +21 -0
  123. package/skills/hallmark/references/macrostructures/13-index-first.md +21 -0
  124. package/skills/hallmark/references/macrostructures/14-narrative-workflow.md +21 -0
  125. package/skills/hallmark/references/macrostructures/15-split-studio.md +21 -0
  126. package/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
  127. package/skills/hallmark/references/macrostructures/17-type-specimen.md +19 -0
  128. package/skills/hallmark/references/macrostructures/18-portfolio-grid.md +21 -0
  129. package/skills/hallmark/references/macrostructures/19-map-diagram.md +21 -0
  130. package/skills/hallmark/references/macrostructures/20-ecosystem-index.md +19 -0
  131. package/skills/hallmark/references/macrostructures/21-component-playground.md +21 -0
  132. package/skills/hallmark/references/macrostructures.md +119 -0
  133. package/skills/hallmark/references/microinteractions.md +80 -0
  134. package/skills/hallmark/references/patterns.md +202 -0
  135. package/skills/hallmark/references/redesign.md +88 -0
  136. package/skills/hallmark/references/responsive.md +46 -0
  137. package/skills/hallmark/references/review-finish.md +19 -0
  138. package/skills/hallmark/references/review-hero.md +20 -0
  139. package/skills/hallmark/references/scroll-choreography.md +110 -0
  140. package/skills/hallmark/references/texture.md +90 -0
  141. package/skills/hallmark/references/themes/README.md +58 -0
  142. package/skills/hallmark/references/themes/almanac.jpg +0 -0
  143. package/skills/hallmark/references/themes/almanac.md +103 -0
  144. package/skills/hallmark/references/themes/arcade.jpg +0 -0
  145. package/skills/hallmark/references/themes/arcade.md +102 -0
  146. package/skills/hallmark/references/themes/atelier.jpg +0 -0
  147. package/skills/hallmark/references/themes/atelier.md +98 -0
  148. package/skills/hallmark/references/themes/bloom.jpg +0 -0
  149. package/skills/hallmark/references/themes/bloom.md +102 -0
  150. package/skills/hallmark/references/themes/blueprint.jpg +0 -0
  151. package/skills/hallmark/references/themes/blueprint.md +103 -0
  152. package/skills/hallmark/references/themes/broadside.jpg +0 -0
  153. package/skills/hallmark/references/themes/broadside.md +108 -0
  154. package/skills/hallmark/references/themes/brutal.jpg +0 -0
  155. package/skills/hallmark/references/themes/brutal.md +95 -0
  156. package/skills/hallmark/references/themes/carnival.jpg +0 -0
  157. package/skills/hallmark/references/themes/carnival.md +104 -0
  158. package/skills/hallmark/references/themes/cobalt.jpg +0 -0
  159. package/skills/hallmark/references/themes/cobalt.md +101 -0
  160. package/skills/hallmark/references/themes/console.jpg +0 -0
  161. package/skills/hallmark/references/themes/console.md +99 -0
  162. package/skills/hallmark/references/themes/coral.jpg +0 -0
  163. package/skills/hallmark/references/themes/coral.md +102 -0
  164. package/skills/hallmark/references/themes/darkroom.jpg +0 -0
  165. package/skills/hallmark/references/themes/darkroom.md +99 -0
  166. package/skills/hallmark/references/themes/dose.jpg +0 -0
  167. package/skills/hallmark/references/themes/dose.md +98 -0
  168. package/skills/hallmark/references/themes/dusk.jpg +0 -0
  169. package/skills/hallmark/references/themes/dusk.md +105 -0
  170. package/skills/hallmark/references/themes/editorial.jpg +0 -0
  171. package/skills/hallmark/references/themes/editorial.md +97 -0
  172. package/skills/hallmark/references/themes/formwork.jpg +0 -0
  173. package/skills/hallmark/references/themes/formwork.md +107 -0
  174. package/skills/hallmark/references/themes/furrow.jpg +0 -0
  175. package/skills/hallmark/references/themes/furrow.md +98 -0
  176. package/skills/hallmark/references/themes/garden.jpg +0 -0
  177. package/skills/hallmark/references/themes/garden.md +97 -0
  178. package/skills/hallmark/references/themes/grid.jpg +0 -0
  179. package/skills/hallmark/references/themes/grid.md +101 -0
  180. package/skills/hallmark/references/themes/halftone.jpg +0 -0
  181. package/skills/hallmark/references/themes/halftone.md +106 -0
  182. package/skills/hallmark/references/themes/hedgerow.jpg +0 -0
  183. package/skills/hallmark/references/themes/hedgerow.md +104 -0
  184. package/skills/hallmark/references/themes/hum.jpg +0 -0
  185. package/skills/hallmark/references/themes/hum.md +99 -0
  186. package/skills/hallmark/references/themes/index.json +2191 -0
  187. package/skills/hallmark/references/themes/ink.jpg +0 -0
  188. package/skills/hallmark/references/themes/ink.md +106 -0
  189. package/skills/hallmark/references/themes/kiln.jpg +0 -0
  190. package/skills/hallmark/references/themes/kiln.md +108 -0
  191. package/skills/hallmark/references/themes/loom.jpg +0 -0
  192. package/skills/hallmark/references/themes/loom.md +106 -0
  193. package/skills/hallmark/references/themes/lumen.jpg +0 -0
  194. package/skills/hallmark/references/themes/lumen.md +103 -0
  195. package/skills/hallmark/references/themes/manifesto.jpg +0 -0
  196. package/skills/hallmark/references/themes/manifesto.md +101 -0
  197. package/skills/hallmark/references/themes/midnight.jpg +0 -0
  198. package/skills/hallmark/references/themes/midnight.md +97 -0
  199. package/skills/hallmark/references/themes/newsprint.jpg +0 -0
  200. package/skills/hallmark/references/themes/newsprint.md +102 -0
  201. package/skills/hallmark/references/themes/overprint.jpg +0 -0
  202. package/skills/hallmark/references/themes/overprint.md +101 -0
  203. package/skills/hallmark/references/themes/pasteup.jpg +0 -0
  204. package/skills/hallmark/references/themes/pasteup.md +100 -0
  205. package/skills/hallmark/references/themes/piste.jpg +0 -0
  206. package/skills/hallmark/references/themes/piste.md +99 -0
  207. package/skills/hallmark/references/themes/ribbon.jpg +0 -0
  208. package/skills/hallmark/references/themes/ribbon.md +106 -0
  209. package/skills/hallmark/references/themes/rim.jpg +0 -0
  210. package/skills/hallmark/references/themes/rim.md +96 -0
  211. package/skills/hallmark/references/themes/riso.jpg +0 -0
  212. package/skills/hallmark/references/themes/riso.md +98 -0
  213. package/skills/hallmark/references/themes/solstice.jpg +0 -0
  214. package/skills/hallmark/references/themes/solstice.md +104 -0
  215. package/skills/hallmark/references/themes/specimen.jpg +0 -0
  216. package/skills/hallmark/references/themes/specimen.md +100 -0
  217. package/skills/hallmark/references/themes/spore.jpg +0 -0
  218. package/skills/hallmark/references/themes/spore.md +104 -0
  219. package/skills/hallmark/references/themes/sport.jpg +0 -0
  220. package/skills/hallmark/references/themes/sport.md +101 -0
  221. package/skills/hallmark/references/themes/squeeze.jpg +0 -0
  222. package/skills/hallmark/references/themes/squeeze.md +99 -0
  223. package/skills/hallmark/references/themes/sticker.jpg +0 -0
  224. package/skills/hallmark/references/themes/sticker.md +106 -0
  225. package/skills/hallmark/references/themes/studio.jpg +0 -0
  226. package/skills/hallmark/references/themes/studio.md +99 -0
  227. package/skills/hallmark/references/themes/terminal.jpg +0 -0
  228. package/skills/hallmark/references/themes/terminal.md +96 -0
  229. package/skills/hallmark/references/themes/terrazzo.jpg +0 -0
  230. package/skills/hallmark/references/themes/terrazzo.md +106 -0
  231. package/skills/hallmark/references/themes/ultraviolet.jpg +0 -0
  232. package/skills/hallmark/references/themes/ultraviolet.md +106 -0
  233. package/skills/hallmark/references/themes/vellum.jpg +0 -0
  234. package/skills/hallmark/references/themes/vellum.md +105 -0
  235. package/skills/hallmark/references/themes/wayfind.jpg +0 -0
  236. package/skills/hallmark/references/themes/wayfind.md +105 -0
  237. package/skills/hallmark/references/themes/werkstatt.jpg +0 -0
  238. package/skills/hallmark/references/themes/werkstatt.md +105 -0
  239. package/skills/hallmark/scripts/capture.mjs +125 -0
  240. package/skills/hallmark/scripts/fixtures/exemplars/atmospheric.html +324 -0
  241. package/skills/hallmark/scripts/fixtures/exemplars/editorial.html +363 -0
  242. package/skills/hallmark/scripts/fixtures/exemplars/modern-minimal.html +527 -0
  243. package/skills/hallmark/scripts/fixtures/exemplars/playful.html +353 -0
  244. package/skills/hallmark/scripts/fixtures/eyebrow-fail/index.html +118 -0
  245. package/skills/hallmark/scripts/fixtures/eyebrow-pass/index.html +137 -0
  246. package/skills/hallmark/scripts/fixtures/kept/after.html +27 -0
  247. package/skills/hallmark/scripts/fixtures/kept/before.html +26 -0
  248. package/skills/hallmark/scripts/fixtures/slopped.html +145 -0
  249. package/skills/hallmark/scripts/fixtures/tailwind-fail/Feature.vue +14 -0
  250. package/skills/hallmark/scripts/fixtures/tailwind-fail/Hero.tsx +33 -0
  251. package/skills/hallmark/scripts/fixtures/tailwind-fail/index.html +74 -0
  252. package/skills/hallmark/scripts/fixtures/tailwind-fail/style.css +6 -0
  253. package/skills/hallmark/scripts/fixtures/tailwind-pass/Hero.tsx +33 -0
  254. package/skills/hallmark/scripts/fixtures/tailwind-pass/index.html +57 -0
  255. package/skills/hallmark/scripts/fixtures/tailwind-pass/style.css +15 -0
  256. package/skills/hallmark/scripts/fixtures/waiver/index.html +54 -0
  257. package/skills/hallmark/scripts/imagegen.mjs +85 -0
  258. package/skills/hallmark/scripts/install-hook.mjs +70 -0
  259. package/skills/hallmark/scripts/install.mjs +141 -0
  260. package/skills/hallmark/scripts/lint-hook.mjs +49 -0
  261. package/skills/hallmark/scripts/photos.json +1 -0
  262. package/skills/hallmark/scripts/picture.mjs +71 -0
  263. package/skills/hallmark/scripts/refcheck.mjs +352 -0
  264. package/skills/hallmark/scripts/sloplint-render.mjs +1252 -0
  265. package/skills/hallmark/scripts/sloplint.mjs +4422 -0
  266. package/skills/hallmark/scripts/theme.mjs +650 -0
  267. package/skills/harness/SKILL.md +15 -8
  268. package/skills/i-have-adhd/SKILL.md +22 -11
  269. package/skills/no-ai-slop/SKILL.md +15 -1
  270. package/skills/plan-add/SKILL.md +33 -26
  271. package/skills/plan-status/SKILL.md +13 -7
  272. package/skills/plan-sync/SKILL.md +18 -12
  273. package/skills/pm-frameworks/SKILL.md +19 -10
  274. package/skills/pm-frameworks/SOURCE +7 -5
  275. package/skills/ponytail/SKILL.md +19 -14
  276. package/skills/product-analysis/SKILL.md +22 -18
  277. package/skills/product-flow/SKILL.md +14 -9
  278. package/skills/research/SKILL.md +75 -39
  279. package/skills/rl-env/SKILL.md +25 -13
  280. package/skills/rl-env/SOURCE +8 -6
  281. package/skills/tezgah-contract/SKILL.md +47 -42
  282. package/bin/tezgah-skill.py +0 -59
  283. /package/skills/ai-research/0-autoresearch-skill/{SKILL.md → ENTRY.md} +0 -0
  284. /package/skills/ai-research/01-model-architecture/litgpt/{SKILL.md → ENTRY.md} +0 -0
  285. /package/skills/ai-research/01-model-architecture/mamba/{SKILL.md → ENTRY.md} +0 -0
  286. /package/skills/ai-research/01-model-architecture/nanogpt/{SKILL.md → ENTRY.md} +0 -0
  287. /package/skills/ai-research/01-model-architecture/rwkv/{SKILL.md → ENTRY.md} +0 -0
  288. /package/skills/ai-research/01-model-architecture/torchtitan/{SKILL.md → ENTRY.md} +0 -0
  289. /package/skills/ai-research/02-tokenization/huggingface-tokenizers/{SKILL.md → ENTRY.md} +0 -0
  290. /package/skills/ai-research/02-tokenization/sentencepiece/{SKILL.md → ENTRY.md} +0 -0
  291. /package/skills/ai-research/03-fine-tuning/axolotl/{SKILL.md → ENTRY.md} +0 -0
  292. /package/skills/ai-research/03-fine-tuning/llama-factory/{SKILL.md → ENTRY.md} +0 -0
  293. /package/skills/ai-research/03-fine-tuning/peft/{SKILL.md → ENTRY.md} +0 -0
  294. /package/skills/ai-research/03-fine-tuning/unsloth/{SKILL.md → ENTRY.md} +0 -0
  295. /package/skills/ai-research/04-mechanistic-interpretability/nnsight/{SKILL.md → ENTRY.md} +0 -0
  296. /package/skills/ai-research/04-mechanistic-interpretability/pyvene/{SKILL.md → ENTRY.md} +0 -0
  297. /package/skills/ai-research/04-mechanistic-interpretability/saelens/{SKILL.md → ENTRY.md} +0 -0
  298. /package/skills/ai-research/04-mechanistic-interpretability/transformer-lens/{SKILL.md → ENTRY.md} +0 -0
  299. /package/skills/ai-research/05-data-processing/nemo-curator/{SKILL.md → ENTRY.md} +0 -0
  300. /package/skills/ai-research/05-data-processing/ray-data/{SKILL.md → ENTRY.md} +0 -0
  301. /package/skills/ai-research/06-post-training/grpo-rl-training/{SKILL.md → ENTRY.md} +0 -0
  302. /package/skills/ai-research/06-post-training/miles/{SKILL.md → ENTRY.md} +0 -0
  303. /package/skills/ai-research/06-post-training/openrlhf/{SKILL.md → ENTRY.md} +0 -0
  304. /package/skills/ai-research/06-post-training/simpo/{SKILL.md → ENTRY.md} +0 -0
  305. /package/skills/ai-research/06-post-training/slime/{SKILL.md → ENTRY.md} +0 -0
  306. /package/skills/ai-research/06-post-training/torchforge/{SKILL.md → ENTRY.md} +0 -0
  307. /package/skills/ai-research/06-post-training/trl-fine-tuning/{SKILL.md → ENTRY.md} +0 -0
  308. /package/skills/ai-research/06-post-training/verl/{SKILL.md → ENTRY.md} +0 -0
  309. /package/skills/ai-research/07-safety-alignment/constitutional-ai/{SKILL.md → ENTRY.md} +0 -0
  310. /package/skills/ai-research/07-safety-alignment/llamaguard/{SKILL.md → ENTRY.md} +0 -0
  311. /package/skills/ai-research/07-safety-alignment/nemo-guardrails/{SKILL.md → ENTRY.md} +0 -0
  312. /package/skills/ai-research/07-safety-alignment/prompt-guard/{SKILL.md → ENTRY.md} +0 -0
  313. /package/skills/ai-research/08-distributed-training/accelerate/{SKILL.md → ENTRY.md} +0 -0
  314. /package/skills/ai-research/08-distributed-training/deepspeed/{SKILL.md → ENTRY.md} +0 -0
  315. /package/skills/ai-research/08-distributed-training/megatron-core/{SKILL.md → ENTRY.md} +0 -0
  316. /package/skills/ai-research/08-distributed-training/pytorch-fsdp2/{SKILL.md → ENTRY.md} +0 -0
  317. /package/skills/ai-research/08-distributed-training/pytorch-lightning/{SKILL.md → ENTRY.md} +0 -0
  318. /package/skills/ai-research/08-distributed-training/ray-train/{SKILL.md → ENTRY.md} +0 -0
  319. /package/skills/ai-research/09-infrastructure/lambda-labs/{SKILL.md → ENTRY.md} +0 -0
  320. /package/skills/ai-research/09-infrastructure/modal/{SKILL.md → ENTRY.md} +0 -0
  321. /package/skills/ai-research/09-infrastructure/skypilot/{SKILL.md → ENTRY.md} +0 -0
  322. /package/skills/ai-research/10-optimization/awq/{SKILL.md → ENTRY.md} +0 -0
  323. /package/skills/ai-research/10-optimization/bitsandbytes/{SKILL.md → ENTRY.md} +0 -0
  324. /package/skills/ai-research/10-optimization/flash-attention/{SKILL.md → ENTRY.md} +0 -0
  325. /package/skills/ai-research/10-optimization/gguf/{SKILL.md → ENTRY.md} +0 -0
  326. /package/skills/ai-research/10-optimization/gptq/{SKILL.md → ENTRY.md} +0 -0
  327. /package/skills/ai-research/10-optimization/hqq/{SKILL.md → ENTRY.md} +0 -0
  328. /package/skills/ai-research/10-optimization/ml-training-recipes/{SKILL.md → ENTRY.md} +0 -0
  329. /package/skills/ai-research/11-evaluation/bigcode-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
  330. /package/skills/ai-research/11-evaluation/lm-evaluation-harness/{SKILL.md → ENTRY.md} +0 -0
  331. /package/skills/ai-research/11-evaluation/nemo-evaluator/{SKILL.md → ENTRY.md} +0 -0
  332. /package/skills/ai-research/12-inference-serving/llama-cpp/{SKILL.md → ENTRY.md} +0 -0
  333. /package/skills/ai-research/12-inference-serving/sglang/{SKILL.md → ENTRY.md} +0 -0
  334. /package/skills/ai-research/12-inference-serving/tensorrt-llm/{SKILL.md → ENTRY.md} +0 -0
  335. /package/skills/ai-research/12-inference-serving/vllm/{SKILL.md → ENTRY.md} +0 -0
  336. /package/skills/ai-research/13-mlops/mlflow/{SKILL.md → ENTRY.md} +0 -0
  337. /package/skills/ai-research/13-mlops/swanlab/{SKILL.md → ENTRY.md} +0 -0
  338. /package/skills/ai-research/13-mlops/tensorboard/{SKILL.md → ENTRY.md} +0 -0
  339. /package/skills/ai-research/13-mlops/weights-and-biases/{SKILL.md → ENTRY.md} +0 -0
  340. /package/skills/ai-research/14-agents/a-evolve/{SKILL.md → ENTRY.md} +0 -0
  341. /package/skills/ai-research/14-agents/autogpt/{SKILL.md → ENTRY.md} +0 -0
  342. /package/skills/ai-research/14-agents/crewai/{SKILL.md → ENTRY.md} +0 -0
  343. /package/skills/ai-research/14-agents/langchain/{SKILL.md → ENTRY.md} +0 -0
  344. /package/skills/ai-research/14-agents/llamaindex/{SKILL.md → ENTRY.md} +0 -0
  345. /package/skills/ai-research/15-rag/chroma/{SKILL.md → ENTRY.md} +0 -0
  346. /package/skills/ai-research/15-rag/faiss/{SKILL.md → ENTRY.md} +0 -0
  347. /package/skills/ai-research/15-rag/pinecone/{SKILL.md → ENTRY.md} +0 -0
  348. /package/skills/ai-research/15-rag/qdrant/{SKILL.md → ENTRY.md} +0 -0
  349. /package/skills/ai-research/15-rag/sentence-transformers/{SKILL.md → ENTRY.md} +0 -0
  350. /package/skills/ai-research/16-prompt-engineering/dspy/{SKILL.md → ENTRY.md} +0 -0
  351. /package/skills/ai-research/16-prompt-engineering/guidance/{SKILL.md → ENTRY.md} +0 -0
  352. /package/skills/ai-research/16-prompt-engineering/instructor/{SKILL.md → ENTRY.md} +0 -0
  353. /package/skills/ai-research/16-prompt-engineering/outlines/{SKILL.md → ENTRY.md} +0 -0
  354. /package/skills/ai-research/17-observability/langsmith/{SKILL.md → ENTRY.md} +0 -0
  355. /package/skills/ai-research/17-observability/phoenix/{SKILL.md → ENTRY.md} +0 -0
  356. /package/skills/ai-research/18-multimodal/audiocraft/{SKILL.md → ENTRY.md} +0 -0
  357. /package/skills/ai-research/18-multimodal/blip-2/{SKILL.md → ENTRY.md} +0 -0
  358. /package/skills/ai-research/18-multimodal/clip/{SKILL.md → ENTRY.md} +0 -0
  359. /package/skills/ai-research/18-multimodal/cosmos-policy/{SKILL.md → ENTRY.md} +0 -0
  360. /package/skills/ai-research/18-multimodal/llava/{SKILL.md → ENTRY.md} +0 -0
  361. /package/skills/ai-research/18-multimodal/openpi/{SKILL.md → ENTRY.md} +0 -0
  362. /package/skills/ai-research/18-multimodal/openvla-oft/{SKILL.md → ENTRY.md} +0 -0
  363. /package/skills/ai-research/18-multimodal/segment-anything/{SKILL.md → ENTRY.md} +0 -0
  364. /package/skills/ai-research/18-multimodal/stable-diffusion/{SKILL.md → ENTRY.md} +0 -0
  365. /package/skills/ai-research/18-multimodal/whisper/{SKILL.md → ENTRY.md} +0 -0
  366. /package/skills/ai-research/19-emerging-techniques/knowledge-distillation/{SKILL.md → ENTRY.md} +0 -0
  367. /package/skills/ai-research/19-emerging-techniques/long-context/{SKILL.md → ENTRY.md} +0 -0
  368. /package/skills/ai-research/19-emerging-techniques/model-merging/{SKILL.md → ENTRY.md} +0 -0
  369. /package/skills/ai-research/19-emerging-techniques/model-pruning/{SKILL.md → ENTRY.md} +0 -0
  370. /package/skills/ai-research/19-emerging-techniques/moe-training/{SKILL.md → ENTRY.md} +0 -0
  371. /package/skills/ai-research/19-emerging-techniques/speculative-decoding/{SKILL.md → ENTRY.md} +0 -0
  372. /package/skills/ai-research/20-ml-paper-writing/academic-plotting/{SKILL.md → ENTRY.md} +0 -0
  373. /package/skills/ai-research/20-ml-paper-writing/ml-paper-writing/{SKILL.md → ENTRY.md} +0 -0
  374. /package/skills/ai-research/20-ml-paper-writing/presenting-conference-talks/{SKILL.md → ENTRY.md} +0 -0
  375. /package/skills/ai-research/20-ml-paper-writing/systems-paper-writing/{SKILL.md → ENTRY.md} +0 -0
  376. /package/skills/ai-research/21-research-ideation/brainstorming-research-ideas/{SKILL.md → ENTRY.md} +0 -0
  377. /package/skills/ai-research/21-research-ideation/creative-thinking-for-research/{SKILL.md → ENTRY.md} +0 -0
  378. /package/skills/ai-research/22-agent-native-research-artifact/compiler/{SKILL.md → ENTRY.md} +0 -0
  379. /package/skills/ai-research/22-agent-native-research-artifact/research-manager/{SKILL.md → ENTRY.md} +0 -0
  380. /package/skills/ai-research/22-agent-native-research-artifact/rigor-reviewer/{SKILL.md → ENTRY.md} +0 -0
  381. /package/skills/design-library/adaptive-interfaces/skills/colour-independence/{SKILL.md → ENTRY.md} +0 -0
  382. /package/skills/design-library/adaptive-interfaces/skills/flexible-typography/{SKILL.md → ENTRY.md} +0 -0
  383. /package/skills/design-library/adaptive-interfaces/skills/information-density/{SKILL.md → ENTRY.md} +0 -0
  384. /package/skills/design-library/adaptive-interfaces/skills/preference-audit/{SKILL.md → ENTRY.md} +0 -0
  385. /package/skills/design-library/adaptive-interfaces/skills/responsive-accessibility/{SKILL.md → ENTRY.md} +0 -0
  386. /package/skills/design-library/adaptive-interfaces/skills/responsive-review/{SKILL.md → ENTRY.md} +0 -0
  387. /package/skills/design-library/adaptive-interfaces/skills/simplified-views/{SKILL.md → ENTRY.md} +0 -0
  388. /package/skills/design-library/adaptive-interfaces/skills/specify/{SKILL.md → ENTRY.md} +0 -0
  389. /package/skills/design-library/adaptive-interfaces/skills/user-preference-respect/{SKILL.md → ENTRY.md} +0 -0
  390. /package/skills/design-library/cognitive-accessibility/skills/adaptive-personalisation/{SKILL.md → ENTRY.md} +0 -0
  391. /package/skills/design-library/cognitive-accessibility/skills/assess-load/{SKILL.md → ENTRY.md} +0 -0
  392. /package/skills/design-library/cognitive-accessibility/skills/cognitive-load-assessment/{SKILL.md → ENTRY.md} +0 -0
  393. /package/skills/design-library/cognitive-accessibility/skills/contextual-help-design/{SKILL.md → ENTRY.md} +0 -0
  394. /package/skills/design-library/cognitive-accessibility/skills/error-prevention-recovery/{SKILL.md → ENTRY.md} +0 -0
  395. /package/skills/design-library/cognitive-accessibility/skills/focus-attention-design/{SKILL.md → ENTRY.md} +0 -0
  396. /package/skills/design-library/cognitive-accessibility/skills/memory-load-reduction/{SKILL.md → ENTRY.md} +0 -0
  397. /package/skills/design-library/cognitive-accessibility/skills/plain-language-design/{SKILL.md → ENTRY.md} +0 -0
  398. /package/skills/design-library/cognitive-accessibility/skills/review/{SKILL.md → ENTRY.md} +0 -0
  399. /package/skills/design-library/cognitive-accessibility/skills/simplify/{SKILL.md → ENTRY.md} +0 -0
  400. /package/skills/design-library/cognitive-accessibility/skills/wayfinding-navigation/{SKILL.md → ENTRY.md} +0 -0
  401. /package/skills/design-library/design-research/skills/behavioural-analytics/{SKILL.md → ENTRY.md} +0 -0
  402. /package/skills/design-library/design-research/skills/qual-quant-triangulation/{SKILL.md → ENTRY.md} +0 -0
  403. /package/skills/design-library/inclusive-interaction/skills/audit/{SKILL.md → ENTRY.md} +0 -0
  404. /package/skills/design-library/inclusive-interaction/skills/design-flow/{SKILL.md → ENTRY.md} +0 -0
  405. /package/skills/design-library/inclusive-interaction/skills/feedback-and-status/{SKILL.md → ENTRY.md} +0 -0
  406. /package/skills/design-library/inclusive-interaction/skills/gesture-alternatives/{SKILL.md → ENTRY.md} +0 -0
  407. /package/skills/design-library/inclusive-interaction/skills/keyboard-navigation/{SKILL.md → ENTRY.md} +0 -0
  408. /package/skills/design-library/inclusive-interaction/skills/keyboard-review/{SKILL.md → ENTRY.md} +0 -0
  409. /package/skills/design-library/inclusive-interaction/skills/motion-sensitivity/{SKILL.md → ENTRY.md} +0 -0
  410. /package/skills/design-library/inclusive-interaction/skills/multi-modal-input/{SKILL.md → ENTRY.md} +0 -0
  411. /package/skills/design-library/inclusive-interaction/skills/touch-target-design/{SKILL.md → ENTRY.md} +0 -0
  412. /package/skills/design-library/inclusive-interaction/skills/voice-interaction/{SKILL.md → ENTRY.md} +0 -0
  413. /package/skills/design-library/visual-critique/skills/critique-affordance/{SKILL.md → ENTRY.md} +0 -0
  414. /package/skills/design-library/visual-critique/skills/critique-brand-consistency/{SKILL.md → ENTRY.md} +0 -0
  415. /package/skills/design-library/visual-critique/skills/critique-color/{SKILL.md → ENTRY.md} +0 -0
  416. /package/skills/design-library/visual-critique/skills/critique-composition/{SKILL.md → ENTRY.md} +0 -0
  417. /package/skills/design-library/visual-critique/skills/critique-information-density/{SKILL.md → ENTRY.md} +0 -0
  418. /package/skills/design-library/visual-critique/skills/critique-typography/{SKILL.md → ENTRY.md} +0 -0
  419. /package/skills/design-library/visual-critique/skills/critique-visual-hierarchy/{SKILL.md → ENTRY.md} +0 -0
  420. /package/skills/pm-frameworks/intended-vs-implemented/{SKILL.md → ENTRY.md} +0 -0
  421. /package/skills/pm-frameworks/opportunity-solution-tree/{SKILL.md → ENTRY.md} +0 -0
  422. /package/skills/rl-env/generate-nemo-gym-env/{SKILL.md → ENTRY.md} +0 -0
  423. /package/skills/rl-env/generate-openenv-env/{SKILL.md → ENTRY.md} +0 -0
  424. /package/skills/rl-env/generate-ors-env/{SKILL.md → ENTRY.md} +0 -0
  425. /package/skills/rl-env/generate-verifiers-env/{SKILL.md → ENTRY.md} +0 -0
  426. /package/skills/rl-env/rl-env-from-description/{SKILL.md → ENTRY.md} +0 -0
@@ -0,0 +1,1252 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join, dirname, basename, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+
6
+ const WIDTHS = [320, 375, 414, 768, 1024, 1280, 1920];
7
+ const CHROMES = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'];
8
+ const isUrl = (t) => /^https?:\/\//.test(t);
9
+ const slugOf = (t) => isUrl(t) ? t.replace(/^https?:\/\//, '').replace(/[^\w.-]+/g, '-').replace(/-+$/, '') : basename(t, '.html');
10
+
11
+ const SHOTS = [];
12
+ let SHOT_DIR = 'shots';
13
+ const frames = (page) => page.evaluate(async () => { if (document.fonts && document.fonts.status !== 'loaded') await Promise.race([document.fonts.ready, new Promise((r) => setTimeout(r, 3000))]); await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); }).catch(() => {});
14
+ const settleImages = (page) => page.evaluate(async () => { for (const i of document.querySelectorAll('img[loading="lazy"]')) i.loading = 'eager'; for (let y = 0; y < document.documentElement.scrollHeight; y += 700) { window.scrollTo(0, y); await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); } window.scrollTo(0, 0); const waiting = [...document.images].filter((i) => !i.complete); await Promise.race([Promise.all(waiting.map((i) => new Promise((r) => { i.addEventListener('load', r, { once: true }); i.addEventListener('error', r, { once: true }); }))), new Promise((r) => setTimeout(r, 15000))]); await Promise.all([...document.images].map((i) => (i.complete && i.naturalWidth && i.decode ? i.decode().catch(() => {}) : null))); }).catch(() => {});
15
+ function contrast(a, b) {
16
+ const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
17
+ const Y = (c) => 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
18
+ const x = Y(a), y = Y(b);
19
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
20
+ }
21
+
22
+ async function pixelBehind(page, probe) {
23
+ const HIDE = [['color', 'transparent'], ['text-shadow', 'none'], ['-webkit-text-stroke', '0'], ['text-decoration-color', 'transparent']];
24
+ const restore = () => page.evaluate((n, props) => {
25
+ const el = document.querySelector(`[data-hm-probe="${n}"]`);
26
+ if (el) for (const [k] of props) el.style.removeProperty(k);
27
+ window.scrollTo(0, 0);
28
+ }, probe, HIDE).catch(() => {});
29
+ try {
30
+ const rect = await page.evaluate((n, props) => {
31
+ const el = document.querySelector(`[data-hm-probe="${n}"]`);
32
+ if (!el) return null;
33
+ el.scrollIntoView({ block: 'center', behavior: 'instant' });
34
+ for (const [k, v] of props) el.style.setProperty(k, v, 'important');
35
+ const r = el.getBoundingClientRect();
36
+ const x = Math.max(0, r.left), y = Math.max(0, r.top);
37
+ return { x: x + scrollX, y: y + scrollY, width: Math.min(r.right, innerWidth) - x, height: Math.min(r.bottom, innerHeight) - y };
38
+ }, probe, HIDE);
39
+ if (!rect || rect.width < 2 || rect.height < 2) { await restore(); return null; }
40
+ await frames(page);
41
+ const data = await page.screenshot({ clip: rect, encoding: 'base64' });
42
+ await restore();
43
+ return await page.evaluate(async (b64) => {
44
+ const img = new Image();
45
+ img.src = 'data:image/png;base64,' + b64;
46
+ await img.decode();
47
+ const c = document.createElement('canvas');
48
+ c.width = img.width; c.height = img.height;
49
+ const x = c.getContext('2d');
50
+ x.drawImage(img, 0, 0);
51
+ const d = x.getImageData(0, 0, c.width, c.height).data;
52
+ const ch = [[], [], []];
53
+ for (let i = 0; i < d.length; i += 4) { ch[0].push(d[i]); ch[1].push(d[i + 1]); ch[2].push(d[i + 2]); }
54
+ return ch.map((a) => a.sort((p, q) => p - q)[a.length >> 1] / 255);
55
+ }, data);
56
+ } catch { await restore(); return null; }
57
+ }
58
+
59
+ async function shoot(page, file, tag, fullPage) {
60
+ try {
61
+ const dir = isUrl(file) ? join(process.cwd(), '.hallmark', SHOT_DIR) : join(dirname(file), '.hallmark', SHOT_DIR);
62
+ mkdirSync(dir, { recursive: true });
63
+ const ignore = join(dirname(dir), '.gitignore');
64
+ if (!existsSync(ignore)) writeFileSync(ignore, 'shots/\nbefore/\nbefore.json\nbefore.txt\ncompare.html\n');
65
+ const out = join(dir, `${slugOf(file)}-${tag}.png`);
66
+ await page.screenshot({ path: out, fullPage });
67
+ SHOTS.push(out);
68
+ } catch {}
69
+ }
70
+
71
+ export async function renderCheck(htmlFiles, metaIn) {
72
+ const failed = [];
73
+ const meta = { genre: null, postureByFile: {}, heroPostureByFile: {}, photoShapeByFile: {}, jobByFile: {}, stage: 'page', scope: 'page', before: false, ...(metaIn || {}) };
74
+ const heroOnly = meta.stage === 'hero';
75
+ SHOT_DIR = meta.before ? 'before' : 'shots';
76
+ const inventories = {};
77
+ let puppeteer = null;
78
+ const { createRequire } = await import('node:module');
79
+ const tryRoots = async (roots) => {
80
+ for (const root of roots) {
81
+ for (const name of ['puppeteer-core', 'puppeteer']) {
82
+ try {
83
+ const req = createRequire(join(root, 'noop.js'));
84
+ const resolved = req.resolve(name);
85
+ puppeteer = (await import(pathToFileURL(resolved).href)).default;
86
+ if (puppeteer) return;
87
+ } catch {}
88
+ }
89
+ }
90
+ };
91
+ const local = [process.cwd(), dirname(fileURLToPath(import.meta.url))];
92
+ if (process.env.HALLMARK_PUPPETEER) local.unshift(process.env.HALLMARK_PUPPETEER);
93
+ await tryRoots(local);
94
+ if (!puppeteer) {
95
+ try {
96
+ const { execSync } = await import('node:child_process');
97
+ const g = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
98
+ if (g) await tryRoots([dirname(g)]);
99
+ } catch {}
100
+ }
101
+ if (!puppeteer) {
102
+ const none = []; none.failed = failed; none.notRun = 'no puppeteer-core found locally, globally or via HALLMARK_PUPPETEER';
103
+ return none;
104
+ }
105
+ let chrome = [process.env.CHROME_PATH, ...CHROMES].find((p) => p && existsSync(p)) || null;
106
+ if (!chrome && typeof puppeteer.executablePath === 'function') {
107
+ try { const bundled = puppeteer.executablePath(); if (bundled && existsSync(bundled)) chrome = bundled; } catch {}
108
+ }
109
+ if (!chrome) {
110
+ const none = []; none.failed = failed; none.notRun = 'no Chrome or Chromium found; set CHROME_PATH';
111
+ return none;
112
+ }
113
+ const findings = [];
114
+ const browser = await puppeteer.launch({ executablePath: chrome, headless: true });
115
+ try {
116
+ for (const file of htmlFiles) {
117
+ const page = await browser.newPage();
118
+ try {
119
+ const scriptErrors = [];
120
+ page.on('pageerror', (e) => scriptErrors.push(String((e && e.message) || e).split('\n')[0]));
121
+ const lost = new Set();
122
+ const lostName = (u) => { try { const x = new URL(u); return x.protocol === 'file:' ? x.pathname.split('/').pop() : x.host; } catch { return String(u).slice(0, 40); } };
123
+ page.on('requestfailed', (r) => { if (['stylesheet', 'font'].includes(r.resourceType())) lost.add(lostName(r.url())); });
124
+ page.on('response', (r) => { try { if (r.status() >= 400 && ['stylesheet', 'font'].includes(r.request().resourceType())) lost.add(lostName(r.url())); } catch {} });
125
+ await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]).catch(() => {});
126
+ const nav = await page.goto(isUrl(file) ? file : pathToFileURL(file).href, { waitUntil: isUrl(file) ? 'networkidle0' : 'load', timeout: 30000 }).then((r) => ({ r }), (e) => ({ e }));
127
+ if (nav.e && !/timeout/i.test(nav.e.message)) throw nav.e;
128
+ if (nav.r && isUrl(file) && !nav.r.ok() && nav.r.status() !== 304) throw new Error(`HTTP ${nav.r.status()} from ${file}`);
129
+ await page.evaluate(async () => {
130
+ document.documentElement.classList.add('hallmark-shot');
131
+ for (const el of [document.documentElement, document.body]) if (el) el.style.setProperty('scroll-behavior', 'auto', 'important');
132
+ const quiet = document.createElement('style');
133
+ quiet.textContent = 'nextjs-portal, astro-dev-toolbar { display: none !important; }';
134
+ document.head.appendChild(quiet);
135
+ if (document.fonts) await document.fonts.ready;
136
+ }).catch(() => {});
137
+ if (isUrl(file)) {
138
+ const stamp = await page.evaluate(async () => { let t = [...document.querySelectorAll('style')].map((x) => x.textContent.slice(0, 4000)).join('\n'); for (const l of document.querySelectorAll('link[rel~="stylesheet"]')) { try { t += '\n' + (await (await fetch(l.href)).text()).slice(0, 4000); } catch {} } return t; }).catch(() => '');
139
+ meta.postureByFile[file] = (/posture:\s*(restrained|committed|full-palette|drenched)/i.exec(stamp)?.[1] || 'restrained').toLowerCase();
140
+ const field = (/hero:\s*([^·*\n]+)/i.exec(stamp) || [])[1] || '';
141
+ let route = ''; try { route = new URL(file).pathname.replace(/\/$/, '') || '/'; } catch {}
142
+ const own = new RegExp(`(settled|banner|poster|ledge|corridor|stage|counter|index)\\s*\\(${route.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, 'i').exec(field);
143
+ meta.photoShapeByFile[file] = /shape:\s*(photographic|portfolio)/i.test(stamp);
144
+ meta.jobByFile[file] = ((/\bjob:\s*([a-z][a-z /-]*)/i.exec(stamp) || [])[1] || '').trim().toLowerCase();
145
+ meta.heroPostureByFile[file] = /theme:\s*custom\b/i.test(stamp) ? 'custom' : ((own || /(settled|banner|poster|ledge|corridor|stage|counter|index)\b/i.exec(field) || [])[1] || '').toLowerCase();
146
+ }
147
+ const pageMeta = { genre: meta.genre, posture: meta.postureByFile[file] || 'restrained' };
148
+ const prefix = heroOnly ? 'hero-' : '';
149
+ for (const width of heroOnly ? [375, 1280] : WIDTHS) {
150
+ await page.setViewport({ width, height: width === 375 && heroOnly ? 812 : 800 });
151
+ await page.evaluate(() => window.scrollTo(0, 0)).catch(() => {});
152
+ await frames(page);
153
+ if (width === 1280) await shoot(page, file, `${prefix}1280x800`, false);
154
+ if (width === 375 && !heroOnly) { await settleImages(page); await frames(page); }
155
+ if (width === 375) await shoot(page, file, heroOnly ? 'hero-375x812' : '375', !heroOnly);
156
+ if (!heroOnly && (width === 375 || width === 1280)) for (const f of await page.evaluate(lineBreaks).catch(() => [])) findings.push({ ...f, file, line: 1 });
157
+ const res = await page.evaluate(observe, width === 1280, pageMeta);
158
+ const measured = new Map();
159
+ for (const f of res) {
160
+ if (f.probe == null) continue;
161
+ if (!measured.has(f.probe)) measured.set(f.probe, await pixelBehind(page, f.probe));
162
+ const bg = measured.get(f.probe);
163
+ if (!bg) continue;
164
+ const real = contrast(f.fg, bg);
165
+ if (real >= f.threshold - 0.05) f.drop = true;
166
+ else f.evidence = f.evidence.replace(/computed contrast [\d.]+:1/, `contrast ${real.toFixed(2)}:1 against the pixels behind`).replace(/\([\d.]+:1\)$/, `(${real.toFixed(2)}:1)`);
167
+ }
168
+ for (const f of res) if (!f.drop) { const { probe, fg, threshold, drop, ...rest } = f; findings.push({ ...rest, file, line: 1, evidence: `[${width}px] ${f.evidence}` }); }
169
+ }
170
+ for (const [w, h] of [[1440, 900], [1920, 1080]]) {
171
+ await page.setViewport({ width: w, height: h });
172
+ await page.evaluate(() => window.scrollTo(0, 0)).catch(() => {});
173
+ await frames(page);
174
+ await shoot(page, file, `${prefix}${w}x${h}`, false);
175
+ if (!heroOnly && w === 1440) {
176
+ await settleImages(page);
177
+ await shoot(page, file, '1440-full', true);
178
+ }
179
+ await frames(page);
180
+ const res = await page.evaluate(observeSpace, w === 1440, heroOnly, meta.heroPostureByFile[file] || '', !!meta.photoShapeByFile[file], meta.jobByFile[file] || '');
181
+ for (const f of res) findings.push({ ...f, file, line: 1, evidence: `[${w}px] ${f.evidence}` });
182
+ }
183
+ if (!heroOnly) {
184
+ await page.setViewport({ width: 1440, height: 900 });
185
+ await frames(page);
186
+ inventories[file] = await page.evaluate(inventory, (meta.selectors && meta.selectors[file]) || []).catch(() => null);
187
+ const quality = await page.evaluate(observeQuality, meta.scope).catch(() => []);
188
+ const hidden = await page.evaluate(observeOcclusion).catch(() => []);
189
+ for (const f of [...quality, ...hidden]) findings.push({ ...f, file, line: 1, evidence: `[1440px] ${f.evidence}` });
190
+ await page.evaluate(() => { window.scrollTo(0, 0); if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); });
191
+ const rings = new Set();
192
+ const bare = [];
193
+ for (let i = 0; i < 16; i++) {
194
+ await page.keyboard.press('Tab');
195
+ await frames(page);
196
+ const ring = await page.evaluate(focusRing);
197
+ if (!ring || rings.has(ring.key)) continue;
198
+ rings.add(ring.key);
199
+ if (ring.bare) { if (bare.length < 4) bare.push(ring.key); continue; }
200
+ if (ring.ratio != null && ring.ratio < 2.95) { const bg = await pixelsAroundRing(page, ring); if (bg) ring.ratio = contrast(ring.ring, bg); }
201
+ if (ring.ratio != null && ring.ratio < 2.95) findings.push({ gate: '26a', grade: 'FAIL', file, line: 1, evidence: `[1440px] focus ring ${ring.ratio.toFixed(2)}:1 against what is behind it on ${ring.key}`, fix: 'A ring at 3:1 or better against the surface it is drawn over: --color-focus, or a ring colour per band' });
202
+ }
203
+ if (bare.length) findings.push({ gate: '26a', grade: 'FAIL', file, line: 1, evidence: `[1440px] nothing shows keyboard focus on ${bare.join(', ')}`, fix: 'Give every control a :focus-visible ring; never remove an outline without putting one back' });
204
+ }
205
+ if (!heroOnly) {
206
+ await page.setViewport({ width: 1440, height: 900 });
207
+ await frames(page);
208
+ await page.evaluate(() => {
209
+ window.__hmSeen = [...document.querySelectorAll('p, li, h1, h2, h3, h4, dd, td, blockquote, figcaption')].filter((el) => {
210
+ const cs = getComputedStyle(el); const r = el.getBoundingClientRect();
211
+ return cs.display !== 'none' && cs.visibility !== 'hidden' && r.width > 1 && r.height > 1 && !el.closest('nav, header, dialog, [hidden], [aria-hidden="true"]');
212
+ });
213
+ }).catch(() => {});
214
+ await page.setViewport({ width: 375, height: 800 });
215
+ await frames(page);
216
+ const cut = await page.evaluate(() => {
217
+ const flat = (t) => (t || '').replace(/\s+/g, ' ').trim();
218
+ const shown = flat(document.body.innerText);
219
+ const gone = (window.__hmSeen || []).filter((el) => {
220
+ const cs = getComputedStyle(el); const r = el.getBoundingClientRect();
221
+ if (!(cs.display === 'none' || cs.visibility === 'hidden' || r.width < 1 || r.height < 1)) return false;
222
+ const t = flat(el.textContent);
223
+ return t.length > 12 && !shown.includes(t);
224
+ });
225
+ const off = [...document.querySelectorAll('table')].flatMap((t) => (t.rows[0] ? [...t.rows[0].cells] : [])).filter((el) => { const r = el.getBoundingClientRect(); return r.width > 1 && r.height > 1 && r.left >= window.innerWidth - 2; });
226
+ return { words: gone.reduce((n, el) => n + flat(el.textContent).split(' ').length, 0), first: gone[0] ? flat(gone[0].textContent).slice(0, 36) : '', off: off.length, col: off[0] ? flat(off[0].textContent).slice(0, 24) : '' };
227
+ }).catch(() => ({ words: 0, off: 0 }));
228
+ if (cut.words >= 40) findings.push({ gate: 'F25', grade: 'WARN', file, line: 1, evidence: `[375px] ${cut.words} words shown at 1440 are hidden on a phone (first: "${cut.first}")`, fix: 'If it matters, make it work on a phone; if it does not, cut it everywhere' });
229
+ if (cut.off >= 2) findings.push({ gate: 'F25', grade: 'WARN', file, line: 1, evidence: `[375px] a table opens with ${cut.off} columns off-screen (from "${cut.col}")`, fix: 'Keep the column that names the row and the one that acts on it in view: restack each row, or pin the first column' });
230
+ }
231
+ if (scriptErrors.length) findings.push({ gate: 'render', grade: 'FAIL', file, line: 1, evidence: `a script threw on load: ${scriptErrors[0].slice(0, 90)}`, fix: 'Fix the script; a page that throws ships half-working' });
232
+ if (lost.size) findings.push({ gate: 'render', grade: 'WARN', file, line: 1, evidence: `the shots show a fallback: fonts or stylesheets did not load (${[...lost].slice(0, 2).join(', ')})`, fix: 'Recapture when the host answers or the path is right; never review or sign off a fallback face' });
233
+ } catch (e) {
234
+ console.error(`render tier: failed on ${file}: ${e.message}`);
235
+ failed.push({ file, message: e.message });
236
+ } finally {
237
+ await page.close().catch(() => {});
238
+ }
239
+ }
240
+ } finally {
241
+ await browser.close().catch(() => {});
242
+ }
243
+ if (SHOTS.length) console.error(heroOnly ? `render tier: ${SHOTS.length} hero screenshots in ${dirname(SHOTS[0])} (1280x800, 1440x900, 1920x1080 and 375x812 first screens)` : `render tier: ${SHOTS.length} screenshots in ${dirname(SHOTS[0])} (1280x800, 1440x900, 1920x1080 folds; 1440 and 375 full pages)`);
244
+
245
+ const seen = new Set();
246
+ const kept = findings.filter((f) => {
247
+ const k = f.file + '|' + f.gate + '|' + f.evidence.replace(/^\[\d+px\] /, '');
248
+ if (seen.has(k)) return false;
249
+ seen.add(k);
250
+ return true;
251
+ });
252
+ kept.failed = failed;
253
+ kept.inventories = inventories;
254
+ return kept;
255
+ }
256
+
257
+ export function writeCompare(targets, note) {
258
+ const groups = new Map();
259
+ for (const t of targets) {
260
+ const dir = isUrl(t) ? join(process.cwd(), '.hallmark') : join(dirname(t), '.hallmark');
261
+ if (!groups.has(dir)) groups.set(dir, []);
262
+ groups.get(dir).push(slugOf(t));
263
+ }
264
+ const esc = (x) => String(x).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
265
+ const made = [];
266
+ for (const [dir, slugs] of groups) {
267
+ const has = (side, slug, tag) => existsSync(join(dir, side, `${slug}-${tag}.png`));
268
+ const pages = slugs.filter((x) => has('before', x, '1440x900') && has('shots', x, '1440x900'));
269
+ if (!pages.length) continue;
270
+ const pair = (slug, tag, label, cls) => (has('before', slug, tag) && has('shots', slug, tag) ? `<div class="pair ${cls}"><figure><img src="before/${esc(slug)}-${tag}.png" alt="${esc(slug)} before, ${label}"><figcaption>Before, ${label}</figcaption></figure><figure><img src="shots/${esc(slug)}-${tag}.png" alt="${esc(slug)} after, ${label}"><figcaption>After, ${label}</figcaption></figure></div>` : '');
271
+ const body = pages.map((x) => `<section><h2>${esc(x)}</h2>${pair(x, '1440x900', 'first screen at 1440', '')}${pair(x, '1440-full', 'whole page at 1440', '')}${pair(x, '375', 'whole page on a phone', 'phone')}</section>`).join('\n');
272
+ const html = `<!doctype html>
273
+ <html lang="en">
274
+ <head>
275
+ <meta charset="utf-8">
276
+ <meta name="viewport" content="width=device-width, initial-scale=1">
277
+ <title>Before and after</title>
278
+ <meta name="description" content="The redesign's screenshots, before and after, side by side.">
279
+ <style>
280
+ :root { color-scheme: light; --paper: oklch(97% 0.004 250); --ink: oklch(22% 0.01 250); --muted: oklch(45% 0.01 250); --rule: oklch(86% 0.006 250); }
281
+ html, body { overflow-x: clip; }
282
+ body { margin: 0; padding: 48px 32px 96px; background: var(--paper); color: var(--ink); font: 16px/1.5 ui-sans-serif, system-ui, sans-serif; }
283
+ h1 { margin: 0 0 8px; font-size: 28px; line-height: 1.15; letter-spacing: -0.01em; }
284
+ p { margin: 0; color: var(--muted); max-width: 72ch; }
285
+ section { margin-top: 72px; }
286
+ h2 { margin: 0 0 16px; font-size: 18px; font-weight: 600; }
287
+ .pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: start; margin-bottom: 32px; }
288
+ .pair.phone { max-width: 760px; }
289
+ figure { margin: 0; }
290
+ img { display: block; width: 100%; height: auto; outline: 1px solid var(--rule); outline-offset: -1px; background: oklch(100% 0 0); }
291
+ figcaption { margin-top: 8px; font-size: 14px; color: var(--muted); }
292
+ @media (max-width: 720px) { body { padding: 32px 16px 64px; } .pair { grid-template-columns: minmax(0, 1fr); } }
293
+ </style>
294
+ </head>
295
+ <body>
296
+ <h1>Before and after</h1>
297
+ <p>${esc(note || 'Each pair shows the page as it stood and as it stands now, at the same size.')}</p>
298
+ ${body}
299
+ </body>
300
+ </html>
301
+ `;
302
+ writeFileSync(join(dir, 'compare.html'), html);
303
+ made.push(join(dir, 'compare.html'));
304
+ }
305
+ return made;
306
+ }
307
+
308
+ function inventory(selectors) {
309
+ const base = location.pathname.slice(0, location.pathname.lastIndexOf('/') + 1);
310
+ const key = (raw) => {
311
+ try {
312
+ const u = new URL(raw, location.href);
313
+ if (!/^(https?|file):$/.test(u.protocol)) return raw.trim();
314
+ if (u.origin !== location.origin) return u.href;
315
+ if (u.pathname === location.pathname && u.search === location.search && u.hash) return u.hash;
316
+ return (location.protocol === 'file:' && u.pathname.startsWith(base) ? u.pathname.slice(base.length) : u.pathname) + u.search + u.hash;
317
+ } catch { return raw.trim(); }
318
+ };
319
+ const uniq = (a) => [...new Set(a.filter(Boolean))];
320
+ const text = (el) => (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
321
+ const quiet = (el) => !!el.closest('script, style, noscript, template');
322
+ const links = uniq([...document.querySelectorAll('a[href]')].map((a) => a.getAttribute('href')).filter((h) => h && h.trim() && h.trim() !== '#').map(key));
323
+ const fieldsIn = (root) => {
324
+ const n = {};
325
+ return uniq([...root.querySelectorAll('input, select, textarea, button[name]')].filter((f) => f.getAttribute('name') || !(f.tagName === 'INPUT' && /^(submit|button|reset|image|hidden)$/i.test(f.getAttribute('type') || ''))).map((f) => {
326
+ const hook = f.getAttribute('data-testid') || f.getAttribute('data-test') || f.getAttribute('data-cy');
327
+ if (f.getAttribute('name')) return f.getAttribute('name');
328
+ if (f.id && !/^(:|radix-|headlessui-|react-aria)/.test(f.id)) return `#${f.id}`;
329
+ if (hook) return `[${hook}]`;
330
+ const t = f.tagName === 'INPUT' ? (f.getAttribute('type') || 'text').toLowerCase() : f.tagName.toLowerCase();
331
+ n[t] = (n[t] || 0) + 1;
332
+ return `unnamed ${t} ${n[t]}`;
333
+ }));
334
+ };
335
+ const fields = fieldsIn(document);
336
+ const actions = uniq([...document.querySelectorAll('form[action]')].map((f) => `${(f.getAttribute('method') || 'get').toLowerCase()} ${key(f.getAttribute('action'))}`));
337
+ const forms = [...document.querySelectorAll('form')].map((f, i) => ({
338
+ key: f.id ? `#${f.id}` : f.getAttribute('action') ? `${(f.getAttribute('method') || 'get').toLowerCase()} ${key(f.getAttribute('action'))}` : `form ${i + 1}`,
339
+ attrs: [`method=${(f.getAttribute('method') || 'get').toLowerCase()}`, f.getAttribute('action') ? `action=${key(f.getAttribute('action'))}` : '', f.hasAttribute('novalidate') ? 'novalidate' : ''].filter(Boolean),
340
+ fields: fieldsIn(f),
341
+ hidden: uniq([...f.querySelectorAll('input[type="hidden"][name]')].map((c) => `${c.getAttribute('name')}=${c.getAttribute('value') || ''}`)),
342
+ submits: f.querySelectorAll('button:not([type]), button[type="submit"], input[type="submit"], input[type="image"]').length,
343
+ controls: uniq([...f.querySelectorAll('button, input, select, textarea')].map((c) => `${c.tagName.toLowerCase()}${c.getAttribute('type') ? `[type=${c.getAttribute('type')}]` : ''}${c.getAttribute('name') ? `[name=${c.getAttribute('name')}]` : ''}`)),
344
+ }));
345
+ const hooked = (selectors || []).filter((x) => { try { return !!document.querySelector(x); } catch { return false; } });
346
+ const headings = uniq([...document.querySelectorAll('h1, h2, h3, h4')].filter((h) => !quiet(h)).map((h) => text(h).toLowerCase().replace(/[\p{Extended_Pictographic}\uFE0F\u200d]/gu, '').replace(/\s+/g, ' ').trim()).filter((t) => t.length > 1));
347
+ const body = text(document.body);
348
+ const figures = uniq((body.match(/[$€£¥]\s?\d[\d,.]*\+?[kKmM]?|\b\d[\d,.]*\s?%|\b\d[\d,.]*[kKmM]?\+(?=\s|$)|\b\d{1,3}(?:,\d{3})+\b|\b\d{1,2}[:.]\d{2}\s?(?:am|pm)?\b|\b(?:19|20)\d{2}\b|\b\d+(?:\.\d+)?\s(?:hours?|minutes?|mins?|days?|weeks?|months?|years?|seconds?|ms|km|kg|g|ml|mm|cm|gb|mb|tb|x)\b|\+?\d[\d\s().-]{7,}\d/gi) || []).map((t) => t.replace(/\s+/g, '').toLowerCase()));
349
+ const images = uniq([...document.querySelectorAll('img[src]')].map((i) => (i.getAttribute('src') || '').split('?')[0].split('/').pop()));
350
+ const controls = document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"], select, textarea, input:not([type="hidden"])').length;
351
+ return { links, fields, actions, forms, hooked, headings, figures, images, controls, words: body ? body.split(' ').length : 0 };
352
+ }
353
+
354
+ function focusRing() {
355
+ let el = document.activeElement;
356
+ if (!el || el === document.body || el === document.documentElement) return null;
357
+ const box = el.getBoundingClientRect();
358
+ if (box.width < 4 || box.height < 4) el = (el.labels && el.labels[0]) || el.closest('label') || el.parentElement;
359
+ if (!el) return null;
360
+ el.style.setProperty('transition', 'none', 'important');
361
+ const cs = getComputedStyle(el);
362
+ const key = el.tagName.toLowerCase() + (el.classList[0] ? '.' + el.classList[0] : '') + ((el.textContent || '').trim() ? ` "${el.textContent.trim().slice(0, 18)}"` : '');
363
+ if (cs.outlineStyle === 'none' || !(parseFloat(cs.outlineWidth) > 0)) {
364
+ const active = document.activeElement;
365
+ const chain = []; for (let n = el, i = 0; n && n !== document.body && i < 5; n = n.parentElement, i++) chain.push(n);
366
+ const look = () => chain.flatMap((n) => [null, '::before', '::after'].map((ps) => { const c = getComputedStyle(n, ps); return [c.outlineStyle, c.outlineWidth, c.boxShadow, c.borderTopColor, c.borderBottomColor, c.borderLeftColor, c.borderBottomWidth, c.backgroundColor, c.backgroundImage, c.color, c.textDecorationLine, c.opacity].join('|'); })).join('~');
367
+ const focused = look();
368
+ active.blur();
369
+ const rest = look();
370
+ active.focus({ preventScroll: true });
371
+ return { key, ratio: null, bare: focused === rest };
372
+ }
373
+ const cv = document.createElement('canvas'); cv.width = cv.height = 1;
374
+ const cx = cv.getContext('2d', { willReadFrequently: true });
375
+ const rgb = (str) => { cx.clearRect(0, 0, 1, 1); cx.fillStyle = 'rgba(0, 0, 0, 0)'; cx.fillStyle = str; cx.fillRect(0, 0, 1, 1); const d = cx.getImageData(0, 0, 1, 1).data; return [d[0] / 255, d[1] / 255, d[2] / 255, d[3] / 255]; };
376
+ const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
377
+ const Y = (c) => 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
378
+ let node = parseFloat(cs.outlineOffset) < 0 ? el : el.parentElement;
379
+ let bg = null;
380
+ for (; node && node !== document.documentElement; node = node.parentElement) { const c = rgb(getComputedStyle(node).backgroundColor); if (c[3] >= 0.9) { bg = c; break; } }
381
+ if (!bg) { const b = rgb(getComputedStyle(document.body).backgroundColor); bg = b[3] > 0 ? b : [1, 1, 1, 1]; }
382
+ const ring = rgb(cs.outlineColor);
383
+ if (ring[3] < 0.5) return { key, ratio: null };
384
+ const a = Y(ring), b = Y(bg);
385
+ const r = el.getBoundingClientRect();
386
+ const pad = Math.max(0, parseFloat(cs.outlineOffset)) + parseFloat(cs.outlineWidth) + 3;
387
+ return { key, ring: ring.slice(0, 3), ratio: (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05), rect: { x: r.left, y: r.top, w: r.width, h: r.height }, pad };
388
+ }
389
+
390
+ async function pixelsAroundRing(page, ring) {
391
+ try {
392
+ await page.evaluate(() => { let el = document.activeElement; if (el) { const b = el.getBoundingClientRect(); if (b.width < 4 || b.height < 4) el = el.closest('label') || el.parentElement; } if (el) { el.setAttribute('data-hm-ring', ''); el.style.setProperty('outline-style', 'none', 'important'); el.style.setProperty('box-shadow', 'none', 'important'); } });
393
+ await frames(page);
394
+ const vw = await page.evaluate(() => [innerWidth, innerHeight, scrollX, scrollY]);
395
+ const x = Math.max(0, ring.rect.x - ring.pad), y = Math.max(0, ring.rect.y - ring.pad);
396
+ const clip = { x: x + vw[2], y: y + vw[3], width: Math.min(vw[0], ring.rect.x + ring.rect.w + ring.pad) - x, height: Math.min(vw[1], ring.rect.y + ring.rect.h + ring.pad) - y };
397
+ if (clip.width < 4 || clip.height < 4) return null;
398
+ const data = await page.screenshot({ clip, encoding: 'base64' });
399
+ return await page.evaluate(async (b64, inner) => {
400
+ const el = document.querySelector('[data-hm-ring]');
401
+ if (el) { el.removeAttribute('data-hm-ring'); el.style.removeProperty('outline-style'); el.style.removeProperty('box-shadow'); }
402
+ const img = new Image(); img.src = 'data:image/png;base64,' + b64; await img.decode();
403
+ const c = document.createElement('canvas'); c.width = img.width; c.height = img.height;
404
+ const x = c.getContext('2d'); x.drawImage(img, 0, 0);
405
+ const d = x.getImageData(0, 0, c.width, c.height).data;
406
+ const ch = [[], [], []];
407
+ for (let py = 0; py < c.height; py++) for (let px = 0; px < c.width; px++) {
408
+ if (px >= inner.l && px < inner.r && py >= inner.t && py < inner.b) continue;
409
+ const i = (py * c.width + px) * 4; ch[0].push(d[i]); ch[1].push(d[i + 1]); ch[2].push(d[i + 2]);
410
+ }
411
+ if (!ch[0].length) return null;
412
+ return ch.map((a) => a.sort((p, q) => p - q)[a.length >> 1] / 255);
413
+ }, data, { l: ring.rect.x - x, t: ring.rect.y - y, r: ring.rect.x - x + ring.rect.w, b: ring.rect.y - y + ring.rect.h });
414
+ } catch { return null; }
415
+ }
416
+
417
+ function observe(checkFold, meta) {
418
+ meta = meta || { genre: null, posture: 'restrained' };
419
+ const out = [];
420
+ const push = (gate, grade, evidence, fix, extra) => out.push({ gate: String(gate), grade, evidence, fix, ...(extra || {}) });
421
+ const describe = (el) => {
422
+ const cls = (el.className && typeof el.className === 'string') ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '';
423
+ return el.tagName.toLowerCase() + cls;
424
+ };
425
+ const visible = (el) => {
426
+ const cs = getComputedStyle(el);
427
+ if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) return false;
428
+ const r = el.getBoundingClientRect();
429
+ return r.width > 1 && r.height > 1;
430
+ };
431
+
432
+ if (document.documentElement.scrollWidth > window.innerWidth + 1) {
433
+ push(34, 'FAIL', `horizontal scroll: scrollWidth ${document.documentElement.scrollWidth} > viewport ${window.innerWidth}`,
434
+ 'overflow-x: clip on html and body; find the overflowing block');
435
+ }
436
+
437
+ if (window.innerWidth === 375) {
438
+ const clipped = (el) => { for (let q = el.parentElement; q && q !== document.body && q !== document.documentElement; q = q.parentElement) { const cs = getComputedStyle(q); if (/hidden|clip|auto|scroll/.test(`${cs.overflowX} ${cs.overflow}`)) return true; } return false; };
439
+ const spill = [...document.querySelectorAll('body *')].find((el) => { if (!visible(el) || getComputedStyle(el).position === 'fixed' || el.closest('svg') || clipped(el)) return false; const r = el.getBoundingClientRect(); return r.right > window.innerWidth + 2 || r.left < -2; });
440
+ if (spill && document.documentElement.scrollWidth <= window.innerWidth + 1) { const r = spill.getBoundingClientRect(); push(34, 'FAIL', `${describe(spill)} runs ${Math.round(Math.max(r.right - window.innerWidth, -r.left))}px past the 375 screen, hidden only by the page's clip`, 'Stack it under 60rem: drop the desktop column gap, widths and offsets on the phone'); }
441
+ for (const sl of document.querySelectorAll('[data-slot]')) { const cs = getComputedStyle(sl); if (cs.display === 'none' || !sl.getClientRects().length || sl.closest('[hidden]')) continue; const r = sl.getBoundingClientRect(); if (r.width < 40 || r.height < 40) { push('F18', 'WARN', `slot "${sl.getAttribute('data-slot')}" renders at ${Math.round(r.width)} x ${Math.round(r.height)}px on a phone`, 'The slot collapsed: give it width: 100% and an aspect-ratio in the stacked layout'); break; } }
442
+ const flush = [...document.querySelectorAll('p, li, h1, h2, h3, blockquote, dd')].find((el) => {
443
+ if (!visible(el) || el.closest('nav, pre, table, [data-slot]')) return false;
444
+ const own = [...el.childNodes].filter((n) => n.nodeType === 3 && n.textContent.trim().length >= 20);
445
+ if (own.reduce((n, t) => n + t.textContent.trim().length, 0) < 40) return false;
446
+ let left = Infinity, right = -Infinity;
447
+ for (const t of own) { const range = document.createRange(); range.selectNodeContents(t); for (const r of range.getClientRects()) { if (r.width < 2) continue; left = Math.min(left, r.left); right = Math.max(right, r.right); } }
448
+ return right - left > 120 && right <= window.innerWidth + 1 && (left < 8 || right > window.innerWidth - 8);
449
+ });
450
+ if (flush) push(72, 'REFLEX', `text runs to the edge of the screen (${describe(flush)})`, 'Give the page a gutter: 16px or more at each side on a phone');
451
+ }
452
+
453
+ for (const el of document.querySelectorAll('a, button, [role="button"], [role="tab"]')) {
454
+ if (!visible(el)) continue;
455
+ const text = (el.textContent || '').trim();
456
+ if (!text) continue;
457
+ const isButton = el.tagName === 'BUTTON' || el.getAttribute('role') === 'button' || /btn|button|cta/i.test(typeof el.className === 'string' ? el.className : '');
458
+ const block = el.closest('p, li, dd, td, blockquote, figcaption');
459
+ if (!isButton && block && (block.innerText || '').trim().length > text.length + 12) continue;
460
+ if ([...el.querySelectorAll('*')].some((d) => !d.closest('svg') && /^(block|flex|grid|list-item|table)$/.test(getComputedStyle(d).display))) continue;
461
+ const cs = getComputedStyle(el);
462
+ const lh = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.4;
463
+ const tops = [];
464
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
465
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
466
+ const host = n.parentElement;
467
+ if (!n.textContent.trim() || !host || host.closest('svg')) continue;
468
+ const hr = host.getBoundingClientRect();
469
+ if (hr.width <= 2 || hr.height <= 2) continue;
470
+ const range = document.createRange();
471
+ range.selectNodeContents(n);
472
+ for (const r of range.getClientRects()) if (r.width > 0 && r.height > 0) tops.push(r.top);
473
+ }
474
+ tops.sort((a, b) => a - b);
475
+ let lines = tops.length ? 1 : 0;
476
+ for (let i = 1; i < tops.length; i++) if (tops[i] - tops[i - 1] > lh * 0.6) lines++;
477
+ if (lines > 1) {
478
+ const primary = isButton || (!!el.closest('nav') && !el.closest('footer') && !el.closest('nav[aria-label*="bread" i]'));
479
+ push(primary ? '49a' : '49b', primary ? 'FAIL' : 'REFLEX', `${primary ? 'control' : 'link'} "${text.slice(0, 32)}" wraps to ${lines} lines (${describe(el)})`,
480
+ 'Shorten the label or set nowrap');
481
+ }
482
+ }
483
+
484
+ if (checkFold) {
485
+ const h1 = document.querySelector('h1');
486
+ if (h1 && visible(h1)) {
487
+ const hr = h1.getBoundingClientRect();
488
+ const scope = h1.closest('section, header, main, div') || document.body;
489
+ let cta = null;
490
+ for (const c of scope.querySelectorAll('a, button')) {
491
+ if (visible(c) && (c.textContent || '').trim()) { cta = c; break; }
492
+ }
493
+ const fold = 800;
494
+ if (hr.bottom > fold || hr.top < 0) {
495
+ push('44b', 'FAIL', `h1 crosses the 1280x800 fold (bottom ${Math.round(hr.bottom)}px)`,
496
+ 'Right-size the display clamp to the fold');
497
+ } else if (cta) {
498
+ const cr = cta.getBoundingClientRect();
499
+ if (cr.bottom > fold) {
500
+ push('44b', 'FAIL', `primary CTA below the 1280x800 fold (bottom ${Math.round(cr.bottom)}px)`,
501
+ 'Trim hero padding so the CTA fits the fold');
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ if (checkFold) {
508
+ const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
509
+ const cvs = document.createElement('canvas'); cvs.width = cvs.height = 1;
510
+ const ctx2 = cvs.getContext('2d', { willReadFrequently: true });
511
+ const toRgb = (str) => { ctx2.clearRect(0, 0, 1, 1); ctx2.fillStyle = 'rgba(0, 0, 0, 0)'; ctx2.fillStyle = str; ctx2.fillRect(0, 0, 1, 1); const d = ctx2.getImageData(0, 0, 1, 1).data; return { r: d[0] / 255, g: d[1] / 255, b: d[2] / 255, a: d[3] / 255 }; };
512
+ const parse = (s) => {
513
+ if (!s || s === 'transparent') return null;
514
+ const m = /^rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\)$/.exec(s.trim());
515
+ if (m) return { r: +m[1] / 255, g: +m[2] / 255, b: +m[3] / 255, a: m[4] == null ? 1 : +m[4] };
516
+ const alpha = /\/\s*([\d.]+%?)\s*\)$/.exec(s);
517
+ const c = toRgb(s);
518
+ if (alpha) c.a = alpha[1].endsWith('%') ? parseFloat(alpha[1]) / 100 : parseFloat(alpha[1]);
519
+ return c;
520
+ };
521
+ const lum = (c) => 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);
522
+ const ratio = (a, b) => {
523
+ const la = lum(a), lb = lum(b);
524
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
525
+ };
526
+ const bgOf = (el) => {
527
+ let node = el;
528
+ while (node && node !== document.documentElement) {
529
+ const c = parse(getComputedStyle(node).backgroundColor);
530
+ if (c && c.a >= 0.9) return c;
531
+ node = node.parentElement;
532
+ }
533
+ const root = parse(getComputedStyle(document.body).backgroundColor);
534
+ return root && root.a > 0 ? root : { r: 1, g: 1, b: 1, a: 1 };
535
+ };
536
+ const over = (top, base) => ({ r: top.r * top.a + base.r * (1 - top.a), g: top.g * top.a + base.g * (1 - top.a), b: top.b * top.a + base.b * (1 - top.a), a: 1 });
537
+ const gradStops = (el, base) => {
538
+ const out = [];
539
+ for (let node = el; node && node !== document.documentElement; node = node.parentElement) {
540
+ const cs2 = getComputedStyle(node);
541
+ if (/gradient/.test(cs2.backgroundImage)) {
542
+ const layers = []; let depth = 0, cur = '';
543
+ for (const ch of cs2.backgroundImage) { if (ch === '(') depth++; if (ch === ')') depth--; if (ch === ',' && depth === 0) { layers.push(cur); cur = ''; } else cur += ch; }
544
+ layers.push(cur);
545
+ const sizes = cs2.backgroundSize.split(',');
546
+ for (const [li, layer] of layers.entries()) {
547
+ const bh = /\s([\d.]+)px\s*$/.exec((sizes[li] || sizes[0] || '').trim()); if (bh && parseFloat(bh[1]) <= 6) continue;
548
+ if (!/^\s*(linear|radial|conic)-gradient/.test(layer) || /[\s,(]\d(?:\.\d+)?px\b/.test(layer)) continue;
549
+ for (const m of layer.matchAll(/(?:oklch|oklab|lab|lch|rgba?|hsla?|color)\([^()]*\)|#[0-9a-fA-F]{3,8}\b/g)) out.push(over(toRgb(m[0]), base));
550
+ }
551
+ }
552
+ const c = parse(cs2.backgroundColor);
553
+ if (c && c.a >= 0.9) break;
554
+ }
555
+ return out;
556
+ };
557
+ let reported = 0;
558
+ const seenSel = new Set();
559
+ for (const el of document.querySelectorAll('body *')) {
560
+ if (reported >= 10) break;
561
+ if (!visible(el) || el.closest('svg')) continue;
562
+ const hasText = [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim().length > 2);
563
+ if (!hasText) continue;
564
+ const cs = getComputedStyle(el);
565
+ const fg = parse(cs.color);
566
+ if (!fg || fg.a < 0.9) continue;
567
+ const bg = bgOf(el);
568
+ const size = parseFloat(cs.fontSize);
569
+ const weight = parseInt(cs.fontWeight, 10) || 400;
570
+ const large = size >= 24 || (size >= 18 && weight >= 700);
571
+ const threshold = large ? 3.0 : 4.5;
572
+ const key = describe(el);
573
+ const where = `${key} "${(el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 24)}"`;
574
+ const stops = gradStops(el, bg);
575
+ if (stops.length && !seenSel.has(key)) {
576
+ const rs = stops.map((c) => ratio(fg, c));
577
+ const worst = Math.min(...rs), best = Math.max(...rs);
578
+ if (best < threshold - 0.05) { seenSel.add(key); reported++; push(40, 'FAIL', `contrast ${best.toFixed(2)}:1 at best over the gradient behind ${where}`, 'Raise the contrast to threshold, or give the text its own ground'); }
579
+ else if (worst < threshold - 0.05) { seenSel.add(key); reported++; push(40, 'WARN', `contrast drops to ${worst.toFixed(2)}:1 over the gradient behind ${where}`, 'Check the text where the gradient is darkest or lightest; give the label its own ground'); }
580
+ continue;
581
+ }
582
+ const r = ratio(fg, bg);
583
+ if (r < threshold - 0.05 && !seenSel.has(key)) {
584
+ seenSel.add(key);
585
+ reported++;
586
+ el.setAttribute('data-hm-probe', String(reported));
587
+ const probe = { probe: reported, fg: [fg.r, fg.g, fg.b], threshold };
588
+ push(40, 'FAIL', `computed contrast ${r.toFixed(2)}:1 < ${threshold}:1 on ${where}`,
589
+ 'Raise the contrast to threshold', probe);
590
+ const btnish = el.closest('a, button, [class*="btn"], [class*="cta"]');
591
+ if (btnish && r < 1.6) {
592
+ push(41, 'FAIL', `button text ~ button fill on ${key} (${r.toFixed(2)}:1)`,
593
+ 'Use accent-ink or paper for the label', probe);
594
+ }
595
+ }
596
+ }
597
+ }
598
+
599
+ if (checkFold && meta.posture === 'restrained') {
600
+ const cs0 = getComputedStyle(document.documentElement);
601
+
602
+ const cv3 = document.createElement('canvas'); cv3.width = cv3.height = 1;
603
+ const cx3 = cv3.getContext('2d', { willReadFrequently: true });
604
+ const toRgb = (str) => {
605
+ str = (str || '').trim();
606
+ if (!str || str === 'transparent') return null;
607
+ cx3.clearRect(0, 0, 1, 1); cx3.fillStyle = 'rgba(1, 2, 3, 0.5)'; cx3.fillStyle = str;
608
+ if (cx3.fillStyle === 'rgba(1, 2, 3, 0.5)') return null;
609
+ cx3.fillRect(0, 0, 1, 1);
610
+ const d = cx3.getImageData(0, 0, 1, 1).data;
611
+ return [d[0], d[1], d[2], d[3] / 255];
612
+ };
613
+ const targets = ['--color-accent', '--color-accent-2']
614
+ .map((t) => toRgb(cs0.getPropertyValue(t).trim())).filter(Boolean);
615
+ if (targets.length) {
616
+ const near = (str) => {
617
+ const p = toRgb(str);
618
+ if (!p || p[3] < 0.5) return false;
619
+ return targets.some((t) => Math.abs(t[0] - p[0]) + Math.abs(t[1] - p[1]) + Math.abs(t[2] - p[2]) <= 30);
620
+ };
621
+ const CW = 16, COLS = Math.ceil(innerWidth / CW), ROWS = Math.ceil(800 / CW);
622
+ const grid = new Uint8Array(COLS * ROWS);
623
+ let walked = 0;
624
+ for (const el of document.querySelectorAll('body, body *')) {
625
+ if (++walked > 4000) break;
626
+ if (el !== document.body && !visible(el)) continue;
627
+ const cs = getComputedStyle(el);
628
+ const isBg = near(cs.backgroundColor);
629
+ const isTxt = near(cs.color) && parseFloat(cs.fontSize) >= 32 &&
630
+ [...el.childNodes].some((nd) => nd.nodeType === 3 && nd.textContent.trim());
631
+ if (!isBg && !isTxt) continue;
632
+ const r = el.getBoundingClientRect();
633
+ const x0 = Math.max(0, Math.floor(r.left / CW));
634
+ const x1 = Math.min(COLS - 1, Math.floor((Math.min(r.right, innerWidth) - 1) / CW));
635
+ const y0 = Math.max(0, Math.floor(r.top / CW));
636
+ const y1 = Math.min(ROWS - 1, Math.floor((Math.min(r.bottom, 800) - 1) / CW));
637
+ const turned = cs.transform !== 'none' && !/^matrix\(1, 0, 0, 1, /.test(cs.transform);
638
+ for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) {
639
+ if (turned) { const hit = document.elementFromPoint(x * CW + CW / 2, y * CW + CW / 2); if (!hit || !(hit === el || el.contains(hit))) continue; }
640
+ grid[y * COLS + x] = 1;
641
+ }
642
+ }
643
+ let filled = 0;
644
+ for (const c of grid) filled += c;
645
+ const pct = (100 * filled) / (COLS * ROWS);
646
+ const warnAt = meta.genre === 'atmospheric' ? 20 : 5;
647
+ const failAt = meta.genre === 'atmospheric' ? 30 : 8;
648
+ if (pct > failAt) {
649
+ push(23, 'FAIL', `accent paints ~${pct.toFixed(1)}% of the 1280x800 viewport (> ${failAt}%)`,
650
+ 'Retreat the accent to <= 5%; large colour belongs to paper/field tokens or a declared posture');
651
+ } else if (pct > warnAt) {
652
+ push(23, 'WARN', `accent paints ~${pct.toFixed(1)}% of the 1280x800 viewport`,
653
+ 'Confirm this is a declared posture surface, else retreat to <= 5%');
654
+ }
655
+ }
656
+ }
657
+
658
+ if (checkFold) {
659
+ const sticky = [];
660
+ for (const el of document.querySelectorAll('body *')) {
661
+ const cs = getComputedStyle(el);
662
+ if (cs.position === 'sticky' && parseFloat(cs.top) === 0) sticky.push(el);
663
+ }
664
+ if (sticky.length >= 2) {
665
+ push(56, 'FAIL', `two sticky top:0 elements: ${sticky.slice(0, 2).map(describe).join(' + ')}`,
666
+ 'Offset the second sticky by banner height');
667
+ }
668
+ }
669
+
670
+ return out;
671
+ }
672
+
673
+ // F26: a paragraph whose last line is one short word (a straggler), or a head of two or three lines whose last line is a stub
674
+ function lineBreaks() {
675
+ const out = [];
676
+ const vis = (el) => { const s = getComputedStyle(el); if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false; const r = el.getBoundingClientRect(); return r.width > 1 && r.height > 1; };
677
+ const lines = (el) => {
678
+ const rows = [];
679
+ const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
680
+ for (let t = w.nextNode(); t; t = w.nextNode()) {
681
+ if (!t.textContent.trim() || t.parentElement.closest('svg, [aria-hidden="true"]')) continue;
682
+ const rg = document.createRange(); rg.selectNodeContents(t);
683
+ for (const q of rg.getClientRects()) {
684
+ if (!q.width || !q.height) continue;
685
+ const row = rows.find((x) => Math.abs(x.mid - (q.top + q.height / 2)) < q.height / 3);
686
+ if (row) { row.l = Math.min(row.l, q.left); row.r = Math.max(row.r, q.right); } else rows.push({ mid: q.top + q.height / 2, l: q.left, r: q.right });
687
+ }
688
+ }
689
+ return rows.sort((a, b) => a.mid - b.mid).map((x) => x.r - x.l);
690
+ };
691
+ const bad = [];
692
+ for (const e of document.querySelectorAll('p, li, blockquote, h1, h2, h3')) {
693
+ if (bad.length >= 3 || !vis(e) || e.closest('nav, footer, svg, pre, code, table, [aria-hidden="true"], [data-slot]') || e.querySelector('br')) continue;
694
+ const text = (e.innerText || '').trim(); const words = text.split(/\s+/).length;
695
+ if (words < 4 || e.getBoundingClientRect().width < 200) continue;
696
+ const cs = getComputedStyle(e); const wrap = `${cs.textWrap || ''} ${cs.textWrapStyle || ''}`;
697
+ if (/flex|grid|table/.test(cs.display) || [...e.children].some((c) => /flex|grid|block|table/.test(getComputedStyle(c).display))) continue;
698
+ const ls = lines(e); if (ls.length < 2) continue;
699
+ const last = ls[ls.length - 1], widest = Math.max(...ls), prev = ls[ls.length - 2];
700
+ const head = /^H[123]$/.test(e.tagName);
701
+ if (head && ls.length <= 3 && !/balance/.test(wrap) && last < prev * 0.35) bad.push(`${e.tagName.toLowerCase()} "${text.replace(/\s+/g, ' ').slice(0, 32)}" ends on a stub`);
702
+ else if (!head && !/pretty|balance/.test(wrap) && !/\s/.test(text.split(/\s+/).slice(-1)[0]) && last < widest * 0.3 && ls.length >= 2 && (e.innerText || '').trim().split('\n').pop().trim().split(/\s+/).length === 1) bad.push(`"${text.replace(/\s+/g, ' ').slice(0, 32)}" leaves one word on its last line`);
703
+ }
704
+ if (bad.length) out.push({ gate: 'F26', grade: 'WARN', evidence: `[${innerWidth}px] ${bad.join('; ')}`, fix: 'text-wrap: balance on heads and pretty on body copy, or rewrite the line; a <br> goes only between phrases and is checked at 375' });
705
+ return out;
706
+ }
707
+
708
+ function observeSpace(atLaptop, heroOnly, heroPosture, photoShape, job) {
709
+ const freeFold = /^(poster|ledge|banner|custom)$/.test(heroPosture || '');
710
+ const out = [];
711
+ const push = (gate, grade, evidence, fix) => out.push({ gate: String(gate), grade, evidence, fix });
712
+ const vw = window.innerWidth, vh = window.innerHeight;
713
+ const vis = (el) => { const s = getComputedStyle(el); if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false; const r = el.getBoundingClientRect(); return r.width > 1 && r.height > 1; };
714
+ const abs = (el) => { const r = el.getBoundingClientRect(); return { top: r.top + scrollY, bottom: r.bottom + scrollY, left: r.left, right: r.right, w: r.width, h: r.height }; };
715
+ let secs = [...document.querySelectorAll('section, main > div, footer')].filter((s) => vis(s) && s.getBoundingClientRect().height > 40);
716
+ secs = secs.filter((s) => !secs.some((o) => o !== s && o.contains(s)));
717
+ const h1 = document.querySelector('h1');
718
+ const heroish = (el) => /^(SECTION|HEADER)$/.test(el.tagName) || [...el.classList].some((c) => /^hero(-[\w-]+)?$/i.test(c) || /(^|-)hero$/i.test(c));
719
+ let hero = null;
720
+ for (let n = h1; n && n !== document.body; n = n.parentElement) if (heroish(n)) { hero = n; break; }
721
+ hero = hero || [...document.querySelectorAll('section, header, [class]')].find(heroish) || secs[0];
722
+ const heroEnd = hero ? abs(hero).bottom : null;
723
+
724
+ let stack = 0, textBox = null, fig = null;
725
+ if (hero && h1) {
726
+ const cta = [...hero.querySelectorAll('a, button')].filter((c) => vis(c) && (c.textContent || '').trim() && !c.closest('nav'))[0];
727
+ const ctaTop = cta ? abs(cta).top : Infinity;
728
+ const texts = [...hero.querySelectorAll('h1, h2, p, small, [class*="sub"], [class*="lede"], [class*="eyebrow"], [class*="kicker"], [class*="label"]')].filter((e) => vis(e) && (e.innerText || '').trim() && !e.closest('nav, svg, table, figure, [data-slot], form, fieldset, label, output, [role="tabpanel"], [role="group"]') && abs(e).top < ctaTop);
729
+ const seen = [];
730
+ for (const e of texts) { if (seen.some((o) => o.contains(e) || e.contains(o))) continue; seen.push(e); }
731
+ stack = seen.length;
732
+ const ink = (el) => { let L = Infinity, T = Infinity, R = -Infinity, B = -Infinity; const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); for (let t = w.nextNode(); t; t = w.nextNode()) { if (!t.textContent.trim() || t.parentElement.closest('svg')) continue; const rg = document.createRange(); rg.selectNodeContents(t); for (const q of rg.getClientRects()) { if (!q.width || !q.height) continue; L = Math.min(L, q.left); T = Math.min(T, q.top); R = Math.max(R, q.right); B = Math.max(B, q.bottom); } } return L === Infinity ? abs(el) : { top: T + scrollY, bottom: B + scrollY, left: L, right: R }; };
733
+ const boxes = [h1, ...seen.filter((e) => Math.abs(abs(e).left - abs(h1).left) < 40)].map(ink);
734
+ textBox = { top: Math.min(...boxes.map((b) => b.top)), bottom: Math.max(...boxes.map((b) => b.bottom), cta ? abs(cta).bottom : 0), left: Math.min(...boxes.map((b) => b.left)), right: Math.max(...boxes.map((b) => b.right)) };
735
+ let bestA = 0;
736
+ const clipped = (el) => { const r = el.getBoundingClientRect(); let L = r.left, T = r.top, R = r.right, B = r.bottom; for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) { const cs = getComputedStyle(p); if (/hidden|clip/.test(`${cs.overflow} ${cs.overflowX} ${cs.overflowY}`)) { const q = p.getBoundingClientRect(); L = Math.max(L, q.left); T = Math.max(T, q.top); R = Math.min(R, q.right); B = Math.min(B, q.bottom); } } R = Math.min(R, vw); return { top: T + scrollY, bottom: B + scrollY, left: L, right: R, w: Math.max(0, R - L), h: Math.max(0, B - T) }; };
737
+ const beside = (b) => b.left >= textBox.right - 40 || b.right <= textBox.left + 40;
738
+ const consider = (e) => {
739
+ if (!vis(e) || e.closest('nav') || e.contains(h1) || seen.some((t) => e.contains(t))) return;
740
+ const b = clipped(e); const a = b.w * b.h;
741
+ if (b.w > 160 && b.h > 120 && a > bestA && b.w < vw * 0.95 && beside(b)) { fig = b; fig.el = e; bestA = a; }
742
+ };
743
+ for (const e of hero.querySelectorAll('svg, canvas, img, figure, video, picture, [data-slot], [class*="fig"], [class*="visual"], [class*="device"], [class*="demo"], [class*="shot"], [class*="illus"], [class*="panel"]')) consider(e);
744
+ if (!fig) {
745
+ const heroBg = getComputedStyle(hero).backgroundColor;
746
+ for (const e of hero.querySelectorAll('div, span, aside, i, b')) {
747
+ const cs = getComputedStyle(e);
748
+ if (cs.backgroundImage !== 'none' || (cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== heroBg) || parseFloat(cs.borderTopWidth) > 0) consider(e);
749
+ }
750
+ }
751
+ if (fig) {
752
+ fig.side = fig.left >= textBox.right - 40 ? 'right' : 'left';
753
+ let lca = h1.parentElement;
754
+ while (lca && !lca.contains(fig.el)) lca = lca.parentElement;
755
+ let col = h1;
756
+ while (col.parentElement && col.parentElement !== lca) col = col.parentElement;
757
+ const cb = abs(col);
758
+ fig.colEdge = fig.side === 'right' ? Math.max(textBox.right, cb.right) : Math.min(textBox.left, cb.left);
759
+ }
760
+ }
761
+
762
+ if (atLaptop) {
763
+
764
+ const words = [];
765
+ const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
766
+ let n;
767
+ while ((n = tw.nextNode())) {
768
+ const p = n.parentElement; if (!p || !vis(p) || p.closest('script, style, noscript, svg, [data-slot], [data-credits]')) continue;
769
+ const t = n.textContent.trim(); if (!t) continue;
770
+ const pr = p.getBoundingClientRect(); const c = t.split(/\s+/).filter((w) => /[A-Za-z]{2,}/.test(w)).length; if (c) words.push({ top: pr.top + scrollY, bottom: pr.bottom + scrollY, c });
771
+ }
772
+ const H = document.documentElement.scrollHeight;
773
+ let maxWords = 0, at = 0;
774
+ for (let y = 0; y < H; y += 50) { const w = Math.round(words.reduce((a, x) => { const h = Math.max(1, x.bottom - x.top); const ov = Math.max(0, Math.min(x.bottom, y + 900) - Math.max(x.top, y)); return a + x.c * Math.min(1, ov / h); }, 0)); if (w > maxWords) { maxWords = w; at = y; } }
775
+ if (maxWords > 95) push(58, 'FAIL', `${maxWords} words on one screen (from y=${at})`, 'One idea per screen: cut to about 80 words, make the section an object');
776
+ else if (maxWords > 80) push(58, 'WARN', `${maxWords} words on one screen (from y=${at})`, 'Close to the 80-word screen budget');
777
+ if (stack > 2) push(58, 'FAIL', `hero carries ${stack} text blocks before its actions`, 'Headline, one sentence, actions; delete the rest');
778
+
779
+ const clipAbs = (el, stop) => { const r0 = el.getBoundingClientRect(); let T = r0.top, B = r0.bottom; for (let q = el.parentElement; q && q !== document.body; q = q.parentElement) { const cs = getComputedStyle(q); if (/hidden|clip/.test(`${cs.overflow} ${cs.overflowY}`)) { const qr = q.getBoundingClientRect(); T = Math.max(T, qr.top); B = Math.min(B, qr.bottom); } if (q === stop) break; } return { top: T + scrollY, bottom: B + scrollY }; };
780
+ const content = (s) => { let top = Infinity, bottom = -Infinity; for (const c of s.querySelectorAll('*')) { if (!vis(c)) continue; if (!(c.innerText || '').trim() && !/^(SVG|IMG|CANVAS|VIDEO)$/.test(c.tagName) && getComputedStyle(c).backgroundColor === 'rgba(0, 0, 0, 0)') continue; if (c.children.length && ![...c.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()) && !/^(SVG)$/.test(c.tagName) && getComputedStyle(c).backgroundColor === 'rgba(0, 0, 0, 0)') continue; const b = clipAbs(c, s); if (b.bottom <= b.top) continue; top = Math.min(top, b.top); bottom = Math.max(bottom, b.bottom); } if (top === Infinity) { const b = abs(s); return b; } return { top, bottom }; };
781
+ // a paper is a colour or a painted image (a gradient, a split ground), whichever the nearest box sets
782
+ const paperOf = (el) => { for (let q = el; q; q = q.parentElement) { const cs = getComputedStyle(q), c = cs.backgroundColor; if (/gradient|url\(/.test(cs.backgroundImage)) return `${c}|${cs.backgroundImage}`; if (!/^(transparent|rgba\(0, 0, 0, 0\))$/.test(c)) return c; } return 'none'; };
783
+ const gaps = [], shifts = [];
784
+ for (let i = 0; !heroOnly && i < secs.length - 1; i++) { gaps.push(Math.round(content(secs[i + 1]).top - content(secs[i]).bottom)); shifts.push(paperOf(secs[i]) !== paperOf(secs[i + 1])); }
785
+ const real = gaps.filter((g) => g > -50);
786
+ const short = gaps.filter((g, i) => g > -50 && g < (shifts[i] ? 64 : 112));
787
+ if (real.length) {
788
+ const min = short.length ? Math.min(...short) : Infinity;
789
+ if (short.length) push(59, 'REFLEX', `smallest gap between sections is ${min}px (gaps: ${real.join(', ')})`, '120px or more between sections, by the --section-gap steps');
790
+ else if (real.length > 2 && new Set(real).size === 1) push(59, 'REFLEX', `every section gap is ${min}px`, 'Vary the rhythm across --section-gap-s, --section-gap and --section-gap-l');
791
+ }
792
+ if (!heroOnly && secs.length > 9) push(59, 'WARN', `${secs.length} sections`, 'Six to nine sections a page, hero and footer included; merge or cut');
793
+ else if (!heroOnly && secs.length < 5) push(77, 'WARN', `${secs.length} sections, hero and footer included`, 'A landing strip, not a page: six to nine sections, five on a thin prompt (layout.md § 3)');
794
+ if (!heroOnly) {
795
+ for (const sl of document.querySelectorAll('[data-slot]')) {
796
+ const cs = getComputedStyle(sl);
797
+ if (cs.display === 'none' || cs.visibility === 'hidden' || !sl.getClientRects().length || sl.closest('[hidden]')) continue;
798
+ const r = sl.getBoundingClientRect();
799
+ if (r.width < 40 || r.height < 40) { push('F18', 'WARN', `slot "${sl.getAttribute('data-slot')}" renders at ${Math.round(r.width)} x ${Math.round(r.height)}px`, 'The slot collapsed: give it a width from its track and a real aspect-ratio, never a percentage of a shrink-to-fit parent'); break; }
800
+ }
801
+ for (const e of document.querySelectorAll('p, li, dd, td, th, h2, h3, h4, figcaption, blockquote')) {
802
+ if (!vis(e) || e.closest('nav, svg, [aria-hidden="true"]')) continue;
803
+ const t = (e.innerText || '').trim();
804
+ if (t.length < 24) continue;
805
+ const cs = getComputedStyle(e);
806
+ const fs = parseFloat(cs.fontSize) || 16;
807
+ const lh = parseFloat(cs.lineHeight) || fs * 1.4;
808
+ const r = e.getBoundingClientRect();
809
+ if (r.width < fs * 8 && r.height / lh >= 3.6) { push('F17', 'WARN', `"${t.slice(0, 28)}" squeezed to ${Math.round(r.width)}px wide over ${Math.round(r.height / lh)} lines (${e.tagName.toLowerCase()}${e.classList[0] ? '.' + e.classList[0] : ''})`, 'A grid placement or a span ran wide; give the text its column back'); break; }
810
+ }
811
+ }
812
+
813
+ const nameOf = (e) => `${e.tagName.toLowerCase()}${e.getAttribute('data-slot') ? `[data-slot=${e.getAttribute('data-slot')}]` : e.classList[0] ? '.' + e.classList[0] : ''}`;
814
+ const dataish = /chart|graph|plot|spark|map|diagram|logo|wordmark|brand|icon|flag|qr|avatar/i;
815
+ const drawn = [];
816
+ for (const svg of document.querySelectorAll('svg')) {
817
+ if (!vis(svg) || svg.parentElement.closest('svg')) continue;
818
+ const r = svg.getBoundingClientRect();
819
+ if (r.width < 120 || r.height < 120) continue;
820
+ let named = false;
821
+ for (let q = svg; q && q !== document.body && !named; q = q.parentElement) named = dataish.test(`${typeof q.className === 'string' ? q.className : (q.className && q.className.baseVal) || ''} ${q.id || ''} ${q.getAttribute('aria-label') || ''} ${q.getAttribute('data-kind') || ''}`);
822
+ if (named || svg.querySelector('text')) continue;
823
+ const shapes = [...svg.querySelectorAll('path, ellipse, circle, polygon')];
824
+ const curvy = shapes.filter((s) => s.tagName === 'ellipse' || s.tagName === 'circle' && parseFloat(s.getAttribute('r') || 0) > 6 || /[CcQqSsAa]/.test(s.getAttribute('d') || '')).length;
825
+ const fills = new Set(shapes.map((s) => getComputedStyle(s).fill).filter((f) => f && f !== 'none'));
826
+ if (curvy >= 3 || (shapes.length >= 6 && fills.size >= 3)) drawn.push(`${nameOf(svg.parentElement || svg)} (${Math.round(r.width)}x${Math.round(r.height)}, ${shapes.length} shapes)`);
827
+ }
828
+ if (drawn.length) push(80, 'REFLEX', `drawn illustration: ${drawn.slice(0, 3).join('; ')}`, 'A photograph (theme.mjs photos), the product built, or type at scale; SVG is for icons, logos and labelled charts or diagrams (imagery.md § 7)');
829
+
830
+ const empty = [];
831
+ const shows = (box) => {
832
+ const b = box.getBoundingClientRect();
833
+ let area = 0;
834
+ for (const m of box.querySelectorAll('img, video, picture, canvas, iframe, svg')) { if (!vis(m) || (m.tagName.toLowerCase() === 'svg' && m.parentElement.closest('svg'))) continue; if (m.tagName === 'IMG' && !(m.getAttribute('src') || m.getAttribute('srcset') || '').trim()) continue; const q = m.getBoundingClientRect(); area += q.width * q.height; if (area >= b.width * b.height * 0.25) return true; }
835
+ const bits = [...box.querySelectorAll('*')].filter((e) => vis(e) && ((e.innerText || '').trim() || /^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.tagName)) && !e.children.length);
836
+ return bits.length >= 4 || (box.innerText || '').trim().length >= 80 || box.matches('[data-kind="product"]') || !!box.closest('[data-kind="product"]');
837
+ };
838
+ for (const sl of document.querySelectorAll('[data-slot]')) { if (!vis(sl) || sl.closest('[hidden]')) continue; const r = sl.getBoundingClientRect(); if (r.width < 160 || r.height < 120) continue; if (!shows(sl)) empty.push(`${nameOf(sl)} ${Math.round(r.width)}x${Math.round(r.height)}`); }
839
+ for (const e of document.querySelectorAll('div, figure, span, i, aside')) {
840
+ if (e.hasAttribute('data-slot') || e.closest('[data-slot]') || !vis(e)) continue;
841
+ const r = e.getBoundingClientRect(); const cs = getComputedStyle(e);
842
+ if (r.width < 300 || r.height < 200 || r.width > vw * 0.9 || !/gradient/.test(cs.backgroundImage) || /url\(/.test(cs.backgroundImage)) continue;
843
+ if ((e.innerText || '').trim().length > 60 || e.querySelector('img, video, canvas, svg, picture, input, table')) continue;
844
+ if (!shows(e)) empty.push(`${nameOf(e)} ${Math.round(r.width)}x${Math.round(r.height)}, a gradient with nothing in it`);
845
+ }
846
+ if (empty.length) push(81, 'REFLEX', `${empty.length} empty picture${empty.length > 1 ? 's' : ''}: ${empty.slice(0, 3).join('; ')}`, 'Put the picture in: the user\'s image, a library photograph (theme.mjs photos) or the product built at full fidelity; the wash is only a loading colour (imagery.md § 3)');
847
+
848
+ const marks = [];
849
+ const colourOf = (e) => { const cs = getComputedStyle(e); return e instanceof SVGElement ? `${cs.fill}|${cs.stroke}` : `${cs.backgroundColor}|${cs.borderTopColor}|${cs.color}`; };
850
+ for (const p of document.querySelectorAll('*')) {
851
+ if (p.children.length < 24 || !vis(p)) continue;
852
+ const kids = [...p.children].filter((k) => !/^(defs|title|desc|style|script)$/i.test(k.tagName));
853
+ if (kids.length < 24) continue;
854
+ const small = kids.filter((k) => { const q = k.getBoundingClientRect(); return !(k.textContent || '').trim() && q.width > 0 && q.height > 0 && ((q.width <= 24 && q.height <= 48) || q.width <= 6 || q.height <= 6); });
855
+ if (small.length < 24 || small.length < kids.length * 0.8) continue;
856
+ const tally = new Map(); for (const k of small) { const c = colourOf(k); tally.set(c, (tally.get(c) || 0) + 1); }
857
+ if (Math.max(...tally.values()) < small.length * 0.6 || tally.size > 4) continue;
858
+ const sizes = small.map((k) => { const q = k.getBoundingClientRect(); return Math.round(q.width) * 1000 + Math.round(q.height); });
859
+ const common = Math.max(...[...new Set(sizes)].map((s) => sizes.filter((x) => x === s).length));
860
+ if (common < small.length * 0.7) continue;
861
+ if (marks.some((m) => m.el.contains(p) || p.contains(m.el))) continue;
862
+ marks.push({ el: p, text: `${nameOf(p)} holds ${small.length} identical marks` });
863
+ }
864
+ const ONE_FOR_EACH = /\bone (?:mark|dot|stroke|tick|line|square|bar|cell|pin|block|circle|seed|bead|stripe|notch)s? (?:for|per) (?:each|every)\b/i;
865
+ const ONE_FOR_EACH_BARE = /\bone (?:for|per) (?:each|every)\b/i;
866
+ for (const e of document.querySelectorAll('[aria-label], figcaption, svg title, p, small, span')) {
867
+ if (marks.length >= 2) break;
868
+ const t = `${e.getAttribute('aria-label') || ''} ${(e.textContent || '').length < 160 ? e.textContent : ''}`;
869
+ const counting = ONE_FOR_EACH.test(t) || (ONE_FOR_EACH_BARE.test(t) && (e.matches('figcaption') || (e.getAttribute('aria-label') && e.closest('svg, figure, [role="img"]'))));
870
+ if (counting && !marks.some((m) => m.el.contains(e))) marks.push({ el: e, text: `"${t.trim().slice(0, 60)}"` });
871
+ }
872
+ if (marks.length) push(82, 'REFLEX', `counting device: ${marks.slice(0, 2).map((m) => m.text).join('; ')}`, 'Set the number in type at the size it deserves; never one mark for each thing (imagery.md § 7)');
873
+
874
+ if (heroEnd != null) {
875
+ const foot = [...document.querySelectorAll('body footer, [role="contentinfo"]')].filter(vis).pop();
876
+ const docH = foot ? Math.min(document.documentElement.scrollHeight, abs(foot).top) : document.documentElement.scrollHeight, from = Math.max(heroEnd, vh);
877
+ const box = document.querySelector('.container') || document.body;
878
+ const cw = Math.min(box.getBoundingClientRect().width || vw, vw);
879
+ let area = 0, n = 0;
880
+ const seenA = [];
881
+ const add = (el) => { if (!vis(el) || el.closest('footer') || seenA.some((o) => o.contains(el))) return; const b = abs(el); const top = Math.max(b.top, from), bot = Math.min(b.bottom, docH); if (bot - top < 80 || b.w < 160) return; seenA.push(el); area += (bot - top) * Math.min(b.w, cw); n++; };
882
+ for (const m of document.querySelectorAll('img, video')) { if (m.closest('[data-kind="product"], [data-product]')) continue; const nw = m.naturalWidth || m.videoWidth || 0; if (m.tagName === 'IMG' && /\.svg(\?|$)/i.test(m.currentSrc || m.src || '')) continue; if (m.tagName === 'IMG' && nw && nw < 200) continue; add(m); }
883
+ for (const e of document.querySelectorAll('section *, main *')) { const bi = getComputedStyle(e).backgroundImage; if (/url\(/.test(bi) && !/svg|data:image\/svg/i.test(bi)) add(e); }
884
+ const share = area / Math.max(1, (docH - from) * cw);
885
+ if (share > (photoShape ? 0.45 : 0.36) && n >= 3) push(83, 'REFLEX', `photographs cover ${Math.round(share * 100)}% of the page below the hero (${n} pictures${photoShape ? ', a photograph-led shape' : ''})`, 'Balance the page: cut or shrink pictures until they cover a quarter to a third of it, and carry the freed sections in type, figures, the product or a list (imagery.md § 5)');
886
+ }
887
+ if (heroEnd != null && !/subscribe|join|attend|listen|visit|newsletter|waitlist/.test(job || '')) {
888
+ const foot = [...document.querySelectorAll('body footer, [role="contentinfo"]')].filter(vis).pop();
889
+ const before = foot ? secs.filter((x) => x !== foot && abs(x).bottom <= abs(foot).top + 2).pop() : secs[secs.length - 1];
890
+ // a form whose submit books, asks or requests is the job's own action, not a mailing list
891
+ const asks = (f) => { const t = [...f.querySelectorAll('button, input[type=submit]')].map((b) => b.innerText || b.value || '').join(' '); return /\b(book|enquir|inquir|request|contact|walkthrough|demo|quote|reserve|apply|start|create|order|buy|download|try)/i.test(t) && !/newsletter|updates|subscribe|the list|mailing/i.test(t); };
892
+ const signup = (box) => box && [...box.querySelectorAll('form, [role="form"]')].find((f) => { if (asks(f)) return false; const fields = [...f.querySelectorAll('input:not([type=hidden]):not([type=submit]):not([type=checkbox]):not([type=radio]), textarea, select')].filter(vis); return fields.length >= 1 && fields.length <= 2 && fields.some((x) => x.type === 'email' || /e-?mail/i.test(`${x.name} ${x.id} ${x.placeholder} ${x.getAttribute('autocomplete') || ''}`)); });
893
+ const hit = signup(before) || signup(foot);
894
+ if (hit) push(85, 'REFLEX', `the page closes on an email sign-up${job ? ` on a ${job} page` : ' and the stamp names no job'}`, 'End on what the visitor came to do (the booking, the buy box, the download, the next date, the latest issue) or on the last strong section; an email close is for a subscribe or join page, and the stamp says job: (anatomy.md)');
895
+ }
896
+
897
+ let heroFoot = null;
898
+ if (hero && !/^(poster|ledge)$/.test(heroPosture || '')) {
899
+ const hb = abs(hero);
900
+ let cTop = Infinity, cBottom = -Infinity;
901
+ for (const c of hero.querySelectorAll('*')) {
902
+ // the site nav is not hero content; a nav lower in the hero (a band of section links) is
903
+ const nv = c.closest('nav');
904
+ if (!vis(c) || (nv && abs(nv).top <= hb.top + 220) || c === hero) continue;
905
+ const cs = getComputedStyle(c);
906
+ const painted = (c.innerText || '').trim() || /^(SVG|IMG|CANVAS|VIDEO|PICTURE)$/.test(c.tagName) || cs.backgroundColor !== 'rgba(0, 0, 0, 0)' || cs.backgroundImage !== 'none' || parseFloat(cs.borderTopWidth) > 0;
907
+ if (!painted || /fixed|sticky/.test(cs.position)) continue;
908
+ const b = abs(c); cTop = Math.min(cTop, b.top); cBottom = Math.max(cBottom, Math.min(b.bottom, hb.bottom));
909
+ }
910
+ let navBottom = hb.top;
911
+ for (const n of document.querySelectorAll('header, nav, [class*="nav"], [class*="bar"]')) { if (!vis(n) || hero.contains(n) && !/fixed|sticky/.test(getComputedStyle(n).position)) continue; const cs = getComputedStyle(n); const b = abs(n); if ((/fixed|sticky/.test(cs.position) || hero.contains(n)) && b.top <= hb.top + 40 && b.bottom < hb.top + 220) navBottom = Math.max(navBottom, b.bottom); }
912
+ const after = secs.find((x) => x !== hero && !hero.contains(x) && abs(x).top >= hb.bottom - 4);
913
+ const bottomEdge = after ? Math.min(content(after).top, hb.bottom + 240) : hb.bottom;
914
+ const topPad = cTop - navBottom, bottomPad = bottomEdge - cBottom;
915
+ if (heroPosture !== 'custom' && isFinite(topPad) && isFinite(bottomPad) && topPad > 24 && bottomPad < topPad - 8) push('44a', 'REFLEX', `hero content sits low: ${Math.round(topPad)}px above it, ${Math.round(bottomPad)}px below`, 'Centre the content in its screen: the space below 1.0 to 1.5 times the space above (a Ledge sits low on purpose; say Ledge in the stamp)');
916
+ heroFoot = { cBottom, navBottom };
917
+ }
918
+ if (heroEnd != null && !freeFold) {
919
+ if (heroEnd > vh + 8) push('44c', 'REFLEX', `hero runs past the 1440x900 fold (hero ends at ${Math.round(heroEnd)}px)`, 'Size the display and the visual so the hero is one screen');
920
+ else if (heroEnd < vh * 0.84 && secs.length > 1) push('44c', 'REFLEX', `hero ends at ${Math.round(heroEnd)}px of a 900px screen`, 'Fill the first screen with the visual or the headline, not with copy');
921
+ }
922
+ if (fig && textBox && !freeFold) {
923
+ const off = Math.round((fig.top + fig.bottom) / 2 - (textBox.top + textBox.bottom) / 2);
924
+ const gap = Math.round(fig.side === 'right' ? fig.left - fig.colEdge : fig.colEdge - fig.right);
925
+ const edge = Math.round(fig.side === 'right' ? vw - fig.right : fig.left);
926
+ if (Math.abs(off) > 60) push('44c', 'REFLEX', `hero visual centre is ${off}px from the text block centre`, 'Align the visual beside the text');
927
+ const inkGap = Math.round(fig.side === 'right' ? fig.left - textBox.right : textBox.left - fig.right);
928
+ if (inkGap > 240 && gap <= 120) push('44c', 'REFLEX', `hero text ends ${inkGap}px short of its visual`, 'Widen the text column or bring the visual in: the text and the visual stand 48 to 120px apart (layout.md § 6)');
929
+ if (gap > 120 || (edge < 8 && gap > 48 && heroPosture !== 'stage')) push('44c', 'REFLEX', `hero visual sits ${gap}px from the text and ${edge}px from the viewport edge`, 'Bring the visual to within 48 to 120px of the text; never pin it to the corner');
930
+ }
931
+
932
+ if (heroFoot && hero && isFinite(heroFoot.cBottom)) {
933
+ const hb = abs(hero);
934
+ const after = secs.find((x) => x !== hero && !hero.contains(x) && abs(x).top >= hb.bottom - 4);
935
+ const nextTop = after ? content(after).top : Infinity;
936
+ const band = Math.round(Math.min(vh, nextTop) - heroFoot.cBottom);
937
+ if (heroFoot.cBottom < vh && band > 160) push(79, 'REFLEX', `${band}px of nothing between the hero's last content (y ${Math.round(heroFoot.cBottom)}) and ${nextTop < vh ? 'the next section' : 'the fold'}`, 'Centre the content in the screen and grow the visual or the head (layout.md § 6); never fill it with copy');
938
+ }
939
+
940
+ if (!heroOnly) {
941
+ const below = secs.filter((x) => x !== hero && !(hero && (hero.contains(x) || x.contains(hero))) && x.tagName !== 'FOOTER' && !x.closest('footer, nav') && (!hero || abs(x).top >= abs(hero).bottom - 4));
942
+ const leavesOf = (root) => {
943
+ const out = [];
944
+ const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
945
+ for (let t = w.nextNode(); t; t = w.nextNode()) { if (!t.textContent.trim()) continue; const p = t.parentElement; if (!p || !vis(p) || p.closest('script, style, noscript')) continue; const rg = document.createRange(); rg.selectNodeContents(t); for (const q of rg.getClientRects()) if (q.width > 1 && q.height > 1) out.push({ top: q.top + scrollY, bottom: q.bottom + scrollY, left: q.left, right: q.right }); }
946
+ for (const e of root.querySelectorAll('*')) {
947
+ if (!vis(e) || (e.closest('svg') && e.tagName.toLowerCase() !== 'svg')) continue;
948
+ const cs = getComputedStyle(e);
949
+ const painted = /^(IMG|SVG|CANVAS|VIDEO|PICTURE|IFRAME|INPUT|TEXTAREA|SELECT|TABLE)$/i.test(e.tagName) || e.hasAttribute('data-slot') || cs.backgroundImage !== 'none' || cs.backgroundColor !== 'rgba(0, 0, 0, 0)' || (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0) + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0) >= 2;
950
+ if (!painted) continue;
951
+ const b = e.getBoundingClientRect(); if (b.width * b.height < 64 || (b.width > vw * 0.97 && !/^(IMG|PICTURE|VIDEO|CANVAS|SVG)$/i.test(e.tagName) && !e.hasAttribute('data-slot'))) continue;
952
+ out.push({ top: b.top + scrollY, bottom: b.bottom + scrollY, left: b.left, right: b.right });
953
+ }
954
+ return out;
955
+ };
956
+ // content a rail scrolls past the screen's edge does not widen the page's measure
957
+ const span = (ls) => ls.filter((b) => b.right > 0 && b.left < vw).reduce((a, b) => ({ left: Math.min(a.left, Math.max(0, b.left)), right: Math.max(a.right, Math.min(vw, b.right)) }), { left: Infinity, right: -Infinity });
958
+ const all = span([...(hero ? [hero] : []), ...below].flatMap(leavesOf));
959
+ const W = all.right - all.left;
960
+ const h1fs = h1 ? parseFloat(getComputedStyle(h1).fontSize) : 0;
961
+ const heads = below.flatMap((x) => [...x.querySelectorAll('h2')]).filter((h) => vis(h) && !h.closest('nav, footer'));
962
+ if (heads.length) {
963
+ const sizes = heads.map((h) => parseFloat(getComputedStyle(h).fontSize)).sort((a, b) => a - b);
964
+ const med = sizes[Math.floor(sizes.length / 2)];
965
+ if (med < 40) push(78, 'REFLEX', `section heads render at ${Math.round(med)}px${h1fs ? ` under a ${Math.round(h1fs)}px hero head` : ''}`, 'h2 at --text-section (about 52px at 1440), item titles at --text-title (layout.md § 2)');
966
+ }
967
+ const thin = [];
968
+ let bands = 0;
969
+ if (W > 400) for (const sec of below) {
970
+ const ls = leavesOf(sec);
971
+ if (!ls.length) continue;
972
+ const u = span(ls);
973
+ const wPct = (Math.min(u.right, all.right) - Math.max(u.left, all.left)) / W;
974
+ const words = (sec.innerText || '').split(/\s+/).filter((x) => /[A-Za-z0-9]/.test(x)).length;
975
+ let mass = false, statement = false;
976
+ const secBg = getComputedStyle(sec).backgroundColor;
977
+ for (const e of sec.querySelectorAll('img, svg, canvas, video, picture, figure, iframe, [data-slot]')) { if (!vis(e) || (e.tagName.toLowerCase() === 'svg' && e.closest('[data-slot], figure') && e.closest('[data-slot], figure') !== e)) continue; if (e.getBoundingClientRect().height >= 300) { mass = true; break; } }
978
+ if (!mass) for (const e of sec.querySelectorAll('div, article, aside, ul, ol, dl, table, form, a')) {
979
+ if (!vis(e)) continue;
980
+ const cs = getComputedStyle(e); const r = e.getBoundingClientRect();
981
+ const filled = (cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== secBg) || cs.backgroundImage !== 'none' || (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0) + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0) >= 2;
982
+ if (filled && r.height >= 240 && r.width >= W * 0.35 && r.width < vw * 0.97) { mass = true; break; }
983
+ }
984
+ if (!mass) for (const t of sec.querySelectorAll('table, ul, ol, dl, div, [class]')) {
985
+ if (!vis(t)) continue;
986
+ if (t.tagName === 'TABLE') { const r = t.getBoundingClientRect(); if (t.querySelectorAll('tr').length >= 4 && r.width >= W * 0.6) { mass = true; break; } continue; }
987
+ const kids = [...t.children].filter((k) => vis(k) && k.getBoundingClientRect().height > 20);
988
+ if (kids.length < 3) continue;
989
+ const rs = kids.map((k) => k.getBoundingClientRect());
990
+ const across = rs.every((r) => Math.abs(r.top - rs[0].top) < 12) && new Set(rs.map((r) => Math.round(r.left))).size === rs.length;
991
+ const spread = Math.max(...rs.map((r) => r.right)) - Math.min(...rs.map((r) => r.left));
992
+ const tall = Math.max(...rs.map((r) => r.bottom)) - Math.min(...rs.map((r) => r.top));
993
+ if ((across && spread >= W * 0.7) || (!across && spread >= W * 0.4 && tall >= 120)) { mass = true; break; }
994
+ }
995
+ if (!mass && [...sec.querySelectorAll('input:not([type=hidden]), select, textarea')].filter(vis).length >= 2) mass = true;
996
+ for (const e of sec.querySelectorAll('h2, h3, p, blockquote, h2 span, p span, div, output, [contenteditable]')) { if (!vis(e) || (/^(DIV|OUTPUT)$/.test(e.tagName) && ![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()))) continue; const cs = getComputedStyle(e); const fs = parseFloat(cs.fontSize); const lh = parseFloat(cs.lineHeight) || fs * 1.15; const r = e.getBoundingClientRect(); const n = (e.innerText || '').split(/\s+/).filter(Boolean).length; if ((fs >= 40 && n <= 40 && r.height >= lh * 1.8 && r.width >= W * 0.5) || fs >= 72) { statement = true; break; } }
997
+ if (statement) mass = true;
998
+ const painted = getComputedStyle(sec).backgroundColor !== 'rgba(0, 0, 0, 0)' || getComputedStyle(sec).backgroundImage !== 'none';
999
+ if (!mass && painted && words <= 40 && sec.querySelector('a, button, form') && !bands) { bands++; continue; }
1000
+ if ((wPct < 0.72 && !statement) || !mass) thin.push(`${(sec.querySelector('h2, h3') || {}).innerText ? `"${sec.querySelector('h2, h3').innerText.trim().slice(0, 28)}"` : sec.tagName.toLowerCase() + (sec.className && typeof sec.className === 'string' ? '.' + sec.className.trim().split(/\s+/)[0] : '')} (${Math.round(wPct * 100)}% wide${mass ? '' : ', no mass'})`);
1001
+ }
1002
+ if (thin.length) push(77, 'REFLEX', `${thin.length} thin section${thin.length > 1 ? 's' : ''}: ${thin.slice(0, 3).join('; ')}`, 'Widen the body to ten of twelve columns and give it mass: a visual 300px tall or more, a set of three across, rows, or a statement (layout.md § 4 and § 5)');
1003
+ }
1004
+ } else {
1005
+
1006
+ if (textBox && textBox.left < 120) push('44d', 'FAIL', `hero text starts ${Math.round(textBox.left)}px from the edge at 1920`, 'Cap the container at 1360 to 1520px and centre it');
1007
+ if (heroEnd != null && heroEnd > vh + 8) push('44d', 'FAIL', `hero runs past the 1920x1080 screen (hero ends at ${Math.round(heroEnd)}px)`, 'Cap vw-sized visuals and display sizes');
1008
+ const inkWidth = (e) => { const range = document.createRange(); range.selectNodeContents(e); return range.getBoundingClientRect().width; };
1009
+ const wide = [...document.querySelectorAll('p, h1, h2, h3, li')].filter((e) => vis(e) && !e.querySelector('p, h1, h2, h3, h4, div, figure, ul, ol, table, section') && (e.innerText || '').trim().length > 40 && inkWidth(e) > 1600);
1010
+ if (wide.length) push('44d', 'FAIL', `${wide.length} text block${wide.length > 1 ? 's run' : ' runs'} wider than 1600px at 1920 (first: ${wide[0].tagName.toLowerCase()} "${(wide[0].innerText || '').trim().slice(0, 28)}")`, 'Cap the container; hold the measure');
1011
+ }
1012
+ return out;
1013
+ }
1014
+
1015
+ function observeQuality(scope) {
1016
+ const out = [];
1017
+ const push = (gate, grade, evidence, fix) => out.push({ gate: String(gate), grade, evidence, fix });
1018
+ const vis = (el) => { const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) return false; const r = el.getBoundingClientRect(); return r.width > 1 && r.height > 1; };
1019
+ const name = (el) => el.tagName.toLowerCase() + (el.classList && el.classList[0] ? '.' + el.classList[0] : '');
1020
+ const abs = (r) => ({ top: r.top + scrollY, bottom: r.bottom + scrollY, left: r.left, right: r.right });
1021
+ const clear = (c) => /^(transparent|rgba\(0, 0, 0, 0\))$/.test(c);
1022
+ const quiet = (el) => !!el.closest('script, style, noscript, [aria-hidden="true"], dialog:not([open])') || !!(el.parentElement && el.parentElement.closest('svg'));
1023
+ const all = [...document.body.querySelectorAll('*')].filter((el) => !quiet(el) && vis(el));
1024
+
1025
+ const boxes = [];
1026
+ for (const el of all) {
1027
+ const media = /^(IMG|svg|CANVAS|VIDEO|PICTURE|IFRAME|INPUT|BUTTON|SELECT|TEXTAREA)$/.test(el.tagName) || el.hasAttribute('data-slot');
1028
+ if (!media && ![...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) continue;
1029
+ boxes.push({ el, ...abs(el.getBoundingClientRect()) });
1030
+ }
1031
+ const overlapX = (a, b) => Math.min(a.right, b.right) - Math.max(a.left, b.left) > 8;
1032
+ const crowded = [];
1033
+ for (const h of document.querySelectorAll('h2, h3, h4')) {
1034
+ if (!vis(h) || quiet(h) || h.closest('nav, footer, table, [data-slot]')) continue;
1035
+ let small = false;
1036
+ for (let p = h.parentElement; p && p !== document.body; p = p.parentElement) {
1037
+ const cs = getComputedStyle(p);
1038
+ if (!clear(cs.backgroundColor) || parseFloat(cs.borderTopWidth) > 0 || cs.boxShadow !== 'none') { small = p.getBoundingClientRect().height < 200; break; }
1039
+ }
1040
+ if (small) continue;
1041
+ const hb = abs(h.getBoundingClientRect());
1042
+ let above = null, below = null;
1043
+ for (const b of boxes) {
1044
+ if (h.contains(b.el) || b.el.contains(h) || !overlapX(b, hb)) continue;
1045
+ if (b.bottom <= hb.top + 1 && (!above || b.bottom > above.bottom)) above = b;
1046
+ if (b.top >= hb.bottom - 1 && (!below || b.top < below.top)) below = b;
1047
+ }
1048
+ if (!above || !below) continue;
1049
+ const up = hb.top - above.bottom, down = below.top - hb.bottom;
1050
+ if (down < 6 || down > 160) continue;
1051
+ if (up < 0.75 * down && down - up >= 12) crowded.push(`${name(h)} "${(h.innerText || '').trim().slice(0, 18)}" ${Math.round(up)}px over, ${Math.round(down)}px under`);
1052
+ }
1053
+ if (crowded.length >= 2) push(71, 'REFLEX', `heads nearer what precedes them than what they head: ${crowded.slice(0, 2).join('; ')}`, 'At least twice the space above a head as below it');
1054
+
1055
+ const tight = [];
1056
+ for (const el of all) {
1057
+ if (tight.length >= 3) break;
1058
+ if (/^(SECTION|MAIN|HEADER|FOOTER|NAV|ARTICLE|INPUT|TEXTAREA|SELECT|svg|IMG|TABLE|THEAD|TBODY|TR|TD|TH|PRE|CODE|IFRAME|VIDEO|CANVAS|DIALOG)$/.test(el.tagName) || el.closest('[data-slot], table')) continue;
1059
+ const r = el.getBoundingClientRect();
1060
+ if (r.width <= 100 || r.height <= 30 || r.width > innerWidth * 0.9) continue;
1061
+ const cs = getComputedStyle(el);
1062
+ if (cs.display === 'inline') continue;
1063
+ if (/auto|scroll/.test(cs.overflowX + cs.overflowY)) continue;
1064
+ const bw = ['Top', 'Right', 'Bottom', 'Left'].map((k) => parseFloat(cs[`border${k}Width`]) || 0);
1065
+ const inked = (k) => bw[k] >= 1 && !clear(cs[`border${['Top', 'Right', 'Bottom', 'Left'][k]}Color`]);
1066
+ let under = 'rgb(255, 255, 255)';
1067
+ for (let p = el.parentElement; p; p = p.parentElement) { const c = getComputedStyle(p).backgroundColor; if (!clear(c)) { under = c; break; } }
1068
+ const filled = !clear(cs.backgroundColor) && cs.backgroundColor !== under;
1069
+ if (!filled && !(inked(1) && inked(3))) continue;
1070
+ const sides = ['left', 'right', 'top', 'bottom'];
1071
+ let L = Infinity, T = Infinity, Rr = -Infinity, B = -Infinity, fs = parseFloat(cs.fontSize) || 16;
1072
+ const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
1073
+ for (let t = walk.nextNode(); t; t = walk.nextNode()) {
1074
+ const host = t.parentElement;
1075
+ if (!t.textContent.trim() || !host || host.closest('svg') || !vis(host)) continue;
1076
+ let nested = false;
1077
+ for (let p = host; p && p !== el; p = p.parentElement) {
1078
+ const ps = getComputedStyle(p);
1079
+ if (/absolute|fixed/.test(ps.position) || (!clear(ps.backgroundColor) && ps.backgroundColor !== cs.backgroundColor) || (parseFloat(ps.borderTopWidth) >= 1 && parseFloat(ps.borderLeftWidth) >= 1)) { nested = true; break; }
1080
+ }
1081
+ if (nested) continue;
1082
+ const rg = document.createRange(); rg.selectNodeContents(t);
1083
+ const hf = parseFloat(getComputedStyle(host).fontSize) || 16; const trim = hf >= 40 ? 0.2 * hf : 0;
1084
+ for (const q of rg.getClientRects()) { if (!q.width || !q.height) continue; L = Math.min(L, q.left); T = Math.min(T, q.top + trim); Rr = Math.max(Rr, q.right); B = Math.max(B, q.bottom - trim); fs = parseFloat(getComputedStyle(host).fontSize) || fs; }
1085
+ }
1086
+ if (L === Infinity) continue;
1087
+ const side = Math.max(8, Math.min(0.5 * fs, 24)) - 0.5, end = Math.max(4, Math.min(0.3 * fs, 12)) - 0.5;
1088
+ const inset = { left: L - (r.left + bw[3]), right: r.right - bw[1] - Rr, top: T - (r.top + bw[0]), bottom: r.bottom - bw[2] - B };
1089
+ const short = Object.entries(inset).filter(([k, v]) => sides.includes(k) && v < (k === 'left' || k === 'right' ? side : end));
1090
+ if (short.length) tight.push(`${name(el)} ${short.map(([k, v]) => `${k} ${Math.round(v)}px`).join(', ')}`);
1091
+ }
1092
+ if (tight.length) push(72, 'REFLEX', `text crowds the edge of its box: ${tight.join('; ')}`, 'Pad the box: max(8px, 0.5em) at the sides, max(4px, 0.3em) above and below');
1093
+
1094
+ const controls = all.filter((el) => (el.tagName === 'BUTTON' || el.getAttribute('role') === 'button' || (el.tagName === 'A' && /btn|button|cta/i.test(String(el.className))) || (el.tagName === 'INPUT' && /^(submit|button|text|email|search|tel|url|password|number)$/.test(el.type))) && !el.closest('nav, header, dialog, [role="dialog"], [data-slot], [role="tablist"]') && (el.tagName === 'INPUT' || (el.innerText || '').trim()));
1095
+ const offSize = controls.map((el) => ({ el, h: el.getBoundingClientRect().height })).filter((c) => c.h > 8 && (c.h < (scope === 'app' ? 30 : 42) || c.h > 54));
1096
+ if (offSize.length) push('39b', 'REFLEX', `control height outside ${scope === 'app' ? '32 to 52px' : '44 to 52px'}: ${offSize.slice(0, 3).map((c) => `${name(c.el)} ${Math.round(c.h)}px`).join(', ')}`, 'Marketing controls stand 44 to 52px; app UI 32 to 40px with a 44px hit target');
1097
+
1098
+ const long = [];
1099
+ for (const el of all) {
1100
+ if (long.length >= 2 || el.tagName !== 'P' || el.closest('nav, footer, table, figure, [data-slot]')) continue;
1101
+ const t = (el.innerText || '').replace(/\s+/g, ' ').trim();
1102
+ if (t.length < 240) continue;
1103
+ const cs = getComputedStyle(el);
1104
+ const lh = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.4;
1105
+ let cols = 1; for (let q = el; q && q !== document.body && cols === 1; q = q.parentElement) { const qc = getComputedStyle(q); if (qc.columnCount !== 'auto' || qc.columnWidth !== 'auto') cols = Math.max(1, Math.round(q.getBoundingClientRect().width / Math.max(1, el.getClientRects()[0]?.width || q.getBoundingClientRect().width))); }
1106
+ const lines = Math.round(el.getBoundingClientRect().height / lh) * cols;
1107
+ if (lines >= 3 && t.length / lines > 88) long.push(`${name(el)} about ${Math.round(t.length / lines)} characters a line`);
1108
+ }
1109
+ if (long.length) push(25, 'REFLEX', `running prose set too wide: ${long.join(', ')}`, 'Cap the measure at 45 to 75ch');
1110
+
1111
+ const tiles = all.find((g) => {
1112
+ if (g.closest('nav, footer, form, table')) return false;
1113
+ const kids = [...g.children].filter(vis);
1114
+ if (kids.length < 3 || kids.length > 4) return false;
1115
+ const rects = kids.map((k) => k.getBoundingClientRect());
1116
+ if (rects.some((r) => Math.abs(r.top - rects[0].top) > 4 || r.width < 160) || Math.max(...rects.map((r) => r.width)) > Math.min(...rects.map((r) => r.width)) * 1.04) return false;
1117
+ return kids.every((k) => {
1118
+ const head = k.querySelector('h2, h3, h4');
1119
+ if (!head) return false;
1120
+ const icon = [...k.querySelectorAll('svg, img, [class*="icon"], span, div, i')].find((n) => {
1121
+ if (n.contains(head) || head.contains(n)) return false;
1122
+ const r = n.getBoundingClientRect();
1123
+ const glyph = (n.textContent || '').trim();
1124
+ const iconish = n.tagName === 'svg' || n.tagName === 'IMG' || /icon/i.test(String(n.className && n.className.baseVal != null ? n.className.baseVal : n.className)) || (glyph.length > 0 && glyph.length <= 2 && /\p{Extended_Pictographic}/u.test(glyph));
1125
+ const media = n.tagName === 'svg' || n.tagName === 'IMG';
1126
+ return iconish && r.height > 8 && r.height <= 96 && (!media || r.width <= 96) && r.bottom <= head.getBoundingClientRect().top + 2;
1127
+ });
1128
+ return !!icon;
1129
+ });
1130
+ });
1131
+ if (tiles) push(3, 'REFLEX', `equal columns of icon-above-heading tiles (${name(tiles)})`, 'Vary the widths, set the icon inline with the heading, or drop the icons');
1132
+
1133
+ const cv = document.createElement('canvas'); cv.width = cv.height = 1;
1134
+ const cx = cv.getContext('2d', { willReadFrequently: true });
1135
+ const rgb = (s) => { cx.clearRect(0, 0, 1, 1); cx.fillStyle = 'rgba(0, 0, 0, 0)'; cx.fillStyle = s; cx.fillRect(0, 0, 1, 1); const d = cx.getImageData(0, 0, 1, 1).data; return [d[0] / 255, d[1] / 255, d[2] / 255, d[3] / 255]; };
1136
+ const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
1137
+ const lum = (c) => 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
1138
+ const ratio = (a, b) => (Math.max(lum(a), lum(b)) + 0.05) / (Math.min(lum(a), lum(b)) + 0.05);
1139
+ const over = (c, b) => [0, 1, 2].map((i) => c[i] * c[3] + b[i] * (1 - c[3]));
1140
+ const bgOf = (n) => { for (; n && n !== document.documentElement; n = n.parentElement) { const c = rgb(getComputedStyle(n).backgroundColor); if (c[3] >= 0.9) return c; } const b = rgb(getComputedStyle(document.body).backgroundColor); return b[3] > 0 ? b : [1, 1, 1, 1]; };
1141
+ const weak = [];
1142
+ for (const f of all.filter((el) => /^(TEXTAREA|SELECT)$/.test(el.tagName) || (el.tagName === 'INPUT' && /^(text|email|search|tel|url|password|number|date|time)$/.test(el.type)))) {
1143
+ const fh = f.getBoundingClientRect().height;
1144
+ const around = bgOf(f.parentElement);
1145
+ let best = ratio(over(rgb(getComputedStyle(f).backgroundColor), around), around);
1146
+ for (let n = f, d = 0; n && d < 3 && n.getBoundingClientRect().height <= fh + 48; n = n.parentElement, d++) {
1147
+ const cs = getComputedStyle(n); const back = d ? bgOf(n.parentElement) : around;
1148
+ for (const s of ['Top', 'Right', 'Bottom', 'Left']) if (parseFloat(cs[`border${s}Width`]) >= 1 && cs[`border${s}Style`] !== 'none') best = Math.max(best, ratio(over(rgb(cs[`border${s}Color`]), back), back));
1149
+ for (const m of (cs.boxShadow || '').matchAll(/((?:rgba?|oklch|oklab|color)\([^)]*\)|#[0-9a-f]{3,8})\s+(-?[\d.]+)px\s+(-?[\d.]+)px\s+([\d.]+)px\s+(-?[\d.]+)px/gi)) if (+m[4] === 0 && (+m[5] >= 1 || Math.abs(+m[3]) >= 1)) best = Math.max(best, ratio(over(rgb(m[1]), back), back));
1150
+ }
1151
+ if (best < 2.95) weak.push(`${name(f)} ${best.toFixed(2)}:1`);
1152
+ }
1153
+ if (weak.length) push(40, 'FAIL', `a field's edge is under 3:1 against what surrounds it: ${weak.slice(0, 3).join(', ')}`, 'Draw the field in --color-muted, or fill it a step off the surface it sits on');
1154
+
1155
+ const weights = new Map();
1156
+ const strained = [];
1157
+ const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
1158
+ for (let t = tw.nextNode(); t; t = tw.nextNode()) {
1159
+ const host = t.parentElement; const txt = t.textContent.trim();
1160
+ if (!txt || !host || quiet(host) || host.closest('svg') || !vis(host)) continue;
1161
+ const hs = getComputedStyle(host);
1162
+ const w = Math.round((parseFloat(hs.fontWeight) || 400) / 100) * 100;
1163
+ weights.set(w, (weights.get(w) || 0) + txt.length);
1164
+ if (strained.length >= 4 || txt.length < 3 || host.closest('sup, sub, [data-slot], figure svg') || strained.some((x) => x.el === host)) continue;
1165
+ const size = parseFloat(hs.fontSize) || 16;
1166
+ const prose = !!host.closest('p, li, dd, blockquote') && txt.length > 60 && size < 28;
1167
+ const lh = parseFloat(hs.lineHeight);
1168
+ if (size < 11 || (prose && size < 12)) strained.push({ el: host, why: `${name(host)} at ${Math.round(size * 10) / 10}px` });
1169
+ else if (prose && lh && lh / size < 1.3 && host.getBoundingClientRect().height > lh * 2.2) strained.push({ el: host, why: `${name(host)} leaded ${(lh / size).toFixed(2)}` });
1170
+ }
1171
+ if (strained.length) push(75, 'REFLEX', `text that strains the eye: ${strained.map((x) => x.why).join(', ')}`, 'Running text 12px or more (16px for body), functional text 11px or more, multi-line prose leaded 1.3 or more');
1172
+ const dead = [...document.querySelectorAll('img')].filter((el) => !quiet(el) && el.complete && el.naturalWidth === 0 && (el.currentSrc || el.getAttribute('src')));
1173
+ if (dead.length) push(76, 'FAIL', `an image that does not load: ${dead.slice(0, 3).map((el) => (el.getAttribute('src') || '').split('/').pop().slice(0, 40)).join(', ')}`, 'Point it at a file that exists, make it a data-slot placeholder, or remove it');
1174
+ const used = [...weights].filter(([, n]) => n >= 30).map(([w]) => w).sort((a, b) => a - b);
1175
+ if (used.length > 3) push('F19', 'WARN', `${used.length} font weights in running use (${used.join(', ')})`, 'Three weights at most, display and body together, picked as extremes');
1176
+
1177
+ for (const g of all) {
1178
+ const cs = getComputedStyle(g);
1179
+ const grid = /grid/.test(cs.display);
1180
+ const wrap = /flex/.test(cs.display) && cs.flexWrap === 'wrap' && cs.flexDirection.startsWith('row');
1181
+ if ((!grid && !wrap) || g.closest('nav, footer, form')) continue;
1182
+ const kids = [...g.children].filter((c) => vis(c) && !/absolute|fixed/.test(getComputedStyle(c).position));
1183
+ if (kids.length < 3) continue;
1184
+ const rects = kids.map((c) => c.getBoundingClientRect());
1185
+ if (grid && Math.max(...rects.map((r) => r.width)) > Math.min(...rects.map((r) => r.width)) * 1.15) continue;
1186
+ if (wrap && !kids.every((c) => c.querySelector('img, svg') || /^(IMG|svg)$/.test(c.tagName) || c.getBoundingClientRect().width >= 120)) continue;
1187
+ const rows = [];
1188
+ for (const r of rects) { const row = rows.find((x) => Math.abs(x.top - r.top) < 4); if (row) row.n++; else rows.push({ top: r.top, n: 1 }); }
1189
+ rows.sort((a, b) => a.top - b.top);
1190
+ if (rows.length >= 2 && rows[rows.length - 1].n === 1 && rows[rows.length - 2].n >= 2) { push('F20', 'WARN', `${name(g)} leaves one item alone on its last row (${rows.map((x) => x.n).join(' + ')})`, 'Change the count or the columns so the last row is not an orphan'); break; }
1191
+ }
1192
+ return out;
1193
+ }
1194
+
1195
+ async function observeOcclusion() {
1196
+ const out = [];
1197
+ const vis = (el) => { const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) return false; const r = el.getBoundingClientRect(); return r.width > 3 && r.height > 3; };
1198
+ const pinned = (el) => { for (let p = el; p && p !== document.body; p = p.parentElement) if (/fixed|sticky/.test(getComputedStyle(p).position)) return true; return false; };
1199
+ const unseen = (el) => { const cs = getComputedStyle(el); return parseFloat(cs.opacity) < 0.05 || (/^(transparent|rgba\(0, 0, 0, 0\))$/.test(cs.backgroundColor) && cs.backgroundImage === 'none' && !(el.innerText || '').trim() && !/^(IMG|svg|CANVAS|VIDEO|PICTURE)$/.test(el.tagName) && !el.querySelector('img, svg, canvas, video')); };
1200
+ const runs = [];
1201
+ let total = 0, hidden = 0;
1202
+ const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
1203
+ for (let t = tw.nextNode(); t; t = tw.nextNode()) {
1204
+ const host = t.parentElement; const txt = t.textContent.trim();
1205
+ if (!txt || !host || host.closest('script, style, noscript, svg, [aria-hidden="true"], nav, header, dialog, [popover], [role="menu"], details:not([open]), [hidden]')) continue;
1206
+ if (getComputedStyle(host).display === 'none') continue;
1207
+ total += txt.length;
1208
+ let gone = false;
1209
+ for (let p = host; p && p !== document.body; p = p.parentElement) { const cs = getComputedStyle(p); if (cs.display === 'none') { gone = null; break; } if (cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) gone = true; }
1210
+ if (gone === null) { total -= txt.length; continue; }
1211
+ if (gone) { hidden += txt.length; continue; }
1212
+ if (!host.closest('[data-slot]') && vis(host)) runs.push({ t, host });
1213
+ }
1214
+ if (total > 500 && hidden > 150 && hidden / total > 0.3) out.push({ gate: '62', grade: 'FAIL', evidence: `${Math.round((100 * hidden) / total)}% of the page's text is still hidden after load`, fix: 'Visible by default: scope the hidden state under a .js class, or show the end state under reduced motion' });
1215
+ const bad = [];
1216
+ const H = document.documentElement.scrollHeight;
1217
+ for (let y = 0; y < H && bad.length < 3; y += Math.max(200, innerHeight - 100)) {
1218
+ window.scrollTo(0, y);
1219
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
1220
+ for (const { t, host } of runs) {
1221
+ if (bad.length >= 3) break;
1222
+ if (pinned(host)) continue;
1223
+ const rg = document.createRange(); rg.selectNodeContents(t);
1224
+ for (const q of rg.getClientRects()) {
1225
+ if (q.width < 6 || q.height < 6) continue;
1226
+ const cx = q.left + q.width / 2, cy = q.top + q.height / 2;
1227
+ if (cy < 0 || cy >= innerHeight || cx < 0 || cx >= innerWidth) continue;
1228
+ const ghost = getComputedStyle(host).pointerEvents === 'none'; if (ghost) host.style.setProperty('pointer-events', 'auto', 'important');
1229
+ const top = document.elementFromPoint(cx, cy);
1230
+ if (ghost) host.style.removeProperty('pointer-events');
1231
+ if (!top || top === host || host.contains(top) || top.contains(host) || pinned(top) || unseen(top)) continue;
1232
+ const tr = top.getBoundingClientRect();
1233
+ if (!(cx >= tr.left && cx <= tr.right && cy >= tr.top && cy <= tr.bottom)) {
1234
+ const clearPseudo = (ps) => /^(none|normal)$/.test(ps.content) || (/^(transparent|rgba\(0, 0, 0, 0\))$/.test(ps.backgroundColor) && ps.backgroundImage === 'none');
1235
+ if (clearPseudo(getComputedStyle(top, '::after')) && clearPseudo(getComputedStyle(top, '::before'))) continue;
1236
+ }
1237
+ bad.push(`${top.tagName.toLowerCase()}${top.classList[0] ? '.' + top.classList[0] : ''} over "${t.textContent.trim().slice(0, 16)}"`);
1238
+ break;
1239
+ }
1240
+ }
1241
+ }
1242
+ window.scrollTo(0, 0);
1243
+ if (bad.length) out.push({ gate: '73', grade: 'FAIL', evidence: `text painted under another element: ${bad.join('; ')}`, fix: 'Give the text its own space, or put it above what covers it on a surface of its own' });
1244
+ return out;
1245
+ }
1246
+
1247
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
1248
+ const htmls = process.argv.slice(2).filter((p) => p.endsWith('.html'));
1249
+ const res = await renderCheck(process.argv.slice(2).filter((p) => isUrl(p)).concat(htmls.map((p) => resolve(process.cwd(), p))));
1250
+ console.log(JSON.stringify(res, null, 2));
1251
+ process.exit(0);
1252
+ }