@litfamily/litclaude 1.0.10 → 1.0.13

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 (718) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +436 -45
  3. package/README_ko-KR.md +403 -17
  4. package/bin/install-ownership.mjs +155 -18
  5. package/bin/litclaude-ai.js +91 -9
  6. package/docs/ab/S11-baseline-desktop.webp +0 -0
  7. package/docs/ab/S11-baseline-phone.webp +0 -0
  8. package/docs/ab/S11-litclaude-desktop.webp +0 -0
  9. package/docs/ab/S11-litclaude-phone.webp +0 -0
  10. package/docs/ab/S3-baseline-desktop.webp +0 -0
  11. package/docs/ab/S3-baseline-phone.webp +0 -0
  12. package/docs/ab/S3-litclaude-desktop.webp +0 -0
  13. package/docs/ab/S3-litclaude-phone.webp +0 -0
  14. package/docs/ab/S4-baseline-desktop.webp +0 -0
  15. package/docs/ab/S4-baseline-phone.webp +0 -0
  16. package/docs/ab/S4-litclaude-desktop.webp +0 -0
  17. package/docs/ab/S4-litclaude-phone.webp +0 -0
  18. package/docs/ab/S5-baseline-pages.webp +0 -0
  19. package/docs/ab/S5-baseline-slides.webp +0 -0
  20. package/docs/ab/S5-litclaude-pages.webp +0 -0
  21. package/docs/ab/S5-litclaude-slides.webp +0 -0
  22. package/docs/ab/S7-litclaude-diagram.webp +0 -0
  23. package/docs/ab/S8-baseline-slides.webp +0 -0
  24. package/docs/ab/S8-litclaude-slides.webp +0 -0
  25. package/docs/ab/S9-litclaude-pages.webp +0 -0
  26. package/docs/agents.md +10 -7
  27. package/docs/assets/cover-motion-still.webp +0 -0
  28. package/docs/assets/cover-motion.webp +0 -0
  29. package/docs/assets/readme/badge-version.svg +1 -1
  30. package/docs/assets/skills/autoconference.webp +0 -0
  31. package/docs/assets/skills/automatic-checks.webp +0 -0
  32. package/docs/assets/skills/autoresearch.webp +0 -0
  33. package/docs/assets/skills/browser-drive.webp +0 -0
  34. package/docs/assets/skills/debugging.webp +0 -0
  35. package/docs/assets/skills/deep-interview.webp +0 -0
  36. package/docs/assets/skills/frontend-ui-ux.webp +0 -0
  37. package/docs/assets/skills/lit-burnoff-file.webp +0 -0
  38. package/docs/assets/skills/lit-burnoff.webp +0 -0
  39. package/docs/assets/skills/lit-code.webp +0 -0
  40. package/docs/assets/skills/lit-commit.webp +0 -0
  41. package/docs/assets/skills/lit-comprehend.webp +0 -0
  42. package/docs/assets/skills/lit-crucible.webp +0 -0
  43. package/docs/assets/skills/lit-diagram-drawer.webp +0 -0
  44. package/docs/assets/skills/lit-docx.webp +0 -0
  45. package/docs/assets/skills/lit-handoff.webp +0 -0
  46. package/docs/assets/skills/lit-humanizer.webp +0 -0
  47. package/docs/assets/skills/lit-init.webp +0 -0
  48. package/docs/assets/skills/lit-loop.webp +0 -0
  49. package/docs/assets/skills/lit-plan.webp +0 -0
  50. package/docs/assets/skills/lit-pptx.webp +0 -0
  51. package/docs/assets/skills/lit-recap.webp +0 -0
  52. package/docs/assets/skills/lit-scientific-visualization.webp +0 -0
  53. package/docs/assets/skills/lit-team.webp +0 -0
  54. package/docs/assets/skills/lit-typographic-motion.webp +0 -0
  55. package/docs/assets/skills/litgoal.webp +0 -0
  56. package/docs/assets/skills/litresearch.webp +0 -0
  57. package/docs/assets/skills/litwork.webp +0 -0
  58. package/docs/assets/skills/lsp-setup.webp +0 -0
  59. package/docs/assets/skills/readme-studio.webp +0 -0
  60. package/docs/assets/skills/refactor.webp +0 -0
  61. package/docs/assets/skills/review-work.webp +0 -0
  62. package/docs/assets/skills/start-work.webp +0 -0
  63. package/docs/assets/skills/structural-search.webp +0 -0
  64. package/docs/assets/skills/visual-qa.webp +0 -0
  65. package/docs/assets/skills/wikify.webp +0 -0
  66. package/docs/hooks.md +185 -41
  67. package/docs/migration.md +6 -3
  68. package/docs/privacy.md +6 -1
  69. package/package.json +4 -2
  70. package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
  71. package/plugins/litclaude/agents/korean-prose-editor.md +1 -0
  72. package/plugins/litclaude/agents/korean-style-analyzer.md +1 -0
  73. package/plugins/litclaude/agents/meaning-preservation-auditor.md +1 -0
  74. package/plugins/litclaude/agents/native-flow-reviewer.md +1 -0
  75. package/plugins/litclaude/agents/polish-orchestrator.md +1 -0
  76. package/plugins/litclaude/bin/litclaude-hook.js +347 -35
  77. package/plugins/litclaude/bin/litclaude-hud.js +29 -1
  78. package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
  79. package/plugins/litclaude/bundled-rules/lit-humanizer.md +14 -0
  80. package/plugins/litclaude/commands/korean-ai-slop-remover.md +2 -2
  81. package/plugins/litclaude/commands/lit-comprehend.md +11 -11
  82. package/plugins/litclaude/commands/lit-humanizer.md +75 -0
  83. package/plugins/litclaude/commands/lit-korean.md +4 -130
  84. package/plugins/litclaude/commands/litresearch.md +12 -10
  85. package/plugins/litclaude/hooks/hooks.json +1 -1
  86. package/plugins/litclaude/lib/canonical-runtime-commitments.mjs +2 -2
  87. package/plugins/litclaude/lib/canonical-skill-catalog.mjs +5 -1
  88. package/plugins/litclaude/lib/canonical-skill-resources.mjs +618 -13
  89. package/plugins/litclaude/lib/deliverable-hedge-guard.mjs +256 -350
  90. package/plugins/litclaude/lib/hud-accent-palette.mjs +33 -0
  91. package/plugins/litclaude/lib/interface-mode.mjs +33 -0
  92. package/plugins/litclaude/lib/interface-probe-gate.mjs +125 -0
  93. package/plugins/litclaude/lib/jev-skill-hint.mjs +445 -0
  94. package/plugins/litclaude/lib/motion-render-gate.mjs +164 -0
  95. package/plugins/litclaude/lib/mutated-file-paths.mjs +32 -1
  96. package/plugins/litclaude/lib/office-runtime-lock/package-lock.json +745 -0
  97. package/plugins/litclaude/lib/office-runtime-lock/package.json +12 -0
  98. package/plugins/litclaude/lib/office-runtime-lock/requirements.lock +15 -0
  99. package/plugins/litclaude/lib/office-runtime.mjs +255 -0
  100. package/plugins/litclaude/lib/office_data.py +493 -0
  101. package/plugins/litclaude/lib/office_runtime_bootstrap.py +61 -0
  102. package/plugins/litclaude/lib/ooxml_integrity.py +199 -0
  103. package/plugins/litclaude/lib/rename-aliases.mjs +3 -1
  104. package/plugins/litclaude/lib/render_pages.py +105 -0
  105. package/plugins/litclaude/skills/autoconference/SKILL.md +2 -0
  106. package/plugins/litclaude/skills/browser-drive/SKILL.md +19 -6
  107. package/plugins/litclaude/skills/browser-drive/scripts/capability-probe.mjs +44 -8
  108. package/plugins/litclaude/skills/frontend-ui-ux/ATTRIBUTION.md +26 -0
  109. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +50 -16
  110. package/plugins/litclaude/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +9 -0
  111. package/plugins/litclaude/skills/frontend-ui-ux/references/complete-contract.md +32 -6
  112. package/plugins/litclaude/skills/frontend-ui-ux/references/craft-floor.md +257 -0
  113. package/plugins/litclaude/skills/frontend-ui-ux/references/default-editorial-pixel.json +6 -0
  114. package/plugins/litclaude/skills/frontend-ui-ux/references/interaction-motion.md +34 -88
  115. package/plugins/litclaude/skills/frontend-ui-ux/references/motion-guide.md +130 -0
  116. package/plugins/litclaude/skills/frontend-ui-ux/references/slop-register.md +150 -0
  117. package/plugins/litclaude/skills/frontend-ui-ux/references/visual-reconstruction.md +39 -85
  118. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +1534 -0
  119. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +141 -0
  120. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-server.mjs +57 -0
  121. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +129 -0
  122. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe.mjs +316 -0
  123. package/plugins/litclaude/skills/lit-comprehend/SKILL.md +34 -43
  124. package/plugins/litclaude/skills/lit-comprehend/references/artifact-template.md +5 -44
  125. package/plugins/litclaude/skills/lit-comprehend/scripts/verify-explainer.mjs +6 -16
  126. package/plugins/litclaude/skills/lit-diagram-drawer/NOTICE +33 -0
  127. package/plugins/litclaude/skills/lit-diagram-drawer/SKILL.md +123 -0
  128. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-architecture-dark.html +1 -0
  129. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-architecture-full.html +1 -0
  130. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-architecture-light.html +1 -0
  131. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bar-dark.html +1 -0
  132. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bar-full.html +1 -0
  133. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bar-light.html +1 -0
  134. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-beeswarm-dark.html +1 -0
  135. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-beeswarm-full.html +1 -0
  136. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-beeswarm-light.html +1 -0
  137. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bubble-dark.html +1 -0
  138. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bubble-full.html +1 -0
  139. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bubble-light.html +1 -0
  140. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bump-dark.html +1 -0
  141. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bump-full.html +1 -0
  142. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bump-light.html +1 -0
  143. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-data-flow-dark.html +1 -0
  144. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-data-flow-full.html +1 -0
  145. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-data-flow-light.html +1 -0
  146. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-datalake-dark.html +1 -0
  147. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-datalake-full.html +1 -0
  148. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-datalake-light.html +1 -0
  149. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-db-schema-dark.html +1 -0
  150. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-db-schema-full.html +1 -0
  151. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-db-schema-light.html +1 -0
  152. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dependency-dark.html +1 -0
  153. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dependency-full.html +1 -0
  154. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dependency-light.html +1 -0
  155. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-deployment-dark.html +1 -0
  156. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-deployment-full.html +1 -0
  157. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-deployment-light.html +1 -0
  158. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-integration-dark.html +1 -0
  159. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-integration-full.html +1 -0
  160. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-integration-light.html +1 -0
  161. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-dark.html +1 -0
  162. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-full.html +1 -0
  163. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-light.html +1 -0
  164. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-er-dark.html +1 -0
  165. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-er-full.html +1 -0
  166. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-er-light.html +1 -0
  167. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-fishbone-dark.html +1 -0
  168. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-fishbone-full.html +1 -0
  169. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-fishbone-light.html +1 -0
  170. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-flowchart-dark.html +1 -0
  171. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-flowchart-full.html +1 -0
  172. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-flowchart-light.html +1 -0
  173. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-gantt-dark.html +1 -0
  174. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-gantt-full.html +1 -0
  175. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-gantt-light.html +1 -0
  176. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-heatmap-dark.html +1 -0
  177. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-heatmap-full.html +1 -0
  178. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-heatmap-light.html +1 -0
  179. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-dark.html +1 -0
  180. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-full.html +1 -0
  181. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-light.html +1 -0
  182. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-dark.html +1 -0
  183. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-full.html +1 -0
  184. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-light.html +1 -0
  185. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-drawio-dark.html +1 -0
  186. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-drawio-full.html +1 -0
  187. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-drawio-light.html +1 -0
  188. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-dark.html +1 -0
  189. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-full.html +1 -0
  190. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-light.html +1 -0
  191. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-dark.html +1 -0
  192. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-full.html +1 -0
  193. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-light.html +1 -0
  194. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-it-state-dark.html +1 -0
  195. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-it-state-full.html +1 -0
  196. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-it-state-light.html +1 -0
  197. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-journey-dark.html +1 -0
  198. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-journey-full.html +1 -0
  199. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-journey-light.html +1 -0
  200. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-kanban-dark.html +1 -0
  201. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-kanban-full.html +1 -0
  202. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-kanban-light.html +1 -0
  203. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-layers-dark.html +1 -0
  204. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-layers-full.html +1 -0
  205. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-layers-light.html +1 -0
  206. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-line-dark.html +1 -0
  207. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-line-full.html +1 -0
  208. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-line-light.html +1 -0
  209. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-dark.html +1 -0
  210. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-full.html +1 -0
  211. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-light.html +1 -0
  212. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-dark.html +1 -0
  213. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-full.html +1 -0
  214. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-light.html +1 -0
  215. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-marimekko-dark.html +1 -0
  216. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-marimekko-full.html +1 -0
  217. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-marimekko-light.html +1 -0
  218. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-medallion-dark.html +1 -0
  219. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-medallion-full.html +1 -0
  220. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-medallion-light.html +1 -0
  221. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-nested-dark.html +1 -0
  222. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-nested-full.html +1 -0
  223. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-nested-light.html +1 -0
  224. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-org-chart-dark.html +1 -0
  225. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-org-chart-full.html +1 -0
  226. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-org-chart-light.html +1 -0
  227. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-dark.html +1 -0
  228. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-full.html +1 -0
  229. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-light.html +1 -0
  230. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-polar-dark.html +1 -0
  231. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-polar-full.html +1 -0
  232. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-polar-light.html +1 -0
  233. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-dark.html +1 -0
  234. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-full.html +1 -0
  235. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-light.html +1 -0
  236. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-process-dark.html +1 -0
  237. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-process-full.html +1 -0
  238. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-process-light.html +1 -0
  239. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-pyramid-dark.html +1 -0
  240. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-pyramid-full.html +1 -0
  241. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-pyramid-light.html +1 -0
  242. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-dark.html +1 -0
  243. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-full.html +1 -0
  244. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-light.html +1 -0
  245. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-dark.html +1 -0
  246. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-full.html +1 -0
  247. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-light.html +1 -0
  248. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-queue-animated-dark.html +1 -0
  249. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-queue-animated-full.html +1 -0
  250. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-queue-animated-light.html +1 -0
  251. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-radar-dark.html +1 -0
  252. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-radar-full.html +1 -0
  253. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-radar-light.html +1 -0
  254. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-ridgeline-dark.html +1 -0
  255. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-ridgeline-full.html +1 -0
  256. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-ridgeline-light.html +1 -0
  257. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sankey-dark.html +1 -0
  258. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sankey-full.html +1 -0
  259. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sankey-light.html +1 -0
  260. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-scatter-dark.html +1 -0
  261. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-scatter-full.html +1 -0
  262. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-scatter-light.html +1 -0
  263. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-dark.html +1 -0
  264. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-full.html +1 -0
  265. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-light.html +1 -0
  266. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-dark.html +1 -0
  267. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-full.html +1 -0
  268. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-light.html +1 -0
  269. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-slopegraph-dark.html +1 -0
  270. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-slopegraph-full.html +1 -0
  271. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-slopegraph-light.html +1 -0
  272. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-dark.html +1 -0
  273. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-full.html +1 -0
  274. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-dark.html +1 -0
  275. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-full.html +1 -0
  276. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-light.html +1 -0
  277. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-light.html +1 -0
  278. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-story-map-dark.html +1 -0
  279. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-story-map-full.html +1 -0
  280. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-story-map-light.html +1 -0
  281. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-streamgraph-dark.html +1 -0
  282. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-streamgraph-full.html +1 -0
  283. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-streamgraph-light.html +1 -0
  284. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-swimlane-dark.html +1 -0
  285. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-swimlane-full.html +1 -0
  286. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-swimlane-light.html +1 -0
  287. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-timeline-dark.html +1 -0
  288. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-timeline-full.html +1 -0
  289. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-timeline-light.html +1 -0
  290. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-dark.html +1 -0
  291. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-full.html +1 -0
  292. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-light.html +1 -0
  293. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-dark.html +1 -0
  294. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-full.html +1 -0
  295. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-light.html +1 -0
  296. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-treemap-dark.html +1 -0
  297. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-treemap-full.html +1 -0
  298. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-treemap-light.html +1 -0
  299. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-uml-class-dark.html +1 -0
  300. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-uml-class-full.html +1 -0
  301. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-uml-class-light.html +1 -0
  302. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-venn-dark.html +1 -0
  303. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-venn-full.html +1 -0
  304. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-venn-light.html +1 -0
  305. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-wardley-dark.html +1 -0
  306. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-wardley-full.html +1 -0
  307. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-wardley-light.html +1 -0
  308. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-waterfall-dark.html +1 -0
  309. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-waterfall-full.html +1 -0
  310. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-waterfall-light.html +1 -0
  311. package/plugins/litclaude/skills/lit-diagram-drawer/assets/fonts/OFL.txt +94 -0
  312. package/plugins/litclaude/skills/lit-diagram-drawer/assets/fonts/PretendardVariable.woff2 +0 -0
  313. package/plugins/litclaude/skills/lit-diagram-drawer/assets/fonts/provenance.json +22 -0
  314. package/plugins/litclaude/skills/lit-diagram-drawer/assets/icons.html +231 -0
  315. package/plugins/litclaude/skills/lit-diagram-drawer/assets/index.html +1 -0
  316. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/devicon-MIT.txt +20 -0
  317. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/diagram-design-MIT.txt +21 -0
  318. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/log-z-MIT.txt +21 -0
  319. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/simple-icons-CC0.md +30 -0
  320. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/tabler-MIT.txt +21 -0
  321. package/plugins/litclaude/skills/lit-diagram-drawer/examples/01-service-architecture/after.html +1 -0
  322. package/plugins/litclaude/skills/lit-diagram-drawer/examples/01-service-architecture/brief.md +33 -0
  323. package/plugins/litclaude/skills/lit-diagram-drawer/examples/02-release-decision/after.html +1 -0
  324. package/plugins/litclaude/skills/lit-diagram-drawer/examples/02-release-decision/brief.md +18 -0
  325. package/plugins/litclaude/skills/lit-diagram-drawer/examples/03-data-retention/after.html +1 -0
  326. package/plugins/litclaude/skills/lit-diagram-drawer/examples/03-data-retention/brief.md +19 -0
  327. package/plugins/litclaude/skills/lit-diagram-drawer/examples/04-oauth-sequence/after.html +1 -0
  328. package/plugins/litclaude/skills/lit-diagram-drawer/examples/04-oauth-sequence/brief.md +17 -0
  329. package/plugins/litclaude/skills/lit-diagram-drawer/examples/05-review-lanes/after.html +1 -0
  330. package/plugins/litclaude/skills/lit-diagram-drawer/examples/05-review-lanes/brief.md +17 -0
  331. package/plugins/litclaude/skills/lit-diagram-drawer/examples/06-rollout-gates/after.html +1 -0
  332. package/plugins/litclaude/skills/lit-diagram-drawer/examples/06-rollout-gates/brief.md +17 -0
  333. package/plugins/litclaude/skills/lit-diagram-drawer/examples/07-deployment-boundary/after.html +1 -0
  334. package/plugins/litclaude/skills/lit-diagram-drawer/examples/07-deployment-boundary/brief.md +19 -0
  335. package/plugins/litclaude/skills/lit-diagram-drawer/examples/08-policy-traces/after.html +1 -0
  336. package/plugins/litclaude/skills/lit-diagram-drawer/examples/08-policy-traces/brief.md +20 -0
  337. package/plugins/litclaude/skills/lit-diagram-drawer/references/accessibility.md +37 -0
  338. package/plugins/litclaude/skills/lit-diagram-drawer/references/export.md +29 -0
  339. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-drawio.md +21 -0
  340. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-excalidraw.md +14 -0
  341. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-mermaid.md +18 -0
  342. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-schema.md +28 -0
  343. package/plugins/litclaude/skills/lit-diagram-drawer/references/korean-typography.md +34 -0
  344. package/plugins/litclaude/skills/lit-diagram-drawer/references/motion.md +29 -0
  345. package/plugins/litclaude/skills/lit-diagram-drawer/references/office-pptx-docx.md +42 -0
  346. package/plugins/litclaude/skills/lit-diagram-drawer/references/output-spec.md +48 -0
  347. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-annotation.md +12 -0
  348. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-icons.md +21 -0
  349. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-sketchy.md +11 -0
  350. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-terminal.md +11 -0
  351. package/plugins/litclaude/skills/lit-diagram-drawer/references/profiles.md +24 -0
  352. package/plugins/litclaude/skills/lit-diagram-drawer/references/semantic-patterns.md +27 -0
  353. package/plugins/litclaude/skills/lit-diagram-drawer/references/style-guide.md +67 -0
  354. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-architecture.md +51 -0
  355. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-bar.md +51 -0
  356. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-beeswarm.md +49 -0
  357. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-bubble.md +49 -0
  358. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-bump.md +49 -0
  359. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-catalog.json +1433 -0
  360. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-data-flow.md +51 -0
  361. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-datalake.md +49 -0
  362. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-db-schema.md +49 -0
  363. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-dependency.md +49 -0
  364. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-deployment.md +49 -0
  365. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-dp-integration.md +49 -0
  366. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-dp-security-matrix.md +49 -0
  367. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-er.md +49 -0
  368. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-fishbone.md +49 -0
  369. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-flowchart.md +49 -0
  370. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-gantt.md +49 -0
  371. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-heatmap.md +53 -0
  372. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-high-level-vertical.md +53 -0
  373. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-high-level.md +53 -0
  374. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-import-drawio.md +53 -0
  375. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-import-excalidraw.md +53 -0
  376. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-import-mermaid.md +53 -0
  377. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-it-state.md +54 -0
  378. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-journey.md +54 -0
  379. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-kanban.md +54 -0
  380. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-layers.md +54 -0
  381. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-line.md +54 -0
  382. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-loop-terminal.md +53 -0
  383. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-loop.md +54 -0
  384. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-marimekko.md +54 -0
  385. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-medallion.md +54 -0
  386. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-nested.md +59 -0
  387. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-org-chart.md +58 -0
  388. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-paved-road-animated.md +72 -0
  389. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-polar.md +67 -0
  390. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-policy-trace-animated.md +69 -0
  391. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-process.md +58 -0
  392. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-pyramid.md +59 -0
  393. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-quadrant-consultant.md +60 -0
  394. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-quadrant.md +57 -0
  395. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-queue-animated.md +63 -0
  396. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-radar.md +57 -0
  397. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-ridgeline.md +63 -0
  398. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-sankey.md +68 -0
  399. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-scatter.md +60 -0
  400. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-sequence-oauth.md +66 -0
  401. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-sequence.md +63 -0
  402. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-slopegraph.md +65 -0
  403. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-state-lifecycle.md +53 -0
  404. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-state.md +59 -0
  405. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-story-map.md +52 -0
  406. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-streamgraph.md +53 -0
  407. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-swimlane.md +52 -0
  408. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-timeline.md +52 -0
  409. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-tree-block-decomposition.md +57 -0
  410. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-tree.md +54 -0
  411. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-treemap.md +54 -0
  412. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-uml-class.md +52 -0
  413. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-venn.md +51 -0
  414. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-wardley.md +52 -0
  415. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-waterfall.md +54 -0
  416. package/plugins/litclaude/skills/lit-diagram-drawer/references/verifier-guide.md +66 -0
  417. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/block-registry.mjs +43 -0
  418. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/brief-contract.mjs +94 -0
  419. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/check-visible-text.mjs +15 -0
  420. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/diagram-metrics.mjs +223 -0
  421. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/doctor.mjs +35 -0
  422. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/drawio-extract.mjs +635 -0
  423. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/excalidraw-extract.mjs +319 -0
  424. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/export.mjs +116 -0
  425. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/mermaid-extract.mjs +438 -0
  426. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/office-safety.mjs +54 -0
  427. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/renderer.mjs +33 -0
  428. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-all.mjs +58 -0
  429. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-brief.mjs +22 -0
  430. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-coverage.mjs +56 -0
  431. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-diagram.mjs +27 -0
  432. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-motion.mjs +12 -0
  433. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-type.mjs +48 -0
  434. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/visual-quality.mjs +376 -0
  435. package/plugins/litclaude/skills/lit-docx/NOTICE +33 -0
  436. package/plugins/litclaude/skills/lit-docx/SKILL.md +170 -0
  437. package/plugins/litclaude/skills/lit-docx/references/frontmatter_schema.md +42 -0
  438. package/plugins/litclaude/skills/lit-docx/references/journal_style_spec.md +17 -0
  439. package/plugins/litclaude/skills/lit-docx/references/markdown_quality_checklist.md +37 -0
  440. package/plugins/litclaude/skills/lit-docx/references/slop_phrase_list.yaml +58 -0
  441. package/plugins/litclaude/skills/lit-docx/references/slop_rules.md +35 -0
  442. package/plugins/litclaude/skills/lit-docx/scripts/clean_markdown.py +95 -0
  443. package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_docx.py +1194 -0
  444. package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_pdf.py +290 -0
  445. package/plugins/litclaude/skills/lit-docx/scripts/convert_pdf.py +139 -0
  446. package/plugins/litclaude/skills/lit-docx/scripts/edit_docx.py +306 -0
  447. package/plugins/litclaude/skills/lit-docx/scripts/embed_images.py +78 -0
  448. package/plugins/litclaude/skills/lit-docx/scripts/generate_docx_templates.py +500 -0
  449. package/plugins/litclaude/skills/lit-docx/scripts/qa_docx.py +177 -0
  450. package/plugins/litclaude/skills/lit-docx/scripts/slop_lint.py +561 -0
  451. package/plugins/litclaude/skills/lit-docx/scripts/visual_audit.py +48 -0
  452. package/plugins/litclaude/skills/lit-docx/templates/docx/acs.docx +0 -0
  453. package/plugins/litclaude/skills/lit-docx/templates/docx/elsevier.docx +0 -0
  454. package/plugins/litclaude/skills/lit-docx/templates/docx/ieee.docx +0 -0
  455. package/plugins/litclaude/skills/lit-docx/templates/docx/korean-generic.docx +0 -0
  456. package/plugins/litclaude/skills/lit-docx/templates/docx/nature.docx +0 -0
  457. package/plugins/litclaude/skills/lit-docx/templates/latex/acs/README.md +5 -0
  458. package/plugins/litclaude/skills/lit-docx/templates/latex/acs/template.tex +37 -0
  459. package/plugins/litclaude/skills/lit-docx/templates/latex/elsevier/README.md +5 -0
  460. package/plugins/litclaude/skills/lit-docx/templates/latex/elsevier/template.tex +38 -0
  461. package/plugins/litclaude/skills/lit-docx/templates/latex/ieee/README.md +5 -0
  462. package/plugins/litclaude/skills/lit-docx/templates/latex/ieee/template.tex +37 -0
  463. package/plugins/litclaude/skills/lit-docx/templates/latex/korean-generic/README.md +5 -0
  464. package/plugins/litclaude/skills/lit-docx/templates/latex/korean-generic/template.tex +37 -0
  465. package/plugins/litclaude/skills/lit-docx/templates/latex/nature/README.md +5 -0
  466. package/plugins/litclaude/skills/lit-docx/templates/latex/nature/template.tex +37 -0
  467. package/plugins/litclaude/skills/lit-docx/templates/registry.yaml +1366 -0
  468. package/plugins/litclaude/skills/lit-humanizer/NOTICE +160 -0
  469. package/plugins/litclaude/skills/lit-humanizer/SKILL.md +164 -0
  470. package/plugins/litclaude/skills/lit-humanizer/assets/always-on-rule.md +9 -0
  471. package/plugins/litclaude/skills/lit-humanizer/assets/report-skeleton.md +25 -0
  472. package/plugins/litclaude/skills/lit-humanizer/assets/slide-text-skeleton.md +28 -0
  473. package/plugins/litclaude/skills/lit-humanizer/bench/prompts.md +73 -0
  474. package/plugins/litclaude/skills/lit-humanizer/examples/accessibility-note/after.md +7 -0
  475. package/plugins/litclaude/skills/lit-humanizer/examples/accessibility-note/before.md +7 -0
  476. package/plugins/litclaude/skills/lit-humanizer/examples/api-reference/after.md +9 -0
  477. package/plugins/litclaude/skills/lit-humanizer/examples/api-reference/before.md +15 -0
  478. package/plugins/litclaude/skills/lit-humanizer/examples/changelog/after.md +5 -0
  479. package/plugins/litclaude/skills/lit-humanizer/examples/changelog/before.md +12 -0
  480. package/plugins/litclaude/skills/lit-humanizer/examples/code-comments/after.md +10 -0
  481. package/plugins/litclaude/skills/lit-humanizer/examples/code-comments/before.md +19 -0
  482. package/plugins/litclaude/skills/lit-humanizer/examples/commit/after.md +5 -0
  483. package/plugins/litclaude/skills/lit-humanizer/examples/commit/before.md +7 -0
  484. package/plugins/litclaude/skills/lit-humanizer/examples/data-release/after.md +7 -0
  485. package/plugins/litclaude/skills/lit-humanizer/examples/data-release/before.md +9 -0
  486. package/plugins/litclaude/skills/lit-humanizer/examples/english-essay/after.md +5 -0
  487. package/plugins/litclaude/skills/lit-humanizer/examples/english-essay/before.md +5 -0
  488. package/plugins/litclaude/skills/lit-humanizer/examples/incident-update/after.md +7 -0
  489. package/plugins/litclaude/skills/lit-humanizer/examples/incident-update/before.md +7 -0
  490. package/plugins/litclaude/skills/lit-humanizer/examples/ko-essay/after.md +7 -0
  491. package/plugins/litclaude/skills/lit-humanizer/examples/ko-essay/before.md +9 -0
  492. package/plugins/litclaude/skills/lit-humanizer/examples/ko-report/after.md +9 -0
  493. package/plugins/litclaude/skills/lit-humanizer/examples/ko-report/before.md +13 -0
  494. package/plugins/litclaude/skills/lit-humanizer/examples/ko-slide/after.md +19 -0
  495. package/plugins/litclaude/skills/lit-humanizer/examples/ko-slide/before.md +21 -0
  496. package/plugins/litclaude/skills/lit-humanizer/examples/meeting-brief/after.md +9 -0
  497. package/plugins/litclaude/skills/lit-humanizer/examples/meeting-brief/before.md +11 -0
  498. package/plugins/litclaude/skills/lit-humanizer/examples/policy-faq/after.md +9 -0
  499. package/plugins/litclaude/skills/lit-humanizer/examples/policy-faq/before.md +9 -0
  500. package/plugins/litclaude/skills/lit-humanizer/examples/pr/after.md +15 -0
  501. package/plugins/litclaude/skills/lit-humanizer/examples/pr/before.md +17 -0
  502. package/plugins/litclaude/skills/lit-humanizer/examples/project-update/after.md +7 -0
  503. package/plugins/litclaude/skills/lit-humanizer/examples/project-update/before.md +9 -0
  504. package/plugins/litclaude/skills/lit-humanizer/examples/readme/after.md +12 -0
  505. package/plugins/litclaude/skills/lit-humanizer/examples/readme/before.md +14 -0
  506. package/plugins/litclaude/skills/lit-humanizer/examples/report/after.md +13 -0
  507. package/plugins/litclaude/skills/lit-humanizer/examples/report/before.md +11 -0
  508. package/plugins/litclaude/skills/lit-humanizer/examples/research-abstract/after.md +5 -0
  509. package/plugins/litclaude/skills/lit-humanizer/examples/research-abstract/before.md +5 -0
  510. package/plugins/litclaude/skills/lit-humanizer/examples/slides/after.md +19 -0
  511. package/plugins/litclaude/skills/lit-humanizer/examples/slides/before.md +19 -0
  512. package/plugins/litclaude/skills/lit-humanizer/examples/troubleshooting/after.md +11 -0
  513. package/plugins/litclaude/skills/lit-humanizer/examples/troubleshooting/before.md +11 -0
  514. package/plugins/litclaude/skills/lit-humanizer/motion-guide.md +123 -0
  515. package/plugins/litclaude/skills/lit-humanizer/references/code-patterns.md +468 -0
  516. package/plugins/litclaude/skills/lit-humanizer/references/deliverable-channels.md +111 -0
  517. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns-checklist.md +43 -0
  518. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns-content.md +243 -0
  519. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns-structure.md +375 -0
  520. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns.md +57 -0
  521. package/plugins/litclaude/skills/lit-humanizer/references/ko-metrics.md +33 -0
  522. package/plugins/litclaude/skills/lit-humanizer/references/ko-patterns-a-d.md +659 -0
  523. package/plugins/litclaude/skills/lit-humanizer/references/ko-patterns-e-j.md +387 -0
  524. package/plugins/litclaude/skills/lit-humanizer/references/ko-patterns.md +31 -0
  525. package/plugins/litclaude/skills/lit-humanizer/references/rewrite-playbook.md +54 -0
  526. package/plugins/litclaude/skills/lit-humanizer/references/taxonomy.md +153 -0
  527. package/plugins/litclaude/skills/lit-humanizer/rules.json +884 -0
  528. package/plugins/litclaude/skills/lit-humanizer/scripts/artifact-text.mjs +38 -0
  529. package/plugins/litclaude/skills/lit-humanizer/scripts/core.mjs +207 -0
  530. package/plugins/litclaude/skills/lit-humanizer/scripts/detect.mjs +64 -0
  531. package/plugins/litclaude/skills/lit-humanizer/scripts/extract_office_text.py +112 -0
  532. package/plugins/litclaude/skills/lit-humanizer/scripts/ko-metrics.mjs +124 -0
  533. package/plugins/litclaude/skills/lit-humanizer/scripts/scan-input.mjs +29 -0
  534. package/plugins/litclaude/skills/lit-loop/SKILL.md +30 -1
  535. package/plugins/litclaude/skills/lit-plan/SKILL.md +4 -1
  536. package/plugins/litclaude/skills/lit-pptx/NOTICE +38 -0
  537. package/plugins/litclaude/skills/lit-pptx/SKILL.md +207 -0
  538. package/plugins/litclaude/skills/lit-pptx/assets/azure/circle-lg.png +0 -0
  539. package/plugins/litclaude/skills/lit-pptx/assets/azure/circle-md.png +0 -0
  540. package/plugins/litclaude/skills/lit-pptx/assets/azure/circle-sm.png +0 -0
  541. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Black.otf +0 -0
  542. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Bold.otf +0 -0
  543. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Light.otf +0 -0
  544. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Medium.otf +0 -0
  545. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Regular.otf +0 -0
  546. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/OFL.txt +94 -0
  547. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/SOURCE.md +29 -0
  548. package/plugins/litclaude/skills/lit-pptx/fonts/pretendard/OFL.txt +94 -0
  549. package/plugins/litclaude/skills/lit-pptx/fonts/pretendard/Pretendard-Bold.otf +0 -0
  550. package/plugins/litclaude/skills/lit-pptx/fonts/pretendard/Pretendard-Regular.otf +0 -0
  551. package/plugins/litclaude/skills/lit-pptx/fonts/provenance.json +78 -0
  552. package/plugins/litclaude/skills/lit-pptx/references/agents/analyst.md +49 -0
  553. package/plugins/litclaude/skills/lit-pptx/references/agents/antithesis.md +59 -0
  554. package/plugins/litclaude/skills/lit-pptx/references/agents/architect.md +44 -0
  555. package/plugins/litclaude/skills/lit-pptx/references/agents/critic.md +42 -0
  556. package/plugins/litclaude/skills/lit-pptx/references/agents/synthesizer.md +47 -0
  557. package/plugins/litclaude/skills/lit-pptx/references/agents/thesis.md +33 -0
  558. package/plugins/litclaude/skills/lit-pptx/references/anti-slop-checklist.md +81 -0
  559. package/plugins/litclaude/skills/lit-pptx/references/authoring-guide.md +173 -0
  560. package/plugins/litclaude/skills/lit-pptx/references/consensus-protocol.md +125 -0
  561. package/plugins/litclaude/skills/lit-pptx/references/data-display-cookbook.md +45 -0
  562. package/plugins/litclaude/skills/lit-pptx/references/design-system-azure.md +58 -0
  563. package/plugins/litclaude/skills/lit-pptx/references/interview-protocol.md +113 -0
  564. package/plugins/litclaude/skills/lit-pptx/references/stages/autopilot.md +48 -0
  565. package/plugins/litclaude/skills/lit-pptx/references/stages/interview.md +52 -0
  566. package/plugins/litclaude/skills/lit-pptx/references/stages/plan.md +42 -0
  567. package/plugins/litclaude/skills/lit-pptx/scripts/compile-deck.js +414 -0
  568. package/plugins/litclaude/skills/lit-pptx/scripts/craft_extras.py +373 -0
  569. package/plugins/litclaude/skills/lit-pptx/scripts/deck_craft.py +321 -0
  570. package/plugins/litclaude/skills/lit-pptx/scripts/embed_fonts.py +215 -0
  571. package/plugins/litclaude/skills/lit-pptx/scripts/forbidden-terms.json +55 -0
  572. package/plugins/litclaude/skills/lit-pptx/scripts/layout_inventory.py +418 -0
  573. package/plugins/litclaude/skills/lit-pptx/scripts/learn_template.py +446 -0
  574. package/plugins/litclaude/skills/lit-pptx/scripts/lib/derive-palette.js +176 -0
  575. package/plugins/litclaude/skills/lit-pptx/scripts/lib/directive-parser.js +244 -0
  576. package/plugins/litclaude/skills/lit-pptx/scripts/lib/font-map.js +79 -0
  577. package/plugins/litclaude/skills/lit-pptx/scripts/lib/gen-bg.js +86 -0
  578. package/plugins/litclaude/skills/lit-pptx/scripts/lib/gen-gradient-assets.js +37 -0
  579. package/plugins/litclaude/skills/lit-pptx/scripts/lib/layout-resolver.js +768 -0
  580. package/plugins/litclaude/skills/lit-pptx/scripts/lib/markdown-loader.js +136 -0
  581. package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-html.js +356 -0
  582. package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +2094 -0
  583. package/plugins/litclaude/skills/lit-pptx/scripts/lib/runtime-require.js +28 -0
  584. package/plugins/litclaude/skills/lit-pptx/scripts/lib/slide-ast.js +561 -0
  585. package/plugins/litclaude/skills/lit-pptx/scripts/lib/spec-validator.js +94 -0
  586. package/plugins/litclaude/skills/lit-pptx/scripts/lib/template-registry.js +394 -0
  587. package/plugins/litclaude/skills/lit-pptx/scripts/package.json +4 -0
  588. package/plugins/litclaude/skills/lit-pptx/scripts/qa_deck.py +766 -0
  589. package/plugins/litclaude/skills/lit-pptx/scripts/validate_pptx.py +215 -0
  590. package/plugins/litclaude/skills/lit-pptx/specs/markdown-slide-spec-v1.md +251 -0
  591. package/plugins/litclaude/skills/lit-pptx/specs/markdown-slide-spec-v2.md +214 -0
  592. package/plugins/litclaude/skills/lit-pptx/specs/slide-ast-v1.schema.json +229 -0
  593. package/plugins/litclaude/skills/lit-pptx/specs/slide-ast-v2.schema.json +144 -0
  594. package/plugins/litclaude/skills/lit-pptx/specs/template-enrollment-contract.md +382 -0
  595. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/capabilities.yaml +146 -0
  596. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +454 -0
  597. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +37 -0
  598. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/capabilities.yaml +154 -0
  599. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +475 -0
  600. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +37 -0
  601. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/capabilities.yaml +179 -0
  602. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/layout-mapping.yaml +299 -0
  603. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/template.yaml +45 -0
  604. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/capabilities.yaml +181 -0
  605. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/layout-mapping.yaml +299 -0
  606. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/template.yaml +44 -0
  607. package/plugins/litclaude/skills/lit-scientific-visualization/SKILL.md +1 -0
  608. package/plugins/litclaude/skills/lit-typographic-motion/SKILL.md +131 -0
  609. package/plugins/litclaude/skills/lit-typographic-motion/engine/NOTICE +23 -0
  610. package/plugins/litclaude/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +56 -0
  611. package/plugins/litclaude/skills/lit-typographic-motion/engine/audio/beat_grid.py +56 -0
  612. package/plugins/litclaude/skills/lit-typographic-motion/engine/audio/requirements.lock +1084 -0
  613. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/constants.mjs +211 -0
  614. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/contrast.mjs +90 -0
  615. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/events.mjs +46 -0
  616. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/flash.mjs +211 -0
  617. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/gate-rules.mjs +550 -0
  618. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/presets.mjs +123 -0
  619. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/text.mjs +100 -0
  620. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/timeline.mjs +142 -0
  621. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/util.mjs +167 -0
  622. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/voices.mjs +19 -0
  623. package/plugins/litclaude/skills/lit-typographic-motion/engine/models/word-timing.json +198 -0
  624. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/analysis.mjs +108 -0
  625. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/brief.mjs +158 -0
  626. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/chrome.mjs +330 -0
  627. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/coverage.mjs +83 -0
  628. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/done.mjs +104 -0
  629. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/encode.mjs +104 -0
  630. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/frame-worker.mjs +36 -0
  631. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/look.mjs +128 -0
  632. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/pipeline.mjs +592 -0
  633. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/png.mjs +112 -0
  634. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/report.mjs +66 -0
  635. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/runtime.mjs +330 -0
  636. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/session.mjs +145 -0
  637. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/sound-cli.mjs +60 -0
  638. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/sound.mjs +641 -0
  639. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-capture.mjs +178 -0
  640. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-gate.mjs +122 -0
  641. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-qa.mjs +295 -0
  642. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-serve.mjs +278 -0
  643. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage.mjs +503 -0
  644. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stills.mjs +100 -0
  645. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/treatment.mjs +343 -0
  646. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/ws-server.mjs +132 -0
  647. package/plugins/litclaude/skills/lit-typographic-motion/engine/package-lock.json +25 -0
  648. package/plugins/litclaude/skills/lit-typographic-motion/engine/package.json +10 -0
  649. package/plugins/litclaude/skills/lit-typographic-motion/engine/stage/init.js +288 -0
  650. package/plugins/litclaude/skills/lit-typographic-motion/engine/stage/qa.js +157 -0
  651. package/plugins/litclaude/skills/lit-typographic-motion/engine/stage/stage-kit.js +701 -0
  652. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/boot.mjs +123 -0
  653. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/engine.mjs +281 -0
  654. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/gl.mjs +319 -0
  655. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/glsl-common.mjs +78 -0
  656. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/index.html +6 -0
  657. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/lines.mjs +112 -0
  658. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/crt.mjs +98 -0
  659. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/dither.mjs +49 -0
  660. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/glitch.mjs +77 -0
  661. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/swiss-grid.mjs +61 -0
  662. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/terminal-ui.mjs +88 -0
  663. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/tidal-gradient.mjs +86 -0
  664. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/post.mjs +165 -0
  665. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scale.mjs +15 -0
  666. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scene.mjs +82 -0
  667. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/end-card.mjs +30 -0
  668. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/index.mjs +16 -0
  669. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/karaoke-line.mjs +47 -0
  670. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/kinetic-list.mjs +45 -0
  671. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/number-counter.mjs +29 -0
  672. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/signature.mjs +73 -0
  673. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/title-slam.mjs +73 -0
  674. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/stroke.mjs +236 -0
  675. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/type.mjs +180 -0
  676. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w100-400.ttf +0 -0
  677. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w100-700.ttf +0 -0
  678. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w100-900.ttf +0 -0
  679. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w125-400.ttf +0 -0
  680. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w125-700.ttf +0 -0
  681. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w125-900.ttf +0 -0
  682. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w75-400.ttf +0 -0
  683. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w75-700.ttf +0 -0
  684. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w75-900.ttf +0 -0
  685. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/OFL.txt +93 -0
  686. package/plugins/litclaude/skills/lit-typographic-motion/fonts/manifest.json +437 -0
  687. package/plugins/litclaude/skills/lit-typographic-motion/fonts/silkscreen/OFL.txt +93 -0
  688. package/plugins/litclaude/skills/lit-typographic-motion/fonts/silkscreen/Silkscreen-Bold.ttf +0 -0
  689. package/plugins/litclaude/skills/lit-typographic-motion/fonts/silkscreen/Silkscreen-Regular.ttf +0 -0
  690. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/CREDITS +42 -0
  691. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSAllure.svg +216 -0
  692. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSFelix.svg +216 -0
  693. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSOsmotron.svg +216 -0
  694. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSReadability.svg +216 -0
  695. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSTech.svg +217 -0
  696. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/OFL.txt +91 -0
  697. package/plugins/litclaude/skills/lit-typographic-motion/fonts/vt323/OFL.txt +93 -0
  698. package/plugins/litclaude/skills/lit-typographic-motion/fonts/vt323/VT323-Regular.ttf +0 -0
  699. package/plugins/litclaude/skills/lit-typographic-motion/references/brief-schema.md +91 -0
  700. package/plugins/litclaude/skills/lit-typographic-motion/references/craft-loop.md +69 -0
  701. package/plugins/litclaude/skills/lit-typographic-motion/references/engine-contract.md +104 -0
  702. package/plugins/litclaude/skills/lit-typographic-motion/references/look.md +106 -0
  703. package/plugins/litclaude/skills/lit-typographic-motion/references/runtime.md +65 -0
  704. package/plugins/litclaude/skills/lit-typographic-motion/references/sound.md +83 -0
  705. package/plugins/litclaude/skills/lit-typographic-motion/references/stage.md +192 -0
  706. package/plugins/litclaude/skills/lit-typographic-motion/references/style-bibles.md +117 -0
  707. package/plugins/litclaude/skills/lit-typographic-motion/references/treatment.md +114 -0
  708. package/plugins/litclaude/skills/lit-typographic-motion/references/type-craft.md +79 -0
  709. package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion-doctor.mjs +78 -0
  710. package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion-runtime.mjs +67 -0
  711. package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion.mjs +132 -0
  712. package/plugins/litclaude/skills/litresearch/SKILL.md +54 -18
  713. package/scripts/doctor.mjs +5 -0
  714. package/scripts/postinstall.mjs +1 -0
  715. package/plugins/litclaude/skills/lit-korean/SKILL.md +0 -327
  716. /package/plugins/litclaude/skills/{lit-korean/references/fidelity-checklist.md → lit-humanizer/references/litclaude-fidelity-checklist.md} +0 -0
  717. /package/plugins/litclaude/skills/{lit-korean/references/rewrite-rules.md → lit-humanizer/references/litclaude-rewrite-rules.md} +0 -0
  718. /package/plugins/litclaude/skills/{lit-korean/references/style-patterns.md → lit-humanizer/references/litclaude-style-patterns.md} +0 -0
@@ -0,0 +1,1534 @@
1
+ // In-page measurement module for scripts/interface-probe.mjs.
2
+ //
3
+ // `measureInterface` runs inside the page under test. The driver serialises it with
4
+ // Function.prototype.toString and evaluates it through agent-browser, so the function stays
5
+ // self-contained: no imports, no closure over module scope, no network. It reads the live DOM,
6
+ // computed styles and same-origin stylesheets and returns plain JSON. It clicks, presses and
7
+ // submits nothing: dialogs are stubbed before the first read, and the only state it touches is the
8
+ // focus sample (CF-202, restored afterwards) and a data attribute naming the hover targets the
9
+ // driver hovers after this read (CF-506). Every rule id, severity and threshold arrives in
10
+ // `options.rules` from interface-probe-rules.mjs.
11
+ export function measureInterface(options) {
12
+ const rules = options.rules;
13
+ const limits = options.limits;
14
+ const pass = options.pass;
15
+ const touch = options.touch === true;
16
+ const findings = [];
17
+ const notVerified = [];
18
+ const perRule = new Map();
19
+ const t = (id) => rules[id]?.threshold ?? {};
20
+ const on = (id) => Boolean(rules[id]);
21
+ const round = (value, digits = 2) => Number(Number(value).toFixed(digits));
22
+
23
+ // MD-011: nothing the probe reads may block on a dialog.
24
+ for (const name of ["alert", "confirm", "prompt"]) {
25
+ try { window[name] = () => (name === "confirm" ? false : null); } catch { /* read-only global */ }
26
+ }
27
+
28
+ const counted = (id) => {
29
+ const count = perRule.get(id) ?? 0;
30
+ perRule.set(id, count + 1);
31
+ return count < limits.findingsPerRule;
32
+ };
33
+ // A judgment rule's hit is a candidate for the reviewer, never a finding (MD-010: Inferred).
34
+ const candidate = (id, finding) => {
35
+ if (!on(id) || !counted(id)) return;
36
+ const why = rules[id].judgment ?? "reviewer decides";
37
+ notVerified.push({ rule: id, reason: `judgment: ${finding.value}${finding.selector ? ` at ${finding.selector}` : ""} (${why})`.slice(0, 220) });
38
+ };
39
+ const add = (id, finding) => {
40
+ const rule = rules[id];
41
+ if (!rule) return;
42
+ if (rule.judgment) { candidate(id, finding); return; }
43
+ if (!counted(id) || findings.length >= limits.findingsPerViewport) return;
44
+ const note = [finding.note, rule.note].filter(Boolean).join("; ");
45
+ findings.push({
46
+ rule: id,
47
+ severity: finding.severity ?? rule.severity,
48
+ tier: finding.tier ?? rule.tier,
49
+ selector: finding.selector ?? null,
50
+ value: finding.value ?? null,
51
+ threshold: finding.threshold ?? null,
52
+ ...(note ? { note: note.slice(0, 160) } : {}),
53
+ });
54
+ };
55
+ const skip = (id, reason) => { if (on(id) || id === "*") notVerified.push({ rule: id, reason }); };
56
+
57
+ const vw = window.innerWidth;
58
+ const vh = window.innerHeight;
59
+ const doc = document.documentElement;
60
+ const body = document.body;
61
+ if (!body) return { viewport: { width: vw, height: vh }, ready: document.readyState, counts: {}, findings, notVerified: [{ rule: "*", reason: "page has no body" }], hoverTargets: [] };
62
+
63
+ // ---- shared helpers -------------------------------------------------------------------------
64
+ const selectorOf = (el) => {
65
+ const part = (node) => {
66
+ if (node.id && /^[A-Za-z][\w-]*$/u.test(node.id)) return `${node.tagName.toLowerCase()}#${node.id}`;
67
+ const classes = typeof node.className === "string"
68
+ ? node.className.trim().split(/\s+/u).filter((name) => /^[A-Za-z_-][\w-]*$/u.test(name)).slice(0, 2)
69
+ : [];
70
+ let text = node.tagName.toLowerCase() + classes.map((name) => `.${name}`).join("");
71
+ const parent = node.parentElement;
72
+ if (parent) {
73
+ const same = [...parent.children].filter((child) => child.tagName === node.tagName);
74
+ if (same.length > 1) text += `:nth-of-type(${same.indexOf(node) + 1})`;
75
+ }
76
+ return text;
77
+ };
78
+ const parts = [];
79
+ for (let node = el; node && node.nodeType === 1 && parts.length < 3; node = node.parentElement) {
80
+ parts.unshift(part(node));
81
+ if (node.id || node === body) break;
82
+ }
83
+ return parts.join(" > ");
84
+ };
85
+ const snippet = (el, length = 40) => (el.innerText ?? el.textContent ?? "").replace(/\s+/gu, " ").trim().slice(0, length);
86
+ const styleOf = (el) => getComputedStyle(el);
87
+ const px = (value) => parseFloat(value) || 0;
88
+ const classText = (el) => `${typeof el.className === "string" ? el.className : el.getAttribute?.("class") ?? ""} ${el.id ?? ""}`;
89
+ const classTokens = (el) => classText(el).toLowerCase().split(/[\s_-]+/u).filter(Boolean);
90
+ const related = (a, b) => a === b || a.contains(b) || b.contains(a);
91
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
92
+ const ms = (value) => String(value).split(",").map((part) => (part.trim().endsWith("ms") ? px(part) : px(part) * 1000));
93
+
94
+ // Visibility scope: rendered, not an accessibility-only span, not parked off canvas.
95
+ const visible = (el) => {
96
+ if (typeof el.checkVisibility === "function" && !el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return false;
97
+ const style = styleOf(el);
98
+ if (el.offsetParent === null && style.position !== "fixed" && el !== body) return false;
99
+ const rect = el.getBoundingClientRect();
100
+ if (rect.width * rect.height <= 4) return false;
101
+ if (style.clipPath === "inset(50%)" || /^rect\(0[a-z]*[, ]+0/u.test(style.clip)) return false;
102
+ if (el.matches(".sr-only, .visually-hidden, .visuallyhidden")) return false;
103
+ if (rect.right < -9000 || rect.left > vw + 9000) return false;
104
+ return true;
105
+ };
106
+ const all = [...body.querySelectorAll("*")]
107
+ .filter((el) => !/^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE|HEAD|META|LINK|BR)$/u.test(el.tagName))
108
+ .slice(0, limits.nodes);
109
+ const shown = all.filter(visible);
110
+ const directText = (el) => [...el.childNodes].filter((node) => node.nodeType === 3 && node.textContent.trim() !== "");
111
+ const textElements = shown.filter((el) => !(el instanceof SVGElement) && directText(el).length > 0);
112
+ const lineRects = (el) => {
113
+ const range = document.createRange();
114
+ range.selectNodeContents(el);
115
+ return [...range.getClientRects()].filter((rect) => rect.width > 1 && rect.height > 1);
116
+ };
117
+ const lines = (el) => {
118
+ const byTop = new Map();
119
+ for (const rect of lineRects(el)) {
120
+ const top = Math.round(rect.top);
121
+ byTop.set(top, (byTop.get(top) ?? 0) + rect.width);
122
+ }
123
+ return [...byTop.entries()].sort((a, b) => a[0] - b[0]).map(([, width]) => width);
124
+ };
125
+ const lineCount = (el) => lines(el).length;
126
+ const cjkShare = (text) => {
127
+ const letters = text.replace(/[\s\d\p{P}\p{S}]/gu, "");
128
+ if (!letters.length) return 0;
129
+ return (letters.match(/[぀-ヿ㐀-鿿가-힯]/gu)?.length ?? 0) / letters.length;
130
+ };
131
+
132
+ // Colour: every CSS colour syntax is normalised through a detached 1x1 canvas.
133
+ const canvas = document.createElement("canvas");
134
+ canvas.width = 1;
135
+ canvas.height = 1;
136
+ const context = canvas.getContext("2d", { willReadFrequently: true });
137
+ const colorCache = new Map();
138
+ const parseColor = (value) => {
139
+ if (colorCache.has(value)) return colorCache.get(value);
140
+ let parsed = null;
141
+ if (value && value !== "transparent" && value !== "none") {
142
+ context.clearRect(0, 0, 1, 1);
143
+ context.fillStyle = "#000";
144
+ context.fillStyle = value;
145
+ context.fillRect(0, 0, 1, 1);
146
+ const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
147
+ parsed = a === 0 ? null : { r: Math.round((r * 255) / a), g: Math.round((g * 255) / a), b: Math.round((b * 255) / a), a: a / 255 };
148
+ }
149
+ colorCache.set(value, parsed);
150
+ return parsed;
151
+ };
152
+ const blend = (top, bottom) => ({ r: top.r * top.a + bottom.r * (1 - top.a), g: top.g * top.a + bottom.g * (1 - top.a), b: top.b * top.a + bottom.b * (1 - top.a), a: 1 });
153
+ const channel = (value) => { const c = value / 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
154
+ const luminance = (c) => 0.2126 * channel(c.r) + 0.7152 * channel(c.g) + 0.0722 * channel(c.b);
155
+ const ratio = (a, b) => { const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); return (hi + 0.05) / (lo + 0.05); };
156
+ // APCA-W3 0.0.98G lightness contrast, reported only as an advisory figure (CF-203).
157
+ const apca = (text, background) => {
158
+ const y = (c) => 0.2126729 * (c.r / 255) ** 2.4 + 0.7151522 * (c.g / 255) ** 2.4 + 0.072175 * (c.b / 255) ** 2.4;
159
+ const clamp = (value) => (value < 0.022 ? value + (0.022 - value) ** 1.414 : value);
160
+ const yt = clamp(y(text));
161
+ const yb = clamp(y(background));
162
+ if (Math.abs(yb - yt) < 0.0005) return 0;
163
+ if (yb > yt) { const s = (yb ** 0.56 - yt ** 0.57) * 1.14; return s < 0.1 ? 0 : (s - 0.027) * 100; }
164
+ const s = (yb ** 0.65 - yt ** 0.62) * 1.14;
165
+ return s > -0.1 ? 0 : (s + 0.027) * 100;
166
+ };
167
+ const hsl = (c) => {
168
+ const r = c.r / 255; const g = c.g / 255; const b = c.b / 255;
169
+ const max = Math.max(r, g, b); const min = Math.min(r, g, b); const l = (max + min) / 2;
170
+ if (max === min) return { h: 0, s: 0, l };
171
+ const d = max - min;
172
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
173
+ const h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
174
+ return { h: h * 60, s, l };
175
+ };
176
+ const spread = (c) => Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
177
+ const hueDistance = (a, b) => { const d = Math.abs(a - b) % 360; return d > 180 ? 360 - d : d; };
178
+ const colorPattern = /(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\([^()]*\)|#[0-9a-f]{3,8}\b/giu;
179
+ const colorsIn = (value) => (value.match(colorPattern) ?? []).map(parseColor).filter(Boolean);
180
+ const lengthsIn = (value) => value.replace(colorPattern, "").match(/-?[\d.]+px/gu)?.map(px) ?? [];
181
+ const shadowsOf = (value) => (value === "none" ? [] : value.split(/,(?![^()]*\))/u)).map((shadow) => {
182
+ const [x = 0, y = 0, blur = 0, spreadPx = 0] = lengthsIn(shadow);
183
+ return { inset: /inset/u.test(shadow), x, y, blur, spread: spreadPx, lengths: lengthsIn(shadow).length, color: colorsIn(shadow)[0] ?? null };
184
+ });
185
+
186
+ const darkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches && /dark/u.test(styleOf(doc).colorScheme);
187
+ const canvasColor = darkScheme ? { r: 18, g: 18, b: 18, a: 1 } : { r: 255, g: 255, b: 255, a: 1 };
188
+ const isMedia = (el) => /^(IMG|VIDEO|CANVAS|PICTURE|IFRAME|SVG)$/iu.test(el.tagName);
189
+
190
+ // Painted background behind an element, nearest layer first. In the viewport the paint stack at
191
+ // the text's centre is walked, so a positioned sibling under the text counts; elsewhere the
192
+ // ancestor chain is used. Returns { color }, { gradient: stops } (CF-204), { image: true } or
193
+ // { overlap: true } when another element paints on top of the text (not verified).
194
+ const paintBehind = (el, { skipSelf = false } = {}) => {
195
+ const rect = el.getBoundingClientRect();
196
+ const cx = rect.left + Math.min(rect.width, 40) / 2;
197
+ const cy = rect.top + rect.height / 2;
198
+ let stack = null;
199
+ if (cx >= 0 && cy >= 0 && cx < vw && cy < vh) {
200
+ const hits = document.elementsFromPoint(cx, cy);
201
+ if (!skipSelf && hits[0] && !related(hits[0], el)) return { overlap: true };
202
+ const own = hits.findIndex((node) => node === el || el.contains(node));
203
+ if (own >= 0) stack = hits.slice(own + 1).filter((node) => !el.contains(node));
204
+ }
205
+ if (!stack) {
206
+ stack = [];
207
+ for (let node = el.parentElement; node; node = node.parentElement) stack.push(node);
208
+ }
209
+ const layers = [];
210
+ for (const node of skipSelf ? stack : [el, ...stack]) {
211
+ if (isMedia(node)) return { image: true };
212
+ const style = styleOf(node);
213
+ if (style.backgroundImage !== "none") {
214
+ if (/url\(/u.test(style.backgroundImage)) return { image: true };
215
+ const stops = colorsIn(style.backgroundImage);
216
+ if (stops.length) return { gradient: stops.map((stop) => blend(stop, canvasColor)) };
217
+ }
218
+ const color = parseColor(style.backgroundColor);
219
+ const opacity = Number(style.opacity);
220
+ if (color) layers.push({ ...color, a: color.a * opacity });
221
+ if (color && color.a >= 0.999 && opacity >= 0.999) break;
222
+ }
223
+ let color = canvasColor;
224
+ for (const layer of layers.reverse()) color = blend(layer, color);
225
+ return { color };
226
+ };
227
+ const solidBehind = (el, options) => paintBehind(el, options).color ?? null;
228
+ const opacityChain = (el) => {
229
+ let value = 1;
230
+ for (let node = el; node && node.nodeType === 1; node = node.parentElement) value *= Number(styleOf(node).opacity);
231
+ return value;
232
+ };
233
+
234
+ // Same-origin stylesheet rules, read once. Cross-origin sheets are listed as not verified.
235
+ const styleRules = [];
236
+ const keyframes = new Map();
237
+ let unreadableSheets = 0;
238
+ const collect = (list) => {
239
+ for (const rule of list) {
240
+ if (rule instanceof CSSKeyframesRule) keyframes.set(rule.name, rule);
241
+ else if (rule instanceof CSSStyleRule) styleRules.push(rule);
242
+ if (rule.cssRules && !(rule instanceof CSSKeyframesRule)) collect(rule.cssRules);
243
+ }
244
+ };
245
+ for (const sheet of document.styleSheets) {
246
+ try { collect(sheet.cssRules); } catch { unreadableSheets += 1; }
247
+ }
248
+ if (unreadableSheets) skip("*", `cross-origin stylesheet (${unreadableSheets}); its rules were not read`);
249
+ const safeMatches = (el, selector) => { try { return el.matches(selector); } catch { return false; } };
250
+ const selectorParts = (rule) => (rule.selectorText ?? "").split(/,(?![^()]*\))/u).map((part) => part.trim());
251
+ const stripPseudo = (selector, pseudo) => selector.replace(new RegExp(`:${pseudo}(?![\\w-])`, "gu"), "").trim() || "*";
252
+
253
+ // Controls and their accessible names (enough of the accname rules for a presence check).
254
+ const controlSelector = "a[href],button,input:not([type=hidden]),select,textarea,summary,[role=button],[role=link],[role=checkbox],[role=radio],[role=switch],[role=tab],[role=menuitem]";
255
+ const controls = [...body.querySelectorAll(controlSelector)].slice(0, limits.nodes);
256
+ const focusables = [...new Set([...controls, ...[...body.querySelectorAll("[tabindex]")].filter((el) => el.tabIndex >= 0)])].slice(0, limits.nodes);
257
+ const accessibleName = (el) => {
258
+ const byId = (el.getAttribute("aria-labelledby") ?? "").split(/\s+/u).map((id) => document.getElementById(id)?.textContent ?? "").join(" ").trim();
259
+ if (byId) return byId;
260
+ const aria = (el.getAttribute("aria-label") ?? "").trim();
261
+ if (aria) return aria;
262
+ if (el.matches("input[type=submit],input[type=reset],input[type=button]")) return el.value || el.type;
263
+ if (el.matches("input[type=image]")) return el.alt ?? "";
264
+ if (el.labels?.length) { const text = [...el.labels].map((label) => label.textContent).join(" ").trim(); if (text) return text; }
265
+ const text = (el.innerText ?? "").trim();
266
+ if (text) return text;
267
+ const img = el.querySelector("img[alt]:not([alt='']), svg title");
268
+ if (img) return (img.getAttribute("alt") ?? img.textContent ?? "").trim();
269
+ return (el.getAttribute("title") ?? el.getAttribute("placeholder") ?? "").trim();
270
+ };
271
+ const filled = (el) => {
272
+ const fill = parseColor(styleOf(el).backgroundColor);
273
+ const behind = el.parentElement ? solidBehind(el, { skipSelf: true }) : null;
274
+ return Boolean(fill && fill.a >= 0.9 && (!behind || ratio(blend(fill, behind), behind) > 1.1));
275
+ };
276
+ // CF-701 primary action: a form's sole submit button, or the first filled button or
277
+ // button-styled link inside the first viewport.
278
+ const primaries = new Set();
279
+ for (const form of document.forms) {
280
+ const submits = [...form.elements].filter((el) => el.matches("button,input[type=submit]") && el.type === "submit");
281
+ if (submits.length === 1) primaries.add(submits[0]);
282
+ }
283
+ const firstFilled = controls.find((el) => el.matches("button,a[href],[role=button]") && visible(el) && el.getBoundingClientRect().top < vh && filled(el));
284
+ if (firstFilled) primaries.add(firstFilled);
285
+ const isPrimary = (el) => primaries.has(el) || /(^|[\s_-])(primary|cta)([\s_-]|$)/iu.test(classText(el)) || el.hasAttribute("data-primary");
286
+ const destructive = (el) => /\b(delete|remove|discard|erase|clear all)\b|삭제|제거|지우기|비우기/iu.test(`${accessibleName(el)} ${classText(el)}`);
287
+ const inProse = (el) => {
288
+ if (el.tagName !== "A" || styleOf(el).display !== "inline") return false;
289
+ const block = el.parentElement;
290
+ return Boolean(block && [...block.childNodes].some((node) => node.nodeType === 3 && node.textContent.trim().length > 0));
291
+ };
292
+ const standalone = controls.filter((el) => (visible(el) || (el.labels?.length && visible(el.labels[0]))) && !inProse(el)).slice(0, limits.pairControls);
293
+
294
+ // MD-008: content behind closed disclosures and cross-origin frames is out of reach, not passed.
295
+ const closed = body.querySelectorAll("details:not([open]), [aria-expanded=false], dialog:not([open]), [role=tabpanel][hidden]").length;
296
+ if (closed) skip("*", `closed disclosures: ${closed}`);
297
+ const foreignFrames = [...body.querySelectorAll("iframe[src]")].filter((frame) => { try { return new URL(frame.src, location.href).origin !== location.origin; } catch { return false; } }).length;
298
+ if (foreignFrames) skip("*", `cross-origin iframes: ${foreignFrames}`);
299
+
300
+ // ---- RS-006 / RS-004: page-level horizontal overflow --------------------------------------
301
+ const overflowRule = pass === "zoom" ? "RS-004" : "RS-006";
302
+ if (on(overflowRule)) {
303
+ const width = Math.max(doc.scrollWidth, body.scrollWidth);
304
+ if (width > vw + t(overflowRule).tolerancePx) {
305
+ const scroller = (el) => {
306
+ for (let node = el.parentElement; node && node !== body; node = node.parentElement) {
307
+ if (/(auto|scroll|hidden|clip)/u.test(styleOf(node).overflowX)) return true;
308
+ }
309
+ return false;
310
+ };
311
+ const culprits = shown
312
+ .map((el) => ({ el, rect: el.getBoundingClientRect() }))
313
+ .filter(({ el, rect }) => (rect.right > vw + 1 || rect.left < -1) && !scroller(el))
314
+ .filter(({ el }, _index, list) => !list.some((other) => other.el !== el && other.el.contains(el)))
315
+ .slice(0, 3);
316
+ add(overflowRule, {
317
+ selector: culprits.map(({ el }) => selectorOf(el)).join(" | ") || "html",
318
+ value: `${width - vw}px wider than the viewport`,
319
+ threshold: `≤ ${t(overflowRule).tolerancePx}px`,
320
+ });
321
+ }
322
+ }
323
+
324
+ // ---- RS-007 / RS-004: clipped, offscreen and overlapping text ------------------------------
325
+ // At the zoom pass clipped and offscreen text is RS-004's; overlap stays RS-007 (MEDIUM).
326
+ const clipRule = pass === "zoom" ? "RS-004" : "RS-007";
327
+ if (on("RS-007")) {
328
+ const th = t("RS-007");
329
+ const clamped = (style) => style.textOverflow === "ellipsis" || [style.webkitLineClamp, style.lineClamp].some((value) => value && value !== "none");
330
+ const inScroller = (el) => {
331
+ for (let node = el.parentElement; node && node !== body; node = node.parentElement) if (/(auto|scroll)/u.test(styleOf(node).overflowX)) return true;
332
+ return false;
333
+ };
334
+ for (const el of textElements) {
335
+ const style = styleOf(el);
336
+ if (clamped(style)) continue;
337
+ let clippedBy = (/(hidden|clip)/u.test(style.overflowX) && el.scrollWidth > el.clientWidth + th.clipPx)
338
+ || (/(hidden|clip)/u.test(style.overflowY) && el.scrollHeight > el.clientHeight + th.clipPx) ? el : null;
339
+ const rects = lineRects(el);
340
+ if (!clippedBy && rects.length) {
341
+ const box = { left: Math.min(...rects.map((r) => r.left)), right: Math.max(...rects.map((r) => r.right)), bottom: Math.max(...rects.map((r) => r.bottom)) };
342
+ for (let node = el.parentElement; node && node !== body; node = node.parentElement) {
343
+ const ancestor = styleOf(node);
344
+ if (!/(hidden|clip)/u.test(`${ancestor.overflowX} ${ancestor.overflowY}`)) continue;
345
+ if (clamped(ancestor)) break;
346
+ const r = node.getBoundingClientRect();
347
+ if (box.right > r.right + th.clipPx || box.bottom > r.bottom + th.clipPx || box.left < r.left - th.clipPx) clippedBy = node;
348
+ break;
349
+ }
350
+ }
351
+ if (clippedBy) {
352
+ add(clipRule, { selector: selectorOf(el), value: `"${snippet(el)}" cut off by ${clippedBy === el ? "its own box" : selectorOf(clippedBy)}`, threshold: "no clipping without text-overflow or line-clamp" });
353
+ continue;
354
+ }
355
+ const rect = el.getBoundingClientRect();
356
+ if ((rect.right > vw + th.offscreenPx || rect.left < -th.offscreenPx) && !inScroller(el)) {
357
+ add(clipRule, { selector: selectorOf(el), value: `"${snippet(el, 30)}" runs ${round(Math.max(rect.right - vw, -rect.left), 0)}px off screen`, threshold: `within ${th.offscreenPx}px of the viewport edge` });
358
+ }
359
+ }
360
+ // Overlap: line boxes cut to the area their clipping ancestors leave visible.
361
+ const clipBox = (el) => {
362
+ let box = { left: -Infinity, top: -Infinity, right: Infinity, bottom: Infinity };
363
+ for (let node = el; node && node !== body; node = node.parentElement) {
364
+ const style = styleOf(node);
365
+ if (!/(hidden|clip|auto|scroll)/u.test(`${style.overflowX} ${style.overflowY}`)) continue;
366
+ const r = node.getBoundingClientRect();
367
+ box = { left: Math.max(box.left, r.left), top: Math.max(box.top, r.top), right: Math.min(box.right, r.right), bottom: Math.min(box.bottom, r.bottom) };
368
+ }
369
+ return box;
370
+ };
371
+ const boxes = textElements.slice(0, 600).map((el) => {
372
+ const clip = clipBox(el);
373
+ const rects = lineRects(el).map((r) => {
374
+ const left = Math.max(r.left, clip.left); const top = Math.max(r.top, clip.top);
375
+ const right = Math.min(r.right, clip.right); const bottom = Math.min(r.bottom, clip.bottom);
376
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
377
+ }).filter((r) => r.width > 1 && r.height > 1);
378
+ return { el, rects };
379
+ });
380
+ for (let i = 0; i < boxes.length; i += 1) {
381
+ for (let j = i + 1; j < boxes.length; j += 1) {
382
+ const a = boxes[i]; const b = boxes[j];
383
+ if (a.el.contains(b.el) || b.el.contains(a.el)) continue;
384
+ let worst = 0;
385
+ for (const ra of a.rects) for (const rb of b.rects) {
386
+ const w = Math.min(ra.right, rb.right) - Math.max(ra.left, rb.left);
387
+ const h = Math.min(ra.bottom, rb.bottom) - Math.max(ra.top, rb.top);
388
+ if (w > 0 && h > 0) worst = Math.max(worst, (w * h) / Math.min(ra.width * ra.height, rb.width * rb.height));
389
+ }
390
+ if (worst >= th.overlapRatio) {
391
+ add("RS-007", { severity: th.overlapSeverity, selector: `${selectorOf(a.el)} × ${selectorOf(b.el)}`, value: `overlap ${round(worst)} of the smaller box ("${snippet(a.el, 24)}" / "${snippet(b.el, 24)}")`, threshold: `< ${th.overlapRatio}` });
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ // ---- RS-008: mobile input font floor (text-entry controls, touch-primary widths) -----------
398
+ if (on("RS-008") && touch) {
399
+ const entry = "input:not([type]),input[type=text],input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=url],textarea,select,[contenteditable]:not([contenteditable=false])";
400
+ for (const el of shown.filter((node) => node.matches(entry))) {
401
+ const size = px(styleOf(el).fontSize);
402
+ if (size + 0.01 < t("RS-008").minFontPx) add("RS-008", { selector: selectorOf(el), value: `${round(size, 1)}px`, threshold: `≥ ${t("RS-008").minFontPx}px` });
403
+ }
404
+ }
405
+
406
+ // ---- RS-009: safe-area offsets, read from authored declarations -----------------------------
407
+ if (on("RS-009") && /viewport-fit\s*=\s*cover/iu.test(document.querySelector("meta[name=viewport]")?.content ?? "")) {
408
+ const edge = t("RS-009").edgePx;
409
+ for (const el of shown.filter((node) => /^(fixed|sticky)$/u.test(styleOf(node).position) && (node.matches(controlSelector) || node.querySelector(controlSelector)))) {
410
+ const r = el.getBoundingClientRect();
411
+ const sides = [["top", r.top <= edge], ["bottom", r.bottom >= vh - edge], ["left", r.left <= edge], ["right", r.right >= vw - edge]].filter(([, touching]) => touching).map(([side]) => side);
412
+ if (!sides.length) continue;
413
+ const authored = styleRules.filter((rule) => safeMatches(el, rule.selectorText)).map((rule) => rule.style.cssText).concat(el.style.cssText).join(" ");
414
+ const missing = sides.filter((side) => !authored.includes(`env(safe-area-inset-${side}`));
415
+ if (!missing.length) continue;
416
+ if (unreadableSheets) skip("RS-009", `${selectorOf(el)} may take its safe-area offset from a cross-origin stylesheet`);
417
+ else add("RS-009", { selector: selectorOf(el), value: `pinned to ${missing.join(", ")} with no env(safe-area-inset-*)`, threshold: "env(safe-area-inset-<edge>) in its offset or padding" });
418
+ }
419
+ }
420
+
421
+ // ---- RS-010: horizontal rail peek, only for rails with no other scroll cue ------------------
422
+ if (on("RS-010")) {
423
+ for (const rail of shown.filter((el) => el !== body && /(auto|scroll)/u.test(styleOf(el).overflowX) && el.scrollWidth > el.clientWidth + 4 && el.children.length >= 2)) {
424
+ const controlled = rail.id && document.querySelector(`[aria-controls~="${CSS.escape(rail.id)}"]`);
425
+ const scope = rail.parentElement ?? rail;
426
+ const cue = controlled || rail.querySelector("button,[role=tab]") || /\b\d+\s*\/\s*\d+\b/u.test(snippet(scope, 400));
427
+ if (cue) continue;
428
+ const r = rail.getBoundingClientRect();
429
+ const next = [...rail.children].map((child) => child.getBoundingClientRect()).find((c) => c.right > r.right + 1 && c.left < r.right);
430
+ const peek = next ? r.right - next.left : 0;
431
+ const faded = styleOf(rail).maskImage && styleOf(rail).maskImage !== "none";
432
+ if (peek < t("RS-010").minPeekPx) {
433
+ add("RS-010", {
434
+ severity: peek === 0 && !faded ? "HIGH" : undefined,
435
+ selector: selectorOf(rail),
436
+ value: `${round(peek, 1)}px of the next item showing`,
437
+ threshold: `${t("RS-010").minPeekPx}-${t("RS-010").maxPeekPx}px`,
438
+ note: peek === 0 && !faded ? "hidden items with no cue at all (MD-011 #10)" : undefined,
439
+ });
440
+ }
441
+ }
442
+ }
443
+
444
+ // ---- RS-002: the dark pass has a dark surface -----------------------------------------------
445
+ if (on("RS-002") && pass === "dark") {
446
+ const th = t("RS-002");
447
+ const points = [[vw / 2, 8], [vw / 2, vh / 2], [12, vh / 2], [vw - 12, vh / 2], [vw / 2, vh - 8]];
448
+ const values = points.map(([x, y]) => {
449
+ const hit = document.elementFromPoint(x, y) ?? body;
450
+ const own = parseColor(styleOf(hit).backgroundColor);
451
+ return luminance(own && own.a >= 0.999 ? own : (solidBehind(hit) ?? canvasColor));
452
+ });
453
+ const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
454
+ if (mean >= th.maxLuminance) {
455
+ // Theme-control fallback: discover, never click (MD-011).
456
+ const words = new RegExp(th.themeControlWords.map(escapeRegExp).join("|"), "iu");
457
+ const toggle = [...body.querySelectorAll("[data-theme-toggle],button,[role=switch],[role=button]")].find((el) => el.matches("[data-theme-toggle]") || words.test(`${accessibleName(el)} ${el.getAttribute("title") ?? ""}`));
458
+ if (toggle) skip("RS-002", `dark path may sit behind ${selectorOf(toggle)}; the probe activates no control`);
459
+ else add("RS-002", { tier: "derived", selector: "html", value: `mean surface luminance ${round(mean)}; the dark media query did not darken the page and no theme control was found`, threshold: `< ${th.maxLuminance}` });
460
+ }
461
+ }
462
+
463
+ // ---- RS-003: reduced motion honoured (motion properties only; progress motion exempt) ------
464
+ if (on("RS-003") && pass === "reduced-motion") {
465
+ const th = t("RS-003");
466
+ const kebab = (name) => name.replace(/[A-Z]/gu, (c) => `-${c.toLowerCase()}`);
467
+ const isMotion = (property) => th.motionProperties.some((name) => property === name || property.startsWith(`${name}-`));
468
+ const essential = (el) => Boolean(el?.closest?.("progress,[role=progressbar],[role=status],[aria-busy=true]"));
469
+ const framesOf = (name) => [...(keyframes.get(name)?.cssRules ?? [])].flatMap((frame) => [...frame.style]);
470
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
471
+ if (!reduced) add("RS-003", { selector: "html", value: "prefers-reduced-motion did not match", threshold: "reduce" });
472
+ let worst = null;
473
+ for (const el of shown) {
474
+ if (essential(el)) continue;
475
+ const style = styleOf(el);
476
+ const properties = style.transitionProperty.split(",").map((part) => part.trim());
477
+ const durations = ms(style.transitionDuration);
478
+ properties.forEach((property, index) => {
479
+ const duration = durations[index % durations.length];
480
+ if (property !== "none" && isMotion(property) && duration > th.maxMotionMs && (!worst || duration > worst.duration)) worst = { el, duration, what: `transition of ${property}` };
481
+ });
482
+ if (style.animationName !== "none") {
483
+ const names = style.animationName.split(",").map((part) => part.trim());
484
+ const animationDurations = ms(style.animationDuration);
485
+ names.forEach((name, index) => {
486
+ const duration = animationDurations[index % animationDurations.length];
487
+ if (duration > th.maxMotionMs && framesOf(name).some(isMotion) && (!worst || duration > worst.duration)) worst = { el, duration, what: `animation ${name}` };
488
+ });
489
+ }
490
+ }
491
+ if (worst) add("RS-003", { selector: selectorOf(worst.el), value: `${round(worst.duration, 0)}ms ${worst.what} still moves`, threshold: `≤ ${th.maxMotionMs}ms of motion` });
492
+ const running = document.getAnimations().filter((animation) => {
493
+ if (animation.playState !== "running" || Number(animation.effect?.getTiming().duration) <= th.maxMotionMs || essential(animation.effect?.target)) return false;
494
+ return (animation.effect?.getKeyframes?.() ?? []).some((frame) => Object.keys(frame).map(kebab).some(isMotion));
495
+ });
496
+ if (running.length) add("RS-003", { selector: running[0].effect?.target ? selectorOf(running[0].effect.target) : null, value: `${running.length} moving animation(s) running`, threshold: `≤ ${th.maxMotionMs}ms of motion` });
497
+ const playing = [...document.querySelectorAll("video")].filter((video) => !video.paused);
498
+ if (playing.length) add("RS-003", { selector: selectorOf(playing[0]), value: `${playing.length} video(s) playing`, threshold: "paused" });
499
+ }
500
+
501
+ // ---- CF-101..CF-105: measure, line-height, wrap ---------------------------------------------
502
+ const lineHeightRatio = (el, style) => {
503
+ const size = px(style.fontSize);
504
+ if (style.lineHeight !== "normal") return px(style.lineHeight) / size;
505
+ const tops = [...new Set(lineRects(el).map((rect) => Math.round(rect.top)))].sort((a, b) => a - b);
506
+ return tops.length >= 2 ? (tops[1] - tops[0]) / size : t("CF-102").normalRatio;
507
+ };
508
+ const prose = textElements.filter((el) => el.matches("p, li, blockquote, dd, td") && !el.closest("nav, footer, header, marquee, button, label, [role=navigation], [role=contentinfo]"));
509
+ const proseSet = new Set(prose);
510
+ const sample = (el) => (el.innerText ?? "").slice(0, limits.textChars);
511
+ const headings = shown.filter((node) => node.matches("h1,h2,h3,h4,h5,h6,[role=heading]"));
512
+ if (on("CF-101")) {
513
+ const th = t("CF-101");
514
+ for (const el of prose) {
515
+ const text = sample(el);
516
+ const widths = lines(el);
517
+ const total = widths.reduce((sum, width) => sum + width, 0);
518
+ if (widths.length < th.minLines || !total) continue;
519
+ const longest = Math.max(...widths.map((width) => Math.round(text.length * (width / total))));
520
+ const cjk = cjkShare(text) > th.cjkShare;
521
+ const max = cjk ? th.cjkMaxCh : th.latinMaxCh;
522
+ if (longest > max) add("CF-101", { selector: selectorOf(el), value: `~${longest} chars on the longest line${cjk ? " (CJK)" : ""}`, threshold: `≤ ${max} (target ${th.targetCh}ch)` });
523
+ }
524
+ }
525
+ if (on("CF-102")) {
526
+ for (const el of headings) {
527
+ if (lineCount(el) < 2) continue;
528
+ const style = styleOf(el);
529
+ const value = lineHeightRatio(el, style);
530
+ const min = px(style.fontSize) >= t("CF-102").displayFontPx ? t("CF-102").displayMin : t("CF-102").min;
531
+ if (value < min - 0.01 || value > t("CF-102").max + 0.01) add("CF-102", { selector: selectorOf(el), value: `line-height ${round(value)}`, threshold: `${min}-${t("CF-102").max}` });
532
+ }
533
+ }
534
+ for (const el of textElements) {
535
+ if (el.matches("h1,h2,h3,h4,h5,h6,[role=heading]")) continue;
536
+ const count = lineCount(el);
537
+ if (count < 2) continue;
538
+ const value = lineHeightRatio(el, styleOf(el));
539
+ if (proseSet.has(el)) {
540
+ if (!on("CF-103")) continue;
541
+ const cjk = cjkShare(sample(el)) > t("CF-103").cjkShare;
542
+ const min = cjk ? t("CF-103").cjkMin : t("CF-103").latinMin;
543
+ if (value < min - 0.01) add("CF-103", { selector: selectorOf(el), value: `line-height ${round(value)}${cjk ? " (CJK)" : ""}`, threshold: `≥ ${min} (target ${t("CF-103").target})` });
544
+ } else if (on("CF-104") && count >= t("CF-104").minLines && value < t("CF-104").min - 0.01) {
545
+ add("CF-104", { selector: selectorOf(el), value: `line-height ${round(value)} over ${count} lines`, threshold: `≥ ${t("CF-104").min}` });
546
+ }
547
+ }
548
+ if (on("CF-105")) {
549
+ const th = t("CF-105");
550
+ // Wrap mode only matters once a heading wraps; a single-line heading is not scored.
551
+ for (const el of headings.filter((node) => lineCount(node) >= 2)) {
552
+ const style = styleOf(el);
553
+ const mode = `${style.textWrap ?? ""} ${style.textWrapStyle ?? ""}`;
554
+ if (!th.headingWrap.some((value) => mode.includes(value))) add("CF-105", { selector: selectorOf(el), value: `wraps to ${lineCount(el)} lines with text-wrap ${mode.trim() || "wrap"}`, threshold: `text-wrap: ${th.headingWrap.join(" or ")}` });
555
+ }
556
+ // The orphan half has no sourced cutoff: a reviewer's call.
557
+ for (const el of prose) {
558
+ const widths = lines(el);
559
+ if (widths.length < 2) continue;
560
+ const before = widths.slice(0, -1);
561
+ const share = widths[widths.length - 1] / (before.reduce((sum, width) => sum + width, 0) / before.length);
562
+ if (share < th.orphanShare) candidate("CF-105", { selector: selectorOf(el), value: `last line ${Math.round(share * 100)}% of the lines before it (possible orphan)` });
563
+ }
564
+ }
565
+
566
+ // ---- CF-106: tabular figures in numeric columns ---------------------------------------------
567
+ if (on("CF-106")) {
568
+ const numeric = (text) => /\d/u.test(text) && /^[\s+\-−±()$€£¥₩%.,:\d0-9]*\d[\s+\-−±()$€£¥₩%.,:\d0-9A-Za-z가-힣]{0,6}$/u.test(text.trim());
569
+ for (const table of shown.filter((el) => el.matches("table,[role=table],[role=grid]"))) {
570
+ const rows = [...table.querySelectorAll("tr,[role=row]")];
571
+ const width = Math.max(0, ...rows.map((row) => row.children.length));
572
+ for (let column = 0; column < width; column += 1) {
573
+ const cells = rows.map((row) => row.children[column]).filter((cell) => cell && cell.matches("td,[role=cell],[role=gridcell]"));
574
+ const numbers = cells.filter((cell) => numeric(cell.innerText ?? ""));
575
+ if (numbers.length < t("CF-106").minCells || numbers.length / Math.max(1, cells.length) < t("CF-106").numericShare) continue;
576
+ const plain = numbers.filter((cell) => {
577
+ const style = styleOf(cell.querySelector("*") ?? cell);
578
+ return !/tabular-nums/u.test(style.fontVariantNumeric) && !/tnum/u.test(style.fontFeatureSettings) && !/mono/iu.test(style.fontFamily);
579
+ });
580
+ if (plain.length) add("CF-106", { selector: `${selectorOf(table)} column ${column + 1}`, value: `${plain.length} numeric cell(s) without tabular-nums`, threshold: "tabular-nums" });
581
+ }
582
+ }
583
+ }
584
+
585
+ // ---- CF-107: sentence case for UI labels ----------------------------------------------------
586
+ if (on("CF-107")) {
587
+ const th = t("CF-107");
588
+ const functionWords = new Set(th.functionWords);
589
+ const [minAcronym, maxAcronym] = th.acronymLetters;
590
+ const acronym = new RegExp(`^\\p{Lu}{${minAcronym},${maxAcronym}}$`, "u");
591
+ const home = [...body.querySelectorAll("a[href]")].find((a) => { try { return new URL(a.href).pathname.replace(/index\.html?$/u, "") === "/" && new URL(a.href).origin === location.origin; } catch { return false; } });
592
+ const brand = new Set([document.title, document.querySelector("meta[property='og:site_name']")?.content, home ? accessibleName(home) : ""]
593
+ .flatMap((text) => (text ?? "").split(/\s+/u)).map((word) => word.replace(/[^\p{L}\p{N}]/gu, "")).filter(Boolean));
594
+ const labels = new Set([...controls.filter(visible).filter((el) => !el.matches("input,select,textarea")), ...shown.filter((node) => node.matches("label,legend,[role=tab]"))]);
595
+ for (const el of labels) {
596
+ const text = (el.innerText ?? "").trim();
597
+ const letters = text.match(/\p{L}/gu) ?? [];
598
+ const cased = letters.filter((letter) => letter.toUpperCase() !== letter.toLowerCase());
599
+ if (!letters.length || cased.length / letters.length <= th.casedShare) continue;
600
+ const words = text.split(/\s+/u).map((word) => word.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter((word) => /\p{L}/u.test(word));
601
+ if (words.length < 2) continue;
602
+ const content = words.slice(1).filter((word) => !functionWords.has(word.toLowerCase()) && !acronym.test(word) && !/\p{Ll}\p{Lu}/u.test(word) && !/\d/u.test(word) && !brand.has(word));
603
+ if (!content.length || !content.every((word) => /^\p{Lu}/u.test(word))) continue;
604
+ add("CF-107", { tier: content.length === 1 ? "derived" : undefined, selector: selectorOf(el), value: `"${text.slice(0, 40)}" (capitalised: ${content.join(", ")})`, threshold: "sentence case", note: content.length === 1 ? "one capitalised word may be a proper noun" : undefined });
605
+ }
606
+ }
607
+
608
+ // ---- CF-108: type-role and family census at the representative width ------------------------
609
+ if (on("CF-108") && pass === "base" && String(vw) === t("CF-108").censusViewport) {
610
+ const th = t("CF-108");
611
+ const bucket = (weight) => (weight <= th.weightBuckets[0] ? "regular" : weight <= th.weightBuckets[1] ? "medium" : "bold");
612
+ const sizes = new Map();
613
+ const families = new Set();
614
+ for (const el of textElements.filter((node) => !node.matches("input,select,textarea"))) {
615
+ const style = styleOf(el);
616
+ const family = style.fontFamily.split(",")[0].replace(/["']/gu, "").trim().toLowerCase();
617
+ families.add(family);
618
+ const key = `${family}|${bucket(Number(style.fontWeight))}`;
619
+ sizes.set(key, [...(sizes.get(key) ?? []), Math.round(px(style.fontSize))]);
620
+ }
621
+ let roles = 0;
622
+ for (const list of sizes.values()) {
623
+ const sorted = [...new Set(list)].sort((a, b) => a - b);
624
+ roles += sorted.filter((size, index) => index === 0 || size - sorted[index - 1] > 1).length;
625
+ }
626
+ const mono = [...families].filter((family) => /mono|courier|consolas|menlo/u.test(family)).length;
627
+ if (roles > th.maxRoles || families.size - mono > th.maxFamilies || mono > th.maxMono) {
628
+ add("CF-108", { selector: "body", value: `${roles} type roles, ${families.size - mono} families + ${mono} mono (${[...families].join(", ").slice(0, 60)})`, threshold: `≤ ${th.maxRoles} roles, ≤ ${th.maxFamilies} families + ${th.maxMono} mono` });
629
+ }
630
+ }
631
+
632
+ // ---- CF-201 / CF-204 / CF-807: contrast (APCA advisory in the note) -------------------------
633
+ if (on("CF-201")) {
634
+ const th = t("CF-201");
635
+ let overlapped = 0;
636
+ let overImage = 0;
637
+ const seen = new Set();
638
+ for (const el of textElements) {
639
+ if (el.closest(":disabled,[aria-disabled='true'],[inert]")) continue;
640
+ const style = styleOf(el);
641
+ const fg = parseColor(style.color);
642
+ if (!fg) continue;
643
+ const behind = paintBehind(el);
644
+ if (behind.overlap) { overlapped += 1; continue; }
645
+ if (behind.image) { overImage += 1; continue; }
646
+ const size = px(style.fontSize);
647
+ const large = size >= th.largeFontPx || (size >= th.boldLargeFontPx && Number(style.fontWeight) >= th.boldWeight);
648
+ const alpha = fg.a * opacityChain(el);
649
+ if (behind.gradient) {
650
+ if (!on("CF-204")) continue;
651
+ const floor = large ? t("CF-204").large : t("CF-204").body;
652
+ const worst = Math.min(...behind.gradient.map((stop) => ratio(blend({ ...fg, a: alpha }, stop), stop)));
653
+ if (worst + 1e-9 < floor) add("CF-204", { severity: large ? th.largeSeverity : undefined, selector: selectorOf(el), value: `${round(worst)}:1 at the worst gradient stop "${snippet(el, 30)}"`, threshold: `≥ ${floor}:1` });
654
+ continue;
655
+ }
656
+ const bg = behind.color;
657
+ const text = blend({ ...fg, a: alpha }, bg);
658
+ const floor = large ? th.large : th.body;
659
+ const value = ratio(text, bg);
660
+ const key = `${selectorOf(el)}|${Math.round(value * 100)}`;
661
+ if (value + 1e-9 < floor && !seen.has(key)) {
662
+ seen.add(key);
663
+ add("CF-201", { severity: large ? th.largeSeverity : undefined, selector: selectorOf(el), value: `${round(value)}:1 "${snippet(el, 30)}"`, threshold: `≥ ${floor}:1`, note: `APCA Lc ${round(apca(text, bg), 1)} (advisory)` });
664
+ }
665
+ }
666
+ if (overlapped) skip("CF-201", `${overlapped} text element(s) with another element painted on top; contrast not sampled`);
667
+ if (overImage) skip("CF-204", `${overImage} text element(s) over an image or media box; not pixel-sampled`);
668
+ }
669
+ if (on("CF-807")) {
670
+ for (const field of shown.filter((el) => el.matches("input[placeholder],textarea[placeholder]") && el.placeholder)) {
671
+ const fg = parseColor(getComputedStyle(field, "::placeholder").color);
672
+ const own = parseColor(styleOf(field).backgroundColor);
673
+ const surface = own && own.a >= 0.999 ? own : solidBehind(field);
674
+ if (!fg || !surface) continue;
675
+ const value = ratio(blend(fg, surface), surface);
676
+ if (value < t("CF-807").min) add("CF-807", { selector: selectorOf(field), value: `${round(value)}:1`, threshold: `≥ ${t("CF-807").min}:1` });
677
+ }
678
+ }
679
+
680
+ // ---- CF-205: one accent per view (semantic status fills excluded) ---------------------------
681
+ if (on("CF-205")) {
682
+ const th = t("CF-205");
683
+ const tokens = new Set(th.statusTokens);
684
+ const statusLike = (el) => {
685
+ if (el.closest("[role=status],[role=alert],[role=alertdialog],[role=log],[aria-live=polite],[aria-live=assertive],[aria-invalid=true]")) return true;
686
+ for (let node = el; node && node !== body; node = node.parentElement) if (classTokens(node).some((token) => tokens.has(token))) return true;
687
+ return false;
688
+ };
689
+ const clusters = [];
690
+ for (const el of shown) {
691
+ const rect = el.getBoundingClientRect();
692
+ if (rect.width < th.minSurfacePx || rect.height < th.minSurfacePx || rect.width > vw * 0.9) continue;
693
+ if (rect.height <= th.maxPillPx && px(styleOf(el).borderTopLeftRadius) >= rect.height / 2 - 1) continue;
694
+ const fill = parseColor(styleOf(el).backgroundColor);
695
+ if (!fill || fill.a < 0.9) continue;
696
+ const tone = hsl(fill);
697
+ if (tone.s < th.minSaturation || tone.l < th.minLightness || tone.l > th.maxLightness || statusLike(el)) continue;
698
+ const cluster = clusters.find((entry) => hueDistance(entry.h, tone.h) <= th.hueToleranceDeg);
699
+ if (cluster) cluster.count += 1; else clusters.push({ h: tone.h, count: 1, el });
700
+ }
701
+ if (clusters.length > th.maxClusters) {
702
+ add("CF-205", { selector: clusters.map((entry) => selectorOf(entry.el)).slice(0, 3).join(" | "), value: `${clusters.length} saturated hue clusters (${clusters.map((entry) => `${Math.round(entry.h)}°`).join(", ")})`, threshold: `≤ ${th.maxClusters}` });
703
+ }
704
+ }
705
+
706
+ // ---- CF-301: spacing on the 4px scale ----------------------------------------------------------
707
+ if (on("CF-301")) {
708
+ const th = t("CF-301");
709
+ const off = new Map();
710
+ for (const el of shown) {
711
+ const style = styleOf(el);
712
+ const values = [style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft, style.rowGap, style.columnGap, style.marginTop, style.marginBottom];
713
+ const parent = el.parentElement;
714
+ const centred = parent && Math.abs(px(style.marginLeft) - px(style.marginRight)) <= th.tolerancePx && el.getBoundingClientRect().width < parent.getBoundingClientRect().width - 1;
715
+ if (!centred) values.push(style.marginLeft, style.marginRight);
716
+ for (const raw of values) {
717
+ const value = Math.abs(px(raw));
718
+ if (value < th.minPx) continue;
719
+ const rest = value % th.stepPx;
720
+ if (Math.min(rest, th.stepPx - rest) > th.tolerancePx && !off.has(round(value, 1))) off.set(round(value, 1), el);
721
+ }
722
+ }
723
+ if (off.size) {
724
+ add("CF-301", {
725
+ severity: off.size > th.lowMax ? th.manySeverity : undefined,
726
+ selector: [...off.values()].slice(0, 3).map(selectorOf).join(" | "),
727
+ value: `${off.size} off-scale value(s): ${[...off.keys()].sort((a, b) => a - b).slice(0, 8).map((value) => `${value}px`).join(", ")}`,
728
+ threshold: `within ±${th.tolerancePx}px of a ${th.stepPx}px multiple`,
729
+ });
730
+ }
731
+ }
732
+
733
+ // ---- CF-304: heading rhythm (a clear inversion only) ------------------------------------------
734
+ if (on("CF-304")) {
735
+ const th = t("CF-304");
736
+ const inFlow = (node) => visible(node) && !/(absolute|fixed)/u.test(styleOf(node).position);
737
+ const bounded = (node) => { const style = styleOf(node); return (parseColor(style.backgroundColor)?.a ?? 0) > 0 || px(style.borderTopWidth) > 0 || style.boxShadow !== "none" || style.backgroundImage !== "none"; };
738
+ const overlapsX = (a, b) => Math.min(a.right, b.right) > Math.max(a.left, b.left);
739
+ for (const heading of headings) {
740
+ const hr = heading.getBoundingClientRect();
741
+ let next = heading.nextElementSibling;
742
+ while (next && !inFlow(next)) next = next.nextElementSibling;
743
+ if (!next) continue;
744
+ const below = next.getBoundingClientRect().top - hr.bottom;
745
+ let top = hr.top;
746
+ let start = heading;
747
+ const label = heading.previousElementSibling;
748
+ if (label && inFlow(label)) {
749
+ const lr = label.getBoundingClientRect();
750
+ if (lr.height <= th.labelMaxPx && hr.top - lr.bottom < th.labelGapPx && lr.bottom <= hr.top + 1) { top = lr.top; start = label; }
751
+ }
752
+ let previous = null;
753
+ for (let node = start; node && node !== body && !previous;) {
754
+ for (let sibling = node.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
755
+ if (inFlow(sibling) && overlapsX(sibling.getBoundingClientRect(), hr)) { previous = sibling; break; }
756
+ }
757
+ if (previous) break;
758
+ const parent = node.parentElement;
759
+ if (!parent || parent === body || bounded(parent)) break;
760
+ node = parent;
761
+ }
762
+ if (!previous) continue;
763
+ const above = top - previous.getBoundingClientRect().bottom;
764
+ if (above < th.ratio * below && below - above >= th.deficitPx) add("CF-304", { selector: selectorOf(heading), value: `${round(above, 0)}px above, ${round(below, 0)}px below`, threshold: `above ≥ ${th.ratio} × below` });
765
+ }
766
+ }
767
+
768
+ // ---- CF-401: concentric radius, compared per corner --------------------------------------------
769
+ if (on("CF-401")) {
770
+ const th = t("CF-401");
771
+ const cap = (value, rect) => Math.min(value, Math.min(rect.width, rect.height) / 2);
772
+ const corners = [["TopLeft", "Top", "Left"], ["TopRight", "Top", "Right"], ["BottomRight", "Bottom", "Right"], ["BottomLeft", "Bottom", "Left"]];
773
+ for (const parent of shown) {
774
+ const ps = styleOf(parent);
775
+ if (corners.every(([corner]) => px(ps[`border${corner}Radius`]) <= 0)) continue;
776
+ const pr = parent.getBoundingClientRect();
777
+ for (const child of [...parent.children].filter(visible)) {
778
+ const cs = styleOf(child);
779
+ const cr = child.getBoundingClientRect();
780
+ if (Math.min(cr.width, cr.height) <= th.minChildPx) continue;
781
+ for (const [corner, v, h] of corners) {
782
+ const outer = px(ps[`border${corner}Radius`]);
783
+ const inner = px(cs[`border${corner}Radius`]);
784
+ const padV = px(ps[`padding${v}`]);
785
+ const padH = px(ps[`padding${h}`]);
786
+ if (outer <= 0 || inner <= 0 || padV <= 0 || Math.abs(padV - padH) > 1 || padV > th.maxParentPaddingPx) continue;
787
+ const edgeV = px(ps[`border${v}Width`]) + padV;
788
+ const edgeH = px(ps[`border${h}Width`]) + padH;
789
+ const reachV = v === "Top" ? Math.abs(cr.top - pr.top - edgeV) : Math.abs(pr.bottom - cr.bottom - edgeV);
790
+ const reachH = h === "Left" ? Math.abs(cr.left - pr.left - edgeH) : Math.abs(pr.right - cr.right - edgeH);
791
+ if (reachV > th.edgeTolerancePx || reachH > th.edgeTolerancePx) continue;
792
+ const expected = Math.max(0, cap(outer, pr) - padV);
793
+ if (Math.abs(cap(inner, cr) - expected) > th.tolerancePx) {
794
+ add("CF-401", { selector: selectorOf(child), value: `${corner} inner ${round(inner, 1)}px in outer ${round(outer, 1)}px with ${round(padV, 1)}px padding`, threshold: `inner ≈ ${round(expected, 1)}px (±${th.tolerancePx})` });
795
+ break;
796
+ }
797
+ }
798
+ }
799
+ }
800
+ }
801
+
802
+ // ---- CF-403: hard offset shadow outside an evidenced register ----------------------------------
803
+ if (on("CF-403")) {
804
+ const th = t("CF-403");
805
+ const hard = shown.filter((el) => shadowsOf(styleOf(el).boxShadow).some((s) => !s.inset && s.blur === 0 && s.spread === 0 && (Math.abs(s.x) > th.hairlinePx || Math.abs(s.y) > th.hairlinePx) && s.color));
806
+ const bordered = hard.filter((el) => { const style = styleOf(el); return style.borderTopStyle === "solid" && px(style.borderTopWidth) >= th.registerBorderPx; });
807
+ if (hard.length && bordered.length < th.registerCount) {
808
+ for (const el of hard) add("CF-403", { selector: selectorOf(el), value: `hard offset shadow ${styleOf(el).boxShadow.slice(0, 40)}`, threshold: `soft shadow, or ≥ ${th.registerCount} bordered elements sharing it (a register)` });
809
+ }
810
+ }
811
+
812
+ // ---- CF-404 / SLOP-010: one glow test, each glowing element reported once -------------------
813
+ const glowOf = (el) => {
814
+ const th = t("CF-404");
815
+ const style = styleOf(el);
816
+ const passes = (s) => s.color && Math.abs(s.x) <= th.maxOffsetPx && Math.abs(s.y) <= th.maxOffsetPx && s.blur >= th.minBlurPx && hsl(s.color).s > th.minSaturation && s.color.a >= th.minAlpha;
817
+ const shadow = shadowsOf(style.boxShadow).find((s) => !s.inset && passes(s));
818
+ if (shadow) return `${round(shadow.blur, 0)}px coloured blur`;
819
+ const drop = /drop-shadow\((.*)\)/u.exec(style.filter);
820
+ if (drop) {
821
+ const [x = 0, y = 0, blur = 0] = lengthsIn(drop[1]);
822
+ if (passes({ x, y, blur, color: colorsIn(drop[1])[0] ?? null })) return `${round(blur, 0)}px coloured drop-shadow`;
823
+ }
824
+ return null;
825
+ };
826
+ if (on("CF-404") || on("SLOP-010")) {
827
+ const glowing = [...new Set([...controls.filter(visible), ...shown.filter((el) => el.matches("h1,h2,h3"))])]
828
+ .map((el) => ({ el, glow: glowOf(el) }))
829
+ .filter((entry) => entry.glow);
830
+ const unrelated = glowing.filter((entry, index) => !glowing.some((other, otherIndex) => otherIndex < index && related(other.el, entry.el)));
831
+ if (unrelated.length >= t("SLOP-010").recurCount) {
832
+ for (const { el, glow } of unrelated) add("SLOP-010", { selector: selectorOf(el), value: `${glow} (on ${unrelated.length} unrelated elements)`, threshold: `< ${t("SLOP-010").recurCount} glowing elements` });
833
+ } else {
834
+ for (const { el, glow } of unrelated.filter((entry) => isPrimary(entry.el) || entry.el.matches("h1,h2,h3"))) add("CF-404", { selector: selectorOf(el), value: glow, threshold: `no chromatic blur ≥ ${t("CF-404").minBlurPx}px` });
835
+ }
836
+ }
837
+
838
+ // ---- CF-406: blurred scrim -----------------------------------------------------------------
839
+ if (on("CF-406")) {
840
+ for (const el of shown) {
841
+ const style = styleOf(el);
842
+ if (!/blur\(/u.test(`${style.backdropFilter ?? ""} ${style.webkitBackdropFilter ?? ""}`) || !/(fixed|absolute)/u.test(style.position)) continue;
843
+ const rect = el.getBoundingClientRect();
844
+ if (rect.width >= vw * t("CF-406").minCover && rect.height >= vh * t("CF-406").minCover) add("CF-406", { selector: selectorOf(el), value: style.backdropFilter, threshold: "solid scrim" });
845
+ }
846
+ }
847
+
848
+ // ---- motion: CF-502 / CF-503 / CF-505 / CF-507 / CF-508 / SLOP-061 ---------------------------
849
+ const scaleStart = (transform, scaleProp) => {
850
+ if (scaleProp && scaleProp !== "none") {
851
+ const values = scaleProp.split(/\s+/u).map(Number).filter(Number.isFinite);
852
+ if (values.length) return Math.min(...values);
853
+ }
854
+ const match = /scale(?:3d|X|Y)?\(\s*([-\d.]+)(?:\s*,\s*([-\d.]+))?/u.exec(transform ?? "");
855
+ if (!match) return null;
856
+ return Math.min(Number(match[1]), match[2] === undefined ? Number(match[1]) : Number(match[2]));
857
+ };
858
+ const overshoots = (timing) => {
859
+ const { minControl, maxControl } = t("CF-505");
860
+ const out = (value) => value < minControl || value > maxControl;
861
+ for (const curve of timing.matchAll(/cubic-bezier\(\s*[-\d.]+\s*,\s*([-\d.]+)\s*,\s*[-\d.]+\s*,\s*([-\d.]+)\s*\)/gu)) {
862
+ if (out(Number(curve[1])) || out(Number(curve[2]))) return true;
863
+ }
864
+ for (const curve of timing.matchAll(/linear\(([^()]*)\)/gu)) {
865
+ if (curve[1].split(",").map((stop) => parseFloat(stop.trim())).some((value) => Number.isFinite(value) && out(value))) return true;
866
+ }
867
+ return false;
868
+ };
869
+ const nameTokens = (name) => name.split(/[-_\s,]+|(?<=\p{Ll})(?=\p{Lu})/u).map((token) => token.toLowerCase());
870
+ const animationNamesOf = (el) => styleOf(el).animationName.split(",").map((name) => name.trim());
871
+ const runningNames = new Set(document.getAnimations().filter((a) => a.playState === "running" && a.effect?.target && visible(a.effect.target)).map((a) => a.animationName).filter(Boolean));
872
+ const usedNames = new Set(shown.flatMap(animationNamesOf));
873
+ for (const [name, rule] of keyframes) {
874
+ if (!usedNames.has(name)) continue;
875
+ for (const frame of rule.cssRules) {
876
+ if (on("CF-503") && /^(0%|from)$/u.test(frame.keyText.trim())) {
877
+ const start = scaleStart(frame.style.transform, frame.style.scale);
878
+ if (start !== null && start < t("CF-503").minScale) {
879
+ const running = runningNames.has(name);
880
+ const target = shown.find((el) => animationNamesOf(el).includes(name));
881
+ const icon = target && target.getBoundingClientRect().width <= t("CF-503").iconMaxPx && (target.matches("svg,i,img") || target.querySelector("svg"));
882
+ if (!icon) add("CF-503", { severity: running ? undefined : t("CF-503").declaredSeverity, tier: running ? "measured" : "derived", selector: `@keyframes ${name}`, value: `starts at scale ${start}${running ? " (running)" : " (applied, not running now)"}`, threshold: `≥ ${t("CF-503").minScale}` });
883
+ }
884
+ }
885
+ if (on("CF-508")) {
886
+ const moved = t("CF-508").properties.filter((property) => [...frame.style].some((prop) => prop === property || prop.startsWith(`${property}-`)));
887
+ if (moved.length) add("CF-508", { selector: `@keyframes ${name}`, value: `animates ${moved.join(", ")}`, threshold: "transform/opacity only" });
888
+ }
889
+ if (on("SLOP-061") && /translateX\(\s*-?(100|50)%/u.test(frame.style.transform ?? "")) {
890
+ const target = shown.find((el) => animationNamesOf(el).includes(name) && styleOf(el).animationIterationCount === "infinite" && (el.innerText ?? "").trim());
891
+ if (target && !document.querySelector("[aria-label*=pause i],[aria-label*=정지]")) add("SLOP-061", { selector: selectorOf(target), value: `@keyframes ${name} loops text sideways`, threshold: "static row or a pause control" });
892
+ }
893
+ }
894
+ }
895
+ if (on("CF-505")) {
896
+ const tokens = new Set(t("CF-505").nameTokens);
897
+ for (const rule of [...styleRules, ...[...keyframes.values()].flatMap((kf) => [...kf.cssRules])]) {
898
+ const timing = `${rule.style.animationTimingFunction ?? ""} ${rule.style.transitionTimingFunction ?? ""}`;
899
+ const where = rule.selectorText ?? `@keyframes ${rule.parentRule?.name ?? ""} ${rule.keyText ?? ""}`;
900
+ if (overshoots(timing)) add("CF-505", { selector: where, value: timing.trim().replace(/\s+/gu, " ").slice(0, 60), threshold: `y control points within ${t("CF-505").minControl}..${t("CF-505").maxControl}` });
901
+ const name = rule.style.animationName ?? "";
902
+ const hit = name && name !== "none" && nameTokens(name).find((token) => tokens.has(token));
903
+ if (hit) add("CF-505", { tier: "derived", selector: where, value: `animation-name ${name}`, threshold: "no bounce or spring on feedback" });
904
+ }
905
+ for (const animation of document.getAnimations()) {
906
+ const easing = animation.effect?.getTiming?.().easing ?? "";
907
+ if (overshoots(easing) && animation.effect?.target) add("CF-505", { selector: selectorOf(animation.effect.target), value: `running easing ${easing.slice(0, 50)}`, threshold: `y control points within ${t("CF-505").minControl}..${t("CF-505").maxControl}` });
908
+ }
909
+ }
910
+ if (on("CF-503")) {
911
+ for (const animation of document.getAnimations()) {
912
+ if (typeof CSSAnimation !== "undefined" && animation instanceof CSSAnimation) continue;
913
+ const target = animation.effect?.target;
914
+ const first = animation.effect?.getKeyframes?.()[0];
915
+ const start = first ? scaleStart(first.transform, first.scale) : null;
916
+ if (target && start !== null && start < t("CF-503").minScale && visible(target)) add("CF-503", { tier: "measured", selector: selectorOf(target), value: `script animation starts at scale ${start}`, threshold: `≥ ${t("CF-503").minScale}` });
917
+ }
918
+ }
919
+ if (on("CF-508")) {
920
+ for (const el of shown) {
921
+ const style = styleOf(el);
922
+ if (style.transitionProperty === "none" || style.transitionProperty === "all" || Math.max(...ms(style.transitionDuration)) === 0) continue;
923
+ if (el.matches("details,[aria-expanded],[class*=accordion],[class*=collapse]") || el.closest("details,[aria-expanded]")) continue;
924
+ const moved = style.transitionProperty.split(",").map((part) => part.trim()).filter((property) => t("CF-508").properties.some((name) => property === name || property.startsWith(`${name}-`) || property === `max-${name}`));
925
+ if (moved.length) add("CF-508", { selector: selectorOf(el), value: `transitions ${moved.join(", ")}`, threshold: "transform/opacity only" });
926
+ }
927
+ }
928
+ if (on("CF-502")) {
929
+ const th = t("CF-502");
930
+ const active = styleRules.filter((rule) => /:active(?![\w-])/u.test(rule.selectorText ?? ""));
931
+ let unread = 0;
932
+ for (const el of standalone.filter((node) => node.matches("button,a[href],[role=button]")).slice(0, th.sample)) {
933
+ const rule = active.find((candidateRule) => selectorParts(candidateRule).some((part) => part.includes(":active") && safeMatches(el, stripPseudo(part, "active"))));
934
+ if (!rule) { unread += 1; continue; }
935
+ const scale = scaleStart(rule.style.transform, rule.style.scale);
936
+ const style = styleOf(el);
937
+ const properties = style.transitionProperty.split(",").map((part) => part.trim());
938
+ const durations = ms(style.transitionDuration);
939
+ const duration = Math.max(0, ...properties.map((property, index) => (/^(all|transform|scale)$/u.test(property) ? durations[index % durations.length] : 0)));
940
+ if (scale !== null && (scale < th.minScale - 1e-6 || scale > th.maxScale + 1e-6)) add("CF-502", { selector: selectorOf(el), value: `:active scale ${scale}`, threshold: `${th.minScale}-${th.maxScale}` });
941
+ if (duration > th.maxMs) add("CF-502", { selector: selectorOf(el), value: `press transition ${round(duration, 0)}ms`, threshold: `≤ ${th.maxMs}ms` });
942
+ }
943
+ if (unread) skip("CF-502", `${unread} sampled control(s) have no readable :active rule`);
944
+ }
945
+ if (on("CF-507")) {
946
+ const th = t("CF-507");
947
+ const wrongOf = (value) => value.split(",").map((part) => part.trim()).filter((part) => part && part !== "auto" && !th.allowed.includes(part));
948
+ // Resting pass only: a hint set on hover or focus is never "left on at rest".
949
+ const animated = new Set(document.getAnimations().filter((a) => a.playState === "running").map((a) => a.effect?.target).filter(Boolean));
950
+ for (const el of shown) {
951
+ const value = styleOf(el).willChange;
952
+ if (!value || value === "auto") continue;
953
+ const wrong = wrongOf(value);
954
+ if (wrong.length) add("CF-507", { selector: selectorOf(el), value: `will-change: ${value}`, threshold: th.allowed.join("/") });
955
+ else if (!animated.has(el)) add("CF-507", { severity: th.atRestSeverity, selector: selectorOf(el), value: `will-change: ${value} with no animation running`, threshold: "only while animating" });
956
+ }
957
+ for (const rule of styleRules.filter((candidateRule) => /:(hover|focus|focus-within)(?![\w-])/u.test(candidateRule.selectorText ?? "") && candidateRule.style.willChange)) {
958
+ const wrong = wrongOf(rule.style.willChange);
959
+ if (wrong.length) add("CF-507", { selector: rule.selectorText, value: `will-change: ${rule.style.willChange} in a state rule`, threshold: th.allowed.join("/") });
960
+ }
961
+ }
962
+
963
+ // ---- CF-202: focus indicator (in-page focus, confirmed :focus-visible) ------------------------
964
+ if (on("CF-202")) {
965
+ const th = t("CF-202");
966
+ const snapshot = (el) => {
967
+ const s = styleOf(el);
968
+ return {
969
+ outline: `${s.outlineStyle} ${s.outlineWidth} ${s.outlineColor} ${s.outlineOffset}`,
970
+ shadow: s.boxShadow,
971
+ border: `${s.borderTopWidth} ${s.borderTopStyle} ${s.borderTopColor} ${s.borderRightColor} ${s.borderBottomColor} ${s.borderLeftColor}`,
972
+ background: s.backgroundColor,
973
+ outlineStyle: s.outlineStyle, outlineWidth: px(s.outlineWidth), outlineColor: s.outlineColor, outlineOffset: px(s.outlineOffset),
974
+ borderWidth: px(s.borderTopWidth), borderColor: s.borderTopColor,
975
+ };
976
+ };
977
+ const before = document.activeElement;
978
+ const [sx, sy] = [window.scrollX, window.scrollY];
979
+ let unverified = 0;
980
+ let sampled = 0;
981
+ for (const el of focusables.filter(visible)) {
982
+ if (sampled >= th.sample) break;
983
+ try { el.focus({ preventScroll: true }); } catch { continue; }
984
+ if (document.activeElement !== el) continue;
985
+ sampled += 1;
986
+ const visibleFocus = el.matches(":focus-visible");
987
+ const focused = snapshot(el);
988
+ el.blur();
989
+ if (!visibleFocus) { unverified += 1; continue; }
990
+ const rest = snapshot(el);
991
+ const changed = ["outline", "shadow", "border", "background"].filter((key) => focused[key] !== rest[key]);
992
+ if (!changed.length) {
993
+ add("CF-202", { selector: selectorOf(el), value: "focus changes no outline, shadow, border or background", threshold: "a visible focus change" });
994
+ continue;
995
+ }
996
+ if (focused.outlineStyle === "auto") continue;
997
+ let width = 0;
998
+ let color = null;
999
+ if (changed.includes("outline") && focused.outlineStyle !== "none") { width = focused.outlineWidth; color = parseColor(focused.outlineColor); }
1000
+ else if (changed.includes("shadow")) {
1001
+ const rings = shadowsOf(focused.shadow).filter((s) => s.color);
1002
+ width = Math.max(0, ...rings.map((s) => s.spread + (s.inset ? 0 : s.blur / 4)));
1003
+ color = rings[0]?.color ?? null;
1004
+ } else if (changed.includes("border")) { width = focused.borderWidth; color = parseColor(focused.borderColor); }
1005
+ else continue;
1006
+ const outside = solidBehind(el, { skipSelf: true }) ?? canvasColor;
1007
+ const surfaces = focused.outlineOffset <= 0 && changed.includes("outline") ? [outside, solidBehind(el) ?? outside] : [outside];
1008
+ const contrast = color ? Math.min(...surfaces.map((surface) => ratio(blend(color, surface), surface))) : 0;
1009
+ if (width < th.minWidthPx || contrast < th.minContrast) {
1010
+ add("CF-202", { severity: th.weakSeverity, selector: selectorOf(el), value: `custom ring ${round(width, 1)}px at ${round(contrast)}:1`, threshold: `≥ ${th.minWidthPx}px and ≥ ${th.minContrast}:1` });
1011
+ }
1012
+ }
1013
+ try { if (before && before !== body && typeof before.focus === "function") before.focus({ preventScroll: true }); else document.activeElement?.blur?.(); } catch { /* focus restore is best effort */ }
1014
+ window.scrollTo(sx, sy);
1015
+ if (unverified) skip("CF-202", `${unverified} control(s) did not reach :focus-visible from in-page focus`);
1016
+ }
1017
+
1018
+ // ---- CF-603 / CF-701 / CF-702 / CF-703 / CF-806: controls -------------------------------------
1019
+ if (on("CF-603")) {
1020
+ for (const el of focusables) {
1021
+ if (!visible(el) && !(el.labels?.length && visible(el.labels[0]))) continue;
1022
+ if (!accessibleName(el)) add("CF-603", { selector: selectorOf(el), value: "no accessible name", threshold: "text, aria-label or aria-labelledby" });
1023
+ }
1024
+ }
1025
+ const targetRect = (control) => {
1026
+ const el = !visible(control) && control.labels?.length && visible(control.labels[0]) ? control.labels[0] : control;
1027
+ let rect = el.getBoundingClientRect();
1028
+ if (control.matches("input[type=checkbox],input[type=radio]") && control.labels?.length) {
1029
+ const label = control.labels[0].getBoundingClientRect();
1030
+ const left = Math.min(rect.left, label.left); const top = Math.min(rect.top, label.top);
1031
+ const right = Math.max(rect.right, label.right); const bottom = Math.max(rect.bottom, label.bottom);
1032
+ rect = { left, top, right, bottom, width: right - left, height: bottom - top };
1033
+ }
1034
+ return { el, rect };
1035
+ };
1036
+ const targets = standalone.map((control) => ({ control, ...targetRect(control) }));
1037
+ const spacedOut = (target) => {
1038
+ const d = t("CF-701").spacingCirclePx;
1039
+ const cx = target.rect.left + target.rect.width / 2;
1040
+ const cy = target.rect.top + target.rect.height / 2;
1041
+ return targets.every((other) => {
1042
+ if (other === target || related(other.control, target.control)) return true;
1043
+ const r = other.rect;
1044
+ const nx = Math.max(r.left, Math.min(cx, r.right));
1045
+ const ny = Math.max(r.top, Math.min(cy, r.bottom));
1046
+ const toBox = Math.hypot(cx - nx, cy - ny);
1047
+ const toCircle = Math.hypot(cx - (r.left + r.width / 2), cy - (r.top + r.height / 2));
1048
+ return toBox >= d / 2 && (Math.min(r.width, r.height) >= d || toCircle >= d);
1049
+ });
1050
+ };
1051
+ for (const target of targets) {
1052
+ const { control, el, rect } = target;
1053
+ if (on("CF-806") && control.matches("input[placeholder],textarea[placeholder]") && !control.labels?.length && !control.getAttribute("aria-label") && !control.getAttribute("aria-labelledby") && !control.getAttribute("title")) {
1054
+ add("CF-806", { selector: selectorOf(control), value: `only a placeholder ("${control.placeholder.slice(0, 30)}")`, threshold: "a real label" });
1055
+ }
1056
+ const size = Math.min(rect.width, rect.height);
1057
+ const dims = `${round(rect.width, 1)}×${round(rect.height, 1)}px "${snippet(el, 24)}"`;
1058
+ if (on("CF-703") && destructive(control) && size + 0.5 < t("CF-703").minPx) {
1059
+ add("CF-703", { selector: selectorOf(el), value: dims, threshold: `≥ ${t("CF-703").minPx}px` });
1060
+ continue;
1061
+ }
1062
+ if (!on("CF-701")) continue;
1063
+ const th = t("CF-701");
1064
+ const spaced = size + 0.5 < th.floorPx && spacedOut(target);
1065
+ if (size + 0.5 < th.floorPx && !spaced) add("CF-701", { selector: selectorOf(el), value: dims, threshold: `≥ ${th.floorPx}px (no spacing exception)` });
1066
+ else if (touch && size + 0.5 < th.touchPx) add("CF-701", { severity: isPrimary(control) ? "HIGH" : th.touchSeverity, selector: selectorOf(el), value: `${dims} on a touch width${isPrimary(control) ? " (primary action)" : ""}`, threshold: `≥ ${th.touchPx}px`, note: spaced ? "under 24px; WCAG 2.5.8 spacing exception holds" : undefined });
1067
+ }
1068
+ if (on("CF-702")) {
1069
+ const gapFloor = touch ? t("CF-702").touchGapPx : t("CF-702").fineGapPx;
1070
+ for (let i = 0; i < targets.length; i += 1) {
1071
+ for (let j = i + 1; j < targets.length; j += 1) {
1072
+ const a = targets[i]; const b = targets[j];
1073
+ if (related(a.control, b.control) || related(a.el, b.el)) continue;
1074
+ const ra = a.rect; const rb = b.rect;
1075
+ const dx = Math.max(rb.left - ra.right, ra.left - rb.right);
1076
+ const dy = Math.max(rb.top - ra.bottom, ra.top - rb.bottom);
1077
+ const gap = dx >= 0 && dy < 0 ? dx : dy >= 0 && dx < 0 ? dy : null;
1078
+ if (gap !== null && gap + 0.5 < gapFloor) add("CF-702", { selector: `${selectorOf(a.el)} × ${selectorOf(b.el)}`, value: `${round(gap, 1)}px apart`, threshold: `≥ ${gapFloor}px` });
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ // ---- CF-704: controls revealed only by an ancestor :hover (stylesheet scan) ------------------
1084
+ if (on("CF-704")) {
1085
+ const shownBy = (style) => (style.opacity && Number(style.opacity) > 0) || style.visibility === "visible" || (style.display && style.display !== "none");
1086
+ const hidden = (el) => { const style = styleOf(el); return Number(style.opacity) === 0 || style.visibility === "hidden" || style.display === "none"; };
1087
+ const focusRules = styleRules.filter((rule) => /:(focus-within|focus-visible|focus)(?![\w-])/u.test(rule.selectorText ?? ""));
1088
+ const reported = new Set();
1089
+ for (const rule of styleRules) {
1090
+ if (!shownBy(rule.style)) continue;
1091
+ for (const part of selectorParts(rule).filter((text) => /:hover(?![\w-])\s*[>~+ ]\s*\S/u.test(text))) {
1092
+ const target = stripPseudo(part, "hover");
1093
+ let matches = [];
1094
+ try { matches = [...body.querySelectorAll(target)].slice(0, 50); } catch { continue; }
1095
+ for (const el of matches) {
1096
+ if (reported.has(el) || !hidden(el) || !(el.matches(controlSelector) || el.querySelector(controlSelector))) continue;
1097
+ const alsoOnFocus = focusRules.some((focusRule) => shownBy(focusRule.style) && selectorParts(focusRule).some((focusPart) => safeMatches(el, focusPart.replace(/:(focus-within|focus-visible|focus)(?![\w-])/gu, ""))));
1098
+ if (alsoOnFocus) continue;
1099
+ reported.add(el);
1100
+ add("CF-704", { selector: selectorOf(el), value: `shown only by "${part.slice(0, 60)}"`, threshold: "also revealed on :focus-within or :focus-visible" });
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+
1106
+ // ---- CF-506: repeated controls the driver hovers after this read ---------------------------
1107
+ const hoverTargets = [];
1108
+ if (options.hoverSample > 0) {
1109
+ const listLike = (node) => node.matches("ul,ol,table,tbody,[role=list],[role=grid],[role=listbox],[role=table]") || /(auto|scroll)/u.test(`${styleOf(node).overflowX} ${styleOf(node).overflowY}`);
1110
+ const groups = new Map();
1111
+ for (const el of standalone) {
1112
+ let container = null;
1113
+ for (let node = el.parentElement; node && node !== body; node = node.parentElement) if (listLike(node)) { container = node; break; }
1114
+ if (!container) continue;
1115
+ const key = `${el.tagName}|${classTokens(el).sort().join(".")}`;
1116
+ groups.set(key, [...(groups.get(key) ?? []), el]);
1117
+ }
1118
+ const repeated = [...groups.values()].filter((list) => list.length >= t("CF-506").minRepeats).flat();
1119
+ repeated.slice(0, options.hoverSample).forEach((el, index) => {
1120
+ el.setAttribute("data-litclaude-probe-hover", String(index));
1121
+ hoverTargets.push({ selector: `[data-litclaude-probe-hover="${index}"]`, name: selectorOf(el) });
1122
+ });
1123
+ }
1124
+
1125
+ // ---- slop register: surfaces and type ---------------------------------------------------------
1126
+ const clippedToText = (style) => /text/u.test(`${style.backgroundClip} ${style.webkitBackgroundClip ?? ""}`);
1127
+ if (on("SLOP-009")) {
1128
+ for (const el of textElements) {
1129
+ const style = styleOf(el);
1130
+ if (clippedToText(style) && /gradient\(/u.test(style.backgroundImage)) add("SLOP-009", { selector: selectorOf(el), value: `"${snippet(el, 30)}" filled with a gradient`, threshold: "solid text colour" });
1131
+ }
1132
+ }
1133
+ let bannedHue = false;
1134
+ if (on("SLOP-008")) {
1135
+ const th = t("SLOP-008");
1136
+ const inBand = (c) => { const tone = hsl(c); return spread(c) >= th.minChannelSpread && tone.h >= th.hueMin && tone.h <= th.hueMax; };
1137
+ const brandRegions = new Set(controls.filter(visible).filter((el) => { const fill = parseColor(styleOf(el).backgroundColor); return fill && fill.a > 0.9 && inBand(fill); }).map((el) => el.closest("header,nav,main,section,footer,aside") ?? body));
1138
+ if (brandRegions.size < 2) {
1139
+ for (const el of shown) {
1140
+ const style = styleOf(el);
1141
+ if (/gradient\(/u.test(style.backgroundImage)) {
1142
+ const stops = colorsIn(style.backgroundImage);
1143
+ if (stops.some(inBand)) { bannedHue = true; add("SLOP-008", { selector: selectorOf(el), value: `gradient stops ${stops.map((c) => `${Math.round(hsl(c).h)}°`).join(" → ")}`, threshold: `no stop in ${th.hueMin}-${th.hueMax}° with channel spread ≥ ${th.minChannelSpread}` }); }
1144
+ }
1145
+ const heading = el.matches("h1,h2,h3") || (directText(el).length && px(style.fontSize) >= th.headingFontPx);
1146
+ const ink = parseColor(style.color);
1147
+ if (heading && ink && ink.a > 0.5 && inBand(ink)) { bannedHue = true; add("SLOP-008", { selector: selectorOf(el), value: `heading text ${Math.round(hsl(ink).h)}°`, threshold: `outside ${th.hueMin}-${th.hueMax}°` }); }
1148
+ }
1149
+ }
1150
+ }
1151
+ let blob = false;
1152
+ if (on("SLOP-011")) {
1153
+ for (const el of shown) {
1154
+ const style = styleOf(el);
1155
+ if (!/(absolute|fixed)/u.test(style.position) || !/radial-gradient/u.test(style.backgroundImage)) continue;
1156
+ if ((el.innerText ?? "").trim() || el.querySelector("img,svg,video,canvas,input,select,textarea,button,a[href]")) continue;
1157
+ const faint = Number(style.opacity) < t("SLOP-011").maxOpacity || colorsIn(style.backgroundImage).every((c) => c.a < t("SLOP-011").maxOpacity);
1158
+ if (faint) { blob = true; candidate("SLOP-011", { selector: selectorOf(el), value: "unbound faint radial gradient" }); }
1159
+ }
1160
+ }
1161
+ if (on("SLOP-012") && (blob || bannedHue)) {
1162
+ const th = t("SLOP-012");
1163
+ for (const el of shown) {
1164
+ const style = styleOf(el);
1165
+ const layers = style.backgroundImage.split(/,(?![^()]*\))/u).filter((layer) => /gradient\(/u.test(layer));
1166
+ const cells = style.backgroundSize.split(",").map((size) => size.trim().split(/\s+/u).map(px));
1167
+ const cell = cells.find((pair) => pair.length && pair.every((value) => value > 0 && value <= th.maxCellPx) && /px/u.test(style.backgroundSize));
1168
+ const pattern = (layers.filter((layer) => /linear-gradient/u.test(layer)).length >= 2 || layers.some((layer) => /radial-gradient/u.test(layer))) && cell;
1169
+ if (!pattern) continue;
1170
+ const scope = el.closest("section,main,header,footer,aside,nav,article,[role=region]") ?? body;
1171
+ const data = scope.querySelector("canvas,table,[role=table],[role=grid],[role=img]") || [...scope.querySelectorAll("svg")].some((svg) => { const r = svg.getBoundingClientRect(); return r.width > th.dataSvgPx && r.height > th.dataSvgPx; });
1172
+ if (!data) add("SLOP-012", { selector: selectorOf(el), value: `${cell.join("×")}px pattern cell beside other decoration`, threshold: "no decorative grid without a data referent" });
1173
+ }
1174
+ }
1175
+ if (on("SLOP-015")) {
1176
+ const th = t("SLOP-015");
1177
+ const status = "[role=status],[role=alert],[role=alertdialog],[role=log],[aria-live=polite],[aria-live=assertive],[role=note]";
1178
+ const callout = /(callout|admonition|alert|notice)/iu;
1179
+ const chromatic = (c) => c && hsl(c).s > th.minSaturation;
1180
+ const scopes = new Map();
1181
+ const count = (el, variant) => { const key = el.parentElement ?? body; const entry = scopes.get(key) ?? { a: [], b: [], c: [] }; entry[variant].push(el); scopes.set(key, entry); };
1182
+ for (const el of shown) {
1183
+ const style = styleOf(el);
1184
+ if (el.closest(status) || callout.test(classText(el))) continue;
1185
+ const rounded = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].some((corner) => px(style[`border${corner}Radius`]) > 0);
1186
+ if (rounded) {
1187
+ const sides = ["Top", "Right", "Bottom", "Left"].map((side) => ({ width: px(style[`border${side}Width`]), color: parseColor(style[`border${side}Color`]) }));
1188
+ const accents = sides.filter((side) => side.width >= th.minStripePx && chromatic(side.color));
1189
+ const quiet = sides.filter((side) => side.width === 0 || (side.width <= th.neutralSidePx && !chromatic(side.color)));
1190
+ if (accents.length === 1 && quiet.length === 3) count(el, "a");
1191
+ if (shadowsOf(style.boxShadow).some((s) => s.inset && s.blur === 0 && s.spread === 0 && Math.max(Math.abs(s.x), Math.abs(s.y)) >= th.barMinPx && Math.max(Math.abs(s.x), Math.abs(s.y)) <= th.barMaxPx && chromatic(s.color))) count(el, "b");
1192
+ }
1193
+ const tabLike = el.matches("a,button,summary,li,tr,td,th,[aria-selected=true],[aria-current]");
1194
+ for (const pseudo of ["::before", "::after"]) {
1195
+ const bar = getComputedStyle(el, pseudo);
1196
+ if (!bar.content || bar.content === "none" || !/(absolute|fixed)/u.test(bar.position)) continue;
1197
+ const fill = parseColor(bar.backgroundColor);
1198
+ if (!fill || fill.a < th.minAlpha || spread(fill) < th.minChannelSpread) continue;
1199
+ const [w, h] = [px(bar.width), px(bar.height)];
1200
+ const edge = (value) => bar[value] !== "auto" && px(bar[value]) === 0;
1201
+ const vertical = w >= th.barMinPx && w <= th.barMaxPx && (edge("left") || edge("right")) && ((edge("top") && edge("bottom")) || (px(bar.top) <= th.barInsetPx && px(bar.bottom) <= th.barInsetPx && bar.top !== "auto" && bar.bottom !== "auto"));
1202
+ const horizontal = h >= th.barMinPx && h <= th.barMaxPx && (edge("top") || edge("bottom")) && edge("left") && edge("right") && !tabLike;
1203
+ if (vertical || horizontal) { count(el, "c"); break; }
1204
+ }
1205
+ }
1206
+ for (const entry of scopes.values()) {
1207
+ const hit = entry.b.length >= 1 ? entry.b : entry.a.length >= th.minCount ? entry.a : entry.c.length >= th.minCount ? entry.c : null;
1208
+ if (hit) add("SLOP-015", { selector: selectorOf(hit[0]), value: `${hit.length} rounded element(s) with a coloured accent stripe`, threshold: `< ${th.minCount} per scope (inset form: none)` });
1209
+ }
1210
+ }
1211
+ const declaredFaces = new Map();
1212
+ for (const face of document.fonts ?? []) declaredFaces.set(face.family.replace(/["']/gu, "").trim().toLowerCase(), face.status);
1213
+ const firstLoadedFamily = (stack) => {
1214
+ for (const raw of stack.split(",")) {
1215
+ const family = raw.replace(/["']/gu, "").trim().toLowerCase();
1216
+ if (/^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-\w+|emoji|math|fangsong)$/u.test(family)) return family;
1217
+ if (declaredFaces.has(family) && declaredFaces.get(family) !== "loaded") continue;
1218
+ return family;
1219
+ }
1220
+ return "";
1221
+ };
1222
+ const families = new Map();
1223
+ for (const el of textElements) {
1224
+ const text = (el.innerText ?? "").trim();
1225
+ if (text.length <= 1 || el.getAttribute("aria-hidden") === "true" || /(icon|material|fa-)/iu.test(`${classText(el)} ${styleOf(el).fontFamily}`)) continue;
1226
+ const family = styleOf(el).fontFamily.split(",")[0].replace(/["']/gu, "").trim().toLowerCase();
1227
+ families.set(family, [...(families.get(family) ?? []), el]);
1228
+ }
1229
+ if (on("SLOP-016")) {
1230
+ const loaded = new Map();
1231
+ for (const el of textElements) {
1232
+ const family = firstLoadedFamily(styleOf(el).fontFamily);
1233
+ if (family && !el.matches("input,button,select,textarea,option") && !/^geist/u.test(family) && t("SLOP-016").families.includes(family)) loaded.set(family, el);
1234
+ }
1235
+ for (const [family, el] of loaded) candidate("SLOP-016", { selector: selectorOf(el), value: `${family} is the first loaded face` });
1236
+ }
1237
+ if (on("SLOP-019") && families.size > t("SLOP-019").max) add("SLOP-019", { selector: "body", value: `${families.size} families: ${[...families.keys()].join(", ").slice(0, 80)}`, threshold: `≤ ${t("SLOP-019").max}` });
1238
+ if (on("SLOP-020")) {
1239
+ for (const el of textElements) {
1240
+ const text = directText(el).map((node) => node.textContent).join(" ").trim();
1241
+ if (styleOf(el).textTransform === "uppercase" && text.length > t("SLOP-020").maxChars && cjkShare(text) <= 0.5) add("SLOP-020", { selector: selectorOf(el), value: `${text.length} chars in uppercase`, threshold: `≤ ${t("SLOP-020").maxChars}` });
1242
+ }
1243
+ }
1244
+ if (on("SLOP-021")) {
1245
+ const th = t("SLOP-021");
1246
+ for (const el of textElements) {
1247
+ const fg = parseColor(styleOf(el).color);
1248
+ const bg = fg ? solidBehind(el) : null;
1249
+ if (!fg || !bg) continue;
1250
+ const tone = hsl(fg);
1251
+ const surface = hsl(bg);
1252
+ if (tone.l >= th.minLightness && tone.l <= th.maxLightness && tone.s < th.maxTextSaturation && surface.s >= th.minSurfaceSaturation && surface.l > th.surfaceLightness[0] && surface.l < th.surfaceLightness[1]) {
1253
+ add("SLOP-021", { selector: selectorOf(el), value: `gray text L${Math.round(tone.l * 100)}% on a ${Math.round(surface.h)}° surface`, threshold: "text tinted from the surface hue" });
1254
+ }
1255
+ }
1256
+ }
1257
+ if (on("SLOP-023")) {
1258
+ const base = parseColor(styleOf(body).backgroundColor) ?? parseColor(styleOf(doc).backgroundColor);
1259
+ const ink = parseColor(styleOf(body).color);
1260
+ const pure = (c) => c && c.a >= 0.999 && ((c.r === 0 && c.g === 0 && c.b === 0) || (c.r === 255 && c.g === 255 && c.b === 255));
1261
+ if (pure(base) && !(darkScheme && base.r === 0)) add("SLOP-023", { selector: "body", value: `background rgb(${base.r}, ${base.g}, ${base.b})`, threshold: "an off-white or off-black base" });
1262
+ if (pure(ink)) add("SLOP-023", { selector: "body", value: `text rgb(${ink.r}, ${ink.g}, ${ink.b})`, threshold: "an off-black or off-white ink" });
1263
+ }
1264
+ if (on("SLOP-027")) {
1265
+ for (const el of shown) {
1266
+ const style = styleOf(el);
1267
+ if (style.animationName === "none" || !/infinite/u.test(style.animationIterationCount)) continue;
1268
+ const bound = [...el.attributes].some((attr) => attr.name.startsWith("data-")) || el.closest("[aria-busy=true]") || /(skeleton|shimmer|loading|spinner)/iu.test(classText(el));
1269
+ const pulse = document.getAnimations().some((a) => a.effect?.target === el && a.effect.getKeyframes().some((frame) => "opacity" in frame || "boxShadow" in frame));
1270
+ if (pulse && !bound) add("SLOP-027", { selector: selectorOf(el), value: `endless ${style.animationName}`, threshold: "bound to a live state" });
1271
+ }
1272
+ }
1273
+
1274
+ // ---- slop register: structure and copy --------------------------------------------------------
1275
+ if (on("SLOP-002")) {
1276
+ const th = t("SLOP-002");
1277
+ const shape = (node) => [...node.children].map((child) => child.tagName).join(",");
1278
+ const tile = (node) => node.querySelector("img,svg,picture,[role=img],[class*=icon]") && node.querySelector("h1,h2,h3,h4,h5,h6,[role=heading]") && node.querySelector("p,span,div:not(:has(*))");
1279
+ for (const grid of shown.filter((el) => !el.matches("ul,ol,table,tbody,thead,tr,dl"))) {
1280
+ const kids = [...grid.children].filter(visible);
1281
+ if (kids.length < th.minTiles || !kids.every(tile)) continue;
1282
+ const first = kids[0].getBoundingClientRect();
1283
+ const same = kids.every((kid) => {
1284
+ const r = kid.getBoundingClientRect();
1285
+ return Math.abs(r.width - first.width) <= th.sizeTolerancePx && Math.abs(r.height - first.height) <= th.sizeTolerancePx && shape(kid) === shape(kids[0]);
1286
+ });
1287
+ if (same) add("SLOP-002", { selector: selectorOf(grid), value: `${kids.length} identical icon + heading + text tiles`, threshold: `< ${th.minTiles} per grid` });
1288
+ }
1289
+ }
1290
+ if (on("SLOP-029")) {
1291
+ const th = t("SLOP-029");
1292
+ const eyebrows = shown.filter((el) => el.matches("h1,h2,h3")).map((heading) => {
1293
+ let prev = heading.previousElementSibling;
1294
+ while (prev && !visible(prev)) prev = prev.previousElementSibling;
1295
+ if (!prev) return null;
1296
+ const style = styleOf(prev);
1297
+ const text = (prev.innerText ?? "").trim();
1298
+ const words = text.split(/\s+/u).filter(Boolean);
1299
+ const caps = style.textTransform === "uppercase" || (/[A-Z]/u.test(text) && text === text.toUpperCase());
1300
+ const tracked = px(style.letterSpacing) / Math.max(1, px(style.fontSize)) >= th.minTrackingEm;
1301
+ const small = px(style.fontSize) <= th.maxSizeRatio * px(styleOf(heading).fontSize);
1302
+ return words.length > 0 && words.length <= th.maxWords && caps && tracked && small ? prev : null;
1303
+ }).filter(Boolean);
1304
+ const sections = shown.filter((el) => el.matches("h2")).length;
1305
+ const allowed = Math.ceil(sections / th.sectionShare);
1306
+ if (eyebrows.length > allowed) add("SLOP-029", { selector: selectorOf(eyebrows[0]), value: `${eyebrows.length} eyebrow label(s) over ${sections} section heading(s)`, threshold: `≤ ${allowed}` });
1307
+ }
1308
+ if (on("SLOP-030")) {
1309
+ const labels = shown.filter((el) => /^0\d\b/u.test((el.innerText ?? "").trim()) && (el.innerText ?? "").trim().length <= 24 && el.nextElementSibling?.matches("h1,h2,h3,h4"));
1310
+ const indices = new Set(labels.map((el) => el.innerText.trim().slice(0, 2)));
1311
+ if (indices.size >= t("SLOP-030").minDistinct) candidate("SLOP-030", { selector: selectorOf(labels[0]), value: `section labels ${[...indices].join(", ")}` });
1312
+ }
1313
+ // Authored copy: visible text nodes of the top document, outside code and form values.
1314
+ const copyNodes = textElements.filter((el) => !el.closest("code,pre,kbd,samp,textarea,select,option,script,style"));
1315
+ const leaf = copyNodes.map((el) => ({ el, text: directText(el).map((node) => node.textContent).join(" ") }));
1316
+ const copy = leaf.map((entry) => entry.text).join("\n").slice(0, limits.textChars * 4);
1317
+ if (on("SLOP-033")) {
1318
+ for (const { el, text } of leaf) {
1319
+ const dots = text.match(/·/gu)?.length ?? 0;
1320
+ if (dots > t("SLOP-033").maxDots) add("SLOP-033", { selector: selectorOf(el), value: `${dots} middle dots on one line`, threshold: `≤ ${t("SLOP-033").maxDots}` });
1321
+ }
1322
+ }
1323
+ const phraseRule = (list) => new RegExp(`(?<![\\p{L}\\p{N}])(?:${list.map(escapeRegExp).join("|")})(?![\\p{L}\\p{N}])`, "iu");
1324
+ const phraseRules = [
1325
+ ["SLOP-036", on("SLOP-036") ? phraseRule(t("SLOP-036").phrases) : null, "stock phrase", copyNodes.filter((el) => !el.closest("blockquote"))],
1326
+ ["SLOP-037", on("SLOP-037") ? new RegExp(`\\b(?:${t("SLOP-037").names.map(escapeRegExp).join("|")})\\b`, "u") : null, "placeholder brand"],
1327
+ ["SLOP-038", /\b(?:John|Jane) (?:Doe|Smith)\b|홍길동|김철수|이영희/u, "filler name"],
1328
+ ["SLOP-060", /lorem ipsum|dolor sit amet|\[placeholder\]|placeholder text|\bTODO\b|\bTBD\b/iu, "filler text"],
1329
+ ];
1330
+ for (const [id, pattern, label, scope] of phraseRules) {
1331
+ if (!on(id) || !pattern) continue;
1332
+ const pool = scope ? leaf.filter((entry) => scope.includes(entry.el)) : leaf;
1333
+ const hit = pool.find((entry) => pattern.test(entry.text));
1334
+ if (hit) add(id, { selector: selectorOf(hit.el), value: `${label}: "${pattern.exec(hit.text)[0]}"`, threshold: "none" });
1335
+ }
1336
+ if (on("SLOP-039")) {
1337
+ const shapes = [/\bNot an? \p{Ll}[^.!]{1,40}[.!]\s+\p{Lu}[^.!]{0,59}[.!]/gu, /\p{Lu}[^.!]{3,79}[.!]\s+(?:No|Just) \p{Ll}[^.!]{1,59}[.!]/gu];
1338
+ const count = shapes.reduce((sum, pattern) => sum + (copy.match(pattern)?.length ?? 0), 0);
1339
+ if (count >= t("SLOP-039").min) add("SLOP-039", { selector: "body", value: `${count} rebuttal-cadence sentence pairs`, threshold: `< ${t("SLOP-039").min}` });
1340
+ }
1341
+ if (on("SLOP-040")) {
1342
+ const th = t("SLOP-040");
1343
+ const han = (text) => { const letters = text.replace(/[\s\p{P}\p{S}\d]/gu, ""); return letters.length ? (letters.match(/[一-鿿]/gu)?.length ?? 0) / letters.length : 0; };
1344
+ const quotedAway = (el) => { const quote = el.closest("blockquote[cite],q[cite]"); if (!quote) return false; try { return new URL(quote.getAttribute("cite"), location.href).origin !== location.origin; } catch { return false; } };
1345
+ let main = 0;
1346
+ let first = null;
1347
+ for (const { el, text } of leaf) {
1348
+ const em = text.match(/—/gu)?.length ?? 0;
1349
+ const en = [...text.matchAll(/(\S)\s+–\s+(\S)/gu)].filter((match) => !(/\d/u.test(match[1]) && /\d/u.test(match[2]))).length;
1350
+ if (!em && !en) continue;
1351
+ if (quotedAway(el) || (han(text) > th.hanShare && /——/u.test(text))) {
1352
+ add("SLOP-040", { severity: th.carveOutSeverity, selector: selectorOf(el), value: `${em + en} dash(es) in a quoted or Han-majority passage`, threshold: `< ${th.min}` });
1353
+ continue;
1354
+ }
1355
+ main += em + en;
1356
+ first = first ?? el;
1357
+ }
1358
+ if (main >= th.min) add("SLOP-040", { selector: selectorOf(first), value: `${main} dash(es), first in "${snippet(first, 30)}"`, threshold: `< ${th.min}` });
1359
+ }
1360
+ if (on("SLOP-041")) {
1361
+ const stat = /(?<![\p{L}\d.,])(\d+(?:[.,]\d+)?\s?%|\d+(?:\.\d+)?\s?[×x](?!\p{L})|\d+(?:\.\d+)?\s?[kKMB]\+?(?!\p{L})|\d{1,3}(?:,\d{3})+\+?|\d+\+(?!\d)|\d+(?:\.\d+)?\s?(?:mm|cm|km|kg|lb|GB|TB)\b|\d+(?:\.\d+)?\s?\p{L}+\/(?:day|week|month|hr|hour))/u;
1362
+ const excluded = /[$€£¥₩]\s?\d|\d\s?(?:원|USD|EUR|KRW)\b|\b(?:19|20)\d{2}\b|\d{1,2}:\d{2}|\bv\d+\.\d+/u;
1363
+ const sourced = /\b(source|according to|as of|example|sample|mock|illustrative)\b|출처|예시/iu;
1364
+ for (const { el, text } of leaf) {
1365
+ const hit = stat.exec(text);
1366
+ if (!hit || excluded.test(text) || el.closest("time,td,th,form,[role=cell]")) continue;
1367
+ const block = el.closest("p,li,section,article,div,figure") ?? el;
1368
+ if (block.querySelector("cite,sup,a[href^='#fn']") || sourced.test(block.innerText ?? "")) continue;
1369
+ candidate("SLOP-041", { selector: selectorOf(el), value: `"${hit[0]}" with no source or mock marker` });
1370
+ }
1371
+ }
1372
+ const media = "img,picture,video,canvas,iframe,object";
1373
+ if (on("SLOP-043")) {
1374
+ const th = t("SLOP-043");
1375
+ for (const el of shown) {
1376
+ const r = el.getBoundingClientRect();
1377
+ if (r.width < th.minWidthPx || r.height < th.minHeightPx || el.matches("main,body") || el.querySelector(media) || el.querySelector(controlSelector)) continue;
1378
+ const inside = el.querySelectorAll("*");
1379
+ if (inside.length > limits.pairControls) continue;
1380
+ const dots = [...inside].filter((node) => { const b = node.getBoundingClientRect(); return b.width > 0 && b.width <= th.dotMaxPx && Math.abs(b.width - b.height) <= 1 && px(styleOf(node).borderTopLeftRadius) >= b.width / 2 - 0.5 && b.top - r.top <= th.chromeBandPx; });
1381
+ const chrome = dots.length >= 3 && new Set(dots.map((node) => Math.round(node.getBoundingClientRect().top))).size <= 2;
1382
+ const tilted = /(rotate|perspective|matrix3d)/u.test(styleOf(el).transform) || (() => { const scale = scaleStart(styleOf(el).transform, styleOf(el).scale); return scale !== null && scale < 1; })();
1383
+ if (chrome || tilted) candidate("SLOP-043", { selector: selectorOf(el), value: chrome ? "window-chrome dots over div-built content" : "tilted div-built mockup" });
1384
+ }
1385
+ }
1386
+ if (on("SLOP-045")) {
1387
+ const th = t("SLOP-045");
1388
+ const statBlock = (kid) => {
1389
+ const texts = [...kid.querySelectorAll("*")].filter((node) => directText(node).length);
1390
+ const figure = texts.find((node) => /^[\s$€£¥₩+−\d.,%×xkKMB]+$/u.test(node.innerText ?? "") && /\d/u.test(node.innerText ?? ""));
1391
+ return figure && texts.some((node) => node !== figure && px(styleOf(figure).fontSize) >= th.sizeRatio * px(styleOf(node).fontSize));
1392
+ };
1393
+ for (const parent of shown) {
1394
+ const kids = [...parent.children].filter(visible);
1395
+ if (kids.length < th.ctaSiblings || kids.length > limits.findingsPerRule || !kids.every(statBlock)) continue;
1396
+ const h = kids[0].getBoundingClientRect().height;
1397
+ if (!kids.every((kid) => Math.abs(kid.getBoundingClientRect().height - h) <= th.heightTolerancePx)) continue;
1398
+ const section = parent.closest("section,header,main") ?? body;
1399
+ const cta = [...section.querySelectorAll("a[href],button")].some((el) => isPrimary(el));
1400
+ if (kids.length >= th.minSiblings || cta) candidate("SLOP-045", { selector: selectorOf(parent), value: `${kids.length} same-shape stat blocks${cta ? " beside a call to action" : ""}` });
1401
+ }
1402
+ }
1403
+ if (on("SLOP-046")) {
1404
+ const th = t("SLOP-046");
1405
+ const gauges = shown.filter((el) => el.matches("progress,meter,[role=progressbar],[role=meter]")
1406
+ || (el.matches("svg") && [...el.querySelectorAll("circle")].some((circle) => { const dash = getComputedStyle(circle).strokeDasharray; return dash && dash !== "none"; }))
1407
+ || (el.matches("svg") && el.getBoundingClientRect().height <= th.sparklineMaxPx && el.querySelectorAll("polyline,path").length === 1 && !el.querySelector("text")));
1408
+ for (const el of gauges) {
1409
+ const named = el.getAttribute("aria-label") || el.getAttribute("aria-labelledby") || el.getAttribute("title") || el.querySelector("title");
1410
+ const around = snippet(el.parentElement ?? el, 200);
1411
+ if (!named && !/\d/u.test(around)) candidate("SLOP-046", { selector: selectorOf(el), value: "gauge or sparkline with no value, axis or name" });
1412
+ }
1413
+ }
1414
+ if (on("SLOP-047") && String(vw) === t("SLOP-047").viewport && pass === "base") {
1415
+ const th = t("SLOP-047");
1416
+ for (const animation of document.getAnimations()) {
1417
+ const target = animation.effect?.target;
1418
+ if (!target || animation.effect.getTiming().iterations !== Infinity) continue;
1419
+ if (!(animation.effect.getKeyframes?.() ?? []).some((frame) => /translate/u.test(frame.transform ?? ""))) continue;
1420
+ const logos = [...target.querySelectorAll("img,svg,[style*=background-image]")].filter(visible);
1421
+ const sources = new Set(logos.map((logo) => logo.currentSrc || styleOf(logo).backgroundImage || logo.outerHTML.slice(0, 200)));
1422
+ const heights = logos.map((logo) => logo.getBoundingClientRect().height).sort((a, b) => a - b);
1423
+ const median = heights[Math.floor(heights.length / 2)] ?? 0;
1424
+ if (logos.length && sources.size <= th.maxLogos && median < th.minLogoPx) add("SLOP-047", { selector: selectorOf(target), value: `${sources.size} logos looping at a median ${round(median, 1)}px`, threshold: `≥ ${th.minLogoPx}px, or a static row` });
1425
+ }
1426
+ }
1427
+ if (on("SLOP-048")) {
1428
+ const th = t("SLOP-048");
1429
+ const dot = (el) => { const r = el.getBoundingClientRect(); return (r.width <= th.dotMaxPx && r.height <= th.dotMaxPx && px(styleOf(el).borderTopLeftRadius) >= Math.min(r.width, r.height) / 2 - 0.5) || /(^|[\s_-])(dot|dots|bullet|indicator)s?([\s_-]|$)/iu.test(classText(el)); };
1430
+ for (const group of shown.filter((el) => { const kids = [...el.children].filter(visible); return kids.length >= th.minDots && kids.every(dot); })) {
1431
+ let root = group.parentElement;
1432
+ for (let depth = 0; root && depth < 3 && root.querySelectorAll("[class*=slide],[aria-roledescription=slide]").length < 2; depth += 1) root = root.parentElement;
1433
+ if (!root || root.querySelectorAll("[class*=slide],[aria-roledescription=slide]").length < 2) continue;
1434
+ const kids = [...group.children];
1435
+ const bound = kids.every((el) => el.tabIndex >= 0 && el.matches("button,[role=button],[role=tab]") && accessibleName(el)) || kids.some((el) => el.hasAttribute("aria-controls") || el.getAttribute("aria-selected") === "true" || el.getAttribute("aria-disabled") === "true" || el.hasAttribute("aria-current"));
1436
+ if (bound) continue;
1437
+ const autoplay = root.closest("[data-autoplay],[data-bs-ride=carousel],[autoplay]");
1438
+ if (autoplay) add("SLOP-048", { selector: selectorOf(group), value: `${kids.length} autoplay dots with no accessible binding`, threshold: "named, focusable dot controls" });
1439
+ else skip("SLOP-048", `${selectorOf(group)}: unbound dots with no declared autoplay; the timed read is not run`);
1440
+ }
1441
+ }
1442
+ if (on("SLOP-051")) {
1443
+ const th = t("SLOP-051");
1444
+ for (const svg of shown.filter((el) => el.matches("svg"))) {
1445
+ const r = svg.getBoundingClientRect();
1446
+ if (r.width < th.minSizePx || r.height < th.minSizePx || svg.querySelector("pattern")) continue;
1447
+ const shapes = [...svg.querySelectorAll("rect,circle,ellipse,polygon")];
1448
+ const fills = new Set(shapes.map((shape) => getComputedStyle(shape).fill).filter((fill) => fill && !/^(none|transparent|rgba\(0, 0, 0, 0\))$/u.test(fill)));
1449
+ if (shapes.length >= th.minShapes && fills.size >= th.minFills && svg.querySelectorAll("text,tspan").length <= th.maxText) candidate("SLOP-051", { selector: selectorOf(svg), value: `${shapes.length} primitives in ${fills.size} fills` });
1450
+ }
1451
+ }
1452
+ if (on("SLOP-052")) {
1453
+ const th = t("SLOP-052");
1454
+ for (const el of shown) {
1455
+ const clip = styleOf(el).clipPath;
1456
+ if (!clip || clip === "none") continue;
1457
+ const polygon = /^polygon\((.*)\)$/u.exec(clip);
1458
+ if (polygon) {
1459
+ const vertices = polygon[1].split(",");
1460
+ const coordinates = vertices.flatMap((vertex) => vertex.trim().split(/\s+/u).map(parseFloat)).filter(Number.isFinite);
1461
+ const offGrid = coordinates.filter((value) => Math.abs(value - Math.round(value / th.grid) * th.grid) > th.gridTolerance).length;
1462
+ if (vertices.length >= th.minVertices && offGrid * 2 >= coordinates.length) add("SLOP-052", { selector: selectorOf(el), value: `polygon with ${vertices.length} off-grid vertices`, threshold: `< ${th.minVertices} vertices, or on the ${th.grid}% grid` });
1463
+ }
1464
+ const path = /^path\((.*)\)$/u.exec(clip);
1465
+ if (path && (path[1].match(/[CcSsQqTtAa]/gu)?.length ?? 0) >= th.minCurves) add("SLOP-052", { selector: selectorOf(el), value: "curved path() mask", threshold: `< ${th.minCurves} curves` });
1466
+ }
1467
+ }
1468
+ if (on("SLOP-053")) {
1469
+ const emoji = /\p{Extended_Pictographic}/u;
1470
+ const candidates = new Set([...controls.filter(visible), ...shown.filter((node) => node.matches("nav a, nav li, [class*=badge], [class*=icon], h2, h3, h4, li"))]);
1471
+ for (const el of candidates) {
1472
+ const text = (el.innerText ?? "").trim();
1473
+ if (!emoji.test(text)) continue;
1474
+ const rect = el.getBoundingClientRect();
1475
+ const sole = text.replace(/[\p{Extended_Pictographic}️‍\s]/gu, "") === "" && Math.max(rect.width, rect.height) <= t("SLOP-053").maxControlPx * 2;
1476
+ const leading = /^\p{Extended_Pictographic}/u.test(text) && text.length <= 60;
1477
+ if (sole || leading) candidate("SLOP-053", { selector: selectorOf(el), value: `"${text.slice(0, 24)}"` });
1478
+ }
1479
+ }
1480
+
1481
+ // ---- slop register: broken or fake controls ---------------------------------------------------
1482
+ if (on("SLOP-057")) {
1483
+ let lazy = 0;
1484
+ const blank = (value) => value !== null && ["", "#", "undefined"].includes(value.trim());
1485
+ for (const img of body.querySelectorAll("img")) {
1486
+ const src = img.getAttribute("src");
1487
+ const bad = blank(src) || blank(img.getAttribute("srcset"));
1488
+ const loader = [...img.attributes].some((attr) => /^data-(src|srcset|lazy)/u.test(attr.name));
1489
+ if (bad && loader) { lazy += 1; continue; }
1490
+ if (bad) { add("SLOP-057", { selector: selectorOf(img), value: `src="${(src ?? "").trim()}"`, threshold: "an image that loads" }); continue; }
1491
+ if (!img.complete) { lazy += 1; continue; }
1492
+ if (img.naturalWidth === 0) add("SLOP-057", { selector: selectorOf(img), value: `"${(img.currentSrc || src || "").slice(0, 40)}" failed to load`, threshold: "an image that loads" });
1493
+ }
1494
+ if (lazy) skip("SLOP-057", `lazy image not fetched at capture (${lazy})`);
1495
+ }
1496
+ if (on("SLOP-058")) {
1497
+ for (const el of body.querySelectorAll("a[href]")) {
1498
+ const href = el.getAttribute("href").trim();
1499
+ if (href !== "#" && !/^javascript:/iu.test(href)) continue;
1500
+ const decorative = !(el.innerText ?? "").trim() || el.closest("[class*=logo],[class*=social]");
1501
+ add("SLOP-058", { severity: decorative ? t("SLOP-058").decorativeSeverity : undefined, selector: selectorOf(el), value: `href="${href}" "${snippet(el, 24)}"`, threshold: "a real destination" });
1502
+ }
1503
+ }
1504
+ if (on("SLOP-059")) {
1505
+ for (const el of body.querySelectorAll("button:not([type])")) {
1506
+ const owner = el.form;
1507
+ if (!owner) continue;
1508
+ const submits = [...owner.elements].filter((node) => node.matches("button,input[type=submit]") && node.type === "submit");
1509
+ if (submits.length >= t("SLOP-059").minSubmitButtons) add("SLOP-059", { selector: selectorOf(el), value: `"${snippet(el, 24)}" defaults to submit beside ${submits.length - 1} other submit button(s)`, threshold: "explicit type" });
1510
+ }
1511
+ }
1512
+ if (on("SLOP-061")) {
1513
+ for (const el of body.querySelectorAll("marquee")) add("SLOP-061", { selector: selectorOf(el), value: "<marquee>", threshold: "static row or a pause control" });
1514
+ }
1515
+
1516
+ return {
1517
+ viewport: { width: vw, height: vh, dpr: window.devicePixelRatio, dark: window.matchMedia("(prefers-color-scheme: dark)").matches, reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches },
1518
+ ready: document.readyState,
1519
+ counts: { elements: all.length, visible: shown.length, text: textElements.length, controls: controls.length },
1520
+ findings,
1521
+ notVerified,
1522
+ hoverTargets,
1523
+ };
1524
+ }
1525
+
1526
+ // Read after the driver hovers one CF-506 target: is it really :hover, and how long is its
1527
+ // transition. Self-contained for the same reason as measureInterface.
1528
+ export function readHover(selector) {
1529
+ const el = document.querySelector(selector);
1530
+ if (!el) return { found: false };
1531
+ const style = getComputedStyle(el);
1532
+ const seconds = (value) => value.split(",").map((part) => (part.trim().endsWith("ms") ? parseFloat(part) : parseFloat(part) * 1000) || 0);
1533
+ return { found: true, hover: el.matches(":hover"), durationMs: Math.max(0, ...seconds(style.transitionDuration)), property: style.transitionProperty };
1534
+ }