@jjlmoya/utils-science 1.42.0 → 1.43.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 (129) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/diacritics_density.test.ts +140 -0
  6. package/src/tests/inverted_punctuation.test.ts +84 -0
  7. package/src/tests/locale_completeness.test.ts +2 -2
  8. package/src/tests/script_density.test.ts +94 -0
  9. package/src/tests/tool_validation.test.ts +2 -3
  10. package/src/tool/conway-life-rule-lab/i18n/de.ts +62 -5
  11. package/src/tool/conway-life-rule-lab/i18n/es.ts +62 -5
  12. package/src/tool/conway-life-rule-lab/i18n/fr.ts +62 -5
  13. package/src/tool/conway-life-rule-lab/i18n/id.ts +102 -45
  14. package/src/tool/conway-life-rule-lab/i18n/it.ts +62 -5
  15. package/src/tool/conway-life-rule-lab/i18n/ja.ts +62 -5
  16. package/src/tool/conway-life-rule-lab/i18n/ko.ts +62 -5
  17. package/src/tool/conway-life-rule-lab/i18n/nl.ts +102 -45
  18. package/src/tool/conway-life-rule-lab/i18n/pl.ts +62 -5
  19. package/src/tool/conway-life-rule-lab/i18n/pt.ts +62 -5
  20. package/src/tool/conway-life-rule-lab/i18n/ru.ts +62 -5
  21. package/src/tool/conway-life-rule-lab/i18n/sv.ts +62 -5
  22. package/src/tool/conway-life-rule-lab/i18n/tr.ts +62 -5
  23. package/src/tool/conway-life-rule-lab/i18n/zh.ts +62 -5
  24. package/src/tool/cosmic-inflation/i18n/es.ts +5 -5
  25. package/src/tool/cosmic-inflation/i18n/it.ts +51 -51
  26. package/src/tool/cosmic-inflation/i18n/ru.ts +42 -150
  27. package/src/tool/double-slit-decoherence/i18n/fr.ts +42 -86
  28. package/src/tool/double-slit-decoherence/i18n/it.ts +28 -72
  29. package/src/tool/double-slit-decoherence/i18n/pt.ts +35 -79
  30. package/src/tool/double-slit-decoherence/i18n/ru.ts +59 -59
  31. package/src/tool/double-slit-decoherence/i18n/sv.ts +33 -81
  32. package/src/tool/double-slit-decoherence/i18n/tr.ts +55 -55
  33. package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +42 -123
  34. package/src/tool/dyson-sphere-energy-capture/i18n/es.ts +6 -6
  35. package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +63 -131
  36. package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +36 -113
  37. package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +66 -42
  38. package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +66 -42
  39. package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +50 -133
  40. package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +51 -132
  41. package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +61 -98
  42. package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +56 -137
  43. package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +61 -142
  44. package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +67 -43
  45. package/src/tool/epidemic-sir-simulator/bibliography.astro +15 -0
  46. package/src/tool/epidemic-sir-simulator/bibliography.ts +37 -0
  47. package/src/tool/epidemic-sir-simulator/component.astro +384 -0
  48. package/src/tool/epidemic-sir-simulator/entry.ts +30 -0
  49. package/src/tool/epidemic-sir-simulator/epidemic-sir-simulator.css +624 -0
  50. package/src/tool/epidemic-sir-simulator/i18n/de.ts +347 -0
  51. package/src/tool/epidemic-sir-simulator/i18n/en.ts +347 -0
  52. package/src/tool/epidemic-sir-simulator/i18n/es.ts +347 -0
  53. package/src/tool/epidemic-sir-simulator/i18n/fr.ts +347 -0
  54. package/src/tool/epidemic-sir-simulator/i18n/id.ts +347 -0
  55. package/src/tool/epidemic-sir-simulator/i18n/it.ts +347 -0
  56. package/src/tool/epidemic-sir-simulator/i18n/ja.ts +347 -0
  57. package/src/tool/epidemic-sir-simulator/i18n/ko.ts +347 -0
  58. package/src/tool/epidemic-sir-simulator/i18n/nl.ts +347 -0
  59. package/src/tool/epidemic-sir-simulator/i18n/pl.ts +347 -0
  60. package/src/tool/epidemic-sir-simulator/i18n/pt.ts +347 -0
  61. package/src/tool/epidemic-sir-simulator/i18n/ru.ts +347 -0
  62. package/src/tool/epidemic-sir-simulator/i18n/sv.ts +347 -0
  63. package/src/tool/epidemic-sir-simulator/i18n/tr.ts +347 -0
  64. package/src/tool/epidemic-sir-simulator/i18n/zh.ts +347 -0
  65. package/src/tool/epidemic-sir-simulator/index.ts +12 -0
  66. package/src/tool/epidemic-sir-simulator/logic.ts +163 -0
  67. package/src/tool/epidemic-sir-simulator/seo.astro +16 -0
  68. package/src/tool/fermi-paradox-filter-lab/bibliography.astro +14 -0
  69. package/src/tool/fermi-paradox-filter-lab/bibliography.ts +24 -0
  70. package/src/tool/fermi-paradox-filter-lab/component.astro +257 -0
  71. package/src/tool/fermi-paradox-filter-lab/entry.ts +26 -0
  72. package/src/tool/fermi-paradox-filter-lab/fermi-paradox-filter-lab.css +228 -0
  73. package/src/tool/fermi-paradox-filter-lab/i18n/de.ts +192 -0
  74. package/src/tool/fermi-paradox-filter-lab/i18n/en.ts +192 -0
  75. package/src/tool/fermi-paradox-filter-lab/i18n/es.ts +193 -0
  76. package/src/tool/fermi-paradox-filter-lab/i18n/fr.ts +192 -0
  77. package/src/tool/fermi-paradox-filter-lab/i18n/id.ts +192 -0
  78. package/src/tool/fermi-paradox-filter-lab/i18n/it.ts +192 -0
  79. package/src/tool/fermi-paradox-filter-lab/i18n/ja.ts +192 -0
  80. package/src/tool/fermi-paradox-filter-lab/i18n/ko.ts +192 -0
  81. package/src/tool/fermi-paradox-filter-lab/i18n/nl.ts +192 -0
  82. package/src/tool/fermi-paradox-filter-lab/i18n/pl.ts +193 -0
  83. package/src/tool/fermi-paradox-filter-lab/i18n/pt.ts +192 -0
  84. package/src/tool/fermi-paradox-filter-lab/i18n/ru.ts +192 -0
  85. package/src/tool/fermi-paradox-filter-lab/i18n/sv.ts +192 -0
  86. package/src/tool/fermi-paradox-filter-lab/i18n/tr.ts +192 -0
  87. package/src/tool/fermi-paradox-filter-lab/i18n/zh.ts +192 -0
  88. package/src/tool/fermi-paradox-filter-lab/index.ts +11 -0
  89. package/src/tool/fermi-paradox-filter-lab/logic.ts +125 -0
  90. package/src/tool/fermi-paradox-filter-lab/seo.astro +15 -0
  91. package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +50 -63
  92. package/src/tool/lorenz-attractor/i18n/de.ts +52 -78
  93. package/src/tool/lorenz-attractor/i18n/es.ts +5 -5
  94. package/src/tool/lorenz-attractor/i18n/fr.ts +51 -77
  95. package/src/tool/lorenz-attractor/i18n/it.ts +36 -36
  96. package/src/tool/lorenz-attractor/i18n/pt.ts +51 -75
  97. package/src/tool/mandelbrot-fractal/i18n/de.ts +31 -58
  98. package/src/tool/mandelbrot-fractal/i18n/es.ts +5 -5
  99. package/src/tool/mandelbrot-fractal/i18n/fr.ts +39 -57
  100. package/src/tool/mandelbrot-fractal/i18n/it.ts +30 -48
  101. package/src/tool/mandelbrot-fractal/i18n/pl.ts +34 -52
  102. package/src/tool/mandelbrot-fractal/i18n/pt.ts +30 -30
  103. package/src/tool/mandelbrot-fractal/i18n/ru.ts +42 -60
  104. package/src/tool/mandelbrot-fractal/i18n/sv.ts +38 -65
  105. package/src/tool/natural-selection-drift/i18n/it.ts +18 -95
  106. package/src/tool/natural-selection-drift/i18n/pt.ts +76 -19
  107. package/src/tool/natural-selection-drift/i18n/ru.ts +27 -24
  108. package/src/tool/natural-selection-drift/i18n/sv.ts +9 -6
  109. package/src/tool/phase-diagram-critical-points/i18n/de.ts +30 -68
  110. package/src/tool/phase-diagram-critical-points/i18n/es.ts +4 -4
  111. package/src/tool/phase-diagram-critical-points/i18n/fr.ts +37 -75
  112. package/src/tool/phase-diagram-critical-points/i18n/it.ts +8 -8
  113. package/src/tool/planet-atmosphere-survival/i18n/ru.ts +49 -192
  114. package/src/tool/radioactive-decay/i18n/de.ts +16 -32
  115. package/src/tool/radioactive-decay/i18n/es.ts +5 -5
  116. package/src/tool/radioactive-decay/i18n/fr.ts +24 -40
  117. package/src/tool/radioactive-decay/i18n/it.ts +18 -35
  118. package/src/tool/radioactive-decay/i18n/pt.ts +34 -34
  119. package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +52 -128
  120. package/src/tool/temperature-timeline/i18n/de.ts +46 -174
  121. package/src/tool/temperature-timeline/i18n/es.ts +5 -5
  122. package/src/tool/temperature-timeline/i18n/fr.ts +69 -144
  123. package/src/tool/temperature-timeline/i18n/it.ts +34 -133
  124. package/src/tool/temperature-timeline/i18n/pt.ts +35 -174
  125. package/src/tool/temperature-timeline/i18n/ru.ts +48 -174
  126. package/src/tool/twin-paradox-visualizer/i18n/es.ts +5 -5
  127. package/src/tool/twin-paradox-visualizer/i18n/fr.ts +34 -34
  128. package/src/tool/twin-paradox-visualizer/i18n/pt.ts +31 -71
  129. package/src/tools.ts +4 -0
@@ -0,0 +1,257 @@
1
+ ---
2
+ import './fermi-paradox-filter-lab.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="fermi-lab" id="fermi-lab" data-max-label={ui.maxLabel} data-year-unit={ui.yearUnit}>
12
+ <div class="fermi-sieve">
13
+ <section class="fermi-metrics" aria-label={ui.metricsLabel}>
14
+ <div><span>{ui.birthRate}</span><strong><span id="fermi-rate">0.00</span><small>/{ui.yearUnit}</small></strong></div>
15
+ <div><span>{ui.peakCivilizations}</span><strong id="fermi-peak">0</strong></div>
16
+ <div><span>{ui.finalCivilizations}</span><strong id="fermi-final">0</strong></div>
17
+ <div><span>{ui.silenceScore}</span><strong><span id="fermi-silence">0</span><small>/{ui.silenceScale}</small></strong></div>
18
+ </section>
19
+
20
+ <section class="fermi-graph" aria-label={ui.chartLabel}>
21
+ <canvas id="fermi-chart" width="920" height="420" aria-label={ui.chartLabel}></canvas>
22
+ </section>
23
+
24
+ <section class="fermi-console" aria-label={ui.controlsLabel}>
25
+ <div class="fermi-steps" aria-label={ui.controlsLabel}>
26
+ <button type="button" class="is-active" data-step="stars" data-label={ui.starFormationRate}>{ui.step01}</button>
27
+ <button type="button" data-step="planets" data-label={ui.habitableFraction}>{ui.step02}</button>
28
+ <button type="button" data-step="life" data-label={ui.lifeFraction}>{ui.step03}</button>
29
+ <button type="button" data-step="intelligence" data-label={ui.intelligenceFraction}>{ui.step04}</button>
30
+ <button type="button" data-step="technology" data-label={ui.technologyFraction}>{ui.step05}</button>
31
+ <button type="button" data-step="lifetime" data-label={ui.signalLifetime}>{ui.step06}</button>
32
+ <button type="button" data-step="filter" data-label={ui.filterSeverity}>{ui.step07}</button>
33
+ <button type="button" data-step="horizon" data-label={ui.horizonYears}>{ui.step08}</button>
34
+ </div>
35
+
36
+ <label class="fermi-active-filter">
37
+ <span id="fermi-active-label">{ui.starFormationRate}</span>
38
+ <output id="fermi-active-value">6.0</output>
39
+ <input id="fermi-active-input" type="range" min="1" max="12" step="0.1" value="6" />
40
+ </label>
41
+ </section>
42
+ </div>
43
+ </div>
44
+
45
+ <script>
46
+ import { simulateFermiFilters } from './logic';
47
+
48
+ const canvas = document.getElementById('fermi-chart') as HTMLCanvasElement | null;
49
+ const context = canvas?.getContext('2d');
50
+ const activeInput = document.getElementById('fermi-active-input') as HTMLInputElement | null;
51
+ const activeLabel = document.getElementById('fermi-active-label');
52
+ const activeValue = document.getElementById('fermi-active-value');
53
+ const stepButtons = Array.from(document.querySelectorAll<HTMLButtonElement>('[data-step]'));
54
+ const labRoot = document.getElementById('fermi-lab');
55
+ const currentLocale = document.documentElement.lang || 'en';
56
+ const maxLabel = labRoot?.dataset.maxLabel ?? '';
57
+ const yearUnit = labRoot?.dataset.yearUnit ?? '';
58
+
59
+ const outputs = {
60
+ rate: document.getElementById('fermi-rate'),
61
+ peak: document.getElementById('fermi-peak'),
62
+ final: document.getElementById('fermi-final'),
63
+ silence: document.getElementById('fermi-silence'),
64
+ };
65
+
66
+ const state = {
67
+ stars: 6,
68
+ planets: 30,
69
+ life: 15,
70
+ intelligence: 8,
71
+ technology: 40,
72
+ lifetime: 4,
73
+ filter: 45,
74
+ horizon: 5.3,
75
+ };
76
+
77
+ const steps = {
78
+ stars: { min: 1, max: 12, step: 0.1, format: (value: number) => value.toFixed(1) },
79
+ planets: { min: 1, max: 100, step: 1, format: (value: number) => `${Math.round(value)}%` },
80
+ life: { min: 1, max: 100, step: 1, format: (value: number) => `${Math.round(value)}%` },
81
+ intelligence: { min: 1, max: 100, step: 1, format: (value: number) => `${Math.round(value)}%` },
82
+ technology: { min: 1, max: 100, step: 1, format: (value: number) => `${Math.round(value)}%` },
83
+ lifetime: { min: 2, max: 6, step: 0.05, format: (value: number) => `${Math.round(10 ** value).toLocaleString(currentLocale)} ${yearUnit}` },
84
+ filter: { min: 0, max: 98, step: 1, format: (value: number) => `${Math.round(value)}%` },
85
+ horizon: { min: 4, max: 6, step: 0.05, format: (value: number) => `${Math.round(10 ** value).toLocaleString(currentLocale)} ${yearUnit}` },
86
+ } as const;
87
+
88
+ type StepKey = keyof typeof steps;
89
+ let activeStep: StepKey = 'stars';
90
+
91
+ const formatCount = (value: number) => value.toLocaleString(currentLocale, { maximumFractionDigits: value < 10 ? 2 : 0 });
92
+
93
+ const chartFrame = () => ({
94
+ width: canvas?.width ?? 0,
95
+ height: canvas?.height ?? 0,
96
+ padX: 44,
97
+ padTop: 34,
98
+ padBottom: 44,
99
+ });
100
+
101
+ const chartY = (value: number, maxValue: number, frame: ReturnType<typeof chartFrame>) =>
102
+ frame.height - frame.padBottom - (value / maxValue) * (frame.height - frame.padTop - frame.padBottom);
103
+
104
+ const chartX = (index: number, count: number, frame: ReturnType<typeof chartFrame>) =>
105
+ frame.padX + (index / (count - 1)) * (frame.width - frame.padX * 2);
106
+
107
+ const chartStyles = () => {
108
+ const styles = getComputedStyle(canvas);
109
+ return {
110
+ bg: styles.getPropertyValue('--fermi-bg').trim() || '#fbfaf4',
111
+ grid: styles.getPropertyValue('--fermi-hairline').trim() || 'rgba(16, 20, 18, 0.18)',
112
+ text: styles.getPropertyValue('--fermi-muted').trim() || '#5e6863',
113
+ line: styles.getPropertyValue('--fermi-curve').trim() || '#101412',
114
+ fill: styles.getPropertyValue('--fermi-fill').trim() || 'rgba(16, 20, 18, 0.055)',
115
+ accent: styles.getPropertyValue('--fermi-accent').trim() || '#006f6f',
116
+ };
117
+ };
118
+
119
+ const drawChartBase = (frame: ReturnType<typeof chartFrame>, styles: ReturnType<typeof chartStyles>) => {
120
+ context.clearRect(0, 0, frame.width, frame.height);
121
+ context.fillStyle = styles.bg;
122
+ context.fillRect(0, 0, frame.width, frame.height);
123
+
124
+ context.strokeStyle = styles.grid;
125
+ context.lineWidth = 1;
126
+ for (let i = 0; i < 5; i += 1) {
127
+ const y = frame.padTop + ((frame.height - frame.padTop - frame.padBottom) / 4) * i;
128
+ context.beginPath();
129
+ context.moveTo(frame.padX, y);
130
+ context.lineTo(frame.width - frame.padX, y);
131
+ context.stroke();
132
+ }
133
+ };
134
+
135
+ const drawChartGates = (frame: ReturnType<typeof chartFrame>, styles: ReturnType<typeof chartStyles>) => {
136
+ const gates = ['stars', 'planets', 'life', 'intelligence', 'technology', 'lifetime', 'filter', 'horizon'];
137
+ gates.forEach((gate, index) => {
138
+ const x = frame.padX + ((index + 0.5) / gates.length) * (frame.width - frame.padX * 2);
139
+ context.globalAlpha = gate === activeStep ? 0.7 : 0.24;
140
+ context.strokeStyle = gate === activeStep ? styles.accent : styles.grid;
141
+ context.beginPath();
142
+ context.moveTo(x, frame.padTop);
143
+ context.lineTo(x, frame.height - frame.padBottom);
144
+ context.stroke();
145
+ });
146
+ context.globalAlpha = 1;
147
+ };
148
+
149
+ const drawChartArea = (
150
+ series: ReturnType<typeof simulateFermiFilters>['series'],
151
+ maxValue: number,
152
+ frame: ReturnType<typeof chartFrame>,
153
+ styles: ReturnType<typeof chartStyles>
154
+ ) => {
155
+ const gradient = context.createLinearGradient(0, frame.padTop, 0, frame.height - frame.padBottom);
156
+ gradient.addColorStop(0, styles.fill);
157
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
158
+
159
+ context.beginPath();
160
+ series.forEach((point, index) => {
161
+ const x = chartX(index, series.length, frame);
162
+ const y = chartY(point.detectable, maxValue, frame);
163
+ if (index === 0) context.moveTo(x, y);
164
+ else context.lineTo(x, y);
165
+ });
166
+ context.lineTo(frame.width - frame.padX, frame.height - frame.padBottom);
167
+ context.lineTo(frame.padX, frame.height - frame.padBottom);
168
+ context.closePath();
169
+ context.fillStyle = gradient;
170
+ context.fill();
171
+ };
172
+
173
+ const drawChartLine = (
174
+ series: ReturnType<typeof simulateFermiFilters>['series'],
175
+ maxValue: number,
176
+ frame: ReturnType<typeof chartFrame>,
177
+ styles: ReturnType<typeof chartStyles>
178
+ ) => {
179
+ context.beginPath();
180
+ series.forEach((point, index) => {
181
+ const x = chartX(index, series.length, frame);
182
+ const y = chartY(point.detectable, maxValue, frame);
183
+ if (index === 0) context.moveTo(x, y);
184
+ else context.lineTo(x, y);
185
+ });
186
+ context.strokeStyle = styles.line;
187
+ context.lineWidth = 2;
188
+ context.stroke();
189
+ };
190
+
191
+ const drawChartLabel = (maxValue: number, frame: ReturnType<typeof chartFrame>, styles: ReturnType<typeof chartStyles>) => {
192
+ context.fillStyle = styles.text;
193
+ context.font = '600 13px system-ui, sans-serif';
194
+ context.fillText(`${formatCount(maxValue)} ${maxLabel}`, frame.padX, 22);
195
+ };
196
+
197
+ const drawChart = (series: ReturnType<typeof simulateFermiFilters>['series']) => {
198
+ if (!canvas || !context) return;
199
+ const frame = chartFrame();
200
+ const styles = chartStyles();
201
+ const maxValue = Math.max(1, ...series.map((point) => point.detectable));
202
+ drawChartBase(frame, styles);
203
+ drawChartGates(frame, styles);
204
+ drawChartArea(series, maxValue, frame, styles);
205
+ drawChartLine(series, maxValue, frame, styles);
206
+ drawChartLabel(maxValue, frame, styles);
207
+ };
208
+
209
+ const calculate = () => simulateFermiFilters({
210
+ starFormationRate: state.stars,
211
+ habitableFraction: state.planets / 100,
212
+ lifeFraction: state.life / 100,
213
+ intelligenceFraction: state.intelligence / 100,
214
+ technologyFraction: state.technology / 100,
215
+ signalLifetime: 10 ** state.lifetime,
216
+ filterSeverity: state.filter / 100,
217
+ horizonYears: 10 ** state.horizon,
218
+ });
219
+
220
+ const syncActiveControl = () => {
221
+ if (!activeInput || !activeLabel || !activeValue) return;
222
+ const config = steps[activeStep];
223
+ activeInput.min = String(config.min);
224
+ activeInput.max = String(config.max);
225
+ activeInput.step = String(config.step);
226
+ activeInput.value = String(state[activeStep]);
227
+ activeLabel.textContent = stepButtons.find((button) => button.dataset.step === activeStep)?.dataset.label ?? '';
228
+ activeValue.textContent = config.format(state[activeStep]);
229
+ stepButtons.forEach((button) => {
230
+ button.classList.toggle('is-active', button.dataset.step === activeStep);
231
+ });
232
+ };
233
+
234
+ const update = () => {
235
+ const result = calculate();
236
+ outputs.rate!.textContent = result.drakeRate.toFixed(3);
237
+ outputs.peak!.textContent = formatCount(result.peakCivilizations);
238
+ outputs.final!.textContent = formatCount(result.finalCivilizations);
239
+ outputs.silence!.textContent = `${Math.round(result.silenceScore)}`;
240
+ syncActiveControl();
241
+ drawChart(result.series);
242
+ };
243
+
244
+ activeInput?.addEventListener('input', () => {
245
+ state[activeStep] = Number(activeInput.value);
246
+ update();
247
+ });
248
+
249
+ stepButtons.forEach((button) => {
250
+ button.addEventListener('click', () => {
251
+ activeStep = (button.dataset.step ?? 'stars') as StepKey;
252
+ update();
253
+ });
254
+ });
255
+
256
+ update();
257
+ </script>
@@ -0,0 +1,26 @@
1
+ import type { ScienceToolEntry } from '../../types';
2
+
3
+ export const fermiParadoxFilterLab: ScienceToolEntry = {
4
+ id: 'fermi-paradox-filter-lab',
5
+ icons: {
6
+ bg: 'mdi:telescope',
7
+ fg: 'mdi:alien',
8
+ },
9
+ i18n: {
10
+ de: () => import('./i18n/de').then((m) => m.content),
11
+ en: () => import('./i18n/en').then((m) => m.content),
12
+ es: () => import('./i18n/es').then((m) => m.content),
13
+ fr: () => import('./i18n/fr').then((m) => m.content),
14
+ id: () => import('./i18n/id').then((m) => m.content),
15
+ it: () => import('./i18n/it').then((m) => m.content),
16
+ ja: () => import('./i18n/ja').then((m) => m.content),
17
+ ko: () => import('./i18n/ko').then((m) => m.content),
18
+ nl: () => import('./i18n/nl').then((m) => m.content),
19
+ pl: () => import('./i18n/pl').then((m) => m.content),
20
+ pt: () => import('./i18n/pt').then((m) => m.content),
21
+ ru: () => import('./i18n/ru').then((m) => m.content),
22
+ sv: () => import('./i18n/sv').then((m) => m.content),
23
+ tr: () => import('./i18n/tr').then((m) => m.content),
24
+ zh: () => import('./i18n/zh').then((m) => m.content),
25
+ },
26
+ };
@@ -0,0 +1,228 @@
1
+ .fermi-lab {
2
+ --fermi-bg: #fbfaf4;
3
+ --fermi-ink: #101412;
4
+ --fermi-muted: #626b66;
5
+ --fermi-hairline: rgba(16, 20, 18, 0.18);
6
+ --fermi-soft: rgba(16, 20, 18, 0.045);
7
+ --fermi-curve: #101412;
8
+ --fermi-fill: rgba(16, 20, 18, 0.06);
9
+ --fermi-accent: #006f6f;
10
+ --fermi-warm: #a85336;
11
+ --fermi-shadow: rgba(22, 24, 20, 0.13);
12
+
13
+ width: min(100%, 980px);
14
+ margin: 0 auto;
15
+ color: var(--fermi-ink);
16
+ }
17
+
18
+ .theme-dark .fermi-lab,
19
+ body.theme-dark .fermi-lab,
20
+ html.theme-dark .fermi-lab {
21
+ --fermi-bg: #05090c;
22
+ --fermi-ink: #eff8f5;
23
+ --fermi-muted: #9aa9a5;
24
+ --fermi-hairline: rgba(239, 248, 245, 0.18);
25
+ --fermi-soft: rgba(239, 248, 245, 0.05);
26
+ --fermi-curve: #f1c84c;
27
+ --fermi-fill: rgba(20, 184, 166, 0.14);
28
+ --fermi-accent: #57d7cb;
29
+ --fermi-warm: #ff755f;
30
+ --fermi-shadow: rgba(0, 0, 0, 0.44);
31
+ }
32
+
33
+ .fermi-sieve {
34
+ display: grid;
35
+ gap: clamp(0.6rem, 1.6vw, 0.95rem);
36
+ padding: clamp(0.9rem, 2vw, 1.25rem);
37
+ border: 1px solid var(--fermi-hairline);
38
+ border-radius: 8px;
39
+ background:
40
+ linear-gradient(90deg, var(--fermi-soft) 1px, transparent 1px),
41
+ linear-gradient(0deg, var(--fermi-soft) 1px, transparent 1px),
42
+ var(--fermi-bg);
43
+ background-size: 28px 28px, 28px 28px, auto;
44
+ box-shadow: 0 22px 70px var(--fermi-shadow);
45
+ }
46
+
47
+ .fermi-graph {
48
+ position: relative;
49
+ min-width: 0;
50
+ min-height: 280px;
51
+ }
52
+
53
+ .fermi-graph canvas {
54
+ display: block;
55
+ width: 100%;
56
+ height: auto;
57
+ }
58
+
59
+ .fermi-metrics {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: clamp(0.75rem, 2vw, 1.6rem);
63
+ justify-content: space-between;
64
+ padding: 0 0.25rem;
65
+ background: transparent;
66
+ }
67
+
68
+ .fermi-metrics div {
69
+ display: grid;
70
+ gap: 0.2rem;
71
+ min-width: max-content;
72
+ padding: 0;
73
+ }
74
+
75
+ .fermi-metrics span,
76
+ .fermi-active-filter span {
77
+ color: var(--fermi-muted);
78
+ font-size: clamp(0.62rem, 1.5vw, 0.76rem);
79
+ font-weight: 760;
80
+ letter-spacing: 0.16em;
81
+ line-height: 1.35;
82
+ text-transform: uppercase;
83
+ }
84
+
85
+ .fermi-metrics strong {
86
+ display: inline-flex;
87
+ align-items: baseline;
88
+ gap: 0.12em;
89
+ color: var(--fermi-ink);
90
+ font-size: clamp(1rem, 2vw, 1.35rem);
91
+ font-weight: 650;
92
+ font-variant-numeric: tabular-nums;
93
+ line-height: 1;
94
+ }
95
+
96
+ .fermi-metrics small {
97
+ color: var(--fermi-muted);
98
+ font-size: 0.64em;
99
+ font-weight: 600;
100
+ line-height: 1;
101
+ }
102
+
103
+ .fermi-console {
104
+ display: grid;
105
+ grid-template-columns: 44px minmax(0, 1fr);
106
+ gap: clamp(0.75rem, 2vw, 1.1rem);
107
+ border-top: 1px solid var(--fermi-hairline);
108
+ padding-top: clamp(0.85rem, 2vw, 1.1rem);
109
+ }
110
+
111
+ .fermi-steps {
112
+ display: grid;
113
+ gap: 0.38rem;
114
+ align-content: center;
115
+ }
116
+
117
+ .fermi-steps button {
118
+ width: 32px;
119
+ height: 20px;
120
+ padding: 0;
121
+ border: 0;
122
+ color: color-mix(in srgb, var(--fermi-muted) 32%, transparent);
123
+ background: transparent;
124
+ font-size: 0.66rem;
125
+ letter-spacing: 0;
126
+ text-align: left;
127
+ cursor: pointer;
128
+ }
129
+
130
+ .fermi-steps button::after {
131
+ display: inline-block;
132
+ width: 5px;
133
+ height: 5px;
134
+ margin-left: 0.45rem;
135
+ border-radius: 50%;
136
+ background: transparent;
137
+ content: "";
138
+ }
139
+
140
+ .fermi-steps button.is-active {
141
+ color: var(--fermi-accent);
142
+ }
143
+
144
+ .fermi-steps button.is-active::after {
145
+ background: var(--fermi-accent);
146
+ }
147
+
148
+ .fermi-active-filter {
149
+ display: grid;
150
+ grid-template-columns: minmax(0, 1fr) auto;
151
+ gap: 0 clamp(0.8rem, 2vw, 1.4rem);
152
+ align-items: center;
153
+ min-width: 0;
154
+ }
155
+
156
+ .fermi-active-filter span {
157
+ align-self: center;
158
+ padding-bottom: 0;
159
+ letter-spacing: 0.15em;
160
+ }
161
+
162
+ .fermi-active-filter output {
163
+ color: var(--fermi-accent);
164
+ font-size: clamp(2rem, 10vw, 5.6rem);
165
+ font-weight: 760;
166
+ font-variant-numeric: tabular-nums;
167
+ letter-spacing: 0;
168
+ line-height: 0.82;
169
+ }
170
+
171
+ .fermi-active-filter input[type='range'] {
172
+ grid-column: 1 / -1;
173
+ width: 100%;
174
+ height: 22px;
175
+ margin: 0;
176
+ appearance: none;
177
+ background: transparent;
178
+ cursor: ew-resize;
179
+ }
180
+
181
+ .fermi-active-filter input[type='range']::-webkit-slider-runnable-track {
182
+ height: 1px;
183
+ background: linear-gradient(90deg, var(--fermi-accent), var(--fermi-hairline));
184
+ }
185
+
186
+ .fermi-active-filter input[type='range']::-webkit-slider-thumb {
187
+ width: 10px;
188
+ height: 24px;
189
+ margin-top: -11.5px;
190
+ border: 0;
191
+ border-radius: 0;
192
+ appearance: none;
193
+ background: var(--fermi-warm);
194
+ box-shadow: 0 0 22px color-mix(in srgb, var(--fermi-warm) 60%, transparent);
195
+ }
196
+
197
+ .fermi-active-filter input[type='range']::-moz-range-track {
198
+ height: 1px;
199
+ background: linear-gradient(90deg, var(--fermi-accent), var(--fermi-hairline));
200
+ }
201
+
202
+ .fermi-active-filter input[type='range']::-moz-range-thumb {
203
+ width: 10px;
204
+ height: 24px;
205
+ border: 0;
206
+ border-radius: 0;
207
+ background: var(--fermi-warm);
208
+ box-shadow: 0 0 22px color-mix(in srgb, var(--fermi-warm) 60%, transparent);
209
+ }
210
+
211
+ .fermi-active-filter:focus-within output {
212
+ color: var(--fermi-warm);
213
+ }
214
+
215
+ @media (max-width: 680px) {
216
+ .fermi-metrics {
217
+ justify-content: flex-start;
218
+ }
219
+
220
+ .fermi-console {
221
+ grid-template-columns: 1fr;
222
+ }
223
+
224
+ .fermi-steps {
225
+ display: flex;
226
+ justify-content: space-between;
227
+ }
228
+ }