@majapisoftwares/ui 6.1.0 → 6.1.2

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 (73) hide show
  1. package/AGENTS.md +27 -27
  2. package/README.md +79 -79
  3. package/THIRD_PARTY_NOTICES.md +39 -39
  4. package/components.json +25 -25
  5. package/docs/MIGRATION-0.2.md +20 -20
  6. package/docs/MIGRATION-6.0.md +33 -33
  7. package/docs/NEXT-STARTER-VALIDATION.md +14 -14
  8. package/docs/STANDARDS.md +41 -41
  9. package/docs/VALIDATION.md +11 -11
  10. package/docs/VERSIONING.md +28 -28
  11. package/docs/validation/preference-titles.md +22 -0
  12. package/package.json +88 -84
  13. package/scripts/check-standards.mjs +41 -41
  14. package/src/components/ui/accordion.tsx +82 -82
  15. package/src/components/ui/alert-dialog.tsx +187 -187
  16. package/src/components/ui/alert.tsx +77 -77
  17. package/src/components/ui/avatar.tsx +108 -108
  18. package/src/components/ui/badge.tsx +55 -55
  19. package/src/components/ui/breadcrumb.tsx +125 -125
  20. package/src/components/ui/button-group.tsx +89 -89
  21. package/src/components/ui/button.tsx +59 -59
  22. package/src/components/ui/card.tsx +101 -101
  23. package/src/components/ui/checkbox.tsx +27 -27
  24. package/src/components/ui/collapsible.tsx +21 -21
  25. package/src/components/ui/context-menu.tsx +271 -271
  26. package/src/components/ui/dialog.tsx +161 -161
  27. package/src/components/ui/dropdown-menu.tsx +271 -271
  28. package/src/components/ui/empty.tsx +105 -105
  29. package/src/components/ui/field.tsx +238 -238
  30. package/src/components/ui/hover-card.tsx +50 -50
  31. package/src/components/ui/input-group.tsx +155 -155
  32. package/src/components/ui/input.tsx +21 -21
  33. package/src/components/ui/kbd.tsx +28 -28
  34. package/src/components/ui/label.tsx +19 -19
  35. package/src/components/ui/orb.css +1002 -1002
  36. package/src/components/ui/orb.tsx +752 -752
  37. package/src/components/ui/popover.tsx +89 -89
  38. package/src/components/ui/progress.tsx +82 -82
  39. package/src/components/ui/radio-group.tsx +37 -37
  40. package/src/components/ui/scroll-area.tsx +54 -54
  41. package/src/components/ui/select.tsx +204 -204
  42. package/src/components/ui/separator.tsx +24 -24
  43. package/src/components/ui/sheet.tsx +139 -139
  44. package/src/components/ui/sidebar.tsx +1039 -1035
  45. package/src/components/ui/skeleton.tsx +15 -15
  46. package/src/components/ui/slider.tsx +53 -53
  47. package/src/components/ui/sonner.tsx +44 -44
  48. package/src/components/ui/spinner.tsx +32 -32
  49. package/src/components/ui/switch.tsx +31 -31
  50. package/src/components/ui/table.tsx +121 -121
  51. package/src/components/ui/tabs.tsx +81 -81
  52. package/src/components/ui/textarea.tsx +19 -19
  53. package/src/components/ui/toggle-group.tsx +89 -89
  54. package/src/components/ui/toggle.tsx +44 -44
  55. package/src/components/ui/tooltip.tsx +65 -65
  56. package/src/hooks/use-mobile.ts +20 -20
  57. package/src/lib/i18n.tsx +109 -109
  58. package/src/lib/utils.ts +1 -1
  59. package/src/patterns/app-shell.tsx +177 -179
  60. package/src/patterns/data-table.tsx +2486 -2486
  61. package/src/patterns/details-sheet.tsx +54 -54
  62. package/src/patterns/filter-tabs.tsx +53 -53
  63. package/src/patterns/majapi-logo.tsx +32 -32
  64. package/src/patterns/page-header.tsx +62 -62
  65. package/src/patterns/preferences.tsx +15 -7
  66. package/src/patterns/status-badge.tsx +24 -24
  67. package/src/patterns/user-menu.tsx +218 -218
  68. package/src/patterns/workspace-switcher.tsx +176 -173
  69. package/src/styles.css +165 -165
  70. package/src/templates/admin-dashboard.tsx +44 -44
  71. package/src/templates/auth-layout.tsx +30 -30
  72. package/src/templates/login-form.tsx +129 -129
  73. package/src/templates/signup-form.tsx +177 -177
@@ -1,1002 +1,1002 @@
1
- /* Adapted from AICSS Orbs (MIT, https://github.com/kvnkld/aicss). */
2
- /* Orbs - two families of agent activity indicator.
3
- *
4
- * The geometry is authored at a 28px stage and scaled with --orb-k, so
5
- * the hand-tuned dot sizes, pitch and blur radii hold at any size.
6
- *
7
- * Per-segment easings inside @keyframes are written as literals: an
8
- * `animation-timing-function` declaration inside a keyframe block is
9
- * read by the animation engine, not resolved against the element, so a
10
- * var() there would not resolve. The numbers mirror the three custom
11
- * properties below exactly. */
12
-
13
- /* Majapi UI: colors follow the theme tokens instead of upstream hex values.
14
- The dots inherit the surrounding text color (e.g. inside a Button). */
15
- .orb-root {
16
- --orb-fg: currentColor;
17
- --orb-pill-bg: var(--popover);
18
- --orb-pill-shadow:
19
- 0 0 0 0.5px color-mix(in oklch, var(--foreground) 8%, transparent),
20
- 0 1px 2px color-mix(in oklch, var(--foreground) 5%, transparent),
21
- 0 2px 4px color-mix(in oklch, var(--foreground) 2%, transparent);
22
- --orb-label: var(--foreground);
23
- --orb-rest-ink: 0.14;
24
- --orb-dim-ink: 0.07;
25
- --orb-ring-rest-ink: 0.22;
26
- }
27
- .dark .orb-root {
28
- --orb-rest-ink: 0.2;
29
- --orb-dim-ink: 0.1;
30
- --orb-ring-rest-ink: 0.3;
31
- }
32
-
33
- .orb-root {
34
- --orb-ease-smooth: cubic-bezier(0.22, 1, 0.36, 1);
35
- --orb-ease-out: cubic-bezier(0.17, 1, 0.32, 1);
36
- --orb-ease-in-out: cubic-bezier(0.66, 0, 0.34, 1);
37
-
38
- display: inline-flex;
39
- align-items: center;
40
- vertical-align: middle;
41
- color: var(--orb-fg);
42
- }
43
-
44
- /* The inline pill form - same component, wrapped. */
45
- .orb-root[data-pill] {
46
- gap: 6px;
47
- height: 32px;
48
- padding: 0 12px 0 6px;
49
- border-radius: 999px;
50
- background: var(--orb-pill-bg);
51
- /* A hairline ring rather than a border, so it can't affect layout, plus
52
- a tight contact shadow and a wider ambient one. */
53
- box-shadow: var(--orb-pill-shadow);
54
- }
55
-
56
- .orb-pill-label {
57
- /* Majapi UI: same type as buttons/badges instead of upstream Inter 11.5px. */
58
- font-family: inherit;
59
- font-size: 0.875rem;
60
- font-weight: 500;
61
- line-height: 1;
62
- color: var(--orb-label);
63
- white-space: nowrap;
64
- }
65
-
66
- .orb-glyph {
67
- position: relative;
68
- display: block;
69
- flex: none;
70
- width: 20px;
71
- height: 20px;
72
- overflow: hidden;
73
- contain: strict;
74
- }
75
-
76
- /* --- Lattice: discrete dots on a fixed 3×3 grid ------------------- */
77
-
78
- .orb-lattice {
79
- position: absolute;
80
- left: 0;
81
- top: 0;
82
- width: 28px;
83
- height: 28px;
84
- transform-origin: 0 0;
85
- /* Three 3px dots on a 6px pitch measure 15px, so the grid is offset to sit
86
- centred on the 28px stage. It deliberately does not fill the stage: that
87
- is what keeps its visual weight level with the Lens circles. */
88
- transform: scale(var(--orb-k, 1)) translate(6.5px, 6.5px);
89
- /* Resting ink of an unlit cell - the grid stays legible between beats.
90
- --orb-dim is for cells sitting a choreography out entirely. */
91
- --orb-rest: var(--orb-rest-ink, 0.14);
92
- --orb-dim: var(--orb-dim-ink, 0.07);
93
- }
94
-
95
- .orb-cell {
96
- position: absolute;
97
- width: 3px;
98
- height: 3px;
99
- border-radius: 50%;
100
- background: currentColor;
101
- opacity: var(--orb-rest);
102
- }
103
-
104
- /* One wave shape drives all three sweeps. What separates them is the pair of
105
- duration and per-cell stagger: the stagger sets how fast the wavefront
106
- travels, the duration how many cells it holds lit at once - which is to
107
- say, how wide the band reads. */
108
- .orb-lattice[data-variant="S1"] .orb-cell {
109
- animation: orb-wave 1.7s var(--orb-ease-in-out) infinite both;
110
- }
111
-
112
- .orb-lattice[data-variant="S2"] .orb-cell {
113
- animation: orb-wave 1.7s var(--orb-ease-in-out) infinite both;
114
- }
115
-
116
- .orb-lattice[data-variant="S4"] .orb-cell {
117
- animation: orb-wave 1.6s var(--orb-ease-in-out) infinite both;
118
- }
119
-
120
- .orb-lattice[data-variant="S3"] .orb-cell {
121
- animation: orb-comet 1.7s var(--orb-ease-smooth) infinite both;
122
- }
123
-
124
- /* Interior cells sit out `orbit` and drop back, so the ring reads as a
125
- ring and the travelling head has something to stand out against. */
126
- .orb-lattice[data-variant="S3"] .orb-cell[data-still] {
127
- animation: none;
128
- opacity: var(--orb-dim);
129
- }
130
-
131
- .orb-lattice[data-variant="S5"] .orb-cell {
132
- animation: orb-comet 1.7s var(--orb-ease-smooth) infinite both;
133
- }
134
-
135
- .orb-lattice[data-variant="S5"] .orb-cell[data-still] {
136
- animation: none;
137
- opacity: var(--orb-dim);
138
- }
139
-
140
- /* Swells and subsides on the same symmetric curve, so there is no flash and
141
- no hard edge - the cell rises out of its resting ink and sinks back into
142
- it. The long tail after 56% is the gap between beats. */
143
- @keyframes orb-wave {
144
- 0% {
145
- opacity: var(--orb-rest);
146
- transform: scale(1);
147
- animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
148
- }
149
- 28% {
150
- opacity: 1;
151
- transform: scale(1.18);
152
- animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
153
- }
154
- 56% {
155
- opacity: var(--orb-rest);
156
- transform: scale(1);
157
- }
158
- 100% {
159
- opacity: var(--orb-rest);
160
- transform: scale(1);
161
- }
162
- }
163
-
164
- /* Starts lit and decays, so staggered ring cells form a head and tail.
165
- The decay spans ~3.5 of the 8 ring positions - enough to read as a comet. */
166
- @keyframes orb-comet {
167
- 0% {
168
- opacity: 1;
169
- transform: scale(1.2);
170
- /* Linear, so the cells behind the head form an even gradient instead
171
- of collapsing to rest within the first two. */
172
- animation-timing-function: linear;
173
- }
174
- 45% {
175
- opacity: var(--orb-rest);
176
- transform: scale(1);
177
- }
178
- 100% {
179
- opacity: var(--orb-rest);
180
- transform: scale(1);
181
- }
182
- }
183
-
184
- /* --- Lens: three circles at depth, blur reads as distance --------- */
185
-
186
- .orb-lens {
187
- position: absolute;
188
- left: 0;
189
- top: 0;
190
- width: 28px;
191
- height: 28px;
192
- transform-origin: 0 0;
193
- transform: scale(var(--orb-k, 1));
194
- }
195
-
196
- .orb-shape {
197
- position: absolute;
198
- left: 50%;
199
- top: 50%;
200
- width: var(--orb-d, 7px);
201
- height: var(--orb-d, 7px);
202
- /* Pulled back by its own half-size, so --orb-d is the only knob a variant
203
- has to touch to resize the cast and it stays centred on the stage. */
204
- margin: calc(var(--orb-d, 7px) / -2) 0 0 calc(var(--orb-d, 7px) / -2);
205
- border-radius: 50%;
206
- background: currentColor;
207
- }
208
-
209
- /* focus - attention travels the cast: each circle pulls into focus in turn.
210
- Four circles on the corners of a square, one size for all of them, so the
211
- only thing separating them is which one is sharp.
212
-
213
- A second longer than the three-circle version it grew out of: the square
214
- has four stations to visit and each one keeps the same unhurried second.
215
-
216
- The delays count down rather than up because a more negative delay seeds a
217
- circle further into its cycle: -3s of a 4s cycle runs three quarters ahead,
218
- which is what sends focus round the square clockwise. */
219
- .orb-lens[data-variant="B1"] .orb-shape {
220
- --orb-d: 6px;
221
- animation: orb-focus 4s var(--orb-ease-smooth) infinite both;
222
- }
223
-
224
- .orb-lens[data-variant="B1"] .orb-shape-a {
225
- --orb-ox: -4.5px;
226
- --orb-oy: -4.5px;
227
- animation-delay: 0s;
228
- }
229
-
230
- .orb-lens[data-variant="B1"] .orb-shape-b {
231
- --orb-ox: 4.5px;
232
- --orb-oy: -4.5px;
233
- animation-delay: -3s;
234
- }
235
-
236
- .orb-lens[data-variant="B1"] .orb-shape-c {
237
- --orb-ox: 4.5px;
238
- --orb-oy: 4.5px;
239
- animation-delay: -2s;
240
- }
241
-
242
- .orb-lens[data-variant="B1"] .orb-shape-d {
243
- --orb-ox: -4.5px;
244
- --orb-oy: 4.5px;
245
- animation-delay: -1s;
246
- }
247
-
248
- /* Opacity gradient around the square: active = 1.0, next neighbour = 0.30,
249
- diagonal = 0.10, far = 0.05. Two circles are always clearly visible, the
250
- rest are ghost hints. */
251
- @keyframes orb-focus {
252
- 0%,
253
- 100% {
254
- opacity: 0.05;
255
- filter: blur(2px);
256
- transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
257
- animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
258
- }
259
- 12% {
260
- opacity: 1;
261
- filter: blur(0);
262
- transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
263
- animation-timing-function: linear;
264
- }
265
- 22% {
266
- opacity: 1;
267
- filter: blur(0);
268
- transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
269
- animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
270
- }
271
- /* Next neighbour - one quarter away: clearly visible */
272
- 38% {
273
- opacity: 0.3;
274
- filter: blur(1.2px);
275
- transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.06);
276
- animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
277
- }
278
- /* Diagonal - half a cycle away: ghost */
279
- 58% {
280
- opacity: 0.1;
281
- filter: blur(1.8px);
282
- transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.1);
283
- animation-timing-function: linear;
284
- }
285
- /* Far neighbour - three quarters away: barely there */
286
- 82% {
287
- opacity: 0.05;
288
- filter: blur(2px);
289
- transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
290
- }
291
- }
292
-
293
- /* drift - the cast circles the stage on one track, sharp at the front and
294
- blurred away at the back, so the orb reads as looking around. Uniform
295
- size: the depth cue is doing the work, a size ladder would fight it. */
296
- .orb-lens[data-variant="B2"] .orb-shape {
297
- animation: orb-revolve 3.3s linear infinite both;
298
- }
299
-
300
- /* Evenly spaced around the track, so one is always at the front. */
301
- .orb-lens[data-variant="B2"] .orb-shape-a {
302
- animation-delay: 0s;
303
- }
304
-
305
- .orb-lens[data-variant="B2"] .orb-shape-b {
306
- animation-delay: -1.1s;
307
- }
308
-
309
- .orb-lens[data-variant="B2"] .orb-shape-c {
310
- animation-delay: -2.2s;
311
- }
312
-
313
- /* rotate() then translateY() walks a circle. Linear all the way: an eased
314
- rotation on a circular path reads as a wobble, not as travel. */
315
- @keyframes orb-revolve {
316
- 0% {
317
- opacity: 1;
318
- filter: blur(0);
319
- transform: rotate(0deg) translateY(6.5px) scale(1);
320
- }
321
- 25% {
322
- opacity: 0.55;
323
- filter: blur(1.3px);
324
- transform: rotate(90deg) translateY(6.5px) scale(0.82);
325
- }
326
- 50% {
327
- opacity: 0.28;
328
- filter: blur(2.4px);
329
- transform: rotate(180deg) translateY(6.5px) scale(0.66);
330
- }
331
- 75% {
332
- opacity: 0.55;
333
- filter: blur(1.3px);
334
- transform: rotate(270deg) translateY(6.5px) scale(0.82);
335
- }
336
- 100% {
337
- opacity: 1;
338
- filter: blur(0);
339
- transform: rotate(360deg) translateY(6.5px) scale(1);
340
- }
341
- }
342
-
343
- /* bloom - shapes emanate from the centre, blurring out as they grow.
344
- Linear keeps the total ink even; on a front-loaded curve the shapes
345
- jump to their large, blurred end state and the orb alternates between
346
- a heavy blot and an empty haze. */
347
- .orb-lens[data-variant="B3"] .orb-shape {
348
- animation: orb-bloom 4.2s linear infinite both;
349
- }
350
-
351
- .orb-lens[data-variant="B3"] .orb-shape-a {
352
- animation-delay: 0s;
353
- }
354
-
355
- .orb-lens[data-variant="B3"] .orb-shape-b {
356
- animation-delay: -1.4s;
357
- }
358
-
359
- .orb-lens[data-variant="B3"] .orb-shape-c {
360
- animation-delay: -2.8s;
361
- }
362
-
363
- /* Each ripple dies at 62% and waits out the rest, so the three overlapping
364
- blooms leave gaps. Without the gap the aggregate is a constant haze and
365
- the outward motion stops reading at all. Sharp circle appears, holds
366
- briefly, then dissolves outward - blur only kicks in once opacity starts
367
- dropping, so the circle stays crisp while it's visible and the blur reads
368
- as the ripple dissipating. */
369
- @keyframes orb-bloom {
370
- 0% {
371
- opacity: 0;
372
- filter: blur(0);
373
- transform: scale(0.35);
374
- animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
375
- }
376
- 8% {
377
- opacity: 1;
378
- filter: blur(0);
379
- transform: scale(0.55);
380
- animation-timing-function: linear;
381
- }
382
- 24% {
383
- opacity: 1;
384
- filter: blur(0);
385
- transform: scale(0.72);
386
- animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
387
- }
388
- 42% {
389
- opacity: 0.1;
390
- filter: blur(1.8px);
391
- transform: scale(1.5);
392
- }
393
- 62% {
394
- opacity: 0;
395
- filter: blur(2.8px);
396
- transform: scale(2.4);
397
- }
398
- 100% {
399
- opacity: 0;
400
- filter: blur(2.8px);
401
- transform: scale(2.4);
402
- }
403
- }
404
-
405
- /* converge - a single circle traces an equilateral triangle (top → bottom-right
406
- → bottom-left → top) with handoff-style easing: full size and sharp at each
407
- vertex, smaller and slightly blurred in transit. orbB breathes at the
408
- centroid as a subtle depth cue; orbC is hidden. */
409
- .orb-lens[data-variant="B4"] .orb-shape-a {
410
- animation: orb-converge 3.6s linear infinite both;
411
- }
412
- .orb-lens[data-variant="B4"] .orb-shape-b {
413
- animation: orb-breathe 3.6s ease-in-out infinite both;
414
- }
415
- .orb-lens[data-variant="B4"] .orb-shape-c {
416
- display: none;
417
- }
418
-
419
- @keyframes orb-converge {
420
- 0% {
421
- transform: translate(0px, -5px) scale(1);
422
- filter: blur(0);
423
- animation-timing-function: linear;
424
- }
425
- 10% {
426
- transform: translate(0px, -5px) scale(1);
427
- filter: blur(0);
428
- animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
429
- }
430
- 22% {
431
- transform: translate(2.15px, -1.25px) scale(0.72);
432
- filter: blur(0.8px);
433
- animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
434
- }
435
- 33% {
436
- transform: translate(4.3px, 2.5px) scale(1);
437
- filter: blur(0);
438
- animation-timing-function: linear;
439
- }
440
- 43% {
441
- transform: translate(4.3px, 2.5px) scale(1);
442
- filter: blur(0);
443
- animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
444
- }
445
- 55% {
446
- transform: translate(0px, 2.5px) scale(0.72);
447
- filter: blur(0.8px);
448
- animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
449
- }
450
- 66% {
451
- transform: translate(-4.3px, 2.5px) scale(1);
452
- filter: blur(0);
453
- animation-timing-function: linear;
454
- }
455
- 77% {
456
- transform: translate(-4.3px, 2.5px) scale(1);
457
- filter: blur(0);
458
- animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
459
- }
460
- 88% {
461
- transform: translate(-2.15px, -1.25px) scale(0.72);
462
- filter: blur(0.8px);
463
- animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
464
- }
465
- 100% {
466
- transform: translate(0px, -5px) scale(1);
467
- filter: blur(0);
468
- }
469
- }
470
-
471
- /* handoff - the cast crosses the focal plane one after another, always left
472
- to right, like work being passed on. The shorthand curve is only a
473
- fallback; every segment below sets its own. */
474
- .orb-lens[data-variant="B5"] .orb-shape {
475
- animation: orb-handoff 2.8s linear infinite both;
476
- }
477
-
478
- /* Half a cycle apart, so one is always at the focal plane while the other is
479
- invisible at an end and the loop point cannot be seen. */
480
- .orb-lens[data-variant="B5"] .orb-shape-a {
481
- animation-delay: 0s;
482
- }
483
-
484
- .orb-lens[data-variant="B5"] .orb-shape-c {
485
- animation-delay: -1.4s;
486
- }
487
-
488
- /* The third holds the centre and breathes - a soft depth cue behind the
489
- traffic rather than another traveller. */
490
- .orb-lens[data-variant="B5"] .orb-shape-b {
491
- animation-name: orb-breathe;
492
- animation-duration: 3.6s;
493
- }
494
-
495
- /* Enters small from the left, reaches standard size at the focal plane, then
496
- shrinks and fades out to the right. At the dwell (centre) the circle is
497
- exactly 1× - no pulsing, no bounce, just a clean handoff. */
498
- @keyframes orb-handoff {
499
- 0% {
500
- opacity: 0;
501
- filter: blur(2.4px);
502
- transform: translateX(-11px) scale(0.55);
503
- animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
504
- }
505
- 22% {
506
- opacity: 1;
507
- filter: blur(0);
508
- transform: translateX(-1px) scale(1);
509
- animation-timing-function: linear;
510
- }
511
- 37% {
512
- opacity: 1;
513
- filter: blur(0);
514
- transform: translateX(0) scale(1);
515
- animation-timing-function: linear;
516
- }
517
- 52% {
518
- opacity: 1;
519
- filter: blur(0);
520
- transform: translateX(1px) scale(1);
521
- animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
522
- }
523
- 70% {
524
- opacity: 0;
525
- filter: blur(2.4px);
526
- transform: translateX(11px) scale(0.55);
527
- }
528
- 100% {
529
- opacity: 0;
530
- filter: blur(2.4px);
531
- transform: translateX(11px) scale(0.55);
532
- }
533
- }
534
-
535
- @keyframes orb-breathe {
536
- 0%,
537
- 100% {
538
- opacity: 0.16;
539
- filter: blur(2.4px);
540
- transform: scale(1.2);
541
- }
542
- 50% {
543
- opacity: 0.32;
544
- filter: blur(1.6px);
545
- transform: scale(0.98);
546
- }
547
- }
548
-
549
- /* --- Ring: eight circles on a fixed ring ----------------------------- */
550
-
551
- .orb-ring {
552
- position: absolute;
553
- inset: 0;
554
- transform: scale(var(--orb-k, 1));
555
- --orb-ring-rest: var(--orb-ring-rest-ink, 0.22);
556
- }
557
-
558
- .orb-ring-dot {
559
- position: absolute;
560
- left: 50%;
561
- top: 50%;
562
- width: 3px;
563
- height: 3px;
564
- margin: -1.5px 0 0 -1.5px;
565
- border-radius: 50%;
566
- background: currentColor;
567
- transform: translate(var(--orb-rx), var(--orb-ry));
568
- }
569
-
570
- .orb-ring[data-variant="C1"] .orb-ring-dot {
571
- opacity: var(--orb-ring-rest);
572
- animation: orb-ring-chase 1.6s linear infinite both;
573
- }
574
-
575
- @keyframes orb-ring-chase {
576
- 0%,
577
- 11% {
578
- opacity: 1;
579
- }
580
- 12.5%,
581
- 100% {
582
- opacity: var(--orb-ring-rest);
583
- }
584
- }
585
-
586
- .orb-ring[data-variant="C2"] .orb-ring-dot {
587
- animation: orb-ring-pulse 2s ease-in-out infinite both;
588
- }
589
-
590
- @keyframes orb-ring-pulse {
591
- 0%,
592
- 100% {
593
- opacity: 0.18;
594
- transform: translate(var(--orb-rx), var(--orb-ry)) scale(0.7);
595
- }
596
- 50% {
597
- opacity: 1;
598
- transform: translate(var(--orb-rx), var(--orb-ry)) scale(1.15);
599
- }
600
- }
601
-
602
- .orb-ring[data-variant="C3"] .orb-ring-dot {
603
- animation: orb-ring-comet 1.8s ease-in-out infinite both;
604
- }
605
-
606
- @keyframes orb-ring-comet {
607
- 0%,
608
- 100% {
609
- opacity: 0.08;
610
- transform: translate(var(--orb-rx), var(--orb-ry));
611
- }
612
- 12% {
613
- opacity: 1;
614
- transform: translate(var(--orb-rx), var(--orb-ry));
615
- animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
616
- }
617
- 35% {
618
- opacity: 0.5;
619
- transform: translate(var(--orb-rx), var(--orb-ry));
620
- }
621
- 60% {
622
- opacity: 0.12;
623
- transform: translate(var(--orb-rx), var(--orb-ry));
624
- }
625
- }
626
-
627
- .orb-ring[data-variant="C4"] .orb-ring-dot {
628
- animation: orb-ring-stagger 1.6s ease-in-out infinite both;
629
- }
630
-
631
- @keyframes orb-ring-stagger {
632
- 0%,
633
- 100% {
634
- opacity: 1;
635
- transform: translate(var(--orb-rx), var(--orb-ry));
636
- }
637
- 50% {
638
- opacity: 0.15;
639
- transform: translate(var(--orb-rx), var(--orb-ry));
640
- }
641
- }
642
-
643
- .orb-ring[data-variant="C5"] .orb-ring-dot {
644
- animation: orb-ring-comet 1.8s ease-in-out infinite both;
645
- }
646
-
647
- /* ---- Globe (Helix family) ---- */
648
- .orb-helix {
649
- position: absolute;
650
- inset: 0;
651
- transform: scale(var(--orb-k, 1));
652
- }
653
-
654
- .orb-helix-dot {
655
- position: absolute;
656
- left: 50%;
657
- top: 50%;
658
- width: 2px;
659
- height: 2px;
660
- margin: -1px 0 0 -1px;
661
- border-radius: 50%;
662
- background: currentColor;
663
- will-change: transform, opacity;
664
- }
665
-
666
- .orb-helix[data-variant="G1"] .orb-helix-dot {
667
- animation: orb-globe-spin 4.5s linear infinite both;
668
- }
669
- .orb-helix[data-variant="G2"] .orb-helix-dot {
670
- animation: orb-globe-spin 3.6s linear infinite both;
671
- }
672
- .orb-helix[data-variant="G3"] .orb-helix-dot {
673
- animation: orb-globe-ringturn 2.8s linear infinite both;
674
- }
675
- .orb-helix[data-variant="G4"] .orb-helix-dot {
676
- animation: orb-globe-ringturn 2.8s linear infinite both;
677
- }
678
- .orb-helix[data-variant="G5"] .orb-helix-dot {
679
- animation: orb-globe-breathe 3.6s linear infinite both;
680
- }
681
-
682
- @keyframes orb-globe-spin {
683
- 0%,
684
- 100% {
685
- transform: translate(var(--g0x), var(--g0y));
686
- opacity: var(--g0o);
687
- }
688
- 12.5% {
689
- transform: translate(var(--g1x), var(--g1y));
690
- opacity: var(--g1o);
691
- }
692
- 25% {
693
- transform: translate(var(--g2x), var(--g2y));
694
- opacity: var(--g2o);
695
- }
696
- 37.5% {
697
- transform: translate(var(--g3x), var(--g3y));
698
- opacity: var(--g3o);
699
- }
700
- 50% {
701
- transform: translate(var(--g4x), var(--g4y));
702
- opacity: var(--g4o);
703
- }
704
- 62.5% {
705
- transform: translate(var(--g5x), var(--g5y));
706
- opacity: var(--g5o);
707
- }
708
- 75% {
709
- transform: translate(var(--g6x), var(--g6y));
710
- opacity: var(--g6o);
711
- }
712
- 87.5% {
713
- transform: translate(var(--g7x), var(--g7y));
714
- opacity: var(--g7o);
715
- }
716
- }
717
-
718
- @keyframes orb-globe-ringturn {
719
- 0% {
720
- transform: translate(var(--g0x), var(--g0y));
721
- opacity: var(--g0o);
722
- }
723
- 2.5% {
724
- transform: translate(var(--g1x), var(--g1y));
725
- opacity: var(--g1o);
726
- }
727
- 5% {
728
- transform: translate(var(--g2x), var(--g2y));
729
- opacity: var(--g2o);
730
- }
731
- 7.5%,
732
- 10% {
733
- transform: translate(var(--g3x), var(--g3y));
734
- opacity: var(--g3o);
735
- }
736
- 12.5% {
737
- transform: translate(var(--g4x), var(--g4y));
738
- opacity: var(--g4o);
739
- }
740
- 15% {
741
- transform: translate(var(--g5x), var(--g5y));
742
- opacity: var(--g5o);
743
- }
744
- 17.5%,
745
- 20% {
746
- transform: translate(var(--g6x), var(--g6y));
747
- opacity: var(--g6o);
748
- }
749
- 22.5% {
750
- transform: translate(var(--g7x), var(--g7y));
751
- opacity: var(--g7o);
752
- }
753
- 25% {
754
- transform: translate(var(--g8x), var(--g8y));
755
- opacity: var(--g8o);
756
- }
757
- 27.5%,
758
- 30% {
759
- transform: translate(var(--g9x), var(--g9y));
760
- opacity: var(--g9o);
761
- }
762
- 32.5% {
763
- transform: translate(var(--g10x), var(--g10y));
764
- opacity: var(--g10o);
765
- }
766
- 35% {
767
- transform: translate(var(--g11x), var(--g11y));
768
- opacity: var(--g11o);
769
- }
770
- 37.5%,
771
- 40% {
772
- transform: translate(var(--g12x), var(--g12y));
773
- opacity: var(--g12o);
774
- }
775
- 42.5% {
776
- transform: translate(var(--g13x), var(--g13y));
777
- opacity: var(--g13o);
778
- }
779
- 45% {
780
- transform: translate(var(--g14x), var(--g14y));
781
- opacity: var(--g14o);
782
- }
783
- 47.5%,
784
- 50% {
785
- transform: translate(var(--g15x), var(--g15y));
786
- opacity: var(--g15o);
787
- }
788
- 52.5% {
789
- transform: translate(var(--g16x), var(--g16y));
790
- opacity: var(--g16o);
791
- }
792
- 55% {
793
- transform: translate(var(--g17x), var(--g17y));
794
- opacity: var(--g17o);
795
- }
796
- 57.5%,
797
- 60% {
798
- transform: translate(var(--g18x), var(--g18y));
799
- opacity: var(--g18o);
800
- }
801
- 62.5% {
802
- transform: translate(var(--g19x), var(--g19y));
803
- opacity: var(--g19o);
804
- }
805
- 65% {
806
- transform: translate(var(--g20x), var(--g20y));
807
- opacity: var(--g20o);
808
- }
809
- 67.5%,
810
- 70% {
811
- transform: translate(var(--g21x), var(--g21y));
812
- opacity: var(--g21o);
813
- }
814
- 72.5% {
815
- transform: translate(var(--g22x), var(--g22y));
816
- opacity: var(--g22o);
817
- }
818
- 75% {
819
- transform: translate(var(--g23x), var(--g23y));
820
- opacity: var(--g23o);
821
- }
822
- 77.5%,
823
- 80% {
824
- transform: translate(var(--g24x), var(--g24y));
825
- opacity: var(--g24o);
826
- }
827
- 82.5% {
828
- transform: translate(var(--g25x), var(--g25y));
829
- opacity: var(--g25o);
830
- }
831
- 85% {
832
- transform: translate(var(--g26x), var(--g26y));
833
- opacity: var(--g26o);
834
- }
835
- 87.5%,
836
- 90% {
837
- transform: translate(var(--g27x), var(--g27y));
838
- opacity: var(--g27o);
839
- }
840
- 92.5% {
841
- transform: translate(var(--g28x), var(--g28y));
842
- opacity: var(--g28o);
843
- }
844
- 95% {
845
- transform: translate(var(--g29x), var(--g29y));
846
- opacity: var(--g29o);
847
- }
848
- 97.5%,
849
- 100% {
850
- transform: translate(var(--g30x), var(--g30y));
851
- opacity: var(--g30o);
852
- }
853
- }
854
-
855
- @keyframes orb-globe-breathe {
856
- 0% {
857
- transform: translate(var(--g0x), var(--g0y));
858
- opacity: var(--g0o);
859
- }
860
- 19% {
861
- transform: translate(var(--g1x), var(--g1y));
862
- opacity: var(--g1o);
863
- }
864
- 25% {
865
- transform: translate(var(--g2x), var(--g2y));
866
- opacity: var(--g2o);
867
- }
868
- 44% {
869
- transform: translate(var(--g3x), var(--g3y));
870
- opacity: var(--g3o);
871
- }
872
- 50% {
873
- transform: translate(var(--g4x), var(--g4y));
874
- opacity: var(--g4o);
875
- }
876
- 69% {
877
- transform: translate(var(--g5x), var(--g5y));
878
- opacity: var(--g5o);
879
- }
880
- 75% {
881
- transform: translate(var(--g6x), var(--g6y));
882
- opacity: var(--g6o);
883
- }
884
- 94% {
885
- transform: translate(var(--g7x), var(--g7y));
886
- opacity: var(--g7o);
887
- }
888
- 100% {
889
- transform: translate(var(--g8x), var(--g8y));
890
- opacity: var(--g8o);
891
- }
892
- }
893
-
894
- /* ---- Morph ---- */
895
- .orb-morph {
896
- position: absolute;
897
- inset: 0;
898
- transform: scale(var(--orb-k, 1));
899
- }
900
-
901
- .orb-morph-dot {
902
- position: absolute;
903
- left: 50%;
904
- top: 50%;
905
- width: 3px;
906
- height: 3px;
907
- margin: -1.5px 0 0 -1.5px;
908
- border-radius: 50%;
909
- background: currentColor;
910
- animation: orb-morph 4.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
911
- }
912
-
913
- @keyframes orb-morph {
914
- 0%,
915
- 5% {
916
- transform: translate(var(--m-1));
917
- }
918
- 25%,
919
- 30% {
920
- transform: translate(var(--m-2));
921
- }
922
- 50%,
923
- 55% {
924
- transform: translate(var(--m-3));
925
- }
926
- 75%,
927
- 80% {
928
- transform: translate(var(--m-4));
929
- }
930
- 100% {
931
- transform: translate(var(--m-1));
932
- }
933
- }
934
-
935
- .orb-morph[data-variant="M2"] {
936
- animation: orb-morph-twist 9.6s linear infinite;
937
- }
938
-
939
- .orb-morph[data-variant="M4"] {
940
- animation: orb-morph-twist 9.6s linear infinite;
941
- }
942
-
943
- .orb-morph[data-variant="M5"] .orb-morph-dot {
944
- animation: orb-morph-scatter 2.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
945
- }
946
-
947
- @keyframes orb-morph-scatter {
948
- 0%,
949
- 12% {
950
- transform: translate(var(--m-1));
951
- opacity: 1;
952
- }
953
- 38%,
954
- 62% {
955
- transform: translate(var(--m-2));
956
- opacity: calc(1 - 0.6 * var(--m-depth, 0));
957
- }
958
- 88%,
959
- 100% {
960
- transform: translate(var(--m-1));
961
- opacity: 1;
962
- }
963
- }
964
-
965
- @keyframes orb-morph-twist {
966
- from {
967
- transform: scale(var(--orb-k, 1)) rotate(0deg);
968
- }
969
- to {
970
- transform: scale(var(--orb-k, 1)) rotate(360deg);
971
- }
972
- }
973
-
974
- @media (prefers-reduced-motion: reduce) {
975
- .orb-cell,
976
- .orb-shape,
977
- .orb-ring-dot,
978
- .orb-helix-dot,
979
- .orb-morph-dot {
980
- animation: none !important;
981
- }
982
- .orb-cell[data-mid] {
983
- opacity: 1 !important;
984
- }
985
- .orb-shape {
986
- opacity: 0.3 !important;
987
- filter: blur(1.4px) !important;
988
- transform: none !important;
989
- }
990
- .orb-shape-a {
991
- opacity: 1 !important;
992
- filter: blur(0) !important;
993
- }
994
- .orb-ring-dot {
995
- opacity: 0.7 !important;
996
- }
997
- .orb-ring,
998
- .orb-helix,
999
- .orb-morph {
1000
- animation: none !important;
1001
- }
1002
- }
1
+ /* Adapted from AICSS Orbs (MIT, https://github.com/kvnkld/aicss). */
2
+ /* Orbs - two families of agent activity indicator.
3
+ *
4
+ * The geometry is authored at a 28px stage and scaled with --orb-k, so
5
+ * the hand-tuned dot sizes, pitch and blur radii hold at any size.
6
+ *
7
+ * Per-segment easings inside @keyframes are written as literals: an
8
+ * `animation-timing-function` declaration inside a keyframe block is
9
+ * read by the animation engine, not resolved against the element, so a
10
+ * var() there would not resolve. The numbers mirror the three custom
11
+ * properties below exactly. */
12
+
13
+ /* Majapi UI: colors follow the theme tokens instead of upstream hex values.
14
+ The dots inherit the surrounding text color (e.g. inside a Button). */
15
+ .orb-root {
16
+ --orb-fg: currentColor;
17
+ --orb-pill-bg: var(--popover);
18
+ --orb-pill-shadow:
19
+ 0 0 0 0.5px color-mix(in oklch, var(--foreground) 8%, transparent),
20
+ 0 1px 2px color-mix(in oklch, var(--foreground) 5%, transparent),
21
+ 0 2px 4px color-mix(in oklch, var(--foreground) 2%, transparent);
22
+ --orb-label: var(--foreground);
23
+ --orb-rest-ink: 0.14;
24
+ --orb-dim-ink: 0.07;
25
+ --orb-ring-rest-ink: 0.22;
26
+ }
27
+ .dark .orb-root {
28
+ --orb-rest-ink: 0.2;
29
+ --orb-dim-ink: 0.1;
30
+ --orb-ring-rest-ink: 0.3;
31
+ }
32
+
33
+ .orb-root {
34
+ --orb-ease-smooth: cubic-bezier(0.22, 1, 0.36, 1);
35
+ --orb-ease-out: cubic-bezier(0.17, 1, 0.32, 1);
36
+ --orb-ease-in-out: cubic-bezier(0.66, 0, 0.34, 1);
37
+
38
+ display: inline-flex;
39
+ align-items: center;
40
+ vertical-align: middle;
41
+ color: var(--orb-fg);
42
+ }
43
+
44
+ /* The inline pill form - same component, wrapped. */
45
+ .orb-root[data-pill] {
46
+ gap: 6px;
47
+ height: 32px;
48
+ padding: 0 12px 0 6px;
49
+ border-radius: 999px;
50
+ background: var(--orb-pill-bg);
51
+ /* A hairline ring rather than a border, so it can't affect layout, plus
52
+ a tight contact shadow and a wider ambient one. */
53
+ box-shadow: var(--orb-pill-shadow);
54
+ }
55
+
56
+ .orb-pill-label {
57
+ /* Majapi UI: same type as buttons/badges instead of upstream Inter 11.5px. */
58
+ font-family: inherit;
59
+ font-size: 0.875rem;
60
+ font-weight: 500;
61
+ line-height: 1;
62
+ color: var(--orb-label);
63
+ white-space: nowrap;
64
+ }
65
+
66
+ .orb-glyph {
67
+ position: relative;
68
+ display: block;
69
+ flex: none;
70
+ width: 20px;
71
+ height: 20px;
72
+ overflow: hidden;
73
+ contain: strict;
74
+ }
75
+
76
+ /* --- Lattice: discrete dots on a fixed 3×3 grid ------------------- */
77
+
78
+ .orb-lattice {
79
+ position: absolute;
80
+ left: 0;
81
+ top: 0;
82
+ width: 28px;
83
+ height: 28px;
84
+ transform-origin: 0 0;
85
+ /* Three 3px dots on a 6px pitch measure 15px, so the grid is offset to sit
86
+ centred on the 28px stage. It deliberately does not fill the stage: that
87
+ is what keeps its visual weight level with the Lens circles. */
88
+ transform: scale(var(--orb-k, 1)) translate(6.5px, 6.5px);
89
+ /* Resting ink of an unlit cell - the grid stays legible between beats.
90
+ --orb-dim is for cells sitting a choreography out entirely. */
91
+ --orb-rest: var(--orb-rest-ink, 0.14);
92
+ --orb-dim: var(--orb-dim-ink, 0.07);
93
+ }
94
+
95
+ .orb-cell {
96
+ position: absolute;
97
+ width: 3px;
98
+ height: 3px;
99
+ border-radius: 50%;
100
+ background: currentColor;
101
+ opacity: var(--orb-rest);
102
+ }
103
+
104
+ /* One wave shape drives all three sweeps. What separates them is the pair of
105
+ duration and per-cell stagger: the stagger sets how fast the wavefront
106
+ travels, the duration how many cells it holds lit at once - which is to
107
+ say, how wide the band reads. */
108
+ .orb-lattice[data-variant="S1"] .orb-cell {
109
+ animation: orb-wave 1.7s var(--orb-ease-in-out) infinite both;
110
+ }
111
+
112
+ .orb-lattice[data-variant="S2"] .orb-cell {
113
+ animation: orb-wave 1.7s var(--orb-ease-in-out) infinite both;
114
+ }
115
+
116
+ .orb-lattice[data-variant="S4"] .orb-cell {
117
+ animation: orb-wave 1.6s var(--orb-ease-in-out) infinite both;
118
+ }
119
+
120
+ .orb-lattice[data-variant="S3"] .orb-cell {
121
+ animation: orb-comet 1.7s var(--orb-ease-smooth) infinite both;
122
+ }
123
+
124
+ /* Interior cells sit out `orbit` and drop back, so the ring reads as a
125
+ ring and the travelling head has something to stand out against. */
126
+ .orb-lattice[data-variant="S3"] .orb-cell[data-still] {
127
+ animation: none;
128
+ opacity: var(--orb-dim);
129
+ }
130
+
131
+ .orb-lattice[data-variant="S5"] .orb-cell {
132
+ animation: orb-comet 1.7s var(--orb-ease-smooth) infinite both;
133
+ }
134
+
135
+ .orb-lattice[data-variant="S5"] .orb-cell[data-still] {
136
+ animation: none;
137
+ opacity: var(--orb-dim);
138
+ }
139
+
140
+ /* Swells and subsides on the same symmetric curve, so there is no flash and
141
+ no hard edge - the cell rises out of its resting ink and sinks back into
142
+ it. The long tail after 56% is the gap between beats. */
143
+ @keyframes orb-wave {
144
+ 0% {
145
+ opacity: var(--orb-rest);
146
+ transform: scale(1);
147
+ animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
148
+ }
149
+ 28% {
150
+ opacity: 1;
151
+ transform: scale(1.18);
152
+ animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
153
+ }
154
+ 56% {
155
+ opacity: var(--orb-rest);
156
+ transform: scale(1);
157
+ }
158
+ 100% {
159
+ opacity: var(--orb-rest);
160
+ transform: scale(1);
161
+ }
162
+ }
163
+
164
+ /* Starts lit and decays, so staggered ring cells form a head and tail.
165
+ The decay spans ~3.5 of the 8 ring positions - enough to read as a comet. */
166
+ @keyframes orb-comet {
167
+ 0% {
168
+ opacity: 1;
169
+ transform: scale(1.2);
170
+ /* Linear, so the cells behind the head form an even gradient instead
171
+ of collapsing to rest within the first two. */
172
+ animation-timing-function: linear;
173
+ }
174
+ 45% {
175
+ opacity: var(--orb-rest);
176
+ transform: scale(1);
177
+ }
178
+ 100% {
179
+ opacity: var(--orb-rest);
180
+ transform: scale(1);
181
+ }
182
+ }
183
+
184
+ /* --- Lens: three circles at depth, blur reads as distance --------- */
185
+
186
+ .orb-lens {
187
+ position: absolute;
188
+ left: 0;
189
+ top: 0;
190
+ width: 28px;
191
+ height: 28px;
192
+ transform-origin: 0 0;
193
+ transform: scale(var(--orb-k, 1));
194
+ }
195
+
196
+ .orb-shape {
197
+ position: absolute;
198
+ left: 50%;
199
+ top: 50%;
200
+ width: var(--orb-d, 7px);
201
+ height: var(--orb-d, 7px);
202
+ /* Pulled back by its own half-size, so --orb-d is the only knob a variant
203
+ has to touch to resize the cast and it stays centred on the stage. */
204
+ margin: calc(var(--orb-d, 7px) / -2) 0 0 calc(var(--orb-d, 7px) / -2);
205
+ border-radius: 50%;
206
+ background: currentColor;
207
+ }
208
+
209
+ /* focus - attention travels the cast: each circle pulls into focus in turn.
210
+ Four circles on the corners of a square, one size for all of them, so the
211
+ only thing separating them is which one is sharp.
212
+
213
+ A second longer than the three-circle version it grew out of: the square
214
+ has four stations to visit and each one keeps the same unhurried second.
215
+
216
+ The delays count down rather than up because a more negative delay seeds a
217
+ circle further into its cycle: -3s of a 4s cycle runs three quarters ahead,
218
+ which is what sends focus round the square clockwise. */
219
+ .orb-lens[data-variant="B1"] .orb-shape {
220
+ --orb-d: 6px;
221
+ animation: orb-focus 4s var(--orb-ease-smooth) infinite both;
222
+ }
223
+
224
+ .orb-lens[data-variant="B1"] .orb-shape-a {
225
+ --orb-ox: -4.5px;
226
+ --orb-oy: -4.5px;
227
+ animation-delay: 0s;
228
+ }
229
+
230
+ .orb-lens[data-variant="B1"] .orb-shape-b {
231
+ --orb-ox: 4.5px;
232
+ --orb-oy: -4.5px;
233
+ animation-delay: -3s;
234
+ }
235
+
236
+ .orb-lens[data-variant="B1"] .orb-shape-c {
237
+ --orb-ox: 4.5px;
238
+ --orb-oy: 4.5px;
239
+ animation-delay: -2s;
240
+ }
241
+
242
+ .orb-lens[data-variant="B1"] .orb-shape-d {
243
+ --orb-ox: -4.5px;
244
+ --orb-oy: 4.5px;
245
+ animation-delay: -1s;
246
+ }
247
+
248
+ /* Opacity gradient around the square: active = 1.0, next neighbour = 0.30,
249
+ diagonal = 0.10, far = 0.05. Two circles are always clearly visible, the
250
+ rest are ghost hints. */
251
+ @keyframes orb-focus {
252
+ 0%,
253
+ 100% {
254
+ opacity: 0.05;
255
+ filter: blur(2px);
256
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
257
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
258
+ }
259
+ 12% {
260
+ opacity: 1;
261
+ filter: blur(0);
262
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
263
+ animation-timing-function: linear;
264
+ }
265
+ 22% {
266
+ opacity: 1;
267
+ filter: blur(0);
268
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
269
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
270
+ }
271
+ /* Next neighbour - one quarter away: clearly visible */
272
+ 38% {
273
+ opacity: 0.3;
274
+ filter: blur(1.2px);
275
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.06);
276
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
277
+ }
278
+ /* Diagonal - half a cycle away: ghost */
279
+ 58% {
280
+ opacity: 0.1;
281
+ filter: blur(1.8px);
282
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.1);
283
+ animation-timing-function: linear;
284
+ }
285
+ /* Far neighbour - three quarters away: barely there */
286
+ 82% {
287
+ opacity: 0.05;
288
+ filter: blur(2px);
289
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
290
+ }
291
+ }
292
+
293
+ /* drift - the cast circles the stage on one track, sharp at the front and
294
+ blurred away at the back, so the orb reads as looking around. Uniform
295
+ size: the depth cue is doing the work, a size ladder would fight it. */
296
+ .orb-lens[data-variant="B2"] .orb-shape {
297
+ animation: orb-revolve 3.3s linear infinite both;
298
+ }
299
+
300
+ /* Evenly spaced around the track, so one is always at the front. */
301
+ .orb-lens[data-variant="B2"] .orb-shape-a {
302
+ animation-delay: 0s;
303
+ }
304
+
305
+ .orb-lens[data-variant="B2"] .orb-shape-b {
306
+ animation-delay: -1.1s;
307
+ }
308
+
309
+ .orb-lens[data-variant="B2"] .orb-shape-c {
310
+ animation-delay: -2.2s;
311
+ }
312
+
313
+ /* rotate() then translateY() walks a circle. Linear all the way: an eased
314
+ rotation on a circular path reads as a wobble, not as travel. */
315
+ @keyframes orb-revolve {
316
+ 0% {
317
+ opacity: 1;
318
+ filter: blur(0);
319
+ transform: rotate(0deg) translateY(6.5px) scale(1);
320
+ }
321
+ 25% {
322
+ opacity: 0.55;
323
+ filter: blur(1.3px);
324
+ transform: rotate(90deg) translateY(6.5px) scale(0.82);
325
+ }
326
+ 50% {
327
+ opacity: 0.28;
328
+ filter: blur(2.4px);
329
+ transform: rotate(180deg) translateY(6.5px) scale(0.66);
330
+ }
331
+ 75% {
332
+ opacity: 0.55;
333
+ filter: blur(1.3px);
334
+ transform: rotate(270deg) translateY(6.5px) scale(0.82);
335
+ }
336
+ 100% {
337
+ opacity: 1;
338
+ filter: blur(0);
339
+ transform: rotate(360deg) translateY(6.5px) scale(1);
340
+ }
341
+ }
342
+
343
+ /* bloom - shapes emanate from the centre, blurring out as they grow.
344
+ Linear keeps the total ink even; on a front-loaded curve the shapes
345
+ jump to their large, blurred end state and the orb alternates between
346
+ a heavy blot and an empty haze. */
347
+ .orb-lens[data-variant="B3"] .orb-shape {
348
+ animation: orb-bloom 4.2s linear infinite both;
349
+ }
350
+
351
+ .orb-lens[data-variant="B3"] .orb-shape-a {
352
+ animation-delay: 0s;
353
+ }
354
+
355
+ .orb-lens[data-variant="B3"] .orb-shape-b {
356
+ animation-delay: -1.4s;
357
+ }
358
+
359
+ .orb-lens[data-variant="B3"] .orb-shape-c {
360
+ animation-delay: -2.8s;
361
+ }
362
+
363
+ /* Each ripple dies at 62% and waits out the rest, so the three overlapping
364
+ blooms leave gaps. Without the gap the aggregate is a constant haze and
365
+ the outward motion stops reading at all. Sharp circle appears, holds
366
+ briefly, then dissolves outward - blur only kicks in once opacity starts
367
+ dropping, so the circle stays crisp while it's visible and the blur reads
368
+ as the ripple dissipating. */
369
+ @keyframes orb-bloom {
370
+ 0% {
371
+ opacity: 0;
372
+ filter: blur(0);
373
+ transform: scale(0.35);
374
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
375
+ }
376
+ 8% {
377
+ opacity: 1;
378
+ filter: blur(0);
379
+ transform: scale(0.55);
380
+ animation-timing-function: linear;
381
+ }
382
+ 24% {
383
+ opacity: 1;
384
+ filter: blur(0);
385
+ transform: scale(0.72);
386
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
387
+ }
388
+ 42% {
389
+ opacity: 0.1;
390
+ filter: blur(1.8px);
391
+ transform: scale(1.5);
392
+ }
393
+ 62% {
394
+ opacity: 0;
395
+ filter: blur(2.8px);
396
+ transform: scale(2.4);
397
+ }
398
+ 100% {
399
+ opacity: 0;
400
+ filter: blur(2.8px);
401
+ transform: scale(2.4);
402
+ }
403
+ }
404
+
405
+ /* converge - a single circle traces an equilateral triangle (top → bottom-right
406
+ → bottom-left → top) with handoff-style easing: full size and sharp at each
407
+ vertex, smaller and slightly blurred in transit. orbB breathes at the
408
+ centroid as a subtle depth cue; orbC is hidden. */
409
+ .orb-lens[data-variant="B4"] .orb-shape-a {
410
+ animation: orb-converge 3.6s linear infinite both;
411
+ }
412
+ .orb-lens[data-variant="B4"] .orb-shape-b {
413
+ animation: orb-breathe 3.6s ease-in-out infinite both;
414
+ }
415
+ .orb-lens[data-variant="B4"] .orb-shape-c {
416
+ display: none;
417
+ }
418
+
419
+ @keyframes orb-converge {
420
+ 0% {
421
+ transform: translate(0px, -5px) scale(1);
422
+ filter: blur(0);
423
+ animation-timing-function: linear;
424
+ }
425
+ 10% {
426
+ transform: translate(0px, -5px) scale(1);
427
+ filter: blur(0);
428
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
429
+ }
430
+ 22% {
431
+ transform: translate(2.15px, -1.25px) scale(0.72);
432
+ filter: blur(0.8px);
433
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
434
+ }
435
+ 33% {
436
+ transform: translate(4.3px, 2.5px) scale(1);
437
+ filter: blur(0);
438
+ animation-timing-function: linear;
439
+ }
440
+ 43% {
441
+ transform: translate(4.3px, 2.5px) scale(1);
442
+ filter: blur(0);
443
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
444
+ }
445
+ 55% {
446
+ transform: translate(0px, 2.5px) scale(0.72);
447
+ filter: blur(0.8px);
448
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
449
+ }
450
+ 66% {
451
+ transform: translate(-4.3px, 2.5px) scale(1);
452
+ filter: blur(0);
453
+ animation-timing-function: linear;
454
+ }
455
+ 77% {
456
+ transform: translate(-4.3px, 2.5px) scale(1);
457
+ filter: blur(0);
458
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
459
+ }
460
+ 88% {
461
+ transform: translate(-2.15px, -1.25px) scale(0.72);
462
+ filter: blur(0.8px);
463
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
464
+ }
465
+ 100% {
466
+ transform: translate(0px, -5px) scale(1);
467
+ filter: blur(0);
468
+ }
469
+ }
470
+
471
+ /* handoff - the cast crosses the focal plane one after another, always left
472
+ to right, like work being passed on. The shorthand curve is only a
473
+ fallback; every segment below sets its own. */
474
+ .orb-lens[data-variant="B5"] .orb-shape {
475
+ animation: orb-handoff 2.8s linear infinite both;
476
+ }
477
+
478
+ /* Half a cycle apart, so one is always at the focal plane while the other is
479
+ invisible at an end and the loop point cannot be seen. */
480
+ .orb-lens[data-variant="B5"] .orb-shape-a {
481
+ animation-delay: 0s;
482
+ }
483
+
484
+ .orb-lens[data-variant="B5"] .orb-shape-c {
485
+ animation-delay: -1.4s;
486
+ }
487
+
488
+ /* The third holds the centre and breathes - a soft depth cue behind the
489
+ traffic rather than another traveller. */
490
+ .orb-lens[data-variant="B5"] .orb-shape-b {
491
+ animation-name: orb-breathe;
492
+ animation-duration: 3.6s;
493
+ }
494
+
495
+ /* Enters small from the left, reaches standard size at the focal plane, then
496
+ shrinks and fades out to the right. At the dwell (centre) the circle is
497
+ exactly 1× - no pulsing, no bounce, just a clean handoff. */
498
+ @keyframes orb-handoff {
499
+ 0% {
500
+ opacity: 0;
501
+ filter: blur(2.4px);
502
+ transform: translateX(-11px) scale(0.55);
503
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
504
+ }
505
+ 22% {
506
+ opacity: 1;
507
+ filter: blur(0);
508
+ transform: translateX(-1px) scale(1);
509
+ animation-timing-function: linear;
510
+ }
511
+ 37% {
512
+ opacity: 1;
513
+ filter: blur(0);
514
+ transform: translateX(0) scale(1);
515
+ animation-timing-function: linear;
516
+ }
517
+ 52% {
518
+ opacity: 1;
519
+ filter: blur(0);
520
+ transform: translateX(1px) scale(1);
521
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
522
+ }
523
+ 70% {
524
+ opacity: 0;
525
+ filter: blur(2.4px);
526
+ transform: translateX(11px) scale(0.55);
527
+ }
528
+ 100% {
529
+ opacity: 0;
530
+ filter: blur(2.4px);
531
+ transform: translateX(11px) scale(0.55);
532
+ }
533
+ }
534
+
535
+ @keyframes orb-breathe {
536
+ 0%,
537
+ 100% {
538
+ opacity: 0.16;
539
+ filter: blur(2.4px);
540
+ transform: scale(1.2);
541
+ }
542
+ 50% {
543
+ opacity: 0.32;
544
+ filter: blur(1.6px);
545
+ transform: scale(0.98);
546
+ }
547
+ }
548
+
549
+ /* --- Ring: eight circles on a fixed ring ----------------------------- */
550
+
551
+ .orb-ring {
552
+ position: absolute;
553
+ inset: 0;
554
+ transform: scale(var(--orb-k, 1));
555
+ --orb-ring-rest: var(--orb-ring-rest-ink, 0.22);
556
+ }
557
+
558
+ .orb-ring-dot {
559
+ position: absolute;
560
+ left: 50%;
561
+ top: 50%;
562
+ width: 3px;
563
+ height: 3px;
564
+ margin: -1.5px 0 0 -1.5px;
565
+ border-radius: 50%;
566
+ background: currentColor;
567
+ transform: translate(var(--orb-rx), var(--orb-ry));
568
+ }
569
+
570
+ .orb-ring[data-variant="C1"] .orb-ring-dot {
571
+ opacity: var(--orb-ring-rest);
572
+ animation: orb-ring-chase 1.6s linear infinite both;
573
+ }
574
+
575
+ @keyframes orb-ring-chase {
576
+ 0%,
577
+ 11% {
578
+ opacity: 1;
579
+ }
580
+ 12.5%,
581
+ 100% {
582
+ opacity: var(--orb-ring-rest);
583
+ }
584
+ }
585
+
586
+ .orb-ring[data-variant="C2"] .orb-ring-dot {
587
+ animation: orb-ring-pulse 2s ease-in-out infinite both;
588
+ }
589
+
590
+ @keyframes orb-ring-pulse {
591
+ 0%,
592
+ 100% {
593
+ opacity: 0.18;
594
+ transform: translate(var(--orb-rx), var(--orb-ry)) scale(0.7);
595
+ }
596
+ 50% {
597
+ opacity: 1;
598
+ transform: translate(var(--orb-rx), var(--orb-ry)) scale(1.15);
599
+ }
600
+ }
601
+
602
+ .orb-ring[data-variant="C3"] .orb-ring-dot {
603
+ animation: orb-ring-comet 1.8s ease-in-out infinite both;
604
+ }
605
+
606
+ @keyframes orb-ring-comet {
607
+ 0%,
608
+ 100% {
609
+ opacity: 0.08;
610
+ transform: translate(var(--orb-rx), var(--orb-ry));
611
+ }
612
+ 12% {
613
+ opacity: 1;
614
+ transform: translate(var(--orb-rx), var(--orb-ry));
615
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
616
+ }
617
+ 35% {
618
+ opacity: 0.5;
619
+ transform: translate(var(--orb-rx), var(--orb-ry));
620
+ }
621
+ 60% {
622
+ opacity: 0.12;
623
+ transform: translate(var(--orb-rx), var(--orb-ry));
624
+ }
625
+ }
626
+
627
+ .orb-ring[data-variant="C4"] .orb-ring-dot {
628
+ animation: orb-ring-stagger 1.6s ease-in-out infinite both;
629
+ }
630
+
631
+ @keyframes orb-ring-stagger {
632
+ 0%,
633
+ 100% {
634
+ opacity: 1;
635
+ transform: translate(var(--orb-rx), var(--orb-ry));
636
+ }
637
+ 50% {
638
+ opacity: 0.15;
639
+ transform: translate(var(--orb-rx), var(--orb-ry));
640
+ }
641
+ }
642
+
643
+ .orb-ring[data-variant="C5"] .orb-ring-dot {
644
+ animation: orb-ring-comet 1.8s ease-in-out infinite both;
645
+ }
646
+
647
+ /* ---- Globe (Helix family) ---- */
648
+ .orb-helix {
649
+ position: absolute;
650
+ inset: 0;
651
+ transform: scale(var(--orb-k, 1));
652
+ }
653
+
654
+ .orb-helix-dot {
655
+ position: absolute;
656
+ left: 50%;
657
+ top: 50%;
658
+ width: 2px;
659
+ height: 2px;
660
+ margin: -1px 0 0 -1px;
661
+ border-radius: 50%;
662
+ background: currentColor;
663
+ will-change: transform, opacity;
664
+ }
665
+
666
+ .orb-helix[data-variant="G1"] .orb-helix-dot {
667
+ animation: orb-globe-spin 4.5s linear infinite both;
668
+ }
669
+ .orb-helix[data-variant="G2"] .orb-helix-dot {
670
+ animation: orb-globe-spin 3.6s linear infinite both;
671
+ }
672
+ .orb-helix[data-variant="G3"] .orb-helix-dot {
673
+ animation: orb-globe-ringturn 2.8s linear infinite both;
674
+ }
675
+ .orb-helix[data-variant="G4"] .orb-helix-dot {
676
+ animation: orb-globe-ringturn 2.8s linear infinite both;
677
+ }
678
+ .orb-helix[data-variant="G5"] .orb-helix-dot {
679
+ animation: orb-globe-breathe 3.6s linear infinite both;
680
+ }
681
+
682
+ @keyframes orb-globe-spin {
683
+ 0%,
684
+ 100% {
685
+ transform: translate(var(--g0x), var(--g0y));
686
+ opacity: var(--g0o);
687
+ }
688
+ 12.5% {
689
+ transform: translate(var(--g1x), var(--g1y));
690
+ opacity: var(--g1o);
691
+ }
692
+ 25% {
693
+ transform: translate(var(--g2x), var(--g2y));
694
+ opacity: var(--g2o);
695
+ }
696
+ 37.5% {
697
+ transform: translate(var(--g3x), var(--g3y));
698
+ opacity: var(--g3o);
699
+ }
700
+ 50% {
701
+ transform: translate(var(--g4x), var(--g4y));
702
+ opacity: var(--g4o);
703
+ }
704
+ 62.5% {
705
+ transform: translate(var(--g5x), var(--g5y));
706
+ opacity: var(--g5o);
707
+ }
708
+ 75% {
709
+ transform: translate(var(--g6x), var(--g6y));
710
+ opacity: var(--g6o);
711
+ }
712
+ 87.5% {
713
+ transform: translate(var(--g7x), var(--g7y));
714
+ opacity: var(--g7o);
715
+ }
716
+ }
717
+
718
+ @keyframes orb-globe-ringturn {
719
+ 0% {
720
+ transform: translate(var(--g0x), var(--g0y));
721
+ opacity: var(--g0o);
722
+ }
723
+ 2.5% {
724
+ transform: translate(var(--g1x), var(--g1y));
725
+ opacity: var(--g1o);
726
+ }
727
+ 5% {
728
+ transform: translate(var(--g2x), var(--g2y));
729
+ opacity: var(--g2o);
730
+ }
731
+ 7.5%,
732
+ 10% {
733
+ transform: translate(var(--g3x), var(--g3y));
734
+ opacity: var(--g3o);
735
+ }
736
+ 12.5% {
737
+ transform: translate(var(--g4x), var(--g4y));
738
+ opacity: var(--g4o);
739
+ }
740
+ 15% {
741
+ transform: translate(var(--g5x), var(--g5y));
742
+ opacity: var(--g5o);
743
+ }
744
+ 17.5%,
745
+ 20% {
746
+ transform: translate(var(--g6x), var(--g6y));
747
+ opacity: var(--g6o);
748
+ }
749
+ 22.5% {
750
+ transform: translate(var(--g7x), var(--g7y));
751
+ opacity: var(--g7o);
752
+ }
753
+ 25% {
754
+ transform: translate(var(--g8x), var(--g8y));
755
+ opacity: var(--g8o);
756
+ }
757
+ 27.5%,
758
+ 30% {
759
+ transform: translate(var(--g9x), var(--g9y));
760
+ opacity: var(--g9o);
761
+ }
762
+ 32.5% {
763
+ transform: translate(var(--g10x), var(--g10y));
764
+ opacity: var(--g10o);
765
+ }
766
+ 35% {
767
+ transform: translate(var(--g11x), var(--g11y));
768
+ opacity: var(--g11o);
769
+ }
770
+ 37.5%,
771
+ 40% {
772
+ transform: translate(var(--g12x), var(--g12y));
773
+ opacity: var(--g12o);
774
+ }
775
+ 42.5% {
776
+ transform: translate(var(--g13x), var(--g13y));
777
+ opacity: var(--g13o);
778
+ }
779
+ 45% {
780
+ transform: translate(var(--g14x), var(--g14y));
781
+ opacity: var(--g14o);
782
+ }
783
+ 47.5%,
784
+ 50% {
785
+ transform: translate(var(--g15x), var(--g15y));
786
+ opacity: var(--g15o);
787
+ }
788
+ 52.5% {
789
+ transform: translate(var(--g16x), var(--g16y));
790
+ opacity: var(--g16o);
791
+ }
792
+ 55% {
793
+ transform: translate(var(--g17x), var(--g17y));
794
+ opacity: var(--g17o);
795
+ }
796
+ 57.5%,
797
+ 60% {
798
+ transform: translate(var(--g18x), var(--g18y));
799
+ opacity: var(--g18o);
800
+ }
801
+ 62.5% {
802
+ transform: translate(var(--g19x), var(--g19y));
803
+ opacity: var(--g19o);
804
+ }
805
+ 65% {
806
+ transform: translate(var(--g20x), var(--g20y));
807
+ opacity: var(--g20o);
808
+ }
809
+ 67.5%,
810
+ 70% {
811
+ transform: translate(var(--g21x), var(--g21y));
812
+ opacity: var(--g21o);
813
+ }
814
+ 72.5% {
815
+ transform: translate(var(--g22x), var(--g22y));
816
+ opacity: var(--g22o);
817
+ }
818
+ 75% {
819
+ transform: translate(var(--g23x), var(--g23y));
820
+ opacity: var(--g23o);
821
+ }
822
+ 77.5%,
823
+ 80% {
824
+ transform: translate(var(--g24x), var(--g24y));
825
+ opacity: var(--g24o);
826
+ }
827
+ 82.5% {
828
+ transform: translate(var(--g25x), var(--g25y));
829
+ opacity: var(--g25o);
830
+ }
831
+ 85% {
832
+ transform: translate(var(--g26x), var(--g26y));
833
+ opacity: var(--g26o);
834
+ }
835
+ 87.5%,
836
+ 90% {
837
+ transform: translate(var(--g27x), var(--g27y));
838
+ opacity: var(--g27o);
839
+ }
840
+ 92.5% {
841
+ transform: translate(var(--g28x), var(--g28y));
842
+ opacity: var(--g28o);
843
+ }
844
+ 95% {
845
+ transform: translate(var(--g29x), var(--g29y));
846
+ opacity: var(--g29o);
847
+ }
848
+ 97.5%,
849
+ 100% {
850
+ transform: translate(var(--g30x), var(--g30y));
851
+ opacity: var(--g30o);
852
+ }
853
+ }
854
+
855
+ @keyframes orb-globe-breathe {
856
+ 0% {
857
+ transform: translate(var(--g0x), var(--g0y));
858
+ opacity: var(--g0o);
859
+ }
860
+ 19% {
861
+ transform: translate(var(--g1x), var(--g1y));
862
+ opacity: var(--g1o);
863
+ }
864
+ 25% {
865
+ transform: translate(var(--g2x), var(--g2y));
866
+ opacity: var(--g2o);
867
+ }
868
+ 44% {
869
+ transform: translate(var(--g3x), var(--g3y));
870
+ opacity: var(--g3o);
871
+ }
872
+ 50% {
873
+ transform: translate(var(--g4x), var(--g4y));
874
+ opacity: var(--g4o);
875
+ }
876
+ 69% {
877
+ transform: translate(var(--g5x), var(--g5y));
878
+ opacity: var(--g5o);
879
+ }
880
+ 75% {
881
+ transform: translate(var(--g6x), var(--g6y));
882
+ opacity: var(--g6o);
883
+ }
884
+ 94% {
885
+ transform: translate(var(--g7x), var(--g7y));
886
+ opacity: var(--g7o);
887
+ }
888
+ 100% {
889
+ transform: translate(var(--g8x), var(--g8y));
890
+ opacity: var(--g8o);
891
+ }
892
+ }
893
+
894
+ /* ---- Morph ---- */
895
+ .orb-morph {
896
+ position: absolute;
897
+ inset: 0;
898
+ transform: scale(var(--orb-k, 1));
899
+ }
900
+
901
+ .orb-morph-dot {
902
+ position: absolute;
903
+ left: 50%;
904
+ top: 50%;
905
+ width: 3px;
906
+ height: 3px;
907
+ margin: -1.5px 0 0 -1.5px;
908
+ border-radius: 50%;
909
+ background: currentColor;
910
+ animation: orb-morph 4.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
911
+ }
912
+
913
+ @keyframes orb-morph {
914
+ 0%,
915
+ 5% {
916
+ transform: translate(var(--m-1));
917
+ }
918
+ 25%,
919
+ 30% {
920
+ transform: translate(var(--m-2));
921
+ }
922
+ 50%,
923
+ 55% {
924
+ transform: translate(var(--m-3));
925
+ }
926
+ 75%,
927
+ 80% {
928
+ transform: translate(var(--m-4));
929
+ }
930
+ 100% {
931
+ transform: translate(var(--m-1));
932
+ }
933
+ }
934
+
935
+ .orb-morph[data-variant="M2"] {
936
+ animation: orb-morph-twist 9.6s linear infinite;
937
+ }
938
+
939
+ .orb-morph[data-variant="M4"] {
940
+ animation: orb-morph-twist 9.6s linear infinite;
941
+ }
942
+
943
+ .orb-morph[data-variant="M5"] .orb-morph-dot {
944
+ animation: orb-morph-scatter 2.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
945
+ }
946
+
947
+ @keyframes orb-morph-scatter {
948
+ 0%,
949
+ 12% {
950
+ transform: translate(var(--m-1));
951
+ opacity: 1;
952
+ }
953
+ 38%,
954
+ 62% {
955
+ transform: translate(var(--m-2));
956
+ opacity: calc(1 - 0.6 * var(--m-depth, 0));
957
+ }
958
+ 88%,
959
+ 100% {
960
+ transform: translate(var(--m-1));
961
+ opacity: 1;
962
+ }
963
+ }
964
+
965
+ @keyframes orb-morph-twist {
966
+ from {
967
+ transform: scale(var(--orb-k, 1)) rotate(0deg);
968
+ }
969
+ to {
970
+ transform: scale(var(--orb-k, 1)) rotate(360deg);
971
+ }
972
+ }
973
+
974
+ @media (prefers-reduced-motion: reduce) {
975
+ .orb-cell,
976
+ .orb-shape,
977
+ .orb-ring-dot,
978
+ .orb-helix-dot,
979
+ .orb-morph-dot {
980
+ animation: none !important;
981
+ }
982
+ .orb-cell[data-mid] {
983
+ opacity: 1 !important;
984
+ }
985
+ .orb-shape {
986
+ opacity: 0.3 !important;
987
+ filter: blur(1.4px) !important;
988
+ transform: none !important;
989
+ }
990
+ .orb-shape-a {
991
+ opacity: 1 !important;
992
+ filter: blur(0) !important;
993
+ }
994
+ .orb-ring-dot {
995
+ opacity: 0.7 !important;
996
+ }
997
+ .orb-ring,
998
+ .orb-helix,
999
+ .orb-morph {
1000
+ animation: none !important;
1001
+ }
1002
+ }