@jjlmoya/utils-science 1.35.0 → 1.37.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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/double-slit-decoherence/bibliography.astro +14 -0
  8. package/src/tool/double-slit-decoherence/bibliography.ts +12 -0
  9. package/src/tool/double-slit-decoherence/component.astro +235 -0
  10. package/src/tool/double-slit-decoherence/double-slit-decoherence-simulator.css +344 -0
  11. package/src/tool/double-slit-decoherence/entry.ts +26 -0
  12. package/src/tool/double-slit-decoherence/i18n/de.ts +181 -0
  13. package/src/tool/double-slit-decoherence/i18n/en.ts +181 -0
  14. package/src/tool/double-slit-decoherence/i18n/es.ts +181 -0
  15. package/src/tool/double-slit-decoherence/i18n/fr.ts +181 -0
  16. package/src/tool/double-slit-decoherence/i18n/id.ts +181 -0
  17. package/src/tool/double-slit-decoherence/i18n/it.ts +181 -0
  18. package/src/tool/double-slit-decoherence/i18n/ja.ts +181 -0
  19. package/src/tool/double-slit-decoherence/i18n/ko.ts +181 -0
  20. package/src/tool/double-slit-decoherence/i18n/nl.ts +181 -0
  21. package/src/tool/double-slit-decoherence/i18n/pl.ts +181 -0
  22. package/src/tool/double-slit-decoherence/i18n/pt.ts +181 -0
  23. package/src/tool/double-slit-decoherence/i18n/ru.ts +181 -0
  24. package/src/tool/double-slit-decoherence/i18n/sv.ts +181 -0
  25. package/src/tool/double-slit-decoherence/i18n/tr.ts +181 -0
  26. package/src/tool/double-slit-decoherence/i18n/zh.ts +181 -0
  27. package/src/tool/double-slit-decoherence/index.ts +11 -0
  28. package/src/tool/double-slit-decoherence/logic.ts +77 -0
  29. package/src/tool/double-slit-decoherence/seo.astro +15 -0
  30. package/src/tool/roche-limit-satellite-disruption/i18n/de.ts +12 -0
  31. package/src/tool/roche-limit-satellite-disruption/i18n/en.ts +12 -0
  32. package/src/tool/roche-limit-satellite-disruption/i18n/es.ts +12 -0
  33. package/src/tool/roche-limit-satellite-disruption/i18n/fr.ts +12 -0
  34. package/src/tool/roche-limit-satellite-disruption/i18n/id.ts +12 -0
  35. package/src/tool/roche-limit-satellite-disruption/i18n/it.ts +12 -0
  36. package/src/tool/roche-limit-satellite-disruption/i18n/ja.ts +12 -0
  37. package/src/tool/roche-limit-satellite-disruption/i18n/ko.ts +12 -0
  38. package/src/tool/roche-limit-satellite-disruption/i18n/nl.ts +12 -0
  39. package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +12 -0
  40. package/src/tool/roche-limit-satellite-disruption/i18n/pt.ts +12 -0
  41. package/src/tool/roche-limit-satellite-disruption/i18n/ru.ts +12 -0
  42. package/src/tool/roche-limit-satellite-disruption/i18n/sv.ts +12 -0
  43. package/src/tool/roche-limit-satellite-disruption/i18n/tr.ts +12 -0
  44. package/src/tool/roche-limit-satellite-disruption/i18n/zh.ts +12 -0
  45. package/src/tool/roche-limit-satellite-disruption/labels.ts +11 -0
  46. package/src/tool/roche-limit-satellite-disruption/script.ts +7 -7
  47. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -11,6 +11,7 @@ import { stellarHabitabilityZone } from '../tool/stellar-habitability-zone/index
11
11
  import { radioactiveDecay } from '../tool/radioactive-decay/index';
12
12
  import { naturalSelectionDrift } from '../tool/natural-selection-drift/index';
13
13
  import { entropySecondLaw } from '../tool/entropy-second-law/index';
14
+ import { doubleSlitDecoherence } from '../tool/double-slit-decoherence/index';
14
15
  import { phaseDiagramCriticalPoints } from '../tool/phase-diagram-critical-points/index';
15
16
  import { twinParadoxVisualizer } from '../tool/twin-paradox-visualizer/index';
16
17
  import { mandelbrotFractal } from '../tool/mandelbrot-fractal/index';
@@ -20,7 +21,7 @@ import { rocheLimitSatelliteDisruption } from '../tool/roche-limit-satellite-dis
20
21
 
21
22
  export const scienceCategory: ScienceCategoryEntry = {
22
23
  icon: 'mdi:flask',
23
- tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption],
24
+ tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption],
24
25
  i18n: {
25
26
  es: () => import('./i18n/es').then((m) => m.content),
26
27
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -11,6 +11,7 @@ export { stellarHabitabilityZone } from './tool/stellar-habitability-zone/entry'
11
11
  export { radioactiveDecay } from './tool/radioactive-decay/entry';
12
12
  export { naturalSelectionDrift } from './tool/natural-selection-drift/entry';
13
13
  export { entropySecondLaw } from './tool/entropy-second-law/entry';
14
+ export { doubleSlitDecoherence } from './tool/double-slit-decoherence/entry';
14
15
  export { phaseDiagramCriticalPoints } from './tool/phase-diagram-critical-points/entry';
15
16
  export { twinParadoxVisualizer } from './tool/twin-paradox-visualizer/entry';
16
17
  export { mandelbrotFractal } from './tool/mandelbrot-fractal/entry';
@@ -30,10 +31,11 @@ import { stellarHabitabilityZone } from './tool/stellar-habitability-zone/entry'
30
31
  import { radioactiveDecay } from './tool/radioactive-decay/entry';
31
32
  import { naturalSelectionDrift } from './tool/natural-selection-drift/entry';
32
33
  import { entropySecondLaw } from './tool/entropy-second-law/entry';
34
+ import { doubleSlitDecoherence } from './tool/double-slit-decoherence/entry';
33
35
  import { phaseDiagramCriticalPoints } from './tool/phase-diagram-critical-points/entry';
34
36
  import { twinParadoxVisualizer } from './tool/twin-paradox-visualizer/entry';
35
37
  import { mandelbrotFractal } from './tool/mandelbrot-fractal/entry';
36
38
  import { planetAtmosphereSurvival } from './tool/planet-atmosphere-survival/entry';
37
39
  import { threeBodyProblem } from './tool/three-body-problem/entry';
38
40
  import { rocheLimitSatelliteDisruption } from './tool/roche-limit-satellite-disruption/entry';
39
- export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption];
41
+ export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption];
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export { STELLAR_HABITABILITY_ZONE_TOOL } from './tool/stellar-habitability-zone
12
12
  export { RADIOACTIVE_DECAY_TOOL } from './tool/radioactive-decay/index';
13
13
  export { NATURAL_SELECTION_DRIFT_TOOL } from './tool/natural-selection-drift/index';
14
14
  export { ENTROPY_SECOND_LAW_TOOL } from './tool/entropy-second-law/index';
15
+ export { DOUBLE_SLIT_DECOHERENCE_TOOL } from './tool/double-slit-decoherence/index';
15
16
  export { PHASE_DIAGRAM_CRITICAL_POINTS_TOOL } from './tool/phase-diagram-critical-points/index';
16
17
  export { TWIN_PARADOX_VISUALIZER_TOOL } from './tool/twin-paradox-visualizer/index';
17
18
  export { MANDELBROT_FRACTAL_TOOL } from './tool/mandelbrot-fractal/index';
@@ -18,7 +18,7 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
  });
20
20
 
21
- it('all 18 tools registered', () => {
22
- expect(ALL_TOOLS.length).toBe(18);
21
+ it('all 19 tools registered', () => {
22
+ expect(ALL_TOOLS.length).toBe(19);
23
23
  });
24
24
  });
@@ -4,8 +4,8 @@ import { scienceCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 18 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(18);
7
+ it('should have 19 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(19);
9
9
  });
10
10
 
11
11
  it('scienceCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { doubleSlitDecoherence } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await doubleSlitDecoherence.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Feynman Lectures on Physics, Volume III, Chapter 1: Quantum Behavior',
6
+ url: 'https://www.feynmanlectures.caltech.edu/III_01.html',
7
+ },
8
+ {
9
+ name: 'Decoherence, the measurement problem, and interpretations of quantum mechanics',
10
+ url: 'https://arxiv.org/abs/quant-ph/0312059',
11
+ },
12
+ ];
@@ -0,0 +1,235 @@
1
+ ---
2
+ import './double-slit-decoherence-simulator.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="dslit-lab" id="dslit-lab">
12
+ <section class="dslit-stage" aria-label={ui.stage}>
13
+ <div class="dslit-apparatus">
14
+ <canvas id="dslit-canvas" width="860" height="520"></canvas>
15
+ <div class="dslit-source" aria-hidden="true"></div>
16
+ <div class="dslit-barrier" aria-hidden="true">
17
+ <span></span>
18
+ <i></i>
19
+ <span></span>
20
+ <i></i>
21
+ <span></span>
22
+ </div>
23
+ <div class="dslit-screen" aria-hidden="true"></div>
24
+ </div>
25
+
26
+ <div class="dslit-mode-strip">
27
+ <span>{ui.unobserved}</span>
28
+ <strong id="dslit-state-label" data-wave={ui.waveMode} data-particle={ui.particleMode}>{ui.waveMode}</strong>
29
+ <span>{ui.observed}</span>
30
+ </div>
31
+ </section>
32
+
33
+ <section class="dslit-panel" aria-label={ui.controls}>
34
+ <div class="dslit-kicker">{ui.kicker}</div>
35
+ <label class="dslit-toggle">
36
+ <input id="dslit-detector-enabled" type="checkbox" />
37
+ <span>{ui.detectorToggle}</span>
38
+ </label>
39
+
40
+ <label class="dslit-field" for="dslit-detector">
41
+ <span>{ui.detectorStrength}</span>
42
+ <output id="dslit-detector-output">0%</output>
43
+ <input id="dslit-detector" type="range" min="0" max="100" step="1" value="0" />
44
+ </label>
45
+
46
+ <label class="dslit-field" for="dslit-separation">
47
+ <span>{ui.slitSeparation}</span>
48
+ <output id="dslit-separation-output">1.80</output>
49
+ <input id="dslit-separation" type="range" min="80" max="280" step="5" value="180" />
50
+ </label>
51
+
52
+ <label class="dslit-field" for="dslit-width">
53
+ <span>{ui.slitWidth}</span>
54
+ <output id="dslit-width-output">0.42</output>
55
+ <input id="dslit-width" type="range" min="16" max="90" step="2" value="42" />
56
+ </label>
57
+ </section>
58
+
59
+ <section class="dslit-readout" aria-label={ui.results}>
60
+ <article>
61
+ <span>{ui.fringeVisibility}</span>
62
+ <strong id="dslit-visibility">1.00</strong>
63
+ </article>
64
+ <article>
65
+ <span>{ui.whichPath}</span>
66
+ <strong id="dslit-path">0%</strong>
67
+ </article>
68
+ <article>
69
+ <span>{ui.coherence}</span>
70
+ <strong id="dslit-coherence">100%</strong>
71
+ </article>
72
+ <p>{ui.readoutNote}</p>
73
+ </section>
74
+ </div>
75
+
76
+ <script>
77
+ import { calculateDoubleSlitPattern, measurePattern } from './logic';
78
+
79
+ const canvas = document.getElementById('dslit-canvas') as HTMLCanvasElement | null;
80
+ const ctx = canvas?.getContext('2d');
81
+ const detectorToggle = document.getElementById('dslit-detector-enabled') as HTMLInputElement | null;
82
+ const detectorInput = document.getElementById('dslit-detector') as HTMLInputElement | null;
83
+ const separationInput = document.getElementById('dslit-separation') as HTMLInputElement | null;
84
+ const widthInput = document.getElementById('dslit-width') as HTMLInputElement | null;
85
+ const detectorOutput = document.getElementById('dslit-detector-output');
86
+ const separationOutput = document.getElementById('dslit-separation-output');
87
+ const widthOutput = document.getElementById('dslit-width-output');
88
+ const visibilityOutput = document.getElementById('dslit-visibility');
89
+ const pathOutput = document.getElementById('dslit-path');
90
+ const coherenceOutput = document.getElementById('dslit-coherence');
91
+ const stateLabel = document.getElementById('dslit-state-label');
92
+
93
+ const state = {
94
+ detectorStrength: 0,
95
+ slitSeparation: 1.8,
96
+ slitWidth: 0.42,
97
+ wavelength: 0.36,
98
+ screenDistance: 4.2,
99
+ phase: 0,
100
+ raf: 0,
101
+ };
102
+
103
+ function isDarkTheme() {
104
+ return document.documentElement.classList.contains('theme-dark') || document.body.classList.contains('theme-dark');
105
+ }
106
+
107
+ function createBeamGradient(sourceX: number, screenX: number, isDark: boolean) {
108
+ const gradient = ctx!.createLinearGradient(sourceX, 0, screenX, 0);
109
+ gradient.addColorStop(0, isDark ? 'rgba(64, 224, 208, 0.1)' : 'rgba(0, 139, 130, 0.16)');
110
+ gradient.addColorStop(0.48, isDark ? 'rgba(64, 224, 208, 0.3)' : 'rgba(0, 139, 130, 0.42)');
111
+ gradient.addColorStop(1, isDark ? 'rgba(255, 207, 102, 0.2)' : 'rgba(181, 111, 0, 0.32)');
112
+
113
+ return gradient;
114
+ }
115
+
116
+ function drawInterferencePaths(
117
+ samples: ReturnType<typeof calculateDoubleSlitPattern>,
118
+ scene: { sourceX: number; slitX: number; screenX: number; centerY: number; slitGap: number },
119
+ isDark: boolean,
120
+ ) {
121
+ ctx!.strokeStyle = createBeamGradient(scene.sourceX, scene.screenX, isDark);
122
+ ctx!.lineWidth = isDark ? 1.2 : 1.45;
123
+ ctx!.globalCompositeOperation = isDark ? 'lighter' : 'multiply';
124
+
125
+ samples.forEach((sample, index) => {
126
+ if (index % 2 !== 0) return;
127
+ const screenY = scene.centerY + sample.y * 68;
128
+ const glow = Math.max(0.04, sample.observedIntensity);
129
+ ctx!.globalAlpha = Math.min(isDark ? 0.62 : 0.74, glow * (isDark ? 0.85 : 1.05));
130
+ ctx!.beginPath();
131
+ ctx!.moveTo(scene.sourceX, scene.centerY + Math.sin(state.phase + index * 0.05) * 8);
132
+ ctx!.quadraticCurveTo(scene.slitX, scene.centerY - scene.slitGap / 2, scene.screenX, screenY);
133
+ ctx!.stroke();
134
+ ctx!.beginPath();
135
+ ctx!.moveTo(scene.sourceX, scene.centerY + Math.cos(state.phase + index * 0.04) * 8);
136
+ ctx!.quadraticCurveTo(scene.slitX, scene.centerY + scene.slitGap / 2, scene.screenX, screenY);
137
+ ctx!.stroke();
138
+ });
139
+
140
+ ctx!.globalAlpha = 1;
141
+ ctx!.globalCompositeOperation = 'source-over';
142
+ }
143
+
144
+ function drawBarrier(scene: { slitX: number; centerY: number; slitGap: number; slitWidth: number }, isDark: boolean) {
145
+ ctx!.fillStyle = isDark ? 'rgba(249, 245, 234, 0.82)' : 'rgba(9, 18, 32, 0.86)';
146
+ ctx!.fillRect(scene.slitX - 12, 58, 24, scene.centerY - scene.slitGap / 2 - scene.slitWidth / 2 - 58);
147
+ ctx!.fillRect(scene.slitX - 12, scene.centerY - scene.slitGap / 2 + scene.slitWidth / 2, 24, scene.slitGap - scene.slitWidth);
148
+ ctx!.fillRect(scene.slitX - 12, scene.centerY + scene.slitGap / 2 + scene.slitWidth / 2, 24, canvas!.height - (scene.centerY + scene.slitGap / 2 + scene.slitWidth / 2) - 58);
149
+ }
150
+
151
+ function drawScreenPattern(
152
+ samples: ReturnType<typeof calculateDoubleSlitPattern>,
153
+ scene: { screenX: number; centerY: number },
154
+ isDark: boolean,
155
+ ) {
156
+ samples.forEach((sample) => {
157
+ const y = scene.centerY + sample.y * 68;
158
+ const intensity = Math.min(1, sample.observedIntensity);
159
+ ctx!.fillStyle = isDark
160
+ ? `rgba(255, ${Math.round(178 + intensity * 60)}, 92, ${0.12 + intensity * 0.8})`
161
+ : `rgba(${Math.round(170 + intensity * 30)}, ${Math.round(95 + intensity * 60)}, 0, ${0.22 + intensity * 0.72})`;
162
+ ctx!.fillRect(scene.screenX, y - 1.2 - intensity * 3, 34 + intensity * 30, 2.4 + intensity * 6);
163
+ });
164
+ }
165
+
166
+ function drawBeam(samples: ReturnType<typeof calculateDoubleSlitPattern>) {
167
+ if (!ctx || !canvas) return;
168
+
169
+ const isDark = isDarkTheme();
170
+ const scene = {
171
+ sourceX: 86,
172
+ slitX: 330,
173
+ screenX: 742,
174
+ centerY: canvas.height / 2,
175
+ slitGap: state.slitSeparation * 58,
176
+ slitWidth: state.slitWidth * 42,
177
+ };
178
+
179
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
180
+ drawInterferencePaths(samples, scene, isDark);
181
+ drawBarrier(scene, isDark);
182
+ drawScreenPattern(samples, scene, isDark);
183
+ }
184
+
185
+ function update() {
186
+ const samples = calculateDoubleSlitPattern(state, 201);
187
+ const metrics = measurePattern(samples, state.detectorStrength);
188
+
189
+ detectorOutput!.textContent = `${Math.round(state.detectorStrength * 100)}%`;
190
+ separationOutput!.textContent = state.slitSeparation.toFixed(2);
191
+ widthOutput!.textContent = state.slitWidth.toFixed(2);
192
+ visibilityOutput!.textContent = metrics.fringeVisibility.toFixed(2);
193
+ pathOutput!.textContent = `${Math.round(metrics.whichPath * 100)}%`;
194
+ coherenceOutput!.textContent = `${Math.round(metrics.coherence * 100)}%`;
195
+ stateLabel!.textContent = state.detectorStrength > 0.55
196
+ ? stateLabel!.getAttribute('data-particle') ?? 'Particle bands'
197
+ : stateLabel!.getAttribute('data-wave') ?? 'Interference';
198
+
199
+ detectorInput?.style.setProperty('--fill', `${state.detectorStrength * 100}%`);
200
+ separationInput?.style.setProperty('--fill', `${((state.slitSeparation * 100 - 80) / 200) * 100}%`);
201
+ widthInput?.style.setProperty('--fill', `${((state.slitWidth * 100 - 16) / 74) * 100}%`);
202
+
203
+ drawBeam(samples);
204
+ }
205
+
206
+ function animate() {
207
+ state.phase += 0.025 * (1 - state.detectorStrength * 0.55);
208
+ update();
209
+ state.raf = window.requestAnimationFrame(animate);
210
+ }
211
+
212
+ detectorToggle?.addEventListener('change', () => {
213
+ state.detectorStrength = detectorToggle.checked ? Math.max(0.72, Number(detectorInput?.value ?? 0) / 100) : 0;
214
+ if (detectorInput) detectorInput.value = String(Math.round(state.detectorStrength * 100));
215
+ update();
216
+ });
217
+
218
+ detectorInput?.addEventListener('input', () => {
219
+ state.detectorStrength = Number(detectorInput.value) / 100;
220
+ if (detectorToggle) detectorToggle.checked = state.detectorStrength > 0.02;
221
+ update();
222
+ });
223
+
224
+ separationInput?.addEventListener('input', () => {
225
+ state.slitSeparation = Number(separationInput.value) / 100;
226
+ update();
227
+ });
228
+
229
+ widthInput?.addEventListener('input', () => {
230
+ state.slitWidth = Number(widthInput.value) / 100;
231
+ update();
232
+ });
233
+
234
+ animate();
235
+ </script>
@@ -0,0 +1,344 @@
1
+ .dslit-lab {
2
+ --dslit-ink: #141722;
3
+ --dslit-muted: rgba(20, 23, 34, 0.62);
4
+ --dslit-line: rgba(20, 23, 34, 0.12);
5
+ --dslit-cyan: #40e0d0;
6
+ --dslit-cyan-ink: #008b82;
7
+ --dslit-amber: #ffcf66;
8
+ --dslit-amber-ink: #a86600;
9
+ --dslit-rose: #ff6f91;
10
+ --dslit-panel: transparent;
11
+ --dslit-track: rgba(20, 23, 34, 0.18);
12
+
13
+ display: grid;
14
+ gap: 1rem;
15
+ width: min(100%, 1220px);
16
+ padding: clamp(0.9rem, 3vw, 2rem);
17
+ color: var(--dslit-ink);
18
+ background:
19
+ linear-gradient(120deg, rgba(64, 224, 208, 0.18), transparent 38%),
20
+ linear-gradient(300deg, rgba(255, 207, 102, 0.18), transparent 44%),
21
+ #f7f3ea;
22
+ border: 1px solid rgba(20, 23, 34, 0.08);
23
+ border-radius: 24px;
24
+ box-shadow: 0 28px 70px rgba(45, 37, 24, 0.14);
25
+ }
26
+
27
+ .theme-dark .dslit-lab {
28
+ --dslit-ink: #f9f5ea;
29
+ --dslit-muted: rgba(249, 245, 234, 0.66);
30
+ --dslit-line: rgba(249, 245, 234, 0.14);
31
+ --dslit-panel: rgba(10, 14, 24, 0.54);
32
+ --dslit-track: rgba(249, 245, 234, 0.18);
33
+
34
+ background:
35
+ linear-gradient(120deg, rgba(64, 224, 208, 0.16), transparent 38%),
36
+ linear-gradient(300deg, rgba(255, 111, 145, 0.14), transparent 44%),
37
+ #090e18;
38
+ border-color: rgba(255, 255, 255, 0.08);
39
+ }
40
+
41
+ .dslit-stage,
42
+ .dslit-panel,
43
+ .dslit-readout {
44
+ min-width: 0;
45
+ }
46
+
47
+ .dslit-stage {
48
+ display: grid;
49
+ gap: 0.85rem;
50
+ }
51
+
52
+ .dslit-apparatus {
53
+ position: relative;
54
+ overflow: hidden;
55
+ min-height: 320px;
56
+ border: 1px solid var(--dslit-line);
57
+ border-radius: 18px;
58
+ background:
59
+ repeating-linear-gradient(90deg, transparent 0 42px, rgba(20, 23, 34, 0.045) 42px 43px),
60
+ linear-gradient(180deg, rgba(255, 255, 255, 0.16), transparent);
61
+ }
62
+
63
+ .theme-dark .dslit-apparatus {
64
+ background:
65
+ repeating-linear-gradient(90deg, transparent 0 42px, rgba(249, 245, 234, 0.035) 42px 43px),
66
+ linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent);
67
+ }
68
+
69
+ #dslit-canvas {
70
+ display: block;
71
+ width: 100%;
72
+ height: auto;
73
+ aspect-ratio: 43 / 26;
74
+ }
75
+
76
+ .dslit-source,
77
+ .dslit-screen,
78
+ .dslit-barrier {
79
+ position: absolute;
80
+ top: 11%;
81
+ bottom: 11%;
82
+ pointer-events: none;
83
+ }
84
+
85
+ .dslit-source {
86
+ left: 8%;
87
+ width: 10px;
88
+ border-radius: 999px;
89
+ background: linear-gradient(180deg, transparent, var(--dslit-cyan-ink), transparent);
90
+ box-shadow: 0 0 22px rgba(0, 139, 130, 0.42);
91
+ }
92
+
93
+ .theme-dark .dslit-source {
94
+ background: linear-gradient(180deg, transparent, var(--dslit-cyan), transparent);
95
+ box-shadow: 0 0 28px rgba(64, 224, 208, 0.72);
96
+ }
97
+
98
+ .dslit-screen {
99
+ right: 10%;
100
+ width: 12px;
101
+ border-radius: 999px;
102
+ background: linear-gradient(180deg, transparent, rgba(168, 102, 0, 0.9), transparent);
103
+ box-shadow: 0 0 24px rgba(168, 102, 0, 0.28);
104
+ }
105
+
106
+ .theme-dark .dslit-screen {
107
+ background: linear-gradient(180deg, transparent, rgba(255, 207, 102, 0.92), transparent);
108
+ box-shadow: 0 0 34px rgba(255, 207, 102, 0.46);
109
+ }
110
+
111
+ .dslit-barrier {
112
+ left: 38.2%;
113
+ display: grid;
114
+ grid-template-rows: 1fr 34px 0.7fr 34px 1fr;
115
+ width: 18px;
116
+ }
117
+
118
+ .dslit-barrier span {
119
+ display: block;
120
+ background: rgba(20, 23, 34, 0.84);
121
+ }
122
+
123
+ .theme-dark .dslit-barrier span {
124
+ background: rgba(249, 245, 234, 0.82);
125
+ }
126
+
127
+ .dslit-barrier i {
128
+ display: block;
129
+ background: rgba(64, 224, 208, 0.22);
130
+ box-shadow: inset 0 0 16px rgba(64, 224, 208, 0.32);
131
+ }
132
+
133
+ .dslit-mode-strip {
134
+ display: grid;
135
+ grid-template-columns: 1fr minmax(8.5rem, auto) 1fr;
136
+ gap: 0.8rem;
137
+ align-items: center;
138
+ min-height: 2.2rem;
139
+ }
140
+
141
+ .dslit-mode-strip span,
142
+ .dslit-kicker,
143
+ .dslit-field span,
144
+ .dslit-readout span {
145
+ color: var(--dslit-muted);
146
+ font-size: 0.72rem;
147
+ font-weight: 700;
148
+ letter-spacing: 0.12em;
149
+ text-transform: uppercase;
150
+ }
151
+
152
+ .dslit-mode-strip span:last-child {
153
+ text-align: right;
154
+ }
155
+
156
+ .dslit-mode-strip strong {
157
+ align-self: center;
158
+ justify-self: center;
159
+ padding: 0.22rem 0;
160
+ border-bottom: 1px solid currentcolor;
161
+ color: var(--dslit-ink);
162
+ font-size: 0.78rem;
163
+ line-height: 1;
164
+ text-align: center;
165
+ }
166
+
167
+ .dslit-panel {
168
+ display: grid;
169
+ gap: 1rem;
170
+ padding: 0.25rem 0 0.25rem 1rem;
171
+ border-left: 1px solid var(--dslit-line);
172
+ background: transparent;
173
+ }
174
+
175
+ .theme-dark .dslit-panel {
176
+ padding: 1rem;
177
+ border: 1px solid var(--dslit-line);
178
+ border-radius: 18px;
179
+ background: var(--dslit-panel);
180
+ }
181
+
182
+ .dslit-toggle {
183
+ display: flex;
184
+ gap: 0.75rem;
185
+ align-items: center;
186
+ min-height: 48px;
187
+ color: var(--dslit-ink);
188
+ font-weight: 750;
189
+ cursor: pointer;
190
+ }
191
+
192
+ .dslit-toggle input {
193
+ width: 46px;
194
+ height: 26px;
195
+ appearance: none;
196
+ border: 1px solid rgba(20, 23, 34, 0.18);
197
+ border-radius: 999px;
198
+ background: rgba(20, 23, 34, 0.045);
199
+ cursor: pointer;
200
+ }
201
+
202
+ .theme-dark .dslit-toggle input {
203
+ border-color: var(--dslit-line);
204
+ background: rgba(249, 245, 234, 0.12);
205
+ }
206
+
207
+ .dslit-toggle input::before {
208
+ display: block;
209
+ width: 20px;
210
+ height: 20px;
211
+ margin: 2px;
212
+ border-radius: 50%;
213
+ background: var(--dslit-cyan-ink);
214
+ transition: transform 180ms ease, background-color 180ms ease;
215
+ content: '';
216
+ }
217
+
218
+ .theme-dark .dslit-toggle input::before {
219
+ background: var(--dslit-ink);
220
+ }
221
+
222
+ .dslit-toggle input:checked::before {
223
+ background: var(--dslit-rose);
224
+ transform: translateX(20px);
225
+ }
226
+
227
+ .dslit-field {
228
+ display: grid;
229
+ gap: 0.55rem;
230
+ width: 100%;
231
+ }
232
+
233
+ .dslit-field output {
234
+ font-size: clamp(2rem, 9vw, 3.2rem);
235
+ font-weight: 760;
236
+ letter-spacing: 0;
237
+ line-height: 0.9;
238
+ }
239
+
240
+ .dslit-field input[type='range'] {
241
+ --fill: 0%;
242
+
243
+ width: 100%;
244
+ height: 24px;
245
+ margin: 0;
246
+ appearance: none;
247
+ background: transparent;
248
+ vertical-align: middle;
249
+ }
250
+
251
+ .dslit-field input[type='range']::-webkit-slider-runnable-track {
252
+ height: 3px;
253
+ border-radius: 999px;
254
+ background: linear-gradient(90deg, var(--dslit-cyan-ink) 0 var(--fill), var(--dslit-track) var(--fill) 100%);
255
+ }
256
+
257
+ .dslit-field input[type='range']::-webkit-slider-thumb {
258
+ width: 16px;
259
+ height: 16px;
260
+ margin-top: -6.5px;
261
+ appearance: none;
262
+ border: 2px solid #f7f3ea;
263
+ border-radius: 50%;
264
+ background: var(--dslit-cyan-ink);
265
+ box-shadow: 0 0 0 3px rgba(0, 139, 130, 0.08);
266
+ cursor: grab;
267
+ }
268
+
269
+ .theme-dark .dslit-field input[type='range']::-webkit-slider-runnable-track {
270
+ background: linear-gradient(90deg, var(--dslit-cyan) 0 var(--fill), var(--dslit-track) var(--fill) 100%);
271
+ }
272
+
273
+ .theme-dark .dslit-field input[type='range']::-webkit-slider-thumb {
274
+ border-color: #090e18;
275
+ background: var(--dslit-ink);
276
+ box-shadow: 0 0 0 3px rgba(64, 224, 208, 0.08);
277
+ }
278
+
279
+ .dslit-field input[type='range']::-moz-range-track {
280
+ height: 3px;
281
+ border-radius: 999px;
282
+ background: var(--dslit-track);
283
+ }
284
+
285
+ .dslit-field input[type='range']::-moz-range-progress {
286
+ height: 3px;
287
+ border-radius: 999px;
288
+ background: var(--dslit-cyan-ink);
289
+ }
290
+
291
+ .dslit-field input[type='range']::-moz-range-thumb {
292
+ width: 16px;
293
+ height: 16px;
294
+ border: 2px solid #f7f3ea;
295
+ border-radius: 50%;
296
+ background: var(--dslit-cyan-ink);
297
+ box-shadow: 0 0 0 3px rgba(0, 139, 130, 0.08);
298
+ cursor: grab;
299
+ }
300
+
301
+ .theme-dark .dslit-field input[type='range']::-moz-range-progress {
302
+ background: var(--dslit-cyan);
303
+ }
304
+
305
+ .theme-dark .dslit-field input[type='range']::-moz-range-thumb {
306
+ border-color: #090e18;
307
+ background: var(--dslit-ink);
308
+ box-shadow: 0 0 0 3px rgba(64, 224, 208, 0.08);
309
+ }
310
+
311
+ .dslit-readout {
312
+ display: grid;
313
+ gap: 0.75rem;
314
+ align-content: start;
315
+ padding: 0.25rem 0 0.25rem 1rem;
316
+ border-left: 1px solid var(--dslit-line);
317
+ }
318
+
319
+ .dslit-readout article {
320
+ display: grid;
321
+ gap: 0.32rem;
322
+ padding: 0.75rem 0;
323
+ border-bottom: 1px solid var(--dslit-line);
324
+ }
325
+
326
+ .dslit-readout strong {
327
+ font-size: clamp(2rem, 8vw, 3.7rem);
328
+ line-height: 0.86;
329
+ }
330
+
331
+ .dslit-readout p {
332
+ max-width: 42ch;
333
+ margin: 0;
334
+ color: var(--dslit-muted);
335
+ font-size: 0.88rem;
336
+ line-height: 1.58;
337
+ }
338
+
339
+ @media (min-width: 940px) {
340
+ .dslit-lab {
341
+ grid-template-columns: minmax(0, 1.55fr) minmax(250px, 0.62fr) minmax(230px, 0.54fr);
342
+ align-items: start;
343
+ }
344
+ }