@jjlmoya/utils-science 1.28.0 → 1.29.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 (30) 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/phase-diagram-critical-points/bibliography.astro +14 -0
  8. package/src/tool/phase-diagram-critical-points/bibliography.ts +16 -0
  9. package/src/tool/phase-diagram-critical-points/component.astro +397 -0
  10. package/src/tool/phase-diagram-critical-points/entry.ts +26 -0
  11. package/src/tool/phase-diagram-critical-points/i18n/de.ts +179 -0
  12. package/src/tool/phase-diagram-critical-points/i18n/en.ts +181 -0
  13. package/src/tool/phase-diagram-critical-points/i18n/es.ts +179 -0
  14. package/src/tool/phase-diagram-critical-points/i18n/fr.ts +179 -0
  15. package/src/tool/phase-diagram-critical-points/i18n/id.ts +179 -0
  16. package/src/tool/phase-diagram-critical-points/i18n/it.ts +179 -0
  17. package/src/tool/phase-diagram-critical-points/i18n/ja.ts +179 -0
  18. package/src/tool/phase-diagram-critical-points/i18n/ko.ts +179 -0
  19. package/src/tool/phase-diagram-critical-points/i18n/nl.ts +179 -0
  20. package/src/tool/phase-diagram-critical-points/i18n/pl.ts +179 -0
  21. package/src/tool/phase-diagram-critical-points/i18n/pt.ts +179 -0
  22. package/src/tool/phase-diagram-critical-points/i18n/ru.ts +179 -0
  23. package/src/tool/phase-diagram-critical-points/i18n/sv.ts +179 -0
  24. package/src/tool/phase-diagram-critical-points/i18n/tr.ts +179 -0
  25. package/src/tool/phase-diagram-critical-points/i18n/zh.ts +179 -0
  26. package/src/tool/phase-diagram-critical-points/index.ts +11 -0
  27. package/src/tool/phase-diagram-critical-points/logic.ts +179 -0
  28. package/src/tool/phase-diagram-critical-points/phase-diagram-critical-points-visualizer.css +542 -0
  29. package/src/tool/phase-diagram-critical-points/seo.astro +15 -0
  30. package/src/tools.ts +2 -0
@@ -0,0 +1,179 @@
1
+ export interface SubstanceProfile {
2
+ id: string;
3
+ name: string;
4
+ tripleTemperature: number;
5
+ triplePressure: number;
6
+ criticalTemperature: number;
7
+ criticalPressure: number;
8
+ normalBoilingPoint: number;
9
+ fusionSlope: number;
10
+ }
11
+
12
+ export interface PhasePoint {
13
+ temperature: number;
14
+ pressure: number;
15
+ }
16
+
17
+ export interface PhaseSample extends PhasePoint {
18
+ phase: 'solid' | 'liquid' | 'gas' | 'supercritical';
19
+ proximityToCritical: number;
20
+ latentHeatIndex: number;
21
+ }
22
+
23
+ export const SUBSTANCES: SubstanceProfile[] = [
24
+ {
25
+ id: 'water',
26
+ name: 'Water',
27
+ tripleTemperature: 273.16,
28
+ triplePressure: 0.006,
29
+ criticalTemperature: 647.1,
30
+ criticalPressure: 22.06,
31
+ normalBoilingPoint: 373.15,
32
+ fusionSlope: -0.075,
33
+ },
34
+ {
35
+ id: 'carbon-dioxide',
36
+ name: 'Carbon dioxide',
37
+ tripleTemperature: 216.58,
38
+ triplePressure: 0.518,
39
+ criticalTemperature: 304.13,
40
+ criticalPressure: 7.38,
41
+ normalBoilingPoint: 194.67,
42
+ fusionSlope: 0.18,
43
+ },
44
+ {
45
+ id: 'nitrogen',
46
+ name: 'Nitrogen',
47
+ tripleTemperature: 63.15,
48
+ triplePressure: 0.0125,
49
+ criticalTemperature: 126.2,
50
+ criticalPressure: 3.4,
51
+ normalBoilingPoint: 77.36,
52
+ fusionSlope: 0.09,
53
+ },
54
+ ];
55
+
56
+ export function getSubstance(id: string): SubstanceProfile {
57
+ return SUBSTANCES.find((substance) => substance.id === id) ?? SUBSTANCES[0];
58
+ }
59
+
60
+ function clamp(value: number, min: number, max: number): number {
61
+ return Math.min(max, Math.max(min, value));
62
+ }
63
+
64
+ export function vaporPressureAt(substance: SubstanceProfile, temperature: number): number {
65
+ const span = substance.criticalTemperature - substance.tripleTemperature;
66
+ const progress = clamp((temperature - substance.tripleTemperature) / span, 0, 1);
67
+ const curved = Math.pow(progress, 1.52);
68
+ const logTriple = Math.log10(substance.triplePressure);
69
+ const logCritical = Math.log10(substance.criticalPressure);
70
+
71
+ return 10 ** (logTriple + (logCritical - logTriple) * curved);
72
+ }
73
+
74
+ export function meltingPressureAt(substance: SubstanceProfile, temperature: number): number {
75
+ const delta = temperature - substance.tripleTemperature;
76
+ const pressure = substance.triplePressure + delta * substance.fusionSlope;
77
+
78
+ return clamp(pressure, 0.001, substance.criticalPressure * 1.4);
79
+ }
80
+
81
+ function metricsForPhase(substance: SubstanceProfile, point: PhasePoint) {
82
+ const criticalDistance = Math.hypot(
83
+ (point.temperature - substance.criticalTemperature) / substance.criticalTemperature,
84
+ (point.pressure - substance.criticalPressure) / substance.criticalPressure,
85
+ );
86
+ const proximityToCritical = clamp(1 - criticalDistance * 3.2, 0, 1);
87
+
88
+ return {
89
+ proximityToCritical,
90
+ latentHeatIndex: clamp(1 - proximityToCritical, 0, 1),
91
+ };
92
+ }
93
+
94
+ function lowTemperaturePhase(substance: SubstanceProfile, point: PhasePoint): PhaseSample['phase'] {
95
+ const sublimationPressure = vaporPressureAt(substance, substance.tripleTemperature)
96
+ * Math.exp((point.temperature - substance.tripleTemperature) / Math.max(18, substance.tripleTemperature * 0.18));
97
+
98
+ return point.pressure >= sublimationPressure ? 'solid' : 'gas';
99
+ }
100
+
101
+ function isSolidAboveMeltingLine(substance: SubstanceProfile, point: PhasePoint): boolean {
102
+ return substance.fusionSlope > 0
103
+ && point.pressure > meltingPressureAt(substance, point.temperature)
104
+ && point.temperature < substance.criticalTemperature * 0.92;
105
+ }
106
+
107
+ function determinePhase(substance: SubstanceProfile, point: PhasePoint): PhaseSample['phase'] {
108
+ if (point.temperature >= substance.criticalTemperature && point.pressure >= substance.criticalPressure) {
109
+ return 'supercritical';
110
+ }
111
+
112
+ if (point.temperature < substance.tripleTemperature) {
113
+ return lowTemperaturePhase(substance, point);
114
+ }
115
+
116
+ if (isSolidAboveMeltingLine(substance, point)) {
117
+ return 'solid';
118
+ }
119
+
120
+ const vaporPressure = vaporPressureAt(substance, point.temperature);
121
+ return point.pressure >= vaporPressure ? 'liquid' : 'gas';
122
+ }
123
+
124
+ export function classifyPhase(substance: SubstanceProfile, point: PhasePoint): PhaseSample {
125
+ const { proximityToCritical, latentHeatIndex } = metricsForPhase(substance, point);
126
+ const phase = determinePhase(substance, point);
127
+
128
+ return { ...point, phase, proximityToCritical, latentHeatIndex };
129
+ }
130
+
131
+ export function buildPhaseCurve(substance: SubstanceProfile, steps = 72): PhasePoint[] {
132
+ return Array.from({ length: steps }, (_, index) => {
133
+ const progress = index / (steps - 1);
134
+ const temperature = substance.tripleTemperature
135
+ + (substance.criticalTemperature - substance.tripleTemperature) * progress;
136
+
137
+ return {
138
+ temperature,
139
+ pressure: vaporPressureAt(substance, temperature),
140
+ };
141
+ });
142
+ }
143
+
144
+ export function formatTemperature(value: number): string {
145
+ return `${Math.round(value)} K`;
146
+ }
147
+
148
+ export function formatPressure(value: number): string {
149
+ if (value < 0.1) return `${value.toFixed(3)} MPa`;
150
+ if (value < 10) return `${value.toFixed(2)} MPa`;
151
+ return `${value.toFixed(1)} MPa`;
152
+ }
153
+
154
+ export type UnitSystem = 'scientific' | 'metric' | 'imperial';
155
+
156
+ export function formatTemperatureForUnits(valueKelvin: number, unitSystem: UnitSystem): string {
157
+ if (unitSystem === 'metric') {
158
+ return `${Math.round(valueKelvin - 273.15)} C`;
159
+ }
160
+
161
+ if (unitSystem === 'imperial') {
162
+ return `${Math.round((valueKelvin - 273.15) * (9 / 5) + 32)} F`;
163
+ }
164
+
165
+ return formatTemperature(valueKelvin);
166
+ }
167
+
168
+ export function formatPressureForUnits(valueMpa: number, unitSystem: UnitSystem): string {
169
+ if (unitSystem === 'metric') {
170
+ const kpa = valueMpa * 1000;
171
+ return kpa >= 1000 ? `${(kpa / 1000).toFixed(2)} MPa` : `${kpa.toFixed(1)} kPa`;
172
+ }
173
+
174
+ if (unitSystem === 'imperial') {
175
+ return `${(valueMpa * 145.038).toFixed(1)} psi`;
176
+ }
177
+
178
+ return formatPressure(valueMpa);
179
+ }
@@ -0,0 +1,542 @@
1
+ .phase-lab {
2
+ --phase-ink: #182028;
3
+ --phase-muted: rgba(24, 32, 40, 0.62);
4
+ --phase-panel: rgba(255, 255, 255, 0.72);
5
+ --phase-line: rgba(24, 32, 40, 0.12);
6
+ --phase-hairline: rgba(24, 32, 40, 0.09);
7
+ --phase-label: #5f6b70;
8
+ --phase-track: rgba(24, 32, 40, 0.25);
9
+ --phase-solid: #8ea3b0;
10
+ --phase-liquid: #2e8bc0;
11
+ --phase-gas: #e3b341;
12
+ --phase-super: #36b18f;
13
+ --phase-critical: #e65355;
14
+
15
+ display: grid;
16
+ gap: 1rem;
17
+ width: min(100%, 1180px);
18
+ padding: clamp(1rem, 3vw, 1.7rem);
19
+ color: var(--phase-ink);
20
+ background:
21
+ linear-gradient(135deg, rgba(227, 179, 65, 0.18), transparent 32%),
22
+ linear-gradient(225deg, rgba(54, 177, 143, 0.18), transparent 34%),
23
+ #eef3f1;
24
+ border: 1px solid rgba(24, 32, 40, 0.08);
25
+ border-radius: 8px;
26
+ box-shadow: 0 28px 70px rgba(32, 58, 64, 0.16);
27
+ }
28
+
29
+ .theme-dark .phase-lab {
30
+ --phase-ink: #f7fbf7;
31
+ --phase-muted: rgba(247, 251, 247, 0.66);
32
+ --phase-panel: rgba(8, 16, 20, 0.74);
33
+ --phase-line: rgba(247, 251, 247, 0.13);
34
+ --phase-hairline: rgba(247, 251, 247, 0.11);
35
+ --phase-label: rgba(247, 251, 247, 0.58);
36
+ --phase-track: rgba(247, 251, 247, 0.24);
37
+
38
+ background:
39
+ linear-gradient(135deg, rgba(227, 179, 65, 0.18), transparent 32%),
40
+ linear-gradient(225deg, rgba(54, 177, 143, 0.22), transparent 34%),
41
+ #081014;
42
+ border-color: rgba(255, 255, 255, 0.08);
43
+ box-shadow: 0 28px 70px rgba(0, 0, 0, 0.38);
44
+ }
45
+
46
+ .phase-panel,
47
+ .phase-map-shell {
48
+ min-width: 0;
49
+ }
50
+
51
+ .phase-panel {
52
+ display: grid;
53
+ gap: 1.15rem;
54
+ align-content: start;
55
+ padding: 0.25rem 0.1rem;
56
+ background: transparent;
57
+ border: 0;
58
+ border-radius: 0;
59
+ }
60
+
61
+ .phase-field {
62
+ display: grid;
63
+ gap: 0.62rem;
64
+ padding-bottom: 1.05rem;
65
+ border-bottom: 1px solid var(--phase-hairline);
66
+ }
67
+
68
+ .phase-field span,
69
+ .phase-readout > span,
70
+ .phase-meter span,
71
+ .phase-coexistence-head span {
72
+ color: var(--phase-muted);
73
+ font-size: 0.72rem;
74
+ font-weight: 700;
75
+ letter-spacing: 0.08em;
76
+ text-transform: uppercase;
77
+ }
78
+
79
+ .phase-field output,
80
+ .phase-readout strong {
81
+ font-weight: 760;
82
+ line-height: 0.92;
83
+ }
84
+
85
+ .phase-field output {
86
+ font-size: clamp(1.65rem, 7vw, 3rem);
87
+ }
88
+
89
+ .phase-field select {
90
+ width: 100%;
91
+ min-height: 42px;
92
+ padding: 0 2.2rem 0 0.45rem;
93
+ color: var(--phase-ink);
94
+ appearance: none;
95
+ background:
96
+ linear-gradient(45deg, transparent 50%, currentcolor 50%) right 0.92rem center / 6px 6px no-repeat,
97
+ linear-gradient(135deg, currentcolor 50%, transparent 50%) right 0.58rem center / 6px 6px no-repeat;
98
+ border: 0;
99
+ border-bottom: 1px solid var(--phase-line);
100
+ border-radius: 0;
101
+ font-weight: 700;
102
+ }
103
+
104
+ .theme-dark .phase-field select {
105
+ color: var(--phase-ink);
106
+ background:
107
+ linear-gradient(45deg, transparent 50%, currentcolor 50%) right 0.92rem center / 6px 6px no-repeat,
108
+ linear-gradient(135deg, currentcolor 50%, transparent 50%) right 0.58rem center / 6px 6px no-repeat,
109
+ #081014;
110
+ }
111
+
112
+ .phase-field select option {
113
+ color: #182028;
114
+ background: #eef3f1;
115
+ }
116
+
117
+ .theme-dark .phase-field select option {
118
+ color: #f7fbf7;
119
+ background: #081014;
120
+ }
121
+
122
+ .phase-field input[type='range'] {
123
+ --fill: 50%;
124
+
125
+ width: 100%;
126
+ height: 24px;
127
+ margin: 0;
128
+ appearance: none;
129
+ background: transparent;
130
+ }
131
+
132
+ .phase-field input[type='range']::-webkit-slider-runnable-track {
133
+ height: 2px;
134
+ border-radius: 0;
135
+ background:
136
+ linear-gradient(90deg, var(--phase-ink) 0, var(--phase-ink) var(--fill), var(--phase-track) var(--fill), var(--phase-track) 100%);
137
+ }
138
+
139
+ .phase-field input[type='range']::-webkit-slider-thumb {
140
+ width: 13px;
141
+ height: 13px;
142
+ margin-top: -5.5px;
143
+ border: 2px solid var(--phase-ink);
144
+ border-radius: 50%;
145
+ appearance: none;
146
+ background: var(--phase-panel);
147
+ box-shadow: none;
148
+ cursor: grab;
149
+ }
150
+
151
+ .phase-field input[type='range']::-moz-range-track {
152
+ height: 2px;
153
+ border-radius: 0;
154
+ background: var(--phase-track);
155
+ }
156
+
157
+ .phase-field input[type='range']::-moz-range-progress {
158
+ height: 2px;
159
+ border-radius: 0;
160
+ background: var(--phase-ink);
161
+ }
162
+
163
+ .phase-field input[type='range']::-moz-range-thumb {
164
+ width: 13px;
165
+ height: 13px;
166
+ border: 2px solid var(--phase-ink);
167
+ border-radius: 50%;
168
+ background: var(--phase-panel);
169
+ box-shadow: none;
170
+ cursor: grab;
171
+ }
172
+
173
+ #phase-reset {
174
+ justify-self: start;
175
+ min-height: 32px;
176
+ padding: 0;
177
+ color: var(--phase-muted);
178
+ background: transparent;
179
+ border: 0;
180
+ border-radius: 0;
181
+ font-size: 0.8rem;
182
+ font-weight: 800;
183
+ letter-spacing: 0.08em;
184
+ text-transform: uppercase;
185
+ cursor: pointer;
186
+ opacity: 0.62;
187
+ transition: opacity 160ms ease;
188
+ }
189
+
190
+ #phase-reset:hover {
191
+ opacity: 1;
192
+ }
193
+
194
+ .theme-dark #phase-reset {
195
+ color: var(--phase-muted);
196
+ background: transparent;
197
+ }
198
+
199
+ .phase-map-shell {
200
+ padding: 0;
201
+ overflow: visible;
202
+ background: transparent;
203
+ border: 0;
204
+ border-radius: 0;
205
+ }
206
+
207
+ .phase-map-frame {
208
+ position: relative;
209
+ padding: 0 0 1.05rem 1.1rem;
210
+ }
211
+
212
+ .theme-dark .phase-map-shell {
213
+ background: transparent;
214
+ }
215
+
216
+ .phase-map {
217
+ display: block;
218
+ width: 100%;
219
+ height: auto;
220
+ min-height: clamp(390px, 52vw, 560px);
221
+ }
222
+
223
+ .phase-axis-label {
224
+ position: absolute;
225
+ color: var(--phase-muted);
226
+ font-size: 0.58rem;
227
+ font-weight: 800;
228
+ letter-spacing: 0.12em;
229
+ opacity: 0.58;
230
+ pointer-events: none;
231
+ text-transform: uppercase;
232
+ }
233
+
234
+ .phase-axis-label-y {
235
+ top: 50%;
236
+ left: 0;
237
+ transform: translate(-48%, -50%) rotate(-90deg);
238
+ transform-origin: center;
239
+ }
240
+
241
+ .phase-axis-label-x {
242
+ bottom: 0;
243
+ left: calc(1.1rem + 50%);
244
+ transform: translateX(-50%);
245
+ }
246
+
247
+ .phase-region {
248
+ stroke: none;
249
+ }
250
+
251
+ .phase-region-gas {
252
+ fill: rgba(227, 179, 65, 0.06);
253
+ }
254
+
255
+ .phase-region-solid {
256
+ fill: rgba(142, 163, 176, 0.08);
257
+ }
258
+
259
+ .phase-region-liquid {
260
+ fill: rgba(46, 139, 192, 0.07);
261
+ }
262
+
263
+ .phase-region-supercritical {
264
+ fill: rgba(54, 177, 143, 0.07);
265
+ }
266
+
267
+ .phase-grid {
268
+ fill: none;
269
+ stroke: var(--phase-hairline);
270
+ stroke-width: 1;
271
+ }
272
+
273
+ .phase-boundary {
274
+ fill: none;
275
+ stroke-linecap: round;
276
+ stroke-linejoin: round;
277
+ }
278
+
279
+ .phase-vapor {
280
+ stroke: var(--phase-critical);
281
+ stroke-width: 1.5;
282
+ }
283
+
284
+ .phase-melt {
285
+ stroke: var(--phase-ink);
286
+ stroke-width: 1.5;
287
+ stroke-dasharray: 8 10;
288
+ opacity: 0.62;
289
+ }
290
+
291
+ .phase-point circle {
292
+ fill: var(--phase-ink);
293
+ stroke: #fff;
294
+ stroke-width: 2;
295
+ }
296
+
297
+ .phase-critical circle {
298
+ fill: var(--phase-critical);
299
+ }
300
+
301
+ .phase-point text,
302
+ .phase-region-label {
303
+ fill: var(--phase-ink);
304
+ font-weight: 800;
305
+ }
306
+
307
+ .phase-point text {
308
+ font-size: 10px;
309
+ letter-spacing: 0.08em;
310
+ opacity: 0.5;
311
+ paint-order: stroke;
312
+ stroke: #eef3f1;
313
+ stroke-width: 4px;
314
+ stroke-linejoin: round;
315
+ text-transform: uppercase;
316
+ }
317
+
318
+ .theme-dark .phase-point text {
319
+ stroke: #081014;
320
+ }
321
+
322
+ .phase-region-label {
323
+ fill: var(--phase-label);
324
+ font-size: 10px;
325
+ letter-spacing: 0.14em;
326
+ opacity: 0.5;
327
+ paint-order: stroke;
328
+ stroke: rgba(238, 243, 241, 0.7);
329
+ stroke-width: 4px;
330
+ stroke-linejoin: round;
331
+ text-transform: uppercase;
332
+ }
333
+
334
+ .theme-dark .phase-region-label {
335
+ stroke: rgba(8, 16, 20, 0.74);
336
+ }
337
+
338
+ .phase-sample-marker {
339
+ transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
340
+ }
341
+
342
+ .phase-sample-trail {
343
+ fill: none;
344
+ stroke: var(--phase-critical);
345
+ transform-origin: center;
346
+ }
347
+
348
+ .phase-sample-trail-outer {
349
+ stroke-width: 1;
350
+ opacity: 0.26;
351
+ animation: phase-pulse 1800ms ease-in-out infinite;
352
+ }
353
+
354
+ .phase-sample-trail-inner {
355
+ stroke-width: 4;
356
+ stroke-dasharray: 7 10;
357
+ opacity: 0.36;
358
+ animation: phase-spin 5200ms linear infinite;
359
+ }
360
+
361
+ .phase-sample-dot {
362
+ fill: var(--phase-critical);
363
+ stroke: #fff;
364
+ stroke-width: 3;
365
+ }
366
+
367
+ .phase-sample-marker[data-phase='solid'] .phase-sample-dot {
368
+ fill: var(--phase-solid);
369
+ }
370
+
371
+ .phase-sample-marker[data-phase='liquid'] .phase-sample-dot {
372
+ fill: var(--phase-liquid);
373
+ }
374
+
375
+ .phase-sample-marker[data-phase='gas'] .phase-sample-dot {
376
+ fill: var(--phase-gas);
377
+ }
378
+
379
+ .phase-sample-marker[data-phase='supercritical'] .phase-sample-dot {
380
+ fill: var(--phase-super);
381
+ }
382
+
383
+ .phase-readout {
384
+ align-content: start;
385
+ padding-top: 1rem;
386
+ border-top: 1px solid var(--phase-hairline);
387
+ }
388
+
389
+ .phase-readout strong {
390
+ font-size: clamp(2.2rem, 10vw, 4.2rem);
391
+ }
392
+
393
+ .phase-readout p {
394
+ margin: 0;
395
+ color: var(--phase-muted);
396
+ line-height: 1.55;
397
+ }
398
+
399
+ .phase-meter {
400
+ display: grid;
401
+ gap: 0.55rem;
402
+ padding-top: 0.75rem;
403
+ border-top: 1px solid var(--phase-hairline);
404
+ }
405
+
406
+ .phase-meter div {
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: space-between;
410
+ gap: 1rem;
411
+ }
412
+
413
+ .phase-meter output {
414
+ font-weight: 800;
415
+ }
416
+
417
+ .phase-meter i {
418
+ --value: 0;
419
+
420
+ display: block;
421
+ height: 4px;
422
+ overflow: hidden;
423
+ background: rgba(24, 32, 40, 0.12);
424
+ border-radius: 0;
425
+ }
426
+
427
+ .phase-meter i::before {
428
+ display: block;
429
+ width: calc(var(--value) * 100%);
430
+ height: 100%;
431
+ background: linear-gradient(90deg, var(--phase-gas), var(--phase-critical), var(--phase-super));
432
+ content: "";
433
+ }
434
+
435
+ .phase-coexistence {
436
+ display: grid;
437
+ gap: 0.65rem;
438
+ padding-top: 0.8rem;
439
+ border-top: 1px solid var(--phase-hairline);
440
+ }
441
+
442
+ .phase-coexistence-head {
443
+ display: grid;
444
+ grid-template-columns: 1fr auto 1fr;
445
+ gap: 0.65rem;
446
+ align-items: center;
447
+ }
448
+
449
+ .phase-coexistence-head span,
450
+ .phase-coexistence-head output {
451
+ color: var(--phase-muted);
452
+ font-size: 0.62rem;
453
+ font-weight: 800;
454
+ letter-spacing: 0.08em;
455
+ text-transform: uppercase;
456
+ }
457
+
458
+ .phase-coexistence-head span:last-child {
459
+ text-align: right;
460
+ }
461
+
462
+ .phase-coexistence-head output {
463
+ color: var(--phase-ink);
464
+ }
465
+
466
+ .phase-coexistence-axis {
467
+ position: relative;
468
+ height: 24px;
469
+ }
470
+
471
+ .phase-coexistence-axis::before {
472
+ position: absolute;
473
+ top: 50%;
474
+ left: 0;
475
+ width: 100%;
476
+ height: 1px;
477
+ background: var(--phase-track);
478
+ content: "";
479
+ }
480
+
481
+ .phase-coexistence-axis::after {
482
+ position: absolute;
483
+ top: 50%;
484
+ right: 0;
485
+ width: calc(var(--coexistence, 0) * 100%);
486
+ height: 1px;
487
+ background: var(--phase-critical);
488
+ box-shadow: 0 0 14px rgba(230, 83, 85, 0.26);
489
+ content: "";
490
+ }
491
+
492
+ .phase-coexistence-axis i {
493
+ --value: 0;
494
+
495
+ position: absolute;
496
+ top: 50%;
497
+ left: calc(var(--value) * 100%);
498
+ width: 9px;
499
+ height: 9px;
500
+ background: var(--phase-critical);
501
+ border-radius: 50%;
502
+ transform: translate(-50%, -50%);
503
+ transition: left 260ms cubic-bezier(0.16, 1, 0.3, 1);
504
+ }
505
+
506
+ @keyframes phase-pulse {
507
+ 0%,
508
+ 100% {
509
+ opacity: 0.16;
510
+ transform: scale(0.9);
511
+ }
512
+
513
+ 50% {
514
+ opacity: 0.34;
515
+ transform: scale(1.12);
516
+ }
517
+ }
518
+
519
+ @keyframes phase-spin {
520
+ to {
521
+ transform: rotate(360deg);
522
+ }
523
+ }
524
+
525
+ @media (min-width: 900px) {
526
+ .phase-lab {
527
+ grid-template-columns: minmax(190px, 0.44fr) minmax(0, 1.9fr) minmax(230px, 0.54fr);
528
+ align-items: stretch;
529
+ }
530
+
531
+ .phase-controls {
532
+ padding-right: clamp(0rem, 2vw, 1rem);
533
+ border-right: 1px solid var(--phase-hairline);
534
+ }
535
+
536
+ .phase-readout {
537
+ padding-top: 0.25rem;
538
+ padding-left: clamp(0rem, 2vw, 1rem);
539
+ border-top: 0;
540
+ border-left: 1px solid var(--phase-hairline);
541
+ }
542
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { phaseDiagramCriticalPoints } 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 phaseDiagramCriticalPoints.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}