@jjlmoya/utils-forensic-science 1.12.0 → 1.14.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 (169) hide show
  1. package/package.json +2 -1
  2. package/src/category/index.ts +63 -59
  3. package/src/category/seo.astro +1 -1
  4. package/src/components/PreviewNavSidebar.astro +1 -1
  5. package/src/components/PreviewToolbar.astro +1 -1
  6. package/src/data.ts +1 -1
  7. package/src/entries.ts +74 -68
  8. package/src/env.d.ts +1 -1
  9. package/src/index.ts +33 -31
  10. package/src/pages/[locale]/[slug].astro +161 -159
  11. package/src/pages/[locale].astro +8 -5
  12. package/src/pages/index.astro +1 -1
  13. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  14. package/src/tests/diacritics_density.test.ts +1 -1
  15. package/src/tests/faq_count.test.ts +19 -19
  16. package/src/tests/i18n_coverage.test.ts +36 -36
  17. package/src/tests/inverted_punctuation.test.ts +1 -1
  18. package/src/tests/locale_completeness.test.ts +2 -2
  19. package/src/tests/mocks/astro_mock.js +1 -1
  20. package/src/tests/no_h1_in_components.test.ts +1 -1
  21. package/src/tests/script_density.test.ts +94 -94
  22. package/src/tests/seo_length.test.ts +46 -46
  23. package/src/tests/seo_parity.test.ts +2 -7
  24. package/src/tests/seo_translation_completeness.test.ts +68 -0
  25. package/src/tests/seo_wellformed_export.test.ts +65 -0
  26. package/src/tests/tool_validation.test.ts +2 -2
  27. package/src/tests/translation_copy.test.ts +23 -24
  28. package/src/tool/bloodstain-pattern-origin-analyzer/component.astro +9 -9
  29. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/zh.ts +1 -1
  30. package/src/tool/dna-profile-match-probability-lab/bibliography.astro +14 -0
  31. package/src/tool/dna-profile-match-probability-lab/bibliography.ts +16 -0
  32. package/src/tool/dna-profile-match-probability-lab/component.astro +121 -0
  33. package/src/tool/dna-profile-match-probability-lab/controller.ts +159 -0
  34. package/src/tool/dna-profile-match-probability-lab/dna-profile-match-probability-lab.css +485 -0
  35. package/src/tool/dna-profile-match-probability-lab/dom-views.ts +93 -0
  36. package/src/tool/dna-profile-match-probability-lab/entry.ts +29 -0
  37. package/src/tool/dna-profile-match-probability-lab/evaluator.ts +19 -0
  38. package/src/tool/dna-profile-match-probability-lab/i18n/de.ts +43 -0
  39. package/src/tool/dna-profile-match-probability-lab/i18n/en.ts +196 -0
  40. package/src/tool/dna-profile-match-probability-lab/i18n/es.ts +40 -0
  41. package/src/tool/dna-profile-match-probability-lab/i18n/fr.ts +10 -0
  42. package/src/tool/dna-profile-match-probability-lab/i18n/id.ts +41 -0
  43. package/src/tool/dna-profile-match-probability-lab/i18n/it.ts +10 -0
  44. package/src/tool/dna-profile-match-probability-lab/i18n/ja.ts +40 -0
  45. package/src/tool/dna-profile-match-probability-lab/i18n/ko.ts +40 -0
  46. package/src/tool/dna-profile-match-probability-lab/i18n/nl.ts +29 -0
  47. package/src/tool/dna-profile-match-probability-lab/i18n/pl.ts +10 -0
  48. package/src/tool/dna-profile-match-probability-lab/i18n/pt.ts +41 -0
  49. package/src/tool/dna-profile-match-probability-lab/i18n/ru.ts +29 -0
  50. package/src/tool/dna-profile-match-probability-lab/i18n/sv.ts +41 -0
  51. package/src/tool/dna-profile-match-probability-lab/i18n/tr.ts +10 -0
  52. package/src/tool/dna-profile-match-probability-lab/i18n/zh.ts +29 -0
  53. package/src/tool/dna-profile-match-probability-lab/index.ts +11 -0
  54. package/src/tool/dna-profile-match-probability-lab/localize.ts +107 -0
  55. package/src/tool/dna-profile-match-probability-lab/logic.test.ts +56 -0
  56. package/src/tool/dna-profile-match-probability-lab/logic.ts +139 -0
  57. package/src/tool/dna-profile-match-probability-lab/seo.astro +15 -0
  58. package/src/tool/dna-profile-match-probability-lab/storage.ts +26 -0
  59. package/src/tool/dna-profile-match-probability-lab/ui.ts +3 -0
  60. package/src/tool/fire-pattern-origin-analyzer/component.astro +1 -1
  61. package/src/tool/fire-pattern-origin-analyzer/i18n/zh.ts +5 -1
  62. package/src/tool/fire-pattern-origin-analyzer/logic.ts +9 -2
  63. package/src/tool/fire-pattern-origin-analyzer/view-bindings.ts +8 -4
  64. package/src/tool/fire-pattern-origin-analyzer/view-model.ts +6 -2
  65. package/src/tool/forensic-age-estimator/bibliography.astro +1 -1
  66. package/src/tool/forensic-age-estimator/component.astro +1 -1
  67. package/src/tool/forensic-age-estimator/entry.ts +17 -17
  68. package/src/tool/forensic-age-estimator/i18n/de.ts +244 -244
  69. package/src/tool/forensic-age-estimator/i18n/en.ts +244 -244
  70. package/src/tool/forensic-age-estimator/i18n/es.ts +244 -244
  71. package/src/tool/forensic-age-estimator/i18n/fr.ts +244 -244
  72. package/src/tool/forensic-age-estimator/i18n/id.ts +244 -244
  73. package/src/tool/forensic-age-estimator/i18n/it.ts +244 -244
  74. package/src/tool/forensic-age-estimator/i18n/nl.ts +244 -244
  75. package/src/tool/forensic-age-estimator/i18n/pl.ts +244 -244
  76. package/src/tool/forensic-age-estimator/i18n/pt.ts +244 -244
  77. package/src/tool/forensic-age-estimator/i18n/ru.ts +244 -244
  78. package/src/tool/forensic-age-estimator/i18n/sv.ts +244 -244
  79. package/src/tool/forensic-age-estimator/i18n/tr.ts +244 -244
  80. package/src/tool/forensic-age-estimator/i18n/zh.ts +245 -245
  81. package/src/tool/forensic-age-estimator/index.ts +1 -1
  82. package/src/tool/forensic-age-estimator/seo.astro +1 -1
  83. package/src/tool/forensic-blood-test-simulator/component.astro +1 -1
  84. package/src/tool/forensic-fiber-comparison-microscope/component.astro +1 -1
  85. package/src/tool/forensic-fiber-comparison-microscope/render.ts +1 -1
  86. package/src/tool/forensic-fiber-comparison-microscope/view.ts +1 -1
  87. package/src/tool/forensic-fingerprint-minutiae-identifier/component.astro +2 -2
  88. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ja.ts +2 -1
  89. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/zh.ts +2 -0
  90. package/src/tool/forensic-fingerprint-minutiae-identifier/renderer.ts +9 -8
  91. package/src/tool/forensic-glass-becke-line-simulator/component.astro +1 -1
  92. package/src/tool/forensic-glass-becke-line-simulator/view.ts +8 -6
  93. package/src/tool/forensic-image-authenticity-analyzer/component.astro +1 -1
  94. package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -105
  95. package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -105
  96. package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -105
  97. package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -105
  98. package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +95 -90
  99. package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -105
  100. package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +7 -2
  101. package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +4 -1
  102. package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +95 -92
  103. package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +95 -92
  104. package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -105
  105. package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +97 -92
  106. package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +97 -93
  107. package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +98 -93
  108. package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +11 -0
  109. package/src/tool/forensic-microcrystal-drug-simulator/component.astro +1 -1
  110. package/src/tool/forensic-sex-determinator/component.astro +3 -2
  111. package/src/tool/forensic-stature-estimator/component.astro +1 -1
  112. package/src/tool/forensic-stature-estimator/i18n/id.ts +4 -0
  113. package/src/tool/forensic-stature-estimator/i18n/nl.ts +4 -0
  114. package/src/tool/forensic-stature-estimator/i18n/ru.ts +4 -0
  115. package/src/tool/forensic-tlc-ink-simulator/component.astro +1 -1
  116. package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +1 -0
  117. package/src/tool/forensic-toolmark-striation-matcher/component.astro +1 -1
  118. package/src/tool/forensic-toolmark-striation-matcher/logic.ts +1 -1
  119. package/src/tool/forensic-toolmark-striation-matcher/renderer.ts +7 -6
  120. package/src/tool/forensic-toolmark-striation-matcher/view.ts +19 -20
  121. package/src/tool/gsr-dispersion-calculator/component.astro +1 -1
  122. package/src/tool/gsr-dispersion-calculator/logic.ts +1 -1
  123. package/src/tool/time-of-death-algor-mortis-calculator/entry.ts +2 -0
  124. package/src/tool/voice-spectrogram-analyzer/audio-runtime.ts +75 -0
  125. package/src/tool/voice-spectrogram-analyzer/bibliography.astro +14 -0
  126. package/src/tool/voice-spectrogram-analyzer/bibliography.ts +16 -0
  127. package/src/tool/voice-spectrogram-analyzer/component.astro +116 -0
  128. package/src/tool/voice-spectrogram-analyzer/controller.ts +219 -0
  129. package/src/tool/voice-spectrogram-analyzer/dom-views.ts +204 -0
  130. package/src/tool/voice-spectrogram-analyzer/entry.ts +31 -0
  131. package/src/tool/voice-spectrogram-analyzer/evaluator.ts +13 -0
  132. package/src/tool/voice-spectrogram-analyzer/fft.ts +64 -0
  133. package/src/tool/voice-spectrogram-analyzer/i18n/de.ts +135 -0
  134. package/src/tool/voice-spectrogram-analyzer/i18n/en.ts +122 -0
  135. package/src/tool/voice-spectrogram-analyzer/i18n/es.ts +122 -0
  136. package/src/tool/voice-spectrogram-analyzer/i18n/fr.ts +135 -0
  137. package/src/tool/voice-spectrogram-analyzer/i18n/id.ts +136 -0
  138. package/src/tool/voice-spectrogram-analyzer/i18n/it.ts +135 -0
  139. package/src/tool/voice-spectrogram-analyzer/i18n/ja.ts +135 -0
  140. package/src/tool/voice-spectrogram-analyzer/i18n/ko.ts +135 -0
  141. package/src/tool/voice-spectrogram-analyzer/i18n/nl.ts +136 -0
  142. package/src/tool/voice-spectrogram-analyzer/i18n/pl.ts +136 -0
  143. package/src/tool/voice-spectrogram-analyzer/i18n/pt.ts +135 -0
  144. package/src/tool/voice-spectrogram-analyzer/i18n/ru.ts +136 -0
  145. package/src/tool/voice-spectrogram-analyzer/i18n/sv.ts +136 -0
  146. package/src/tool/voice-spectrogram-analyzer/i18n/tr.ts +136 -0
  147. package/src/tool/voice-spectrogram-analyzer/i18n/zh.ts +135 -0
  148. package/src/tool/voice-spectrogram-analyzer/index.ts +11 -0
  149. package/src/tool/voice-spectrogram-analyzer/logic.test.ts +46 -0
  150. package/src/tool/voice-spectrogram-analyzer/logic.ts +163 -0
  151. package/src/tool/voice-spectrogram-analyzer/seo.astro +15 -0
  152. package/src/tool/voice-spectrogram-analyzer/storage.ts +33 -0
  153. package/src/tool/voice-spectrogram-analyzer/ui.ts +49 -0
  154. package/src/tool/voice-spectrogram-analyzer/voice-spectrogram-analyzer.css +547 -0
  155. package/src/tool/widmark-alcohol-simulator/component.astro +1 -1
  156. package/src/tool/widmark-alcohol-simulator/i18n/de.ts +5 -0
  157. package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +2 -1
  158. package/src/tool/widmark-alcohol-simulator/i18n/id.ts +1 -0
  159. package/src/tool/widmark-alcohol-simulator/i18n/it.ts +4 -1
  160. package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +4 -1
  161. package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +5 -1
  162. package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +5 -1
  163. package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +5 -1
  164. package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +4 -1
  165. package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +5 -1
  166. package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +4 -1
  167. package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +4 -1
  168. package/src/tools.ts +39 -35
  169. package/src/types.ts +4 -4
@@ -0,0 +1,33 @@
1
+ export type SpectrogramView = 'mirror' | 'split';
2
+
3
+ export interface SpectrogramSettings {
4
+ ceilingHz: number;
5
+ view: SpectrogramView;
6
+ }
7
+
8
+ const STORAGE_KEY = 'voice-spectrogram-analyzer-settings';
9
+ const DEFAULTS: SpectrogramSettings = { ceilingHz: 6000, view: 'mirror' };
10
+
11
+ function isSettings(value: unknown): value is SpectrogramSettings {
12
+ if (!value || typeof value !== 'object') return false;
13
+ const candidate = value as Partial<SpectrogramSettings>;
14
+ return [4000, 6000, 8000].includes(candidate.ceilingHz ?? 0)
15
+ && ['mirror', 'split'].includes(candidate.view ?? '');
16
+ }
17
+
18
+ export function loadSettings(): SpectrogramSettings {
19
+ try {
20
+ const stored = localStorage.getItem(STORAGE_KEY);
21
+ if (!stored) return DEFAULTS;
22
+ const parsed: unknown = JSON.parse(stored);
23
+ return isSettings(parsed) ? parsed : DEFAULTS;
24
+ } catch {
25
+ return DEFAULTS;
26
+ }
27
+ }
28
+
29
+ export function saveSettings(settings: SpectrogramSettings): void {
30
+ try {
31
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
32
+ } catch {}
33
+ }
@@ -0,0 +1,49 @@
1
+ export interface VoiceSpectrogramUI extends Record<string, string> {
2
+ privacyBadge: string;
3
+ privacyNote: string;
4
+ loadHeading: string;
5
+ sampleALabel: string;
6
+ sampleBLabel: string;
7
+ chooseFileLabel: string;
8
+ replaceFileLabel: string;
9
+ dropHint: string;
10
+ presetHint: string;
11
+ presetWarmLabel: string;
12
+ presetBrightLabel: string;
13
+ emptySampleLabel: string;
14
+ readySampleLabel: string;
15
+ decodingSampleLabel: string;
16
+ errorSampleLabel: string;
17
+ durationLabel: string;
18
+ ceilingHeading: string;
19
+ ceilingFourLabel: string;
20
+ ceilingSixLabel: string;
21
+ ceilingEightLabel: string;
22
+ stageLabel: string;
23
+ mirrorViewLabel: string;
24
+ splitViewLabel: string;
25
+ playALabel: string;
26
+ playBLabel: string;
27
+ stopLabel: string;
28
+ timeAxisLabel: string;
29
+ frequencyAxisLabel: string;
30
+ intensityLegendLabel: string;
31
+ formantLegendLabel: string;
32
+ sampleAEmptyCanvasLabel: string;
33
+ sampleBEmptyCanvasLabel: string;
34
+ comparisonHeading: string;
35
+ comparisonNote: string;
36
+ formantOneLabel: string;
37
+ formantTwoLabel: string;
38
+ formantThreeLabel: string;
39
+ averageLabel: string;
40
+ differenceLabel: string;
41
+ unavailableLabel: string;
42
+ statusEmptyLabel: string;
43
+ statusSingleLabel: string;
44
+ statusReadyLabel: string;
45
+ limitError: string;
46
+ decodeError: string;
47
+ browserError: string;
48
+ educationalNote: string;
49
+ }
@@ -0,0 +1,547 @@
1
+ :root {
2
+ --n-va-paper: #f1eee4;
3
+ --n-va-ink: #10242a;
4
+ --n-va-muted: #52676b;
5
+ --n-va-panel: #e4ded0;
6
+ --n-va-panel-soft: #faf8f2;
7
+ --n-va-canvas: #06161c;
8
+ --n-va-grid: #254048;
9
+ --n-va-a: #35d6c5;
10
+ --n-va-b: #ff739e;
11
+ --n-va-a-line: #b8fff6;
12
+ --n-va-b-line: #ffd0df;
13
+ --n-va-cursor: #ffe99a;
14
+ --n-va-border: #a9afa7;
15
+ --n-va-shadow: rgba(10, 28, 33, 0.18);
16
+ --n-va-glow-a: rgba(53, 214, 197, 0.22);
17
+ --n-va-glow-b: rgba(255, 115, 158, 0.18);
18
+ --n-va-white: #fff;
19
+ --n-va-black: #000;
20
+ }
21
+
22
+ .theme-dark {
23
+ --n-va-paper: #111c20;
24
+ --n-va-ink: #eef4ee;
25
+ --n-va-muted: #a7b8b7;
26
+ --n-va-panel: #1c2b2e;
27
+ --n-va-panel-soft: #162326;
28
+ --n-va-canvas: #020a0d;
29
+ --n-va-grid: #1d363d;
30
+ --n-va-a: #43e7d5;
31
+ --n-va-b: #ff82aa;
32
+ --n-va-a-line: #c4fff8;
33
+ --n-va-b-line: #ffd7e3;
34
+ --n-va-cursor: #ffefa4;
35
+ --n-va-border: #42575a;
36
+ --n-va-shadow: rgba(0, 0, 0, 0.42);
37
+ --n-va-glow-a: rgba(67, 231, 213, 0.2);
38
+ --n-va-glow-b: rgba(255, 130, 170, 0.17);
39
+ --n-va-white: #fff;
40
+ --n-va-black: #000;
41
+ }
42
+
43
+ .voice-spectrogram {
44
+ position: relative;
45
+ overflow: hidden;
46
+ color: var(--n-va-ink);
47
+ background:
48
+ radial-gradient(circle at 10% 2%, var(--n-va-glow-a), transparent 28%),
49
+ radial-gradient(circle at 92% 22%, var(--n-va-glow-b), transparent 26%),
50
+ var(--n-va-paper);
51
+ border: 1px solid var(--n-va-border);
52
+ border-radius: 1.8rem;
53
+ box-shadow: 0 1.8rem 4rem var(--n-va-shadow);
54
+ }
55
+
56
+ .voice-spectrogram button,
57
+ .voice-spectrogram input {
58
+ font-size: inherit;
59
+ }
60
+
61
+ .voice-spectrogram button {
62
+ color: var(--n-va-ink);
63
+ }
64
+
65
+ .voice-spectrogram-privacy {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 0.8rem;
69
+ padding: 0.85rem clamp(1rem, 3vw, 2rem);
70
+ color: var(--n-va-muted);
71
+ background: var(--n-va-panel-soft);
72
+ border-bottom: 1px solid var(--n-va-border);
73
+ }
74
+
75
+ .voice-spectrogram-privacy span {
76
+ flex: 0 0 auto;
77
+ padding: 0.28rem 0.58rem;
78
+ color: var(--n-va-canvas);
79
+ background: var(--n-va-a);
80
+ border-radius: 99rem;
81
+ font-size: 0.72rem;
82
+ font-weight: 800;
83
+ letter-spacing: 0.08em;
84
+ text-transform: uppercase;
85
+ }
86
+
87
+ .voice-spectrogram-privacy p,
88
+ .voice-spectrogram-comparison p,
89
+ .voice-spectrogram-educational {
90
+ margin: 0;
91
+ }
92
+
93
+ .voice-spectrogram-load,
94
+ .voice-spectrogram-comparison {
95
+ padding: clamp(1.1rem, 3vw, 2rem);
96
+ }
97
+
98
+ .voice-spectrogram-section-intro,
99
+ .voice-spectrogram-instrument-head {
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: space-between;
103
+ gap: 1rem;
104
+ margin-bottom: 1rem;
105
+ }
106
+
107
+ .voice-spectrogram-kicker {
108
+ margin: 0;
109
+ font-size: clamp(0.92rem, 1.8vw, 1.15rem);
110
+ font-weight: 900;
111
+ letter-spacing: 0.08em;
112
+ text-transform: uppercase;
113
+ }
114
+
115
+ .voice-spectrogram-section-intro > span {
116
+ color: var(--n-va-muted);
117
+ font-size: 0.82rem;
118
+ }
119
+
120
+ .voice-spectrogram-samples {
121
+ display: grid;
122
+ grid-template-columns: repeat(2, minmax(0, 1fr));
123
+ gap: 1rem;
124
+ }
125
+
126
+ .voice-spectrogram-sample {
127
+ position: relative;
128
+ min-width: 0;
129
+ padding: 0.85rem;
130
+ background: var(--n-va-panel-soft);
131
+ border: 1px solid var(--n-va-border);
132
+ border-radius: 1.2rem;
133
+ box-shadow: 0 0.8rem 1.8rem var(--n-va-shadow);
134
+ }
135
+
136
+ .voice-spectrogram-sample::before {
137
+ position: absolute;
138
+ inset: 0.55rem auto 0.55rem 0.55rem;
139
+ width: 0.22rem;
140
+ background: var(--n-va-a);
141
+ border-radius: 99rem;
142
+ content: "";
143
+ }
144
+
145
+ .voice-spectrogram-sample-b::before {
146
+ background: var(--n-va-b);
147
+ }
148
+
149
+ .voice-spectrogram-sample-index {
150
+ position: absolute;
151
+ top: -0.65rem;
152
+ left: 1.35rem;
153
+ padding: 0.22rem 0.55rem;
154
+ color: var(--n-va-canvas);
155
+ background: var(--n-va-a);
156
+ border-radius: 0.4rem;
157
+ font-size: 0.7rem;
158
+ font-weight: 900;
159
+ letter-spacing: 0.08em;
160
+ text-transform: uppercase;
161
+ }
162
+
163
+ .voice-spectrogram-sample-b .voice-spectrogram-sample-index {
164
+ background: var(--n-va-b);
165
+ }
166
+
167
+ .voice-spectrogram-drop {
168
+ display: grid;
169
+ grid-template-columns: minmax(0, 1fr) auto;
170
+ gap: 0.65rem 1rem;
171
+ align-items: center;
172
+ min-height: 6.8rem;
173
+ padding: 1rem 1rem 0.75rem 1.35rem;
174
+ border: 1px dashed var(--n-va-border);
175
+ border-radius: 0.8rem;
176
+ }
177
+
178
+ .voice-spectrogram-drop strong,
179
+ .voice-spectrogram-drop span {
180
+ display: block;
181
+ overflow: hidden;
182
+ text-overflow: ellipsis;
183
+ white-space: nowrap;
184
+ }
185
+
186
+ .voice-spectrogram-drop strong {
187
+ margin-bottom: 0.25rem;
188
+ }
189
+
190
+ .voice-spectrogram-drop span,
191
+ .voice-spectrogram-drop small,
192
+ .voice-spectrogram-sample-foot {
193
+ color: var(--n-va-muted);
194
+ font-size: 0.76rem;
195
+ }
196
+
197
+ .voice-spectrogram-drop small {
198
+ grid-column: 1 / -1;
199
+ }
200
+
201
+ .voice-spectrogram-file {
202
+ display: inline-flex;
203
+ padding: 0.55rem 0.75rem;
204
+ color: var(--n-va-paper);
205
+ background: var(--n-va-ink);
206
+ border-radius: 0.55rem;
207
+ cursor: pointer;
208
+ font-size: 0.78rem;
209
+ font-weight: 800;
210
+ }
211
+
212
+ .voice-spectrogram-file input {
213
+ position: absolute;
214
+ width: 1px;
215
+ height: 1px;
216
+ overflow: hidden;
217
+ clip-path: inset(50%);
218
+ white-space: nowrap;
219
+ }
220
+
221
+ .voice-spectrogram-sample-foot {
222
+ display: flex;
223
+ justify-content: space-between;
224
+ gap: 0.8rem;
225
+ padding: 0.75rem 0.15rem 0.05rem 0.55rem;
226
+ }
227
+
228
+ .voice-spectrogram-presets {
229
+ display: flex;
230
+ flex-wrap: wrap;
231
+ justify-content: flex-end;
232
+ gap: 0.35rem;
233
+ }
234
+
235
+ .voice-spectrogram-presets button,
236
+ .voice-spectrogram-segmented button {
237
+ padding: 0.28rem 0.5rem;
238
+ background: transparent;
239
+ border: 1px solid var(--n-va-border);
240
+ border-radius: 99rem;
241
+ cursor: pointer;
242
+ font-size: 0.7rem;
243
+ }
244
+
245
+ .voice-spectrogram-instrument {
246
+ margin: 0 clamp(0.65rem, 2vw, 1.25rem);
247
+ padding: clamp(0.75rem, 2vw, 1.25rem);
248
+ color: var(--n-va-white);
249
+ background: var(--n-va-canvas);
250
+ border: 1px solid var(--n-va-grid);
251
+ border-radius: 1.35rem;
252
+ box-shadow: inset 0 0 3rem var(--n-va-black), 0 1.2rem 2.8rem var(--n-va-shadow);
253
+ }
254
+
255
+ .voice-spectrogram-instrument-head > div:first-child {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 0.55rem;
259
+ font-size: 0.78rem;
260
+ letter-spacing: 0.05em;
261
+ text-transform: uppercase;
262
+ }
263
+
264
+ .voice-spectrogram-live-dot {
265
+ width: 0.6rem;
266
+ height: 0.6rem;
267
+ background: var(--n-va-a);
268
+ border-radius: 50%;
269
+ box-shadow: 0 0 0.8rem var(--n-va-a);
270
+ }
271
+
272
+ .voice-spectrogram-control-clusters {
273
+ display: flex;
274
+ flex-wrap: wrap;
275
+ justify-content: flex-end;
276
+ gap: 0.6rem;
277
+ }
278
+
279
+ .voice-spectrogram-segmented {
280
+ display: flex;
281
+ align-items: center;
282
+ gap: 0.3rem;
283
+ }
284
+
285
+ .voice-spectrogram-segmented > span {
286
+ margin-right: 0.25rem;
287
+ color: var(--n-va-muted);
288
+ font-size: 0.68rem;
289
+ text-transform: uppercase;
290
+ }
291
+
292
+ .voice-spectrogram-segmented button {
293
+ color: var(--n-va-muted);
294
+ border-color: var(--n-va-grid);
295
+ }
296
+
297
+ .voice-spectrogram-segmented button.is-active {
298
+ color: var(--n-va-canvas);
299
+ background: var(--n-va-cursor);
300
+ border-color: var(--n-va-cursor);
301
+ }
302
+
303
+ .voice-spectrogram-plate {
304
+ position: relative;
305
+ overflow: hidden;
306
+ border: 1px solid var(--n-va-grid);
307
+ border-radius: 0.9rem;
308
+ }
309
+
310
+ .voice-spectrogram-plate canvas {
311
+ display: block;
312
+ width: 100%;
313
+ height: clamp(24rem, 54vw, 34rem);
314
+ }
315
+
316
+ .voice-spectrogram-plate-glow {
317
+ position: absolute;
318
+ inset: 0;
319
+ pointer-events: none;
320
+ background: linear-gradient(90deg, var(--n-va-glow-a), transparent 38%, transparent 62%, var(--n-va-glow-b));
321
+ mix-blend-mode: screen;
322
+ }
323
+
324
+ .voice-spectrogram-transport {
325
+ display: grid;
326
+ grid-template-columns: 1fr auto 1fr;
327
+ gap: 0.7rem;
328
+ align-items: center;
329
+ max-width: 34rem;
330
+ margin: 1rem auto 0;
331
+ }
332
+
333
+ .voice-spectrogram-transport button {
334
+ min-height: 2.8rem;
335
+ color: var(--n-va-white);
336
+ background: var(--n-va-grid);
337
+ border: 1px solid var(--n-va-grid);
338
+ border-radius: 99rem;
339
+ cursor: pointer;
340
+ font-weight: 800;
341
+ }
342
+
343
+ .voice-spectrogram-transport button:not(.voice-spectrogram-stop) {
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ gap: 0.55rem;
348
+ }
349
+
350
+ .voice-spectrogram-transport button span {
351
+ display: grid;
352
+ width: 1.5rem;
353
+ height: 1.5rem;
354
+ place-items: center;
355
+ color: var(--n-va-canvas);
356
+ background: var(--n-va-a);
357
+ border-radius: 50%;
358
+ }
359
+
360
+ .voice-spectrogram-transport button:last-child span {
361
+ background: var(--n-va-b);
362
+ }
363
+
364
+ .voice-spectrogram-stop {
365
+ padding: 0 1rem;
366
+ color: var(--n-va-cursor);
367
+ background: transparent;
368
+ }
369
+
370
+ .voice-spectrogram-legend {
371
+ display: flex;
372
+ flex-wrap: wrap;
373
+ justify-content: center;
374
+ gap: 0.6rem 1.3rem;
375
+ margin-top: 0.85rem;
376
+ color: var(--n-va-muted);
377
+ font-size: 0.68rem;
378
+ }
379
+
380
+ .voice-spectrogram-legend span {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 0.35rem;
384
+ }
385
+
386
+ .voice-spectrogram-swatch {
387
+ width: 0.75rem;
388
+ height: 0.75rem;
389
+ background: var(--n-va-a);
390
+ border-radius: 0.2rem;
391
+ }
392
+
393
+ .voice-spectrogram-swatch-b {
394
+ background: var(--n-va-b);
395
+ }
396
+
397
+ .voice-spectrogram-line {
398
+ width: 1.2rem;
399
+ border-top: 2px solid var(--n-va-cursor);
400
+ }
401
+
402
+ .voice-spectrogram-comparison {
403
+ display: grid;
404
+ grid-template-columns: minmax(12rem, 0.7fr) minmax(0, 1.6fr);
405
+ gap: 1.4rem;
406
+ align-items: start;
407
+ }
408
+
409
+ .voice-spectrogram-comparison-copy p:last-child {
410
+ margin-top: 0.65rem;
411
+ color: var(--n-va-muted);
412
+ line-height: 1.6;
413
+ }
414
+
415
+ .voice-spectrogram-formants {
416
+ display: grid;
417
+ grid-template-columns: repeat(3, minmax(0, 1fr));
418
+ gap: 0.65rem;
419
+ }
420
+
421
+ .voice-spectrogram-formants article {
422
+ position: relative;
423
+ overflow: hidden;
424
+ padding: 1rem;
425
+ background: var(--n-va-panel-soft);
426
+ border: 1px solid var(--n-va-border);
427
+ border-radius: 0.85rem;
428
+ }
429
+
430
+ .voice-spectrogram-formant-index {
431
+ display: block;
432
+ color: var(--n-va-muted);
433
+ font-size: 1.8rem;
434
+ font-weight: 900;
435
+ opacity: 0.4;
436
+ }
437
+
438
+ .voice-spectrogram-formants strong {
439
+ display: block;
440
+ min-height: 2.5rem;
441
+ font-size: 0.85rem;
442
+ }
443
+
444
+ .voice-spectrogram-formants dl,
445
+ .voice-spectrogram-formants dd {
446
+ margin: 0;
447
+ }
448
+
449
+ .voice-spectrogram-formants dl > div {
450
+ display: flex;
451
+ justify-content: space-between;
452
+ gap: 0.5rem;
453
+ padding: 0.38rem 0;
454
+ border-top: 1px solid var(--n-va-border);
455
+ font-size: 0.75rem;
456
+ }
457
+
458
+ .voice-spectrogram-formants dt {
459
+ color: var(--n-va-muted);
460
+ }
461
+
462
+ .voice-spectrogram-formants dd {
463
+ min-width: 4.6rem;
464
+ text-align: right;
465
+ font-variant-numeric: tabular-nums;
466
+ font-weight: 800;
467
+ }
468
+
469
+ .voice-spectrogram-educational {
470
+ padding: 0 2rem 1.5rem;
471
+ color: var(--n-va-muted);
472
+ font-size: 0.72rem;
473
+ line-height: 1.5;
474
+ text-align: center;
475
+ }
476
+
477
+ .voice-spectrogram button:focus-visible,
478
+ .voice-spectrogram input:focus-visible,
479
+ .voice-spectrogram-file:focus-within {
480
+ outline: 3px solid var(--n-va-cursor);
481
+ outline-offset: 3px;
482
+ }
483
+
484
+ @media (max-width: 760px) {
485
+ .voice-spectrogram-samples,
486
+ .voice-spectrogram-comparison {
487
+ grid-template-columns: 1fr;
488
+ }
489
+
490
+ .voice-spectrogram-instrument-head,
491
+ .voice-spectrogram-section-intro {
492
+ align-items: flex-start;
493
+ flex-direction: column;
494
+ }
495
+
496
+ .voice-spectrogram-control-clusters {
497
+ justify-content: flex-start;
498
+ }
499
+
500
+ .voice-spectrogram-plate canvas {
501
+ height: 27rem;
502
+ }
503
+ }
504
+
505
+ @media (max-width: 520px) {
506
+ .voice-spectrogram-privacy {
507
+ align-items: flex-start;
508
+ }
509
+
510
+ .voice-spectrogram-drop {
511
+ grid-template-columns: 1fr;
512
+ }
513
+
514
+ .voice-spectrogram-drop small {
515
+ grid-column: auto;
516
+ }
517
+
518
+ .voice-spectrogram-sample-foot {
519
+ align-items: flex-start;
520
+ flex-direction: column;
521
+ }
522
+
523
+ .voice-spectrogram-presets {
524
+ justify-content: flex-start;
525
+ }
526
+
527
+ .voice-spectrogram-formants {
528
+ grid-template-columns: 1fr;
529
+ }
530
+
531
+ .voice-spectrogram-transport {
532
+ grid-template-columns: 1fr 1fr;
533
+ }
534
+
535
+ .voice-spectrogram-stop {
536
+ grid-column: 1 / -1;
537
+ grid-row: 2;
538
+ }
539
+ }
540
+
541
+ @media (prefers-reduced-motion: reduce) {
542
+ .voice-spectrogram *,
543
+ .voice-spectrogram *::before,
544
+ .voice-spectrogram *::after {
545
+ scroll-behavior: auto;
546
+ }
547
+ }
@@ -163,7 +163,7 @@ const { ui } = Astro.props;
163
163
  </main>
164
164
  </div>
165
165
 
166
- <script id="widmark-ui-data" type="application/json" set:html={JSON.stringify(ui)}></script>
166
+ <script is:inline id="widmark-ui-data" type="application/json" set:html={JSON.stringify(ui)}></script>
167
167
 
168
168
  <script>
169
169
  import { WidmarkEngine } from './logic';
@@ -154,6 +154,11 @@ export const content: ToolLocaleContent = {
154
154
  type: 'paragraph',
155
155
  html: 'Nach dem Eintritt in den Blutkreislauf wird Alkohol primär in der Leber durch das Enzym Alkoholdehydrogenase (ADH) abgebaut. Da dieses Enzym bereits bei sehr geringen Konzentrationen gesättigt ist, verläuft der Abbau nach einer Kinetik nullter Ordnung. Dies bedeutet, dass pro Zeiteinheit eine konstante Alkoholmenge eliminiert wird. Der forensisch etablierte Mittelwert (&beta;<sub>60</sub>) beträgt 0,15 g/L pro Stunde, was dem Abbau von etwa 10 g reinem Alkohol pro Stunde entspricht. Dieser lineare Abbau erzeugt den geradlinigen Abfall der Kurve.',
156
156
  },
157
+ {
158
+ type: 'paragraph',
159
+ html: 'Für eine Rückrechnung muss außerdem der Zeitpunkt der Messung eindeutig feststehen. Eine Schätzung zum Trinkende ist keine direkte Messung der Resorption: Der Blutalkohol kann nach dem letzten Getränk noch ansteigen, während sich der Magen entleert. Deshalb sollten Trinkmengen, Zeitpunkte, Körpergewicht, angenommener Verteilungsfaktor und Eliminationsrate getrennt dokumentiert werden. Die Anzeige eignet sich zum Vergleichen dieser Szenarien, nicht als alleinige Grundlage für eine rechtliche Entscheidung.',
160
+ },
161
+ { type: 'paragraph', html: 'Alle Werte bleiben Näherungen und müssen mit ihren Annahmen dokumentiert werden.' },
157
162
 
158
163
  { type: 'paragraph', html: "Nutzen Sie das Ergebnis zur Planung und zum Szenarienvergleich; es ersetzt keine offizielle Berechnung oder Fachberatung." },
159
164
  { type: 'paragraph', html: "Deuten Sie das Ergebnis gemeinsam mit den angezeigten Annahmen, bevor Sie es verwenden." },
@@ -157,7 +157,8 @@ export const content: ToolLocaleContent = {
157
157
 
158
158
  { type: 'paragraph', html: "Utilisez le résultat pour planifier et comparer des scénarios ; il ne remplace ni un calcul officiel ni un avis professionnel." },
159
159
  { type: 'paragraph', html: "Interprétez le résultat avec les hypothèses affichées par le calculateur avant de l'utiliser." },
160
- { type: 'paragraph', html: "Vérifiez les données, unités, arrondi, date et juridiction, car chacun de ces éléments peut modifier l'estimation." },],
160
+ { type: 'paragraph', html: "Vérifiez les données, unités, arrondi, date et juridiction, car chacun de ces éléments peut modifier l'estimation." },
161
+ { type: 'paragraph', html: "Pour une rétrogradation fiable, notez séparément l'heure du dernier verre, la durée d'absorption supposée, le poids, le facteur de distribution et le taux d'élimination. Une valeur calculée sert à comparer des scénarios et ne remplace pas une expertise médico-légale complète. L'absorption peut se poursuivre après le dernier verre et la mesure dépend de l'état d'hydratation, du contenu de l'estomac et de la cinétique retenue. Chaque hypothèse doit donc rester visible dans le rapport. Le résultat doit être présenté avec son incertitude et son contexte." },],
161
162
  faq,
162
163
  bibliography,
163
164
  howTo,
@@ -154,6 +154,7 @@ export const content: ToolLocaleContent = {
154
154
  type: 'paragraph',
155
155
  html: 'Setelah masuk ke sirkulasi darah, alkohol dimetabolisme terutama di hati oleh enzim alkohol dehidrogenase (ADH). Enzim ini mengalami kejenuhan pada konsentrasi yang sangat rendah (0,02 g/L), sehingga metabolisme berlangsung dengan kecepatan konstan tanpa dipengaruhi kadar alkohol dalam darah. Rata-rata laju eliminasi forensik (&beta;<sub>60</sub>) ditetapkan sebesar 0,15 g/L per jam. Penurunan konstan ini menghasilkan kemiringan linier lurus pada kurva hingga mencapai angka nol.',
156
156
  },
157
+ { type: 'paragraph', html: 'Untuk membaca simulasi dengan benar, pisahkan waktu konsumsi dari waktu pengukuran dan tuliskan semua asumsi tentang absorpsi, distribusi, hidrasi, dan eliminasi. Kadar yang dihitung adalah perkiraan matematis untuk membandingkan skenario. Keputusan resmi harus menggunakan catatan pemeriksaan, metode tervalidasi, dan penilaian ahli yang sesuai dengan yurisdiksi. Faktor tubuh, makanan, obat, kesehatan, dan ketidakpastian waktu dapat memengaruhi interpretasi. Tampilkan rentang dan asumsi secara terbuka agar hasil tidak disalahpahami sebagai ukuran langsung.' },
157
158
 
158
159
  { type: 'paragraph', html: "Gunakan hasil untuk perencanaan dan perbandingan skenario; hasil ini bukan pengganti perhitungan resmi atau nasihat ahli." },
159
160
  { type: 'paragraph', html: "Tafsirkan hasil bersama asumsi yang ditampilkan kalkulator sebelum menggunakannya." },
@@ -105,6 +105,8 @@ export const content: ToolLocaleContent = {
105
105
  text: 'Tossicologia Forense e Cinetica dell\'Etanolo',
106
106
  level: 2,
107
107
  },
108
+ { type: 'paragraph', html: "Il valore visualizzato dipende dal momento scelto e dai parametri inseriti. Conserva i dati originali, indica quali informazioni sono state stimate e confronta più scenari quando l'orario non è certo. Questo uso didattico rende chiaro il margine di errore e impedisce di leggere la curva come una prova isolata." },
109
+ { type: 'paragraph', html: "Una stima trasparente distingue i dati osservati dalle ipotesi del modello. Documenta la sequenza temporale, la fase possibile di assorbimento e l'incertezza dell'eliminazione. Il grafico serve a confrontare scenari e non determina da solo responsabilità o conclusioni giudiziarie." },
108
110
  {
109
111
  type: 'paragraph',
110
112
  html: 'La comprensione dei processi di <strong>assorbimento, distribuzione ed eliminazione dell\'etanolo</strong> è un requisito essenziale in tossicologia forense e nella ricostruzione degli incidenti stradali. In seguito all\'assunzione, l\'alcol scende lungo l\'esofago fino allo stomaco, dove ha luogo un primo, modesto assorbimento. Tuttavia, la parte preponderante dell\'assorbimento dell\'etanolo avviene nel duodeno e nell\'intestino tenue per via dell\'enorme area superficiale disponibile. Lo svuotamento gastrico rappresenta il passaggio limitante, spiegando perché i pasti rallentino vistosamente la curva alcolemica.',
@@ -157,7 +159,8 @@ export const content: ToolLocaleContent = {
157
159
 
158
160
  { type: 'paragraph', html: "Usa il risultato per pianificare e confrontare scenari; non sostituisce un calcolo ufficiale o una consulenza professionale." },
159
161
  { type: 'paragraph', html: "Interpreta il risultato insieme alle ipotesi mostrate dal calcolatore prima di utilizzarlo." },
160
- { type: 'paragraph', html: "Controlla dati, unità, arrotondamento, data e giurisdizione, perché ciascun elemento può cambiare la stima." },],
162
+ { type: 'paragraph', html: "Controlla dati, unità, arrotondamento, data e giurisdizione, perché ciascun elemento può cambiare la stima." },
163
+ { type: 'paragraph', html: "Per interpretare una retrostima documenta separatamente l'ora dell'ultimo drink, l'assorbimento ipotizzato, il peso, il fattore di distribuzione e la velocità di eliminazione. L'assorbimento può continuare dopo l'ultima bevanda e la disidratazione o il contenuto gastrico possono cambiare la curva. Il risultato è un confronto matematico tra scenari, non una misura diretta né un parere forense conclusivo." },],
161
164
  faq,
162
165
  bibliography,
163
166
  howTo,