@jjlmoya/utils-forensic-science 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/package.json +69 -0
  2. package/scripts/crystal-lattice-catalog-translations.mjs +459 -0
  3. package/scripts/postinstall.mjs +27 -0
  4. package/scripts/sync-crystal-lattice-i18n.mjs +50 -0
  5. package/src/category/i18n/de.ts +55 -0
  6. package/src/category/i18n/en.ts +55 -0
  7. package/src/category/i18n/es.ts +55 -0
  8. package/src/category/i18n/fr.ts +55 -0
  9. package/src/category/i18n/id.ts +55 -0
  10. package/src/category/i18n/it.ts +55 -0
  11. package/src/category/i18n/ja.ts +56 -0
  12. package/src/category/i18n/ko.ts +56 -0
  13. package/src/category/i18n/nl.ts +55 -0
  14. package/src/category/i18n/pl.ts +55 -0
  15. package/src/category/i18n/pt.ts +55 -0
  16. package/src/category/i18n/ru.ts +55 -0
  17. package/src/category/i18n/sv.ts +55 -0
  18. package/src/category/i18n/tr.ts +55 -0
  19. package/src/category/i18n/zh.ts +56 -0
  20. package/src/category/index.ts +48 -0
  21. package/src/category/seo.astro +15 -0
  22. package/src/components/PreviewNavSidebar.astro +116 -0
  23. package/src/components/PreviewToolbar.astro +143 -0
  24. package/src/data.ts +10 -0
  25. package/src/entries.ts +48 -0
  26. package/src/env.d.ts +5 -0
  27. package/src/index.ts +29 -0
  28. package/src/layouts/PreviewLayout.astro +117 -0
  29. package/src/pages/[locale]/[slug].astro +162 -0
  30. package/src/pages/[locale].astro +261 -0
  31. package/src/pages/index.astro +3 -0
  32. package/src/tests/diacritics_density.test.ts +140 -0
  33. package/src/tests/faq_count.test.ts +19 -0
  34. package/src/tests/i18n_coverage.test.ts +36 -0
  35. package/src/tests/inverted_punctuation.test.ts +84 -0
  36. package/src/tests/locale_completeness.test.ts +24 -0
  37. package/src/tests/mocks/astro_mock.js +2 -0
  38. package/src/tests/no_en_dash.test.ts +70 -0
  39. package/src/tests/no_h1_in_components.test.ts +48 -0
  40. package/src/tests/schemas_fulfillment.test.ts +23 -0
  41. package/src/tests/script_density.test.ts +94 -0
  42. package/src/tests/seo_length.test.ts +46 -0
  43. package/src/tests/shared-test-helpers.ts +56 -0
  44. package/src/tests/slug_language_code_format.test.ts +23 -0
  45. package/src/tests/slug_uniqueness.test.ts +81 -0
  46. package/src/tests/title_quality.test.ts +55 -0
  47. package/src/tests/tool_exports.test.ts +34 -0
  48. package/src/tests/tool_validation.test.ts +16 -0
  49. package/src/tests/widmark.test.ts +28 -0
  50. package/src/tool/forensic-age-estimator/bibliography.astro +14 -0
  51. package/src/tool/forensic-age-estimator/bibliography.ts +16 -0
  52. package/src/tool/forensic-age-estimator/component.astro +321 -0
  53. package/src/tool/forensic-age-estimator/dental-skeletal-third-molar-age-estimator.css +634 -0
  54. package/src/tool/forensic-age-estimator/entry.ts +32 -0
  55. package/src/tool/forensic-age-estimator/i18n/de.ts +245 -0
  56. package/src/tool/forensic-age-estimator/i18n/en.ts +245 -0
  57. package/src/tool/forensic-age-estimator/i18n/es.ts +245 -0
  58. package/src/tool/forensic-age-estimator/i18n/fr.ts +245 -0
  59. package/src/tool/forensic-age-estimator/i18n/id.ts +245 -0
  60. package/src/tool/forensic-age-estimator/i18n/it.ts +245 -0
  61. package/src/tool/forensic-age-estimator/i18n/ja.ts +245 -0
  62. package/src/tool/forensic-age-estimator/i18n/ko.ts +245 -0
  63. package/src/tool/forensic-age-estimator/i18n/nl.ts +245 -0
  64. package/src/tool/forensic-age-estimator/i18n/pl.ts +245 -0
  65. package/src/tool/forensic-age-estimator/i18n/pt.ts +245 -0
  66. package/src/tool/forensic-age-estimator/i18n/ru.ts +245 -0
  67. package/src/tool/forensic-age-estimator/i18n/sv.ts +245 -0
  68. package/src/tool/forensic-age-estimator/i18n/tr.ts +245 -0
  69. package/src/tool/forensic-age-estimator/i18n/zh.ts +246 -0
  70. package/src/tool/forensic-age-estimator/index.ts +11 -0
  71. package/src/tool/forensic-age-estimator/logic.ts +81 -0
  72. package/src/tool/forensic-age-estimator/seo.astro +15 -0
  73. package/src/tool/forensic-blood-test-simulator/bibliography.astro +6 -0
  74. package/src/tool/forensic-blood-test-simulator/bibliography.ts +12 -0
  75. package/src/tool/forensic-blood-test-simulator/component.astro +168 -0
  76. package/src/tool/forensic-blood-test-simulator/dom-utils.ts +40 -0
  77. package/src/tool/forensic-blood-test-simulator/entry.ts +32 -0
  78. package/src/tool/forensic-blood-test-simulator/forensic-presumptive-blood-testing-luminol-kastle-meyer-simulator.css +532 -0
  79. package/src/tool/forensic-blood-test-simulator/helpers.ts +16 -0
  80. package/src/tool/forensic-blood-test-simulator/i18n/de.ts +185 -0
  81. package/src/tool/forensic-blood-test-simulator/i18n/en.ts +185 -0
  82. package/src/tool/forensic-blood-test-simulator/i18n/es.ts +185 -0
  83. package/src/tool/forensic-blood-test-simulator/i18n/fr.ts +220 -0
  84. package/src/tool/forensic-blood-test-simulator/i18n/id.ts +220 -0
  85. package/src/tool/forensic-blood-test-simulator/i18n/it.ts +220 -0
  86. package/src/tool/forensic-blood-test-simulator/i18n/ja.ts +220 -0
  87. package/src/tool/forensic-blood-test-simulator/i18n/ko.ts +220 -0
  88. package/src/tool/forensic-blood-test-simulator/i18n/nl.ts +220 -0
  89. package/src/tool/forensic-blood-test-simulator/i18n/pl.ts +220 -0
  90. package/src/tool/forensic-blood-test-simulator/i18n/pt.ts +220 -0
  91. package/src/tool/forensic-blood-test-simulator/i18n/ru.ts +220 -0
  92. package/src/tool/forensic-blood-test-simulator/i18n/sv.ts +220 -0
  93. package/src/tool/forensic-blood-test-simulator/i18n/tr.ts +220 -0
  94. package/src/tool/forensic-blood-test-simulator/i18n/zh.ts +220 -0
  95. package/src/tool/forensic-blood-test-simulator/index.ts +11 -0
  96. package/src/tool/forensic-blood-test-simulator/logic.ts +174 -0
  97. package/src/tool/forensic-blood-test-simulator/script.ts +192 -0
  98. package/src/tool/forensic-blood-test-simulator/seo.astro +15 -0
  99. package/src/tool/forensic-fiber-comparison-microscope/bibliography.astro +6 -0
  100. package/src/tool/forensic-fiber-comparison-microscope/bibliography.ts +16 -0
  101. package/src/tool/forensic-fiber-comparison-microscope/component.astro +146 -0
  102. package/src/tool/forensic-fiber-comparison-microscope/entry.ts +32 -0
  103. package/src/tool/forensic-fiber-comparison-microscope/forensic-fiber-comparison-microscope.css +629 -0
  104. package/src/tool/forensic-fiber-comparison-microscope/i18n/de.ts +250 -0
  105. package/src/tool/forensic-fiber-comparison-microscope/i18n/en.ts +246 -0
  106. package/src/tool/forensic-fiber-comparison-microscope/i18n/es.ts +246 -0
  107. package/src/tool/forensic-fiber-comparison-microscope/i18n/fr.ts +246 -0
  108. package/src/tool/forensic-fiber-comparison-microscope/i18n/id.ts +250 -0
  109. package/src/tool/forensic-fiber-comparison-microscope/i18n/it.ts +246 -0
  110. package/src/tool/forensic-fiber-comparison-microscope/i18n/ja.ts +250 -0
  111. package/src/tool/forensic-fiber-comparison-microscope/i18n/ko.ts +246 -0
  112. package/src/tool/forensic-fiber-comparison-microscope/i18n/nl.ts +246 -0
  113. package/src/tool/forensic-fiber-comparison-microscope/i18n/pl.ts +250 -0
  114. package/src/tool/forensic-fiber-comparison-microscope/i18n/pt.ts +250 -0
  115. package/src/tool/forensic-fiber-comparison-microscope/i18n/ru.ts +250 -0
  116. package/src/tool/forensic-fiber-comparison-microscope/i18n/sv.ts +250 -0
  117. package/src/tool/forensic-fiber-comparison-microscope/i18n/tr.ts +246 -0
  118. package/src/tool/forensic-fiber-comparison-microscope/i18n/zh.ts +250 -0
  119. package/src/tool/forensic-fiber-comparison-microscope/index.ts +11 -0
  120. package/src/tool/forensic-fiber-comparison-microscope/logic.ts +244 -0
  121. package/src/tool/forensic-fiber-comparison-microscope/render.ts +265 -0
  122. package/src/tool/forensic-fiber-comparison-microscope/seo.astro +15 -0
  123. package/src/tool/forensic-fiber-comparison-microscope/view.ts +267 -0
  124. package/src/tool/forensic-glass-becke-line-simulator/bibliography.astro +6 -0
  125. package/src/tool/forensic-glass-becke-line-simulator/bibliography.ts +16 -0
  126. package/src/tool/forensic-glass-becke-line-simulator/component.astro +81 -0
  127. package/src/tool/forensic-glass-becke-line-simulator/entry.ts +32 -0
  128. package/src/tool/forensic-glass-becke-line-simulator/forensic-glass-becke-line-simulator.css +392 -0
  129. package/src/tool/forensic-glass-becke-line-simulator/i18n/de.ts +231 -0
  130. package/src/tool/forensic-glass-becke-line-simulator/i18n/en.ts +231 -0
  131. package/src/tool/forensic-glass-becke-line-simulator/i18n/es.ts +231 -0
  132. package/src/tool/forensic-glass-becke-line-simulator/i18n/fr.ts +231 -0
  133. package/src/tool/forensic-glass-becke-line-simulator/i18n/id.ts +231 -0
  134. package/src/tool/forensic-glass-becke-line-simulator/i18n/it.ts +231 -0
  135. package/src/tool/forensic-glass-becke-line-simulator/i18n/ja.ts +231 -0
  136. package/src/tool/forensic-glass-becke-line-simulator/i18n/ko.ts +231 -0
  137. package/src/tool/forensic-glass-becke-line-simulator/i18n/nl.ts +231 -0
  138. package/src/tool/forensic-glass-becke-line-simulator/i18n/pl.ts +231 -0
  139. package/src/tool/forensic-glass-becke-line-simulator/i18n/pt.ts +231 -0
  140. package/src/tool/forensic-glass-becke-line-simulator/i18n/ru.ts +231 -0
  141. package/src/tool/forensic-glass-becke-line-simulator/i18n/sv.ts +231 -0
  142. package/src/tool/forensic-glass-becke-line-simulator/i18n/tr.ts +231 -0
  143. package/src/tool/forensic-glass-becke-line-simulator/i18n/zh.ts +231 -0
  144. package/src/tool/forensic-glass-becke-line-simulator/index.ts +11 -0
  145. package/src/tool/forensic-glass-becke-line-simulator/logic.ts +100 -0
  146. package/src/tool/forensic-glass-becke-line-simulator/seo.astro +15 -0
  147. package/src/tool/forensic-glass-becke-line-simulator/view.ts +281 -0
  148. package/src/tool/forensic-image-authenticity-analyzer/bibliography.astro +9 -0
  149. package/src/tool/forensic-image-authenticity-analyzer/bibliography.ts +7 -0
  150. package/src/tool/forensic-image-authenticity-analyzer/component.astro +250 -0
  151. package/src/tool/forensic-image-authenticity-analyzer/entry.ts +29 -0
  152. package/src/tool/forensic-image-authenticity-analyzer/forensic-image-metadata-authenticity-analyzer.css +679 -0
  153. package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -0
  154. package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -0
  155. package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -0
  156. package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -0
  157. package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +76 -0
  158. package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -0
  159. package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +72 -0
  160. package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +72 -0
  161. package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +75 -0
  162. package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +75 -0
  163. package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -0
  164. package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +75 -0
  165. package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +76 -0
  166. package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +76 -0
  167. package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +71 -0
  168. package/src/tool/forensic-image-authenticity-analyzer/index.ts +11 -0
  169. package/src/tool/forensic-image-authenticity-analyzer/logic.ts +283 -0
  170. package/src/tool/forensic-image-authenticity-analyzer/seo.astro +10 -0
  171. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.astro +6 -0
  172. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.ts +12 -0
  173. package/src/tool/forensic-microcrystal-drug-simulator/component.astro +240 -0
  174. package/src/tool/forensic-microcrystal-drug-simulator/entry.ts +32 -0
  175. package/src/tool/forensic-microcrystal-drug-simulator/forensic-microcrystal-drug-simulator.css +430 -0
  176. package/src/tool/forensic-microcrystal-drug-simulator/i18n/de.ts +244 -0
  177. package/src/tool/forensic-microcrystal-drug-simulator/i18n/en.ts +244 -0
  178. package/src/tool/forensic-microcrystal-drug-simulator/i18n/es.ts +244 -0
  179. package/src/tool/forensic-microcrystal-drug-simulator/i18n/fr.ts +244 -0
  180. package/src/tool/forensic-microcrystal-drug-simulator/i18n/id.ts +244 -0
  181. package/src/tool/forensic-microcrystal-drug-simulator/i18n/it.ts +244 -0
  182. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ja.ts +244 -0
  183. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ko.ts +244 -0
  184. package/src/tool/forensic-microcrystal-drug-simulator/i18n/nl.ts +244 -0
  185. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pl.ts +244 -0
  186. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pt.ts +244 -0
  187. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ru.ts +244 -0
  188. package/src/tool/forensic-microcrystal-drug-simulator/i18n/sv.ts +244 -0
  189. package/src/tool/forensic-microcrystal-drug-simulator/i18n/tr.ts +244 -0
  190. package/src/tool/forensic-microcrystal-drug-simulator/i18n/zh.ts +244 -0
  191. package/src/tool/forensic-microcrystal-drug-simulator/index.ts +11 -0
  192. package/src/tool/forensic-microcrystal-drug-simulator/logic.ts +189 -0
  193. package/src/tool/forensic-microcrystal-drug-simulator/seo.astro +15 -0
  194. package/src/tool/forensic-sex-determinator/bibliography.astro +14 -0
  195. package/src/tool/forensic-sex-determinator/bibliography.ts +12 -0
  196. package/src/tool/forensic-sex-determinator/component.astro +463 -0
  197. package/src/tool/forensic-sex-determinator/entry.ts +32 -0
  198. package/src/tool/forensic-sex-determinator/forensic-sex-determinator.css +413 -0
  199. package/src/tool/forensic-sex-determinator/i18n/de.ts +211 -0
  200. package/src/tool/forensic-sex-determinator/i18n/en.ts +211 -0
  201. package/src/tool/forensic-sex-determinator/i18n/es.ts +211 -0
  202. package/src/tool/forensic-sex-determinator/i18n/fr.ts +211 -0
  203. package/src/tool/forensic-sex-determinator/i18n/id.ts +211 -0
  204. package/src/tool/forensic-sex-determinator/i18n/it.ts +211 -0
  205. package/src/tool/forensic-sex-determinator/i18n/ja.ts +211 -0
  206. package/src/tool/forensic-sex-determinator/i18n/ko.ts +211 -0
  207. package/src/tool/forensic-sex-determinator/i18n/nl.ts +211 -0
  208. package/src/tool/forensic-sex-determinator/i18n/pl.ts +211 -0
  209. package/src/tool/forensic-sex-determinator/i18n/pt.ts +211 -0
  210. package/src/tool/forensic-sex-determinator/i18n/ru.ts +211 -0
  211. package/src/tool/forensic-sex-determinator/i18n/sv.ts +211 -0
  212. package/src/tool/forensic-sex-determinator/i18n/tr.ts +211 -0
  213. package/src/tool/forensic-sex-determinator/i18n/zh.ts +211 -0
  214. package/src/tool/forensic-sex-determinator/index.ts +11 -0
  215. package/src/tool/forensic-sex-determinator/logic.ts +89 -0
  216. package/src/tool/forensic-sex-determinator/seo.astro +15 -0
  217. package/src/tool/forensic-stature-estimator/bibliography.astro +14 -0
  218. package/src/tool/forensic-stature-estimator/bibliography.ts +12 -0
  219. package/src/tool/forensic-stature-estimator/component.astro +49 -0
  220. package/src/tool/forensic-stature-estimator/components/EstimationPanel.astro +65 -0
  221. package/src/tool/forensic-stature-estimator/components/OsteometricBoard.astro +39 -0
  222. package/src/tool/forensic-stature-estimator/components/OsteometricSelector.astro +109 -0
  223. package/src/tool/forensic-stature-estimator/dom-utils.ts +71 -0
  224. package/src/tool/forensic-stature-estimator/entry.ts +32 -0
  225. package/src/tool/forensic-stature-estimator/forensic-stature-estimator.css +689 -0
  226. package/src/tool/forensic-stature-estimator/helpers.ts +51 -0
  227. package/src/tool/forensic-stature-estimator/i18n/de.ts +196 -0
  228. package/src/tool/forensic-stature-estimator/i18n/en.ts +196 -0
  229. package/src/tool/forensic-stature-estimator/i18n/es.ts +196 -0
  230. package/src/tool/forensic-stature-estimator/i18n/fr.ts +196 -0
  231. package/src/tool/forensic-stature-estimator/i18n/id.ts +196 -0
  232. package/src/tool/forensic-stature-estimator/i18n/it.ts +196 -0
  233. package/src/tool/forensic-stature-estimator/i18n/ja.ts +196 -0
  234. package/src/tool/forensic-stature-estimator/i18n/ko.ts +196 -0
  235. package/src/tool/forensic-stature-estimator/i18n/nl.ts +196 -0
  236. package/src/tool/forensic-stature-estimator/i18n/pl.ts +196 -0
  237. package/src/tool/forensic-stature-estimator/i18n/pt.ts +196 -0
  238. package/src/tool/forensic-stature-estimator/i18n/ru.ts +196 -0
  239. package/src/tool/forensic-stature-estimator/i18n/sv.ts +196 -0
  240. package/src/tool/forensic-stature-estimator/i18n/tr.ts +196 -0
  241. package/src/tool/forensic-stature-estimator/i18n/zh.ts +196 -0
  242. package/src/tool/forensic-stature-estimator/index.ts +11 -0
  243. package/src/tool/forensic-stature-estimator/logic.ts +119 -0
  244. package/src/tool/forensic-stature-estimator/script.ts +288 -0
  245. package/src/tool/forensic-stature-estimator/seo.astro +15 -0
  246. package/src/tool/forensic-tlc-ink-simulator/bibliography.astro +6 -0
  247. package/src/tool/forensic-tlc-ink-simulator/bibliography.ts +16 -0
  248. package/src/tool/forensic-tlc-ink-simulator/component.astro +245 -0
  249. package/src/tool/forensic-tlc-ink-simulator/entry.ts +32 -0
  250. package/src/tool/forensic-tlc-ink-simulator/forensic-tlc-ink-simulator.css +462 -0
  251. package/src/tool/forensic-tlc-ink-simulator/i18n/de.ts +243 -0
  252. package/src/tool/forensic-tlc-ink-simulator/i18n/en.ts +243 -0
  253. package/src/tool/forensic-tlc-ink-simulator/i18n/es.ts +243 -0
  254. package/src/tool/forensic-tlc-ink-simulator/i18n/fr.ts +243 -0
  255. package/src/tool/forensic-tlc-ink-simulator/i18n/id.ts +243 -0
  256. package/src/tool/forensic-tlc-ink-simulator/i18n/it.ts +243 -0
  257. package/src/tool/forensic-tlc-ink-simulator/i18n/ja.ts +235 -0
  258. package/src/tool/forensic-tlc-ink-simulator/i18n/ko.ts +235 -0
  259. package/src/tool/forensic-tlc-ink-simulator/i18n/nl.ts +243 -0
  260. package/src/tool/forensic-tlc-ink-simulator/i18n/pl.ts +243 -0
  261. package/src/tool/forensic-tlc-ink-simulator/i18n/pt.ts +243 -0
  262. package/src/tool/forensic-tlc-ink-simulator/i18n/ru.ts +243 -0
  263. package/src/tool/forensic-tlc-ink-simulator/i18n/sv.ts +243 -0
  264. package/src/tool/forensic-tlc-ink-simulator/i18n/tr.ts +243 -0
  265. package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +235 -0
  266. package/src/tool/forensic-tlc-ink-simulator/index.ts +11 -0
  267. package/src/tool/forensic-tlc-ink-simulator/logic.ts +152 -0
  268. package/src/tool/forensic-tlc-ink-simulator/seo.astro +15 -0
  269. package/src/tool/gsr-dispersion-calculator/bibliography.astro +6 -0
  270. package/src/tool/gsr-dispersion-calculator/bibliography.ts +16 -0
  271. package/src/tool/gsr-dispersion-calculator/component.astro +294 -0
  272. package/src/tool/gsr-dispersion-calculator/entry.ts +32 -0
  273. package/src/tool/gsr-dispersion-calculator/gsr-dispersion-calculator.css +305 -0
  274. package/src/tool/gsr-dispersion-calculator/i18n/de.ts +272 -0
  275. package/src/tool/gsr-dispersion-calculator/i18n/en.ts +272 -0
  276. package/src/tool/gsr-dispersion-calculator/i18n/es.ts +272 -0
  277. package/src/tool/gsr-dispersion-calculator/i18n/fr.ts +272 -0
  278. package/src/tool/gsr-dispersion-calculator/i18n/id.ts +272 -0
  279. package/src/tool/gsr-dispersion-calculator/i18n/it.ts +272 -0
  280. package/src/tool/gsr-dispersion-calculator/i18n/ja.ts +272 -0
  281. package/src/tool/gsr-dispersion-calculator/i18n/ko.ts +272 -0
  282. package/src/tool/gsr-dispersion-calculator/i18n/nl.ts +272 -0
  283. package/src/tool/gsr-dispersion-calculator/i18n/pl.ts +272 -0
  284. package/src/tool/gsr-dispersion-calculator/i18n/pt.ts +272 -0
  285. package/src/tool/gsr-dispersion-calculator/i18n/ru.ts +272 -0
  286. package/src/tool/gsr-dispersion-calculator/i18n/sv.ts +272 -0
  287. package/src/tool/gsr-dispersion-calculator/i18n/tr.ts +272 -0
  288. package/src/tool/gsr-dispersion-calculator/i18n/zh.ts +272 -0
  289. package/src/tool/gsr-dispersion-calculator/index.ts +11 -0
  290. package/src/tool/gsr-dispersion-calculator/logic.ts +148 -0
  291. package/src/tool/gsr-dispersion-calculator/seo.astro +15 -0
  292. package/src/tool/widmark-alcohol-simulator/bibliography.astro +14 -0
  293. package/src/tool/widmark-alcohol-simulator/bibliography.ts +12 -0
  294. package/src/tool/widmark-alcohol-simulator/component.astro +453 -0
  295. package/src/tool/widmark-alcohol-simulator/entry.ts +32 -0
  296. package/src/tool/widmark-alcohol-simulator/i18n/de.ts +193 -0
  297. package/src/tool/widmark-alcohol-simulator/i18n/en.ts +206 -0
  298. package/src/tool/widmark-alcohol-simulator/i18n/es.ts +193 -0
  299. package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +193 -0
  300. package/src/tool/widmark-alcohol-simulator/i18n/id.ts +193 -0
  301. package/src/tool/widmark-alcohol-simulator/i18n/it.ts +193 -0
  302. package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +193 -0
  303. package/src/tool/widmark-alcohol-simulator/i18n/ko.ts +193 -0
  304. package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +193 -0
  305. package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +193 -0
  306. package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +193 -0
  307. package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +193 -0
  308. package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +193 -0
  309. package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +193 -0
  310. package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +193 -0
  311. package/src/tool/widmark-alcohol-simulator/index.ts +11 -0
  312. package/src/tool/widmark-alcohol-simulator/logic.ts +97 -0
  313. package/src/tool/widmark-alcohol-simulator/seo.astro +15 -0
  314. package/src/tool/widmark-alcohol-simulator/widmark-alcohol-simulator.css +386 -0
  315. package/src/tools.ts +27 -0
  316. package/src/types.ts +70 -0
@@ -0,0 +1,294 @@
1
+ ---
2
+ import './gsr-dispersion-calculator.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <section class="gsr-shell" data-gsr-calculator>
12
+ <div class="gsr-card">
13
+ <div class="gsr-visual">
14
+ <div class="gsr-target-head">
15
+ <span>{ui.target}</span>
16
+ <strong id="gsr-radius">-</strong>
17
+ </div>
18
+ <canvas id="gsr-canvas" width="720" height="720" aria-label={ui.canvasAria}></canvas>
19
+ </div>
20
+
21
+ <aside class="gsr-controls">
22
+ <div class="gsr-unit-toggle" role="group" aria-label={ui.units}>
23
+ <button type="button" data-gsr-unit="metric" data-active="true">{ui.metric}</button>
24
+ <button type="button" data-gsr-unit="imperial" data-active="false">{ui.imperial}</button>
25
+ </div>
26
+
27
+ <label class="gsr-slider">
28
+ <span>{ui.distance}</span>
29
+ <strong id="gsr-distance-label">-</strong>
30
+ <input id="gsr-distance" type="range" min="0" max="300" value="75" />
31
+ </label>
32
+
33
+ <label class="gsr-select">
34
+ <span>{ui.ammo}</span>
35
+ <select id="gsr-ammo">
36
+ <option value="pistol9mm">{ui.ammoPistol9mm}</option>
37
+ <option value="revolver38">{ui.ammoRevolver38}</option>
38
+ <option value="shotgun12">{ui.ammoShotgun12}</option>
39
+ </select>
40
+ </label>
41
+
42
+ <div class="gsr-cone" aria-label={ui.cone}>
43
+ <span class="gsr-muzzle"></span>
44
+ <span class="gsr-cone-beam" id="gsr-cone-beam"></span>
45
+ <span class="gsr-plane"></span>
46
+ </div>
47
+
48
+ <article class="gsr-reading">
49
+ <small>{ui.category}</small>
50
+ <h3 id="gsr-category">-</h3>
51
+ <p id="gsr-expected">-</p>
52
+ </article>
53
+
54
+ <div class="gsr-metrics">
55
+ <div>
56
+ <span>{ui.density}</span>
57
+ <strong id="gsr-density">-</strong>
58
+ </div>
59
+ <div>
60
+ <span>{ui.thermal}</span>
61
+ <strong id="gsr-thermal">-</strong>
62
+ </div>
63
+ </div>
64
+ </aside>
65
+
66
+ <nav class="gsr-range-strip" aria-label={ui.rangeStripAria}>
67
+ <span data-gsr-range="contact">{ui.contact}</span>
68
+ <span data-gsr-range="near-contact">{ui.near}</span>
69
+ <span data-gsr-range="close-range">{ui.close}</span>
70
+ <span data-gsr-range="distant">{ui.distant}</span>
71
+ </nav>
72
+
73
+ <p class="gsr-disclaimer">{ui.disclaimer}</p>
74
+ </div>
75
+ </section>
76
+
77
+ <script id="gsr-ui" type="application/json" set:html={JSON.stringify(ui)}></script>
78
+ <script>
79
+ import { GsrDispersionModel } from './logic';
80
+ import type { AmmoType, GsrParticle } from './logic';
81
+
82
+ const model = new GsrDispersionModel();
83
+ const ui = JSON.parse(document.getElementById('gsr-ui')?.textContent || '{}');
84
+ const STORAGE_KEY = 'gsr-dispersion-calculator-state';
85
+ const canvas = document.getElementById('gsr-canvas') as HTMLCanvasElement | null;
86
+ const distanceInput = document.getElementById('gsr-distance') as HTMLInputElement | null;
87
+ const ammoSelect = document.getElementById('gsr-ammo') as HTMLSelectElement | null;
88
+ const context = canvas?.getContext('2d');
89
+ let unitSystem: 'metric' | 'imperial' = 'metric';
90
+
91
+ function getParticleColors(isDark: boolean): Record<GsrParticle['element'], string> {
92
+ if (isDark) {
93
+ return {
94
+ lead: '156, 188, 220',
95
+ barium: '104, 224, 180',
96
+ antimony: '255, 184, 92',
97
+ soot: '246, 236, 214',
98
+ };
99
+ }
100
+
101
+ return {
102
+ lead: '108, 126, 146',
103
+ barium: '88, 160, 124',
104
+ antimony: '205, 132, 58',
105
+ soot: '62, 58, 52',
106
+ };
107
+ }
108
+
109
+ function setText(id: string, value: string): void {
110
+ const element = document.getElementById(id);
111
+ if (element) element.textContent = value;
112
+ }
113
+
114
+ function updateUnitButtons(): void {
115
+ document.querySelectorAll<HTMLButtonElement>('[data-gsr-unit]').forEach((item) => {
116
+ item.dataset.active = item.dataset.gsrUnit === unitSystem ? 'true' : 'false';
117
+ });
118
+ }
119
+
120
+ function saveState(): void {
121
+ const distanceCm = Number(distanceInput?.value ?? 75);
122
+ const ammo = ammoSelect?.value ?? 'pistol9mm';
123
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
124
+ distanceCm,
125
+ ammo,
126
+ unitSystem,
127
+ }));
128
+ }
129
+
130
+ function parseSavedState(): { distanceCm?: number; ammo?: string; unitSystem?: 'metric' | 'imperial' } | null {
131
+ try {
132
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as {
133
+ distanceCm?: number;
134
+ ammo?: string;
135
+ unitSystem?: 'metric' | 'imperial';
136
+ };
137
+ } catch {
138
+ localStorage.removeItem(STORAGE_KEY);
139
+ return null;
140
+ }
141
+ }
142
+
143
+ function applySavedDistance(distanceCm?: number): void {
144
+ if (typeof distanceCm === 'number' && distanceInput) {
145
+ distanceInput.value = String(Math.max(0, Math.min(300, distanceCm)));
146
+ }
147
+ }
148
+
149
+ function applySavedAmmo(ammo?: string): void {
150
+ if ((ammo === 'pistol9mm' || ammo === 'revolver38' || ammo === 'shotgun12') && ammoSelect) {
151
+ ammoSelect.value = ammo;
152
+ }
153
+ }
154
+
155
+ function applySavedUnitSystem(savedUnitSystem?: 'metric' | 'imperial'): void {
156
+ if (savedUnitSystem === 'metric' || savedUnitSystem === 'imperial') {
157
+ unitSystem = savedUnitSystem;
158
+ }
159
+ }
160
+
161
+ function restoreState(): void {
162
+ const saved = parseSavedState();
163
+ applySavedDistance(saved?.distanceCm);
164
+ applySavedAmmo(saved?.ammo);
165
+ applySavedUnitSystem(saved?.unitSystem);
166
+ updateUnitButtons();
167
+ }
168
+
169
+ function formatLength(cm: number): string {
170
+ if (unitSystem === 'metric') {
171
+ return cm >= 100 ? `${(cm / 100).toFixed(2)} ${ui.unitMeter}` : `${Math.round(cm)} ${ui.unitCentimeter}`;
172
+ }
173
+ const inches = cm / 2.54;
174
+ return inches >= 12 ? `${(inches / 12).toFixed(2)} ${ui.unitFoot}` : `${inches.toFixed(1)} ${ui.unitInch}`;
175
+ }
176
+
177
+ function getCategoryLabel(category: 'contact' | 'near-contact' | 'close-range' | 'distant'): string {
178
+ if (category === 'contact') return ui.categoryContactLabel;
179
+ if (category === 'near-contact') return ui.categoryNearContactLabel;
180
+ if (category === 'close-range') return ui.categoryCloseRangeLabel;
181
+ if (category === 'distant') return ui.categoryDistantLabel;
182
+ return '';
183
+ }
184
+
185
+ function getExpectedFinding(category: 'contact' | 'near-contact' | 'close-range' | 'distant'): string {
186
+ if (category === 'contact') return ui.expectedContact;
187
+ if (category === 'near-contact') return ui.expectedNearContact;
188
+ if (category === 'close-range') return ui.expectedCloseRange;
189
+ return ui.expectedDistant;
190
+ }
191
+
192
+ function drawCloth(): void {
193
+ if (!canvas || !context) return;
194
+ const { width, height } = canvas;
195
+ const isDark = document.documentElement.classList.contains('theme-dark');
196
+ const gradient = context.createRadialGradient(width * 0.5, height * 0.42, 20, width * 0.5, height * 0.5, width * 0.65);
197
+ gradient.addColorStop(0, isDark ? '#5a5241' : '#f7f0df');
198
+ gradient.addColorStop(1, isDark ? '#312c25' : '#d6ccb8');
199
+ context.fillStyle = gradient;
200
+ context.fillRect(0, 0, width, height);
201
+ context.strokeStyle = isDark ? 'rgba(244, 234, 214, 0.12)' : 'rgba(78, 72, 59, 0.11)';
202
+ context.lineWidth = 1;
203
+ for (let index = -width; index < width * 2; index += 24) {
204
+ context.beginPath();
205
+ context.moveTo(index, 0);
206
+ context.lineTo(index + width, height);
207
+ context.stroke();
208
+ context.beginPath();
209
+ context.moveTo(index + width, 0);
210
+ context.lineTo(index, height);
211
+ context.stroke();
212
+ }
213
+ }
214
+
215
+ function drawThermalHalo(center: number, thermalScore: number, isDark: boolean): void {
216
+ if (!context || thermalScore <= 0) return;
217
+ const burn = context.createRadialGradient(center, center, 4, center, center, 95 + thermalScore * 0.7);
218
+ burn.addColorStop(0, isDark ? `rgba(255, 206, 128, ${0.18 + thermalScore / 320})` : `rgba(96, 58, 24, ${0.18 + thermalScore / 340})`);
219
+ burn.addColorStop(0.45, isDark ? `rgba(255, 156, 76, ${thermalScore / 420})` : `rgba(188, 120, 52, ${thermalScore / 520})`);
220
+ burn.addColorStop(1, isDark ? 'rgba(255, 156, 76, 0)' : 'rgba(188, 120, 52, 0)');
221
+ context.fillStyle = burn;
222
+ context.beginPath();
223
+ context.arc(center, center, 120 + thermalScore * 0.65, 0, Math.PI * 2);
224
+ context.fill();
225
+ }
226
+
227
+ function drawParticleCloud(particles: GsrParticle[], scale: number, center: number, isDark: boolean): void {
228
+ if (!context) return;
229
+ const colors = getParticleColors(isDark);
230
+ const opacityBoost = isDark ? 0.16 : 0.1;
231
+ const radiusBoost = isDark ? 0.35 : 0.18;
232
+
233
+ for (const particle of particles) {
234
+ context.fillStyle = `rgba(${colors[particle.element]}, ${Math.min(1, particle.opacity + opacityBoost)})`;
235
+ context.beginPath();
236
+ context.arc(center + particle.x * scale, center + particle.y * scale, particle.radius + radiusBoost, 0, Math.PI * 2);
237
+ context.fill();
238
+ }
239
+ }
240
+
241
+ function drawPatternRadius(center: number, patternRadiusCm: number, scale: number, isDark: boolean): void {
242
+ if (!context) return;
243
+ context.strokeStyle = isDark ? 'rgba(242, 240, 232, 0.62)' : 'rgba(90, 96, 101, 0.55)';
244
+ context.lineWidth = isDark ? 3.4 : 3.2;
245
+ context.setLineDash(isDark ? [12, 9] : [11, 9]);
246
+ context.beginPath();
247
+ context.arc(center, center, Math.min(300, patternRadiusCm * scale), 0, Math.PI * 2);
248
+ context.stroke();
249
+ context.setLineDash([]);
250
+ }
251
+
252
+ function drawParticles(particles: GsrParticle[], patternRadiusCm: number, thermalScore: number): void {
253
+ if (!canvas || !context) return;
254
+ const isDark = document.documentElement.classList.contains('theme-dark');
255
+ const center = canvas.width / 2;
256
+ const scale = Math.min(5.8, 250 / Math.max(patternRadiusCm, 16));
257
+ drawThermalHalo(center, thermalScore, isDark);
258
+ drawParticleCloud(particles, scale, center, isDark);
259
+ drawPatternRadius(center, patternRadiusCm, scale, isDark);
260
+ }
261
+
262
+ function render(): void {
263
+ const distanceCm = Number(distanceInput?.value ?? 75);
264
+ const ammo = (ammoSelect?.value ?? 'pistol9mm') as AmmoType;
265
+ const result = model.simulate(distanceCm, ammo);
266
+ drawCloth();
267
+ drawParticles(result.particles, result.patternRadiusCm, result.thermalScore);
268
+ setText('gsr-distance-label', formatLength(distanceCm));
269
+ setText('gsr-category', getCategoryLabel(result.category));
270
+ setText('gsr-expected', getExpectedFinding(result.category));
271
+ setText('gsr-density', `${result.densityScore}%`);
272
+ setText('gsr-thermal', `${result.thermalScore}%`);
273
+ setText('gsr-radius', `${formatLength(result.patternRadiusCm)} ${ui.radiusSuffix}`);
274
+ document.querySelectorAll<HTMLElement>('[data-gsr-range]').forEach((item) => {
275
+ item.dataset.active = item.dataset.gsrRange === result.category ? 'true' : 'false';
276
+ });
277
+ const beam = document.getElementById('gsr-cone-beam') as HTMLElement | null;
278
+ if (beam) beam.style.transform = `scaleY(${0.35 + distanceCm / 260})`;
279
+ saveState();
280
+ }
281
+
282
+ distanceInput?.addEventListener('input', render);
283
+ ammoSelect?.addEventListener('change', render);
284
+ new MutationObserver(render).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
285
+ document.querySelectorAll<HTMLButtonElement>('[data-gsr-unit]').forEach((button) => {
286
+ button.addEventListener('click', () => {
287
+ unitSystem = button.dataset.gsrUnit === 'imperial' ? 'imperial' : 'metric';
288
+ updateUnitButtons();
289
+ render();
290
+ });
291
+ });
292
+ restoreState();
293
+ render();
294
+ </script>
@@ -0,0 +1,32 @@
1
+ import type { ScienceToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export interface GsrDispersionUI {
4
+ [key: string]: string;
5
+ }
6
+
7
+ export type GsrDispersionLocaleContent = ToolLocaleContent<GsrDispersionUI>;
8
+
9
+ export const gsrDispersionCalculator: ScienceToolEntry<GsrDispersionUI> = {
10
+ id: 'gsr-dispersion-calculator',
11
+ icons: {
12
+ bg: 'mdi:target',
13
+ fg: 'mdi:chart-scatter-plot',
14
+ },
15
+ i18n: {
16
+ de: () => import('./i18n/de').then((m) => m.content),
17
+ en: () => import('./i18n/en').then((m) => m.content),
18
+ es: () => import('./i18n/es').then((m) => m.content),
19
+ fr: () => import('./i18n/fr').then((m) => m.content),
20
+ id: () => import('./i18n/id').then((m) => m.content),
21
+ it: () => import('./i18n/it').then((m) => m.content),
22
+ ja: () => import('./i18n/ja').then((m) => m.content),
23
+ ko: () => import('./i18n/ko').then((m) => m.content),
24
+ nl: () => import('./i18n/nl').then((m) => m.content),
25
+ pl: () => import('./i18n/pl').then((m) => m.content),
26
+ pt: () => import('./i18n/pt').then((m) => m.content),
27
+ ru: () => import('./i18n/ru').then((m) => m.content),
28
+ sv: () => import('./i18n/sv').then((m) => m.content),
29
+ tr: () => import('./i18n/tr').then((m) => m.content),
30
+ zh: () => import('./i18n/zh').then((m) => m.content),
31
+ },
32
+ };
@@ -0,0 +1,305 @@
1
+ .gsr-shell {
2
+ --ink: #172229;
3
+ --muted: #53616b;
4
+ --paper: #f7f2e4;
5
+ --steel: #7d8790;
6
+ --signal: #d68e45;
7
+ --green: #61ab81;
8
+ --card: #fff;
9
+ --panel: #f9faf7;
10
+ --soft: #f5f7f6;
11
+ --cloth: #ece4d2;
12
+ --range-bg: #eef1ee;
13
+ --border: rgba(23, 34, 41, 0.12);
14
+ --border-soft: rgba(23, 34, 41, 0.1);
15
+ --shadow: rgba(23, 34, 41, 0.08);
16
+
17
+ color: var(--ink);
18
+ margin: 0 auto;
19
+ padding: 0.75rem;
20
+ width: 100%;
21
+ }
22
+
23
+ .gsr-card {
24
+ background: var(--card);
25
+ border: 1px solid var(--border);
26
+ border-radius: 8px;
27
+ box-shadow: 0 18px 44px var(--shadow);
28
+ display: grid;
29
+ gap: 0.75rem;
30
+ overflow: hidden;
31
+ padding: 0.75rem;
32
+ }
33
+
34
+ .gsr-visual {
35
+ background: var(--cloth);
36
+ border: 1px solid var(--border-soft);
37
+ border-radius: 8px;
38
+ overflow: hidden;
39
+ }
40
+
41
+ .gsr-target-head {
42
+ align-items: center;
43
+ display: flex;
44
+ justify-content: space-between;
45
+ padding: 0.85rem 1rem;
46
+ }
47
+
48
+ .gsr-target-head span,
49
+ .gsr-target-head strong {
50
+ font-size: 0.82rem;
51
+ font-weight: 800;
52
+ text-transform: uppercase;
53
+ }
54
+
55
+ .gsr-target-head strong {
56
+ color: var(--muted);
57
+ }
58
+
59
+ #gsr-canvas {
60
+ aspect-ratio: 1;
61
+ display: block;
62
+ height: auto;
63
+ object-fit: contain;
64
+ width: 100%;
65
+ }
66
+
67
+ .gsr-controls {
68
+ background: var(--panel);
69
+ border: 1px solid var(--border-soft);
70
+ border-radius: 8px;
71
+ display: grid;
72
+ gap: 0.75rem;
73
+ padding: 0.75rem;
74
+ }
75
+
76
+ .gsr-slider,
77
+ .gsr-select {
78
+ display: grid;
79
+ gap: 0.55rem;
80
+ }
81
+
82
+ .gsr-unit-toggle {
83
+ background: var(--soft);
84
+ border: 1px solid var(--border-soft);
85
+ border-radius: 8px;
86
+ display: grid;
87
+ gap: 0.25rem;
88
+ grid-template-columns: repeat(2, minmax(0, 1fr));
89
+ padding: 0.25rem;
90
+ }
91
+
92
+ .gsr-unit-toggle button {
93
+ background: transparent;
94
+ border: 0;
95
+ border-radius: 6px;
96
+ color: var(--muted);
97
+ cursor: pointer;
98
+ font-size: 0.82rem;
99
+ font-weight: 800;
100
+ line-height: inherit;
101
+ min-height: 36px;
102
+ text-transform: uppercase;
103
+ }
104
+
105
+ .gsr-unit-toggle button[data-active="true"] {
106
+ background: var(--card);
107
+ box-shadow: 0 8px 18px var(--shadow);
108
+ color: var(--ink);
109
+ }
110
+
111
+ .gsr-slider span,
112
+ .gsr-select span,
113
+ .gsr-reading small,
114
+ .gsr-metrics span {
115
+ color: var(--muted);
116
+ font-size: 0.78rem;
117
+ font-weight: 800;
118
+ text-transform: uppercase;
119
+ }
120
+
121
+ .gsr-slider strong {
122
+ font-size: 1.85rem;
123
+ line-height: 1;
124
+ }
125
+
126
+ .gsr-slider input {
127
+ accent-color: var(--signal);
128
+ width: 100%;
129
+ }
130
+
131
+ .gsr-select select {
132
+ background: var(--soft);
133
+ border: 1px solid var(--border);
134
+ border-radius: 6px;
135
+ color: var(--ink);
136
+ font-size: inherit;
137
+ font-weight: inherit;
138
+ line-height: inherit;
139
+ min-height: 44px;
140
+ padding: 0.65rem 0.75rem;
141
+ }
142
+
143
+ .gsr-cone {
144
+ align-items: center;
145
+ background: linear-gradient(90deg, #101820, #25353d);
146
+ border-radius: 8px;
147
+ display: grid;
148
+ grid-template-columns: 22px 1fr 8px;
149
+ min-height: 86px;
150
+ overflow: hidden;
151
+ padding: 0.75rem;
152
+ position: relative;
153
+ }
154
+
155
+ .gsr-muzzle {
156
+ background: #cfd5d7;
157
+ border-radius: 3px;
158
+ height: 42px;
159
+ width: 18px;
160
+ z-index: 2;
161
+ }
162
+
163
+ .gsr-cone-beam {
164
+ background: linear-gradient(90deg, rgba(214, 142, 69, 0.9), rgba(97, 171, 129, 0.08));
165
+ clip-path: polygon(0 42%, 100% 0, 100% 100%, 0 58%);
166
+ height: 64px;
167
+ transform-origin: left center;
168
+ transition: transform 180ms ease;
169
+ }
170
+
171
+ .gsr-plane {
172
+ background: var(--paper);
173
+ height: 64px;
174
+ opacity: 0.9;
175
+ width: 8px;
176
+ }
177
+
178
+ .gsr-reading {
179
+ border-left: 5px solid var(--signal);
180
+ display: grid;
181
+ gap: 0.45rem;
182
+ padding-left: 0.9rem;
183
+ }
184
+
185
+ .gsr-reading h3 {
186
+ font-size: 1.08rem;
187
+ margin: 0;
188
+ }
189
+
190
+ .gsr-reading p,
191
+ .gsr-disclaimer {
192
+ color: var(--muted);
193
+ font-size: 0.92rem;
194
+ line-height: 1.45;
195
+ margin: 0;
196
+ }
197
+
198
+ .gsr-metrics {
199
+ display: grid;
200
+ gap: 0.75rem;
201
+ grid-template-columns: repeat(2, minmax(0, 1fr));
202
+ }
203
+
204
+ .gsr-metrics div {
205
+ background: var(--soft);
206
+ border-radius: 8px;
207
+ display: grid;
208
+ gap: 0.35rem;
209
+ min-height: 72px;
210
+ padding: 0.7rem;
211
+ }
212
+
213
+ .gsr-metrics strong {
214
+ font-size: 1.55rem;
215
+ }
216
+
217
+ .gsr-range-strip {
218
+ display: grid;
219
+ gap: 0.4rem;
220
+ grid-template-columns: repeat(2, minmax(0, 1fr));
221
+ }
222
+
223
+ .gsr-range-strip span {
224
+ background: var(--range-bg);
225
+ border-radius: 6px;
226
+ color: var(--muted);
227
+ font-size: 0.78rem;
228
+ font-weight: 800;
229
+ padding: 0.65rem;
230
+ text-align: center;
231
+ text-transform: uppercase;
232
+ transition: background 160ms ease, color 160ms ease, transform 160ms ease;
233
+ }
234
+
235
+ .gsr-range-strip span[data-active="true"] {
236
+ background: var(--ink);
237
+ color: #fff;
238
+ transform: translateY(-1px);
239
+ }
240
+
241
+ .gsr-range-strip span:nth-child(2)[data-active="true"] {
242
+ background: var(--signal);
243
+ }
244
+
245
+ .gsr-range-strip span:nth-child(3)[data-active="true"] {
246
+ background: var(--green);
247
+ }
248
+
249
+ .gsr-range-strip span:nth-child(4)[data-active="true"] {
250
+ background: var(--steel);
251
+ }
252
+
253
+ .gsr-disclaimer {
254
+ border-top: 1px solid var(--border-soft);
255
+ padding-top: 0.75rem;
256
+ }
257
+
258
+ .theme-dark .gsr-shell {
259
+ --ink: #eef4f2;
260
+ --muted: #a9b6b7;
261
+ --paper: #ded2b5;
262
+ --steel: #82909b;
263
+ --signal: #f0a35a;
264
+ --green: #77c696;
265
+ --card: #12191d;
266
+ --panel: #172126;
267
+ --soft: #202b31;
268
+ --cloth: #383329;
269
+ --range-bg: #202b31;
270
+ --border: rgba(238, 244, 242, 0.14);
271
+ --border-soft: rgba(238, 244, 242, 0.1);
272
+ --shadow: rgba(0, 0, 0, 0.32);
273
+ }
274
+
275
+ .theme-dark .gsr-cone {
276
+ background: linear-gradient(90deg, #070b0d, #142127);
277
+ }
278
+
279
+ .theme-dark .gsr-muzzle {
280
+ background: #d7dedc;
281
+ }
282
+
283
+ .theme-dark .gsr-plane {
284
+ opacity: 0.82;
285
+ }
286
+
287
+ @media (min-width: 860px) {
288
+ .gsr-shell {
289
+ padding: 1rem;
290
+ }
291
+
292
+ .gsr-card {
293
+ align-items: stretch;
294
+ grid-template-columns: minmax(420px, 1fr) minmax(300px, 0.34fr);
295
+ }
296
+
297
+ .gsr-range-strip {
298
+ grid-column: 1 / -1;
299
+ grid-template-columns: repeat(4, minmax(0, 1fr));
300
+ }
301
+
302
+ .gsr-disclaimer {
303
+ grid-column: 1 / -1;
304
+ }
305
+ }