@salmexio/ui 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/dist/dialogs/ContextMenu/ContextMenu.svelte +6 -6
  3. package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts +1 -1
  4. package/dist/dialogs/Modal/Modal.svelte +3 -3
  5. package/dist/dialogs/Modal/Modal.svelte.d.ts +1 -1
  6. package/dist/feedback/Alert/Alert.svelte +16 -11
  7. package/dist/feedback/Alert/Alert.svelte.d.ts +1 -1
  8. package/dist/feedback/ProgressBar/ProgressBar.svelte +23 -4
  9. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts +1 -1
  10. package/dist/feedback/Skeleton/Skeleton.svelte +7 -3
  11. package/dist/feedback/Skeleton/Skeleton.svelte.d.ts +1 -1
  12. package/dist/feedback/Spinner/Spinner.svelte +3 -3
  13. package/dist/feedback/Spinner/Spinner.svelte.d.ts +2 -2
  14. package/dist/feedback/Toast/Toaster.svelte +10 -10
  15. package/dist/feedback/Toast/Toaster.svelte.d.ts +1 -1
  16. package/dist/forms/Checkbox/Checkbox.svelte +13 -8
  17. package/dist/forms/Checkbox/Checkbox.svelte.d.ts +1 -1
  18. package/dist/forms/Select/Select.svelte +11 -11
  19. package/dist/forms/Select/Select.svelte.d.ts +1 -1
  20. package/dist/forms/Slider/Slider.svelte +27 -27
  21. package/dist/forms/Slider/Slider.svelte.d.ts +1 -1
  22. package/dist/forms/TextInput/TextInput.svelte +16 -6
  23. package/dist/forms/TextInput/TextInput.svelte.d.ts +1 -1
  24. package/dist/forms/Textarea/Textarea.svelte +5 -5
  25. package/dist/forms/Textarea/Textarea.svelte.d.ts +1 -1
  26. package/dist/forms/Toggle/Toggle.svelte +8 -8
  27. package/dist/forms/Toggle/Toggle.svelte.d.ts +1 -1
  28. package/dist/layout/Card/Card.svelte +6 -4
  29. package/dist/layout/Card/Card.svelte.d.ts +1 -1
  30. package/dist/layout/Container/Container.svelte +1 -1
  31. package/dist/layout/Container/Container.svelte.d.ts +1 -1
  32. package/dist/layout/ThermalBackground/ThermalBackground.svelte +313 -0
  33. package/dist/layout/ThermalBackground/ThermalBackground.svelte.d.ts +16 -0
  34. package/dist/layout/ThermalBackground/ThermalBackground.svelte.d.ts.map +1 -0
  35. package/dist/layout/ThermalBackground/index.d.ts +2 -0
  36. package/dist/layout/ThermalBackground/index.d.ts.map +1 -0
  37. package/dist/layout/ThermalBackground/index.js +1 -0
  38. package/dist/layout/index.d.ts +1 -0
  39. package/dist/layout/index.d.ts.map +1 -1
  40. package/dist/layout/index.js +1 -0
  41. package/dist/navigation/CommandPalette/CommandPalette.svelte +8 -8
  42. package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts +1 -1
  43. package/dist/navigation/Tabs/Tabs.svelte +43 -10
  44. package/dist/navigation/Tabs/Tabs.svelte.d.ts +1 -1
  45. package/dist/primitives/Badge/Badge.svelte +16 -14
  46. package/dist/primitives/Badge/Badge.svelte.d.ts +1 -1
  47. package/dist/primitives/Button/Button.svelte +87 -16
  48. package/dist/primitives/Button/Button.svelte.d.ts +1 -1
  49. package/dist/primitives/Tooltip/Tooltip.svelte +1 -1
  50. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts +1 -1
  51. package/dist/styles/tokens.css +201 -64
  52. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  @component Slider
3
3
 
4
- Neo-Brutalist Dark — Range slider with keyboard support, optional value display,
4
+ INFRARED — Range slider with keyboard support, optional value display,
5
5
  step marks, min/max labels, and full ARIA compliance.
6
6
 
7
7
  @example
@@ -199,12 +199,12 @@ function handleChange(e: Event) {
199
199
 
200
200
  /* Focused track glow — WebKit */
201
201
  .sx-slider-input:focus-visible::-webkit-slider-runnable-track {
202
- box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-cyan));
202
+ box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-primary));
203
203
  }
204
204
 
205
205
  /* Focused track glow — Firefox */
206
206
  .sx-slider-input:focus-visible::-moz-range-track {
207
- box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-cyan));
207
+ box-shadow: 0 0 0 1px var(--sx-slider-accent, var(--sx-color-primary));
208
208
  }
209
209
 
210
210
  /* Track — WebKit */
@@ -213,8 +213,8 @@ function handleChange(e: Event) {
213
213
  border-radius: var(--sx-radius-full);
214
214
  background: linear-gradient(
215
215
  to right,
216
- var(--sx-slider-accent, var(--sx-color-cyan)) 0%,
217
- var(--sx-slider-accent, var(--sx-color-cyan)) var(--sx-slider-fill),
216
+ var(--sx-slider-accent, var(--sx-color-primary)) 0%,
217
+ var(--sx-slider-accent, var(--sx-color-primary)) var(--sx-slider-fill),
218
218
  var(--sx-color-surface-2) var(--sx-slider-fill),
219
219
  var(--sx-color-surface-2) 100%
220
220
  );
@@ -231,7 +231,7 @@ function handleChange(e: Event) {
231
231
  .sx-slider-input::-moz-range-progress {
232
232
  height: var(--sx-slider-track-h, 6px);
233
233
  border-radius: var(--sx-radius-full);
234
- background: var(--sx-slider-accent, var(--sx-color-cyan));
234
+ background: var(--sx-slider-accent, var(--sx-color-primary));
235
235
  }
236
236
 
237
237
  /* Thumb — WebKit */
@@ -241,7 +241,7 @@ function handleChange(e: Event) {
241
241
  height: var(--sx-slider-thumb, 18px);
242
242
  border-radius: var(--sx-radius-full);
243
243
  background: var(--sx-color-text);
244
- border: 2px solid var(--sx-slider-accent, var(--sx-color-cyan));
244
+ border: 2px solid var(--sx-slider-accent, var(--sx-color-primary));
245
245
  margin-top: calc((var(--sx-slider-track-h, 6px) - var(--sx-slider-thumb, 18px)) / 2);
246
246
  transition:
247
247
  box-shadow var(--sx-transition-fast),
@@ -256,10 +256,10 @@ function handleChange(e: Event) {
256
256
 
257
257
  .sx-slider-input:focus-visible::-webkit-slider-thumb {
258
258
  box-shadow:
259
- 0 0 0 3px var(--sx-slider-ring, rgba(0, 212, 255, 0.35)),
260
- 0 0 12px var(--sx-slider-glow, rgba(0, 212, 255, 0.25));
259
+ 0 0 0 3px var(--sx-slider-ring, rgba(255, 107, 53, 0.35)),
260
+ 0 0 12px var(--sx-slider-glow, rgba(255, 107, 53, 0.25));
261
261
  transform: scale(1.2);
262
- background: var(--sx-slider-accent, var(--sx-color-cyan));
262
+ background: var(--sx-slider-accent, var(--sx-color-primary));
263
263
  border-color: var(--sx-color-text);
264
264
  }
265
265
 
@@ -269,7 +269,7 @@ function handleChange(e: Event) {
269
269
  height: var(--sx-slider-thumb, 18px);
270
270
  border-radius: var(--sx-radius-full);
271
271
  background: var(--sx-color-text);
272
- border: 2px solid var(--sx-slider-accent, var(--sx-color-cyan));
272
+ border: 2px solid var(--sx-slider-accent, var(--sx-color-primary));
273
273
  transition:
274
274
  box-shadow var(--sx-transition-fast),
275
275
  transform var(--sx-transition-fast),
@@ -283,38 +283,38 @@ function handleChange(e: Event) {
283
283
 
284
284
  .sx-slider-input:focus-visible::-moz-range-thumb {
285
285
  box-shadow:
286
- 0 0 0 3px var(--sx-slider-ring, rgba(0, 212, 255, 0.35)),
287
- 0 0 12px var(--sx-slider-glow, rgba(0, 212, 255, 0.25));
286
+ 0 0 0 3px var(--sx-slider-ring, rgba(255, 107, 53, 0.35)),
287
+ 0 0 12px var(--sx-slider-glow, rgba(255, 107, 53, 0.25));
288
288
  transform: scale(1.2);
289
- background: var(--sx-slider-accent, var(--sx-color-cyan));
289
+ background: var(--sx-slider-accent, var(--sx-color-primary));
290
290
  border-color: var(--sx-color-text);
291
291
  }
292
292
 
293
293
  /* Color variants — accent + focus ring colors */
294
294
  .sx-slider-color-cyan {
295
- --sx-slider-accent: var(--sx-color-cyan);
296
- --sx-slider-ring: rgba(0, 212, 255, 0.35);
297
- --sx-slider-glow: rgba(0, 212, 255, 0.25);
295
+ --sx-slider-accent: var(--sx-color-primary);
296
+ --sx-slider-ring: rgba(255, 107, 53, 0.35);
297
+ --sx-slider-glow: rgba(255, 107, 53, 0.25);
298
298
  }
299
299
  .sx-slider-color-green {
300
300
  --sx-slider-accent: var(--sx-color-green);
301
- --sx-slider-ring: rgba(0, 255, 136, 0.35);
302
- --sx-slider-glow: rgba(0, 255, 136, 0.25);
301
+ --sx-slider-ring: rgba(74, 222, 128, 0.35);
302
+ --sx-slider-glow: rgba(74, 222, 128, 0.25);
303
303
  }
304
304
  .sx-slider-color-gold {
305
- --sx-slider-accent: var(--sx-color-gold);
306
- --sx-slider-ring: rgba(255, 184, 0, 0.35);
307
- --sx-slider-glow: rgba(255, 184, 0, 0.25);
305
+ --sx-slider-accent: var(--sx-color-secondary);
306
+ --sx-slider-ring: rgba(200, 168, 78, 0.35);
307
+ --sx-slider-glow: rgba(200, 168, 78, 0.25);
308
308
  }
309
309
  .sx-slider-color-red {
310
310
  --sx-slider-accent: var(--sx-color-red);
311
- --sx-slider-ring: rgba(255, 82, 82, 0.35);
312
- --sx-slider-glow: rgba(255, 82, 82, 0.25);
311
+ --sx-slider-ring: rgba(220, 38, 38, 0.35);
312
+ --sx-slider-glow: rgba(220, 38, 38, 0.25);
313
313
  }
314
314
  .sx-slider-color-purple {
315
- --sx-slider-accent: var(--sx-color-purple);
316
- --sx-slider-ring: rgba(168, 85, 247, 0.35);
317
- --sx-slider-glow: rgba(168, 85, 247, 0.25);
315
+ --sx-slider-accent: var(--sx-color-teal);
316
+ --sx-slider-ring: rgba(61, 139, 139, 0.35);
317
+ --sx-slider-glow: rgba(61, 139, 139, 0.25);
318
318
  }
319
319
 
320
320
  /* Size variants */
@@ -36,7 +36,7 @@ interface Props {
36
36
  /**
37
37
  * Slider
38
38
  *
39
- * Neo-Brutalist Dark — Range slider with keyboard support, optional value display,
39
+ * INFRARED — Range slider with keyboard support, optional value display,
40
40
  * step marks, min/max labels, and full ARIA compliance.
41
41
  *
42
42
  * @example
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  @component TextInput
3
3
 
4
- Neo-Brutalist Dark — Clean text field with subtle borders and cyan accents.
4
+ INFRARED — Clean text field with subtle borders and vermilion accents.
5
5
  Label, error, hint, prefix/suffix, clearable, password toggle. Full a11y.
6
6
  -->
7
7
  <script lang="ts">
@@ -341,13 +341,14 @@ function handleKeyDown(e: KeyboardEvent) {
341
341
  }
342
342
 
343
343
  .sx-input-focused {
344
- border-color: var(--sx-color-cyan);
345
- box-shadow: 0 0 0 3px var(--sx-color-cyan-ring);
344
+ border-color: var(--sx-color-primary);
345
+ animation: sx-focus-breathe 2s ease-in-out infinite;
346
346
  }
347
347
 
348
348
  .sx-input-error {
349
349
  border-color: var(--sx-color-red);
350
350
  box-shadow: 0 0 0 3px var(--sx-color-red-ring);
351
+ animation: sx-error-shake 300ms ease-out;
351
352
  }
352
353
 
353
354
  .sx-input-valid {
@@ -476,7 +477,7 @@ function handleKeyDown(e: KeyboardEvent) {
476
477
  .sx-input-loading {
477
478
  display: flex;
478
479
  align-items: center;
479
- color: var(--sx-color-cyan);
480
+ color: var(--sx-color-primary);
480
481
  }
481
482
 
482
483
  .sx-input-spinner {
@@ -513,7 +514,7 @@ function handleKeyDown(e: KeyboardEvent) {
513
514
 
514
515
  .sx-input-clear:focus-visible,
515
516
  .sx-input-password-toggle:focus-visible {
516
- outline: 2px solid var(--sx-color-cyan);
517
+ outline: 2px solid var(--sx-color-primary);
517
518
  outline-offset: 2px;
518
519
  }
519
520
 
@@ -549,12 +550,21 @@ function handleKeyDown(e: KeyboardEvent) {
549
550
  }
550
551
 
551
552
  .sx-input-charcount-warn {
552
- color: var(--sx-color-gold);
553
+ color: var(--sx-color-secondary);
553
554
  }
554
555
 
555
556
  @media (prefers-reduced-motion: reduce) {
556
557
  .sx-input-spinner {
557
558
  animation: none;
558
559
  }
560
+
561
+ .sx-input-focused {
562
+ animation: none;
563
+ box-shadow: 0 0 0 3px var(--sx-color-primary-ring);
564
+ }
565
+
566
+ .sx-input-error {
567
+ animation: none;
568
+ }
559
569
  }
560
570
  </style>
@@ -41,7 +41,7 @@ interface Props extends Omit<HTMLInputAttributes, 'class' | 'size' | 'inputmode'
41
41
  /**
42
42
  * TextInput
43
43
  *
44
- * Neo-Brutalist Dark — Clean text field with subtle borders and cyan accents.
44
+ * INFRARED — Clean text field with subtle borders and vermilion accents.
45
45
  * Label, error, hint, prefix/suffix, clearable, password toggle. Full a11y.
46
46
  */
47
47
  declare const TextInput: import("svelte").Component<Props, {}, "value">;
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  @component Textarea
3
3
 
4
- Neo-Brutalist Dark — Multi-line text input with custom resize handle,
4
+ INFRARED — Multi-line text input with custom resize handle,
5
5
  auto-resize, character count, validation states, and full accessibility.
6
6
  Follows TextInput visual patterns.
7
7
 
@@ -391,8 +391,8 @@ onMount(() => {
391
391
  }
392
392
 
393
393
  .sx-textarea-focused {
394
- border-color: var(--sx-color-cyan);
395
- box-shadow: 0 0 0 3px var(--sx-color-cyan-ring);
394
+ border-color: var(--sx-color-primary);
395
+ box-shadow: 0 0 0 3px var(--sx-color-primary-ring);
396
396
  }
397
397
 
398
398
  .sx-textarea-error-state {
@@ -480,7 +480,7 @@ onMount(() => {
480
480
  }
481
481
 
482
482
  .sx-textarea-handle-active {
483
- color: var(--sx-color-cyan);
483
+ color: var(--sx-color-primary);
484
484
  }
485
485
 
486
486
  /* Vertical: bar along the bottom edge */
@@ -580,7 +580,7 @@ onMount(() => {
580
580
  }
581
581
 
582
582
  .sx-textarea-charcount-warning {
583
- color: var(--sx-color-gold);
583
+ color: var(--sx-color-secondary);
584
584
  }
585
585
 
586
586
  .sx-textarea-charcount-exceeded {
@@ -32,7 +32,7 @@ interface Props {
32
32
  /**
33
33
  * Textarea
34
34
  *
35
- * Neo-Brutalist Dark — Multi-line text input with custom resize handle,
35
+ * INFRARED — Multi-line text input with custom resize handle,
36
36
  * auto-resize, character count, validation states, and full accessibility.
37
37
  * Follows TextInput visual patterns.
38
38
  *
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  @component Toggle
3
3
 
4
- Neo-Brutalist Dark — Accessible toggle switch with on/off states,
4
+ INFRARED — Accessible toggle switch with on/off states,
5
5
  label positioning, size variants, and keyboard support.
6
6
  Uses role="switch" with aria-checked for screen readers.
7
7
 
@@ -139,27 +139,27 @@ function toggle() {
139
139
 
140
140
  .sx-toggle-track:focus-visible {
141
141
  outline: none;
142
- box-shadow: 0 0 0 3px var(--sx-color-cyan-ring);
142
+ box-shadow: 0 0 0 3px var(--sx-color-primary-ring);
143
143
  }
144
144
 
145
145
  .sx-toggle-checked {
146
- background: var(--sx-color-cyan);
147
- border-color: var(--sx-color-cyan);
146
+ background: var(--sx-color-primary);
147
+ border-color: var(--sx-color-primary);
148
148
  }
149
149
 
150
150
  .sx-toggle-checked:hover:not(:disabled) {
151
- border-color: var(--sx-color-cyan);
152
- background: var(--sx-color-cyan-bright, var(--sx-color-cyan));
151
+ border-color: var(--sx-color-primary);
152
+ background: var(--sx-color-primary);
153
153
  }
154
154
 
155
- /* Thumb */
155
+ /* Thumb — spring easing for a snappy, physical feel */
156
156
  .sx-toggle-thumb {
157
157
  display: block;
158
158
  width: var(--sx-toggle-thumb, 18px);
159
159
  height: var(--sx-toggle-thumb, 18px);
160
160
  border-radius: var(--sx-radius-full);
161
161
  background: var(--sx-color-text);
162
- transition: transform var(--sx-transition-fast);
162
+ transition: transform 200ms var(--sx-ease-spring);
163
163
  box-shadow: var(--sx-shadow-sm);
164
164
  }
165
165
 
@@ -24,7 +24,7 @@ interface Props {
24
24
  /**
25
25
  * Toggle
26
26
  *
27
- * Neo-Brutalist Dark — Accessible toggle switch with on/off states,
27
+ * INFRARED — Accessible toggle switch with on/off states,
28
28
  * label positioning, size variants, and keyboard support.
29
29
  * Uses role="switch" with aria-checked for screen readers.
30
30
  *
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  @component Card
3
3
 
4
- Neo-Brutalist Dark — Glassmorphic panel with frosted background, subtle borders.
4
+ INFRARED — Glassmorphic panel with frosted background, subtle borders.
5
5
  Optional header/footer, semantic element support.
6
6
 
7
7
  @example
@@ -120,12 +120,14 @@ const currentPadding = $derived(paddingValues[padding]);
120
120
 
121
121
  .sx-card-clickable:hover {
122
122
  transform: translateY(-2px);
123
- border-color: var(--sx-color-cyan-active);
124
- box-shadow: var(--sx-shadow-md);
123
+ border-color: rgba(255, 107, 53, 0.15);
124
+ box-shadow:
125
+ var(--sx-shadow-md),
126
+ 0 0 24px -6px rgba(255, 107, 53, 0.08);
125
127
  }
126
128
 
127
129
  .sx-card-clickable:focus-visible {
128
- outline: 2px solid var(--sx-color-cyan);
130
+ outline: 2px solid var(--sx-color-primary);
129
131
  outline-offset: 2px;
130
132
  }
131
133
 
@@ -16,7 +16,7 @@ interface Props {
16
16
  /**
17
17
  * Card
18
18
  *
19
- * Neo-Brutalist Dark — Glassmorphic panel with frosted background, subtle borders.
19
+ * INFRARED — Glassmorphic panel with frosted background, subtle borders.
20
20
  * Optional header/footer, semantic element support.
21
21
  *
22
22
  * @example
@@ -1,7 +1,7 @@
1
1
  <!--
2
2
  @component Container
3
3
 
4
- Neo-Brutalist Dark — Responsive layout wrapper. Max-width, padding, optional centering.
4
+ INFRARED — Responsive layout wrapper. Max-width, padding, optional centering.
5
5
  Pure layout utility; supports semantic elements.
6
6
  -->
7
7
  <script lang="ts">
@@ -16,7 +16,7 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'class'> {
16
16
  /**
17
17
  * Container
18
18
  *
19
- * Neo-Brutalist Dark — Responsive layout wrapper. Max-width, padding, optional centering.
19
+ * INFRARED — Responsive layout wrapper. Max-width, padding, optional centering.
20
20
  * Pure layout utility; supports semantic elements.
21
21
  */
22
22
  declare const Container: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,313 @@
1
+ <!--
2
+ ThermalBackground — Dynamic INFRARED background
3
+
4
+ Layered ambient background with thermal blob drift, radar sweep,
5
+ and film grain. Pure CSS — zero JS runtime cost.
6
+
7
+ Usage:
8
+ <ThermalBackground /> — default (all layers)
9
+ <ThermalBackground grain={false} /> — no grain overlay
10
+ <ThermalBackground sweep={false} /> — no radar sweep
11
+ <ThermalBackground intensity="low" /> — subtler blobs
12
+ -->
13
+
14
+ <script lang="ts">
15
+ interface Props {
16
+ /** Show film grain overlay */
17
+ grain?: boolean;
18
+ /** Show radar sweep line */
19
+ sweep?: boolean;
20
+ /** Blob intensity: low = barely visible, default = subtle, high = pronounced */
21
+ intensity?: 'low' | 'default' | 'high';
22
+ /** Additional CSS class */
23
+ class?: string;
24
+ /** Test ID */
25
+ testId?: string;
26
+ }
27
+
28
+ let {
29
+ grain = true,
30
+ sweep = true,
31
+ intensity = 'default',
32
+ class: className = '',
33
+ testId = undefined,
34
+ }: Props = $props();
35
+ </script>
36
+
37
+ <div
38
+ class="sx-thermal-bg sx-thermal-{intensity} {className}"
39
+ aria-hidden="true"
40
+ data-testid={testId}
41
+ >
42
+ <!-- Layer 1: Thermal blobs — large diffuse orbs drifting independently -->
43
+ <div class="sx-thermal-blobs">
44
+ <div class="sx-thermal-blob sx-thermal-blob-1"></div>
45
+ <div class="sx-thermal-blob sx-thermal-blob-2"></div>
46
+ <div class="sx-thermal-blob sx-thermal-blob-3"></div>
47
+ <div class="sx-thermal-blob sx-thermal-blob-4"></div>
48
+ </div>
49
+
50
+ <!-- Layer 2: Radar/thermal sweep — horizontal scan line -->
51
+ {#if sweep}
52
+ <div class="sx-thermal-sweep"></div>
53
+ {/if}
54
+
55
+ <!-- Layer 3: Animated film grain -->
56
+ {#if grain}
57
+ <div class="sx-thermal-grain"></div>
58
+ {/if}
59
+ </div>
60
+
61
+ <style>
62
+ /* ========================================
63
+ ROOT CONTAINER — fixed fullscreen backdrop
64
+ ======================================== */
65
+
66
+ .sx-thermal-bg {
67
+ position: fixed;
68
+ inset: 0;
69
+ z-index: 0;
70
+ overflow: hidden;
71
+ pointer-events: none;
72
+ background: var(--sx-color-base);
73
+ }
74
+
75
+ /* ========================================
76
+ THERMAL BLOBS — Drifting infrared orbs
77
+ Large, soft radial gradients that move on
78
+ independent orbits. Each has a unique size,
79
+ color, speed, and path.
80
+ ======================================== */
81
+
82
+ .sx-thermal-blobs {
83
+ position: absolute;
84
+ inset: -50%;
85
+ width: 200%;
86
+ height: 200%;
87
+ }
88
+
89
+ .sx-thermal-blob {
90
+ position: absolute;
91
+ border-radius: 50%;
92
+ filter: blur(80px);
93
+ will-change: transform;
94
+ }
95
+
96
+ /* Blob 1 — Vermilion/primary — largest, slowest */
97
+ .sx-thermal-blob-1 {
98
+ width: 45vmax;
99
+ height: 45vmax;
100
+ top: 10%;
101
+ left: 15%;
102
+ background: radial-gradient(
103
+ circle,
104
+ rgba(255, 107, 53, 0.08) 0%,
105
+ rgba(255, 107, 53, 0.02) 50%,
106
+ transparent 70%
107
+ );
108
+ animation: sx-thermal-drift-1 35s ease-in-out infinite;
109
+ }
110
+
111
+ /* Blob 2 — Brass/secondary — medium, offset orbit */
112
+ .sx-thermal-blob-2 {
113
+ width: 35vmax;
114
+ height: 35vmax;
115
+ top: 55%;
116
+ right: 10%;
117
+ background: radial-gradient(
118
+ circle,
119
+ rgba(200, 168, 78, 0.06) 0%,
120
+ rgba(200, 168, 78, 0.015) 50%,
121
+ transparent 70%
122
+ );
123
+ animation: sx-thermal-drift-2 42s ease-in-out infinite;
124
+ }
125
+
126
+ /* Blob 3 — Teal — smallest, fastest, cool counterpoint */
127
+ .sx-thermal-blob-3 {
128
+ width: 30vmax;
129
+ height: 30vmax;
130
+ bottom: 15%;
131
+ left: 40%;
132
+ background: radial-gradient(
133
+ circle,
134
+ rgba(61, 139, 139, 0.06) 0%,
135
+ rgba(61, 139, 139, 0.015) 50%,
136
+ transparent 70%
137
+ );
138
+ animation: sx-thermal-drift-3 28s ease-in-out infinite;
139
+ }
140
+
141
+ /* Blob 4 — Deep vermilion — secondary heat signature */
142
+ .sx-thermal-blob-4 {
143
+ width: 38vmax;
144
+ height: 38vmax;
145
+ top: 40%;
146
+ left: 60%;
147
+ background: radial-gradient(
148
+ circle,
149
+ rgba(224, 85, 32, 0.05) 0%,
150
+ rgba(224, 85, 32, 0.01) 50%,
151
+ transparent 70%
152
+ );
153
+ animation: sx-thermal-drift-4 50s ease-in-out infinite;
154
+ }
155
+
156
+ /* ========================================
157
+ BLOB DRIFT ORBITS
158
+ Each blob traces a unique Lissajous-like path
159
+ using translate transforms. Different durations
160
+ ensure they never sync up, creating organic motion.
161
+ ======================================== */
162
+
163
+ @keyframes sx-thermal-drift-1 {
164
+ 0% { transform: translate(0%, 0%) scale(1); }
165
+ 25% { transform: translate(12%, -8%) scale(1.05); }
166
+ 50% { transform: translate(-5%, 15%) scale(0.95); }
167
+ 75% { transform: translate(-15%, -5%) scale(1.08); }
168
+ 100% { transform: translate(0%, 0%) scale(1); }
169
+ }
170
+
171
+ @keyframes sx-thermal-drift-2 {
172
+ 0% { transform: translate(0%, 0%) scale(1); }
173
+ 20% { transform: translate(-18%, 8%) scale(1.1); }
174
+ 40% { transform: translate(8%, -12%) scale(0.9); }
175
+ 60% { transform: translate(-8%, -18%) scale(1.05); }
176
+ 80% { transform: translate(15%, 5%) scale(0.95); }
177
+ 100% { transform: translate(0%, 0%) scale(1); }
178
+ }
179
+
180
+ @keyframes sx-thermal-drift-3 {
181
+ 0% { transform: translate(0%, 0%) rotate(0deg) scale(1); }
182
+ 33% { transform: translate(20%, -15%) rotate(5deg) scale(1.1); }
183
+ 66% { transform: translate(-15%, 10%) rotate(-3deg) scale(0.92); }
184
+ 100% { transform: translate(0%, 0%) rotate(0deg) scale(1); }
185
+ }
186
+
187
+ @keyframes sx-thermal-drift-4 {
188
+ 0% { transform: translate(0%, 0%) scale(1); }
189
+ 30% { transform: translate(-10%, 12%) scale(1.06); }
190
+ 60% { transform: translate(15%, -8%) scale(0.94); }
191
+ 100% { transform: translate(0%, 0%) scale(1); }
192
+ }
193
+
194
+ /* ========================================
195
+ INTENSITY VARIANTS
196
+ Control blob visibility without changing motion
197
+ ======================================== */
198
+
199
+ .sx-thermal-low .sx-thermal-blobs {
200
+ opacity: 0.5;
201
+ }
202
+
203
+ .sx-thermal-default .sx-thermal-blobs {
204
+ opacity: 1;
205
+ }
206
+
207
+ .sx-thermal-high .sx-thermal-blobs {
208
+ opacity: 1.5;
209
+ }
210
+
211
+ .sx-thermal-high .sx-thermal-blob-1 {
212
+ background: radial-gradient(
213
+ circle,
214
+ rgba(255, 107, 53, 0.14) 0%,
215
+ rgba(255, 107, 53, 0.04) 50%,
216
+ transparent 70%
217
+ );
218
+ }
219
+
220
+ .sx-thermal-high .sx-thermal-blob-2 {
221
+ background: radial-gradient(
222
+ circle,
223
+ rgba(200, 168, 78, 0.10) 0%,
224
+ rgba(200, 168, 78, 0.03) 50%,
225
+ transparent 70%
226
+ );
227
+ }
228
+
229
+ .sx-thermal-high .sx-thermal-blob-3 {
230
+ background: radial-gradient(
231
+ circle,
232
+ rgba(61, 139, 139, 0.10) 0%,
233
+ rgba(61, 139, 139, 0.03) 50%,
234
+ transparent 70%
235
+ );
236
+ }
237
+
238
+ .sx-thermal-high .sx-thermal-blob-4 {
239
+ background: radial-gradient(
240
+ circle,
241
+ rgba(224, 85, 32, 0.09) 0%,
242
+ rgba(224, 85, 32, 0.02) 50%,
243
+ transparent 70%
244
+ );
245
+ }
246
+
247
+ /* ========================================
248
+ RADAR SWEEP — Horizontal scan line
249
+ A faint horizontal gradient that sweeps
250
+ top to bottom like a thermal scanner or
251
+ radar display. Very subtle.
252
+ ======================================== */
253
+
254
+ .sx-thermal-sweep {
255
+ position: absolute;
256
+ inset: 0;
257
+ background: linear-gradient(
258
+ 180deg,
259
+ transparent 0%,
260
+ rgba(255, 107, 53, 0.015) 48%,
261
+ rgba(255, 107, 53, 0.04) 50%,
262
+ rgba(255, 107, 53, 0.015) 52%,
263
+ transparent 100%
264
+ );
265
+ background-size: 100% 300%;
266
+ animation: sx-thermal-scan 8s linear infinite;
267
+ opacity: 0.7;
268
+ }
269
+
270
+ @keyframes sx-thermal-scan {
271
+ 0% { background-position: 0% 0%; }
272
+ 100% { background-position: 0% 100%; }
273
+ }
274
+
275
+ /* ========================================
276
+ FILM GRAIN — Animated noise texture
277
+ SVG feTurbulence noise with subtle position
278
+ stepping for a living grain effect.
279
+ ======================================== */
280
+
281
+ .sx-thermal-grain {
282
+ position: absolute;
283
+ inset: -50%;
284
+ width: 300%;
285
+ height: 300%;
286
+ opacity: 0.03;
287
+ mix-blend-mode: overlay;
288
+ pointer-events: none;
289
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
290
+ background-size: 256px 256px;
291
+ animation: sx-thermal-grain-drift 0.4s steps(4) infinite;
292
+ }
293
+
294
+ @keyframes sx-thermal-grain-drift {
295
+ 0% { transform: translate(0, 0); }
296
+ 25% { transform: translate(-5%, -5%); }
297
+ 50% { transform: translate(3%, -8%); }
298
+ 75% { transform: translate(-8%, 2%); }
299
+ 100% { transform: translate(0, 0); }
300
+ }
301
+
302
+ /* ========================================
303
+ REDUCED MOTION — Static fallback
304
+ ======================================== */
305
+
306
+ @media (prefers-reduced-motion: reduce) {
307
+ .sx-thermal-blob,
308
+ .sx-thermal-sweep,
309
+ .sx-thermal-grain {
310
+ animation: none !important;
311
+ }
312
+ }
313
+ </style>