@lavalogic/scoria 0.40.8 → 0.40.10

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.
@@ -1,130 +1,1179 @@
1
1
  <svelte:options runes />
2
2
 
3
+ <script
4
+ lang="ts"
5
+ module
6
+ >
7
+ /**
8
+ * Warm, on-brand phrases printed inside the box's rear flap. One is chosen at
9
+ * random when the loader mounts and a fresh one is shown on each loop. Pass
10
+ * the `messages` prop to override this set.
11
+ */
12
+ export const defaultLoaderMessages: string[] = [
13
+ 'You rock',
14
+ 'Have a great day',
15
+ 'Thanks for waiting',
16
+ 'Almost there',
17
+ 'Hang tight',
18
+ 'Worth the wait',
19
+ "You're a star",
20
+ 'Nearly done',
21
+ 'Sit back and relax',
22
+ 'Good things come to those who wait',
23
+ 'On its way',
24
+ 'Making it happen',
25
+ 'Fresh out the box',
26
+ 'Hold tight, nearly there',
27
+ ];
28
+
29
+ // ---- geometry constants for the thrown-letter extrusion ----
30
+ const MAXLW = 200; // max combined letter width, box-local px (long words shrink to fit)
31
+ const DEPTH = 19; // extrusion layers behind each letter face
32
+ const EVX = 0.22; // extrusion direction: across the box's resting angle
33
+ const EVY = -0.24;
34
+ const EVZ = -1.0;
35
+ const NFLUTES = 4; // corrugation waves shaded across the thickness
36
+
37
+ function shuffle(a: number[]): number[] {
38
+ for (let i = a.length - 1; i > 0; i--) {
39
+ const j = (Math.random() * (i + 1)) | 0;
40
+ const t = a[i];
41
+ a[i] = a[j];
42
+ a[j] = t;
43
+ }
44
+ return a;
45
+ }
46
+ function rand(min: number, max: number): number {
47
+ return min + Math.random() * (max - min);
48
+ }
49
+ function n1(v: number): number {
50
+ return Math.round(v * 10) / 10;
51
+ }
52
+ function parseColor(c: string): [number, number, number] {
53
+ c = (c || '').trim();
54
+ if (c.charAt(0) === '#') {
55
+ if (c.length === 4) {
56
+ c = '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];
57
+ }
58
+ return [
59
+ parseInt(c.substr(1, 2), 16),
60
+ parseInt(c.substr(3, 2), 16),
61
+ parseInt(c.substr(5, 2), 16),
62
+ ];
63
+ }
64
+ const m = c.match(/(\d+)[,\s]+(\d+)[,\s]+(\d+)/);
65
+ if (m) {
66
+ return [+m[1], +m[2], +m[3]];
67
+ }
68
+ return [154, 160, 166];
69
+ }
70
+ function mixRGB(
71
+ a: [number, number, number],
72
+ b: [number, number, number],
73
+ t: number
74
+ ): [number, number, number] {
75
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
76
+ return [
77
+ Math.round(a[0] + (b[0] - a[0]) * t),
78
+ Math.round(a[1] + (b[1] - a[1]) * t),
79
+ Math.round(a[2] + (b[2] - a[2]) * t),
80
+ ];
81
+ }
82
+ function rgbStr(c: [number, number, number]): string {
83
+ return 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
84
+ }
85
+ // Shade a single extrusion layer like stacked fluted cardboard: it darkens
86
+ // toward the back, with flute banding so the thickness reads as corrugated
87
+ // sheets — matching the box stylistically.
88
+ function fluteCol(
89
+ k: number,
90
+ faceRGB: [number, number, number],
91
+ deepRGB: [number, number, number]
92
+ ): string {
93
+ const t = k / DEPTH; // 0 = just behind the face, 1 = deepest
94
+ const wave = 0.5 - 0.5 * Math.cos(t * Math.PI * 2 * NFLUTES); // 0 = ridge, 1 = valley
95
+ const ridge = mixRGB(faceRGB, deepRGB, 0.46 + 0.42 * t);
96
+ const valley = mixRGB(faceRGB, deepRGB, 0.74 + 0.26 * t);
97
+ return rgbStr(mixRGB(ridge, valley, wave));
98
+ }
99
+ </script>
100
+
3
101
  <script lang="ts">
4
- import { loadingText } from '../Helpers/Helpers.svelte.js';
5
102
  import type { LoadingAnimationProps } from './LoadingAnimationProps.js';
6
103
 
7
- const { showText = true, label = 'Loading' }: LoadingAnimationProps = $props();
104
+ const {
105
+ name = '',
106
+ showText = true,
107
+ label = 'Loading',
108
+ caption = 'Loading',
109
+ messages = defaultLoaderMessages,
110
+ letterFace = '#cdab73',
111
+ letterDeep = '#2a1d0d',
112
+ }: LoadingAnimationProps = $props();
113
+
114
+ // The word the box spells: the caller's first name, or the product name as a
115
+ // friendly fallback. Block capitals tumble cleanly and dodge lowercase
116
+ // descenders clipping the baseline on landing.
117
+ const word = $derived((name.trim() || 'FloWMS').toUpperCase());
118
+
119
+ // Per-letter render data: each letter is a stack of DEPTH layers (painted
120
+ // back-to-front) topped by the lit face, so it has genuine 3D thickness.
121
+ const geom = $derived.by(() => {
122
+ const chars = word.split('');
123
+ const N = Math.max(chars.length, 1);
124
+ const spacing = Math.min(40, MAXLW / N);
125
+ const fontSize = Math.max(13, Math.min(34, spacing * 0.92));
126
+ const perStep = Math.max(0.6, fontSize * 0.046);
127
+ const faceRGB = parseColor(letterFace);
128
+ const deepRGB = parseColor(letterDeep);
129
+ const letters = chars.map((ch) => {
130
+ const glyph = ch === ' ' ? ' ' : ch;
131
+ const layers: { transform: string; color: string }[] = [];
132
+ for (let k = DEPTH; k >= 1; k--) {
133
+ layers.push({
134
+ transform:
135
+ 'translate(-50%,-50%) translate3d(' +
136
+ n1(EVX * perStep * k) +
137
+ 'px,' +
138
+ n1(EVY * perStep * k) +
139
+ 'px,' +
140
+ n1(EVZ * perStep * k) +
141
+ 'px)',
142
+ color: fluteCol(k, faceRGB, deepRGB),
143
+ });
144
+ }
145
+ return { glyph, layers };
146
+ });
147
+ return { N, spacing, fontSize, letters };
148
+ });
149
+
150
+ // Confetti launch vectors — fixed once per instance so the burst is stable.
151
+ const fills = Array.from({ length: 16 }, () => ({
152
+ dx: rand(-50, 56),
153
+ dz: rand(-40, 40),
154
+ rise: rand(48, 90),
155
+ }));
156
+
157
+ let rootRef: HTMLDivElement | undefined = $state();
158
+ let boxRef: HTMLDivElement | undefined = $state();
159
+ let glowRef: HTMLDivElement | undefined = $state();
160
+ let s0pivot: HTMLDivElement | undefined = $state();
161
+ let s1pivot: HTMLDivElement | undefined = $state();
162
+ let s2pivot: HTMLDivElement | undefined = $state();
163
+ let s3pivot: HTMLDivElement | undefined = $state();
164
+ let letterRefs: (HTMLSpanElement | undefined)[] = $state([]);
165
+ let fillRefs: (HTMLSpanElement | undefined)[] = $state([]);
166
+
167
+ let msgIndex = $state(0);
168
+
169
+ $effect(() => {
170
+ const rootEl = rootRef;
171
+ const boxEl = boxRef;
172
+ if (!rootEl || !boxEl) {
173
+ return;
174
+ }
175
+
176
+ // Read reactive inputs so the timeline rebuilds when the word changes.
177
+ const { N, spacing } = geom;
178
+ const letterEls = letterRefs
179
+ .slice(0, geom.letters.length)
180
+ .filter((x): x is HTMLSpanElement => !!x);
181
+
182
+ const reduce =
183
+ window.matchMedia &&
184
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
185
+ if (reduce) {
186
+ return;
187
+ }
188
+
189
+ const cs = getComputedStyle(rootEl);
190
+ const hh = parseFloat(cs.getPropertyValue('--hh')) || 55;
191
+ const openDeg = parseFloat(cs.getPropertyValue('--open')) || 130;
192
+ const landY = 30;
193
+ const landZ = 112; // box-local: on the floor plane, out in front of the box
194
+
195
+ const anims: Animation[] = [];
196
+ const track = (a: Animation | null | undefined): void => {
197
+ if (a) {
198
+ anims.push(a);
199
+ }
200
+ };
201
+
202
+ // ---- timeline (ms), scaled to the word length ----
203
+ const D_rotin = 700;
204
+ const outerOpen: [number, number] = [600, 1080];
205
+ const innerOpen: [number, number] = [1020, 1520];
206
+ const outStagger = 205;
207
+ const outFlight = 860;
208
+ const outStart = 1560;
209
+ const outEnd = outStart + (N - 1) * outStagger + outFlight;
210
+ const confStart = outEnd + 240;
211
+ const confDur = 1300;
212
+ const confEnd = confStart + confDur;
213
+ const backStagger = 120;
214
+ const backFlight = 520;
215
+ const backStart = confEnd + 200;
216
+ const backEnd = backStart + (N - 1) * backStagger + backFlight;
217
+ const closeStart = backEnd + 180;
218
+ const innerClose: [number, number] = [closeStart, closeStart + 460];
219
+ const outerClose: [number, number] = [closeStart + 360, closeStart + 880];
220
+ const spinStart = outerClose[1] + 60;
221
+ const spinEnd = spinStart + 980;
222
+ const T = spinEnd + 460;
223
+
224
+ const o = (t: number): number => Math.max(0, Math.min(1, t / T));
225
+ const loop: KeyframeAnimationOptions = {
226
+ duration: T,
227
+ iterations: Infinity,
228
+ easing: 'linear',
229
+ };
230
+
231
+ // box: flatten -> tip to resting angle -> hold -> spin shut
232
+ const boxKF: Keyframe[] = [
233
+ {
234
+ offset: 0,
235
+ transform: 'rotateX(0deg) rotateY(0deg)',
236
+ easing: 'cubic-bezier(.5,0,.3,1)',
237
+ },
238
+ {
239
+ offset: o(D_rotin),
240
+ transform: 'rotateX(-26deg) rotateY(-32deg)',
241
+ easing: 'linear',
242
+ },
243
+ {
244
+ offset: o(spinStart),
245
+ transform: 'rotateX(-26deg) rotateY(-32deg)',
246
+ easing: 'cubic-bezier(.6,0,.3,1)',
247
+ },
248
+ {
249
+ offset: o(spinEnd),
250
+ transform: 'rotateX(0deg) rotateY(-360deg)',
251
+ easing: 'linear',
252
+ },
253
+ { offset: 1, transform: 'rotateX(0deg) rotateY(-360deg)' },
254
+ ];
255
+ track(boxEl.animate(boxKF, loop));
256
+
257
+ // flaps: open, hold, close
258
+ function flapKF(open: [number, number], close: [number, number]): Keyframe[] {
259
+ const ov = 'rotateX(' + -openDeg + 'deg)';
260
+ return [
261
+ { offset: 0, transform: 'rotateX(0deg)' },
262
+ {
263
+ offset: o(open[0]),
264
+ transform: 'rotateX(0deg)',
265
+ easing: 'cubic-bezier(.4,0,.2,1)',
266
+ },
267
+ { offset: o(open[1]), transform: ov, easing: 'ease-in-out' },
268
+ {
269
+ offset: o(close[0]),
270
+ transform: ov,
271
+ easing: 'cubic-bezier(.4,0,.2,1)',
272
+ },
273
+ { offset: o(close[1]), transform: 'rotateX(0deg)' },
274
+ { offset: 1, transform: 'rotateX(0deg)' },
275
+ ];
276
+ }
277
+ [s0pivot, s2pivot].forEach((p) => {
278
+ if (p) {
279
+ track(p.animate(flapKF(outerOpen, outerClose), loop));
280
+ }
281
+ });
282
+ [s1pivot, s3pivot].forEach((p) => {
283
+ if (p) {
284
+ track(p.animate(flapKF(innerOpen, innerClose), loop));
285
+ }
286
+ });
287
+
288
+ // letters: random pop order out, eruption pause, random pop order back in.
289
+ // They live in their own stage drawn ABOVE the box, so they sail up out of
290
+ // the opening and land in front of everything — never behind or through the
291
+ // front flap.
292
+ const outOrder = shuffle(letterEls.map((_, i) => i));
293
+ const backOrder = shuffle(letterEls.map((_, i) => i));
294
+ const outSlot: number[] = [];
295
+ outOrder.forEach((idx, k) => (outSlot[idx] = k));
296
+ const backSlot: number[] = [];
297
+ backOrder.forEach((idx, k) => (backSlot[idx] = k));
298
+
299
+ letterEls.forEach((el, i) => {
300
+ const lx = (i - (N - 1) / 2) * spacing;
301
+ const ot = outStart + outSlot[i] * outStagger;
302
+ const bt = backStart + backSlot[i] * backStagger;
303
+ const f = outFlight;
304
+ const bf = backFlight;
305
+ const sgn = Math.random() < 0.5 ? -1 : 1;
306
+ const spin = sgn * rand(55, 120); // tumble through the air
307
+ const wonky = sgn * rand(12, 28); // askew angle on landing, tips upright on settle
308
+ const base = 'translate(-50%,-50%) ';
309
+ function pose(
310
+ x: number,
311
+ y: number,
312
+ z: number,
313
+ rz: number,
314
+ sx: number,
315
+ sy: number
316
+ ): string {
317
+ return (
318
+ base +
319
+ 'translate3d(' +
320
+ n1(x) +
321
+ 'px,' +
322
+ n1(y) +
323
+ 'px,' +
324
+ n1(z) +
325
+ 'px) rotateZ(' +
326
+ n1(rz) +
327
+ 'deg) scale(' +
328
+ n1(sx) +
329
+ ',' +
330
+ n1(sy) +
331
+ ')'
332
+ );
333
+ }
334
+ function smooth(a: number): number {
335
+ a = a < 0 ? 0 : a > 1 ? 1 : a;
336
+ return a * a * (3 - 2 * a);
337
+ }
338
+ const pHide = pose(rand(-8, 8), -hh * 0.22, 0, 0, 0.12, 0.12);
339
+ const SAMP = 15;
340
+
341
+ // THROW OUT: a real projectile sampled at even time steps — fast launch
342
+ // out of the opening, a hang at the apex, then a gravity-driven drop that
343
+ // lands it on the floor in front of the box.
344
+ const oy0 = -hh - 4;
345
+ const oyA = -hh - 80;
346
+ const oy1 = landY;
347
+ const oRise = oy0 - oyA;
348
+ const oFall = oy1 - oyA;
349
+ const otA = Math.sqrt(oRise) / (Math.sqrt(oRise) + Math.sqrt(oFall));
350
+ const oC = oRise / (otA * otA);
351
+ const oZlo = 6;
352
+ const oZ0 = 0.3;
353
+ const oZ1 = 0.6;
354
+ const FLYo = 0.84;
355
+
356
+ const kf: Keyframe[] = [
357
+ { offset: 0, transform: pHide, opacity: 0 },
358
+ {
359
+ offset: o(ot),
360
+ transform: pose(lx * 0.03, oy0, oZlo, 0, 0.45, 0.45),
361
+ opacity: 0,
362
+ easing: 'linear',
363
+ },
364
+ ];
365
+ for (let sI = 1; sI <= SAMP; sI++) {
366
+ const s = sI / SAMP;
367
+ const oy = oyA + oC * (s - otA) * (s - otA);
368
+ const ox = lx * (0.03 + 0.97 * s);
369
+ const oz = oZlo + (landZ - oZlo) * smooth((s - oZ0) / (oZ1 - oZ0));
370
+ const orz = wonky * s + spin * Math.sin(Math.PI * s) * 0.4;
371
+ const osc = s < 0.1 ? 0.45 + 5.5 * s : 1;
372
+ kf.push({
373
+ offset: o(ot + f * FLYo * s),
374
+ transform: pose(ox, oy, oz, orz, osc, osc),
375
+ opacity: Math.min(1, s / 0.05),
376
+ easing: 'linear',
377
+ });
378
+ }
379
+ kf.push({
380
+ offset: o(ot + f * 0.88),
381
+ transform: pose(lx, landY, landZ, wonky, 1.08, 0.84),
382
+ opacity: 1,
383
+ easing: 'cubic-bezier(.2,.7,.4,1)',
384
+ }); // contact + squash
385
+ kf.push({
386
+ offset: o(ot + f * 0.94),
387
+ transform: pose(lx, landY - 5, landZ, wonky * 0.35, 0.98, 1.05),
388
+ opacity: 1,
389
+ easing: 'ease-out',
390
+ }); // rebound
391
+ kf.push({
392
+ offset: o(ot + f),
393
+ transform: pose(lx, landY, landZ, 0, 1, 1),
394
+ opacity: 1,
395
+ easing: 'linear',
396
+ }); // settle
397
+
398
+ // PULL BACK IN: the same physics reversed — up in front, hang, then drop
399
+ // back down into the box.
400
+ const by0 = landY;
401
+ const byA = -hh - 70;
402
+ const by1 = -hh * 0.18;
403
+ const bRise = by0 - byA;
404
+ const bFall = by1 - byA;
405
+ const btA = Math.sqrt(bRise) / (Math.sqrt(bRise) + Math.sqrt(bFall));
406
+ const bC = bRise / (btA * btA);
407
+ const bZ0 = 0.4;
408
+ const bZ1 = 0.72;
409
+ kf.push({
410
+ offset: o(bt),
411
+ transform: pose(lx, landY, landZ, 0, 1, 1),
412
+ opacity: 1,
413
+ easing: 'linear',
414
+ });
415
+ for (let bI = 1; bI <= SAMP; bI++) {
416
+ const bs = bI / SAMP;
417
+ const by = byA + bC * (bs - btA) * (bs - btA);
418
+ const bx = lx * (1 - bs);
419
+ const bz = landZ - (landZ - 8) * smooth((bs - bZ0) / (bZ1 - bZ0));
420
+ const brz = -spin * Math.sin(Math.PI * bs) * 0.4;
421
+ const bsc = bs > 0.84 ? Math.max(0.12, 1 - 5.5 * (bs - 0.84)) : 1;
422
+ const bop = Math.max(0, Math.min(1, 1 - (bs - 0.52) / 0.2));
423
+ kf.push({
424
+ offset: o(bt + bf * bs),
425
+ transform: pose(bx, by, bz, brz, bsc, bsc),
426
+ opacity: bop,
427
+ easing: 'linear',
428
+ });
429
+ }
430
+ kf.push({ offset: o(bt + bf), transform: pHide, opacity: 0 });
431
+ kf.push({ offset: 1, transform: pHide, opacity: 0 });
432
+ track(el.animate(kf, loop));
433
+ });
434
+
435
+ // confetti eruption
436
+ fills.forEach((p, i) => {
437
+ const elf = fillRefs[i];
438
+ if (!elf) {
439
+ return;
440
+ }
441
+ const { dx, dz, rise } = p;
442
+ track(
443
+ elf.animate(
444
+ [
445
+ {
446
+ offset: 0,
447
+ transform: 'translate3d(0,0,0) scale(.3)',
448
+ opacity: 0,
449
+ },
450
+ {
451
+ offset: o(confStart),
452
+ transform: 'translate3d(0,0,0) scale(.3)',
453
+ opacity: 0,
454
+ easing: 'cubic-bezier(.2,.6,.3,1)',
455
+ },
456
+ {
457
+ offset: o(confStart + confDur * 0.16),
458
+ transform:
459
+ 'translate3d(' +
460
+ n1(dx * 0.3) +
461
+ 'px,-8px,' +
462
+ n1(dz * 0.3) +
463
+ 'px) scale(1)',
464
+ opacity: 1,
465
+ },
466
+ {
467
+ offset: o(confStart + confDur * 0.46),
468
+ transform:
469
+ 'translate3d(' +
470
+ n1(dx) +
471
+ 'px,' +
472
+ n1(-rise) +
473
+ 'px,' +
474
+ n1(dz) +
475
+ 'px) scale(1)',
476
+ opacity: 1,
477
+ easing: 'ease-in',
478
+ },
479
+ {
480
+ offset: o(confStart + confDur * 0.95),
481
+ transform:
482
+ 'translate3d(' +
483
+ n1(dx * 1.2) +
484
+ 'px,26px,' +
485
+ n1(dz * 1.2) +
486
+ 'px) scale(.5)',
487
+ opacity: 0,
488
+ },
489
+ {
490
+ offset: 1,
491
+ transform:
492
+ 'translate3d(' +
493
+ n1(dx * 1.2) +
494
+ 'px,26px,' +
495
+ n1(dz * 1.2) +
496
+ 'px) scale(.5)',
497
+ opacity: 0,
498
+ },
499
+ ],
500
+ loop
501
+ )
502
+ );
503
+ });
504
+
505
+ if (glowRef) {
506
+ track(
507
+ glowRef.animate(
508
+ [
509
+ { offset: 0, transform: 'scale(.4)', opacity: 0 },
510
+ {
511
+ offset: o(confStart),
512
+ transform: 'scale(.4)',
513
+ opacity: 0,
514
+ easing: 'ease-out',
515
+ },
516
+ {
517
+ offset: o(confStart + confDur * 0.2),
518
+ transform: 'scale(1)',
519
+ opacity: 0.9,
520
+ },
521
+ {
522
+ offset: o(confStart + confDur * 0.6),
523
+ transform: 'scale(1.15)',
524
+ opacity: 0.5,
525
+ },
526
+ {
527
+ offset: o(confStart + confDur * 0.92),
528
+ transform: 'scale(.6)',
529
+ opacity: 0,
530
+ },
531
+ { offset: 1, transform: 'scale(.6)', opacity: 0 },
532
+ ],
533
+ loop
534
+ )
535
+ );
536
+ }
537
+
538
+ // rotate the rear-flap message once per loop
539
+ let timer: ReturnType<typeof setInterval> | undefined;
540
+ if (messages.length > 1) {
541
+ msgIndex = (Math.random() * messages.length) | 0;
542
+ timer = setInterval(() => {
543
+ msgIndex = (msgIndex + 1) % messages.length;
544
+ }, T);
545
+ } else {
546
+ msgIndex = 0;
547
+ }
548
+
549
+ return () => {
550
+ anims.forEach((a) => a.cancel());
551
+ if (timer) {
552
+ clearInterval(timer);
553
+ }
554
+ };
555
+ });
8
556
  </script>
9
557
 
10
558
  <div
11
- class="container"
559
+ bind:this={rootRef}
560
+ class="flowms-loader"
12
561
  role="status"
13
562
  aria-live="polite"
14
563
  aria-label={label}
15
564
  >
16
- <div id="cube">
17
- <div class="front"></div>
18
- <div class="front">
19
- {#if showText}<span>{loadingText}</span>{/if}
565
+ <svg
566
+ width="0"
567
+ height="0"
568
+ style="position:absolute"
569
+ aria-hidden="true"
570
+ >
571
+ <filter
572
+ id="flowms-stamp"
573
+ x="-15%"
574
+ y="-15%"
575
+ width="130%"
576
+ height="130%"
577
+ >
578
+ <feTurbulence
579
+ type="fractalNoise"
580
+ baseFrequency="0.03 0.045"
581
+ numOctaves="3"
582
+ seed="6"
583
+ result="warp"
584
+ />
585
+ <feDisplacementMap
586
+ in="SourceGraphic"
587
+ in2="warp"
588
+ scale="3.4"
589
+ xChannelSelector="R"
590
+ yChannelSelector="G"
591
+ result="rough"
592
+ />
593
+ <feTurbulence
594
+ type="fractalNoise"
595
+ baseFrequency="0.5 0.55"
596
+ numOctaves="2"
597
+ seed="11"
598
+ result="grain"
599
+ />
600
+ <feColorMatrix
601
+ in="grain"
602
+ type="matrix"
603
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.3 0 0 0 0.05"
604
+ result="grainA"
605
+ />
606
+ <feComposite
607
+ in="rough"
608
+ in2="grainA"
609
+ operator="in"
610
+ />
611
+ </filter>
612
+ </svg>
613
+
614
+ <div class="scene">
615
+ <div class="shadow"></div>
616
+ <div
617
+ bind:this={boxRef}
618
+ class="box"
619
+ >
620
+ <div class="face back"></div>
621
+ <div class="face left">
622
+ <svg
623
+ class="flo-logo"
624
+ viewBox="0 0 172 300"
625
+ xmlns="http://www.w3.org/2000/svg"
626
+ aria-hidden="true"
627
+ >
628
+ <path
629
+ d="M1.54715 179.524C4.94675 176.764 11.6045 178.966 13.4335 180.132L13.44 180.125L29.0209 188.966L25.7231 191.604C13.44 200.508 18.7976 216.338 37.1425 217.942L54.0844 219.353L56.3929 219.471V268.062C56.3929 269.123 55.7069 270.574 54.5791 271.201L54.2886 271.366L5.44142 299.569C2.62507 300.967 0 298.711 0 296.429V185.886C0 183.535 0.318398 180.522 1.54715 179.524Z"
630
+ />
631
+ <path
632
+ d="M62.3754 133.49C66.3328 131.155 71.4576 130.799 76.0086 133.424L111.784 154.082L128.952 144.169C130.963 143.008 133.536 144.01 134.228 146.226L153.092 206.478V206.498L153.099 206.517C153.547 208.845 151.674 210.976 149.306 210.818L56.3865 204.855V204.861L39.0593 203.747C35.4781 203.516 34.3703 198.78 37.4768 196.986L55.4036 186.632L19.0811 165.703L19.0746 165.71C17.3158 164.58 12.8616 162.913 7.58502 165.222L62.3754 133.477V133.49Z"
633
+ />
634
+ <path
635
+ d="M19.2923 34.575C10.3024 29.1402 0 35.2873 0 45.7151V145.85C0 148.133 2.98124 150.626 5.44142 148.99L54.5725 120.622C55.4893 120.094 56.3863 118.544 56.3863 117.483V55.6152L19.2923 34.5816V34.575Z"
636
+ />
637
+ <path
638
+ d="M61.9664 2.15088C65.9765 -0.408228 71.2992 -0.890029 76.0085 1.83397L170.036 52.5942C173.393 54.5333 172.147 59.361 167.992 59.3618L167.813 59.3483L132.375 60.3248C130.264 60.3248 129.605 62.329 129.229 64.2422L125.357 84.5503C124.658 88.3819 123.135 90.4663 120.252 88.956L56.3864 55.6215V55.615L19.2923 34.5817C17.5446 33.3614 12.7433 31.4565 7.51929 33.5988L61.9664 2.15088Z"
639
+ />
640
+ </svg>
641
+ </div>
642
+ <div class="face right">
643
+ <svg
644
+ class="flo-logo"
645
+ viewBox="0 0 172 300"
646
+ xmlns="http://www.w3.org/2000/svg"
647
+ aria-hidden="true"
648
+ >
649
+ <path
650
+ d="M1.54715 179.524C4.94675 176.764 11.6045 178.966 13.4335 180.132L13.44 180.125L29.0209 188.966L25.7231 191.604C13.44 200.508 18.7976 216.338 37.1425 217.942L54.0844 219.353L56.3929 219.471V268.062C56.3929 269.123 55.7069 270.574 54.5791 271.201L54.2886 271.366L5.44142 299.569C2.62507 300.967 0 298.711 0 296.429V185.886C0 183.535 0.318398 180.522 1.54715 179.524Z"
651
+ />
652
+ <path
653
+ d="M62.3754 133.49C66.3328 131.155 71.4576 130.799 76.0086 133.424L111.784 154.082L128.952 144.169C130.963 143.008 133.536 144.01 134.228 146.226L153.092 206.478V206.498L153.099 206.517C153.547 208.845 151.674 210.976 149.306 210.818L56.3865 204.855V204.861L39.0593 203.747C35.4781 203.516 34.3703 198.78 37.4768 196.986L55.4036 186.632L19.0811 165.703L19.0746 165.71C17.3158 164.58 12.8616 162.913 7.58502 165.222L62.3754 133.477V133.49Z"
654
+ />
655
+ <path
656
+ d="M19.2923 34.575C10.3024 29.1402 0 35.2873 0 45.7151V145.85C0 148.133 2.98124 150.626 5.44142 148.99L54.5725 120.622C55.4893 120.094 56.3863 118.544 56.3863 117.483V55.6152L19.2923 34.5816V34.575Z"
657
+ />
658
+ <path
659
+ d="M61.9664 2.15088C65.9765 -0.408228 71.2992 -0.890029 76.0085 1.83397L170.036 52.5942C173.393 54.5333 172.147 59.361 167.992 59.3618L167.813 59.3483L132.375 60.3248C130.264 60.3248 129.605 62.329 129.229 64.2422L125.357 84.5503C124.658 88.3819 123.135 90.4663 120.252 88.956L56.3864 55.6215V55.615L19.2923 34.5817C17.5446 33.3614 12.7433 31.4565 7.51929 33.5988L61.9664 2.15088Z"
660
+ />
661
+ </svg>
662
+ </div>
663
+ <div class="face floor"></div>
664
+ <div class="face front">
665
+ <div class="label-wrap">
666
+ <div class="label">Goods<br />Loading</div>
667
+ </div>
668
+ </div>
669
+
670
+ <div class="vent">
671
+ <div
672
+ bind:this={glowRef}
673
+ class="glow"
674
+ ></div>
675
+ {#each fills as _fill, i (i)}
676
+ <span
677
+ bind:this={fillRefs[i]}
678
+ class="fill"
679
+ ></span>
680
+ {/each}
681
+ </div>
682
+
683
+ <div class="flap-unit s0 outer">
684
+ <div
685
+ bind:this={s0pivot}
686
+ class="flap-pivot"
687
+ >
688
+ <div class="flap-panel">
689
+ <div class="flap-face out"></div>
690
+ <div class="flap-face in"></div>
691
+ </div>
692
+ </div>
693
+ </div>
694
+ <div class="flap-unit s2 outer">
695
+ <div
696
+ bind:this={s2pivot}
697
+ class="flap-pivot"
698
+ >
699
+ <div class="flap-panel">
700
+ <div class="flap-face out"></div>
701
+ <div class="flap-face in">
702
+ {#if messages.length}
703
+ <div class="flap-msg">{messages[msgIndex] ?? ''}</div>
704
+ {/if}
705
+ </div>
706
+ </div>
707
+ </div>
708
+ </div>
709
+ <div class="flap-unit s1 inner">
710
+ <div
711
+ bind:this={s1pivot}
712
+ class="flap-pivot"
713
+ >
714
+ <div class="flap-panel">
715
+ <div class="flap-face out"></div>
716
+ <div class="flap-face in"></div>
717
+ </div>
718
+ </div>
719
+ </div>
720
+ <div class="flap-unit s3 inner">
721
+ <div
722
+ bind:this={s3pivot}
723
+ class="flap-pivot"
724
+ >
725
+ <div class="flap-panel">
726
+ <div class="flap-face out"></div>
727
+ <div class="flap-face in"></div>
728
+ </div>
729
+ </div>
730
+ </div>
731
+ </div>
732
+
733
+ </div>
734
+
735
+ <!-- Letters live in their own perspective stage, drawn after (above) the box,
736
+ so they erupt from the opening and land in front of everything. -->
737
+ <div class="letterstage">
738
+ <div class="l3d">
739
+ {#each geom.letters as letter, i (i)}
740
+ <span
741
+ bind:this={letterRefs[i]}
742
+ class="letter"
743
+ style="font-size:{geom.fontSize}px"
744
+ >
745
+ {#each letter.layers as layer, k (k)}
746
+ <span
747
+ class="lyr"
748
+ style="transform:{layer.transform};color:{layer.color}"
749
+ >{letter.glyph}</span
750
+ >
751
+ {/each}
752
+ <span
753
+ class="lface"
754
+ style="transform:translate(-50%,-50%);color:{letterFace}"
755
+ >{letter.glyph}</span
756
+ >
757
+ </span>
758
+ {/each}
20
759
  </div>
21
- <div class="back"></div>
22
- <div class="right"></div>
23
- <div class="left"></div>
24
- <div class="top"></div>
25
- <div class="bottom"></div>
26
760
  </div>
761
+
762
+ <!-- Caption sits above the box and is the loader's last child, so it is never
763
+ hidden by the erupting letters. The dots are three always-present spans
764
+ whose opacity cycles, so the pill width stays fixed as they animate. -->
765
+ {#if showText}
766
+ <div class="caption">
767
+ <span class="caption-label">{caption}</span><span
768
+ class="caption-dots"
769
+ aria-hidden="true"><span>.</span><span>.</span><span>.</span></span
770
+ >
771
+ </div>
772
+ {/if}
27
773
  </div>
28
774
 
29
- <style>.container {
30
- width: var(--loading-animation-size, 100px);
31
- height: var(--loading-animation-size, 100px);
775
+ <style>@charset "UTF-8";
776
+ .flowms-loader {
777
+ --w: 120px; /* footprint width / depth (square) */
778
+ --d: 120px;
779
+ --h: 110px; /* box height */
780
+ --open: 130deg; /* how far the flaps splay open */
781
+ --kraft-front: #cda770;
782
+ --kraft-side: #b8915a;
783
+ --kraft-back: #ab8650;
784
+ --kraft-flap: #d4af77;
785
+ --kraft-flap2: #c6a065;
786
+ --kraft-inner: #8a6b43;
787
+ --seam: rgba(0, 0, 0, 0.14);
788
+ --ink: #3c4045;
789
+ /* brand-blue confetti */
790
+ --fill-1: #259fc7;
791
+ --fill-2: #1b87b0;
792
+ --fill-3: #5cc4e6;
793
+ --fill-4: #0f6e93;
794
+ --fill-5: #86d6ef;
795
+ --hw: calc(var(--w) / 2);
796
+ --hd: calc(var(--d) / 2);
797
+ --hh: calc(var(--h) / 2);
798
+ --y-outer: calc(var(--hh) * -1 + 0.5px);
799
+ --y-inner: calc(var(--hh) * -1 + 1.5px);
800
+ display: grid;
801
+ place-items: center;
32
802
  position: relative;
33
- perspective: 1000px;
803
+ width: 300px;
804
+ height: 300px;
34
805
  }
35
- .container #cube {
36
- width: 100%;
37
- height: 100%;
38
- position: absolute;
806
+ .flowms-loader .scene {
807
+ width: var(--w);
808
+ height: var(--h);
809
+ perspective: 820px;
810
+ perspective-origin: 50% 40%;
39
811
  transform-style: preserve-3d;
812
+ position: relative;
813
+ }
814
+ .flowms-loader .shadow {
815
+ position: absolute;
816
+ left: 50%;
817
+ top: calc(50% + var(--hh) + 24px);
818
+ width: calc(var(--w) * 1.1);
819
+ height: 22px;
820
+ transform: translate(-50%, -50%);
821
+ background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 70%);
822
+ border-radius: 50%;
40
823
  }
41
824
  @media (prefers-reduced-motion: no-preference) {
42
- .container #cube {
43
- animation: rotatecube 10s infinite;
825
+ .flowms-loader .shadow {
826
+ animation: flowms-shadow 4.2s ease-in-out infinite;
44
827
  }
45
828
  }
46
- .container #cube div {
47
- width: var(--loading-animation-size, 100px);
48
- height: var(--loading-animation-size, 100px);
49
- display: block;
829
+ .flowms-loader .box {
50
830
  position: absolute;
831
+ inset: 0;
832
+ transform-style: preserve-3d;
833
+ }
834
+ .flowms-loader .face {
835
+ position: absolute;
836
+ top: 50%;
837
+ left: 50%;
51
838
  box-sizing: border-box;
52
- border: 1px solid rgba(120, 110, 95, 0.45);
53
- line-height: 20px;
839
+ backface-visibility: visible;
840
+ transform-style: preserve-3d;
841
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05), inset 0 0 18px rgba(0, 0, 0, 0.14);
842
+ }
843
+ .flowms-loader .front {
844
+ width: var(--w);
845
+ height: var(--h);
846
+ margin-left: calc(var(--w) / -2);
847
+ margin-top: calc(var(--h) / -2);
848
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.1333333333), rgba(0, 0, 0, 0.0941176471)), var(--kraft-front);
849
+ transform: translateZ(var(--hd));
850
+ }
851
+ .flowms-loader .back {
852
+ width: var(--w);
853
+ height: var(--h);
854
+ margin-left: calc(var(--w) / -2);
855
+ margin-top: calc(var(--h) / -2);
856
+ background: var(--kraft-back);
857
+ transform: rotateY(180deg) translateZ(var(--hd));
858
+ }
859
+ .flowms-loader .right {
860
+ width: var(--d);
861
+ height: var(--h);
862
+ margin-left: calc(var(--d) / -2);
863
+ margin-top: calc(var(--h) / -2);
864
+ background: linear-gradient(160deg, rgba(0, 0, 0, 0.062745098), rgba(0, 0, 0, 0.1882352941)), var(--kraft-side);
865
+ transform: rotateY(90deg) translateZ(var(--hw));
866
+ }
867
+ .flowms-loader .left {
868
+ width: var(--d);
869
+ height: var(--h);
870
+ margin-left: calc(var(--d) / -2);
871
+ margin-top: calc(var(--h) / -2);
872
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.0941176471), rgba(0, 0, 0, 0.1333333333)), var(--kraft-side);
873
+ transform: rotateY(-90deg) translateZ(var(--hw));
874
+ }
875
+ .flowms-loader .floor {
876
+ width: var(--w);
877
+ height: var(--d);
878
+ margin-left: calc(var(--w) / -2);
879
+ margin-top: calc(var(--d) / -2);
880
+ background: var(--kraft-inner);
881
+ transform: rotateX(-90deg) translateZ(var(--hh));
882
+ }
883
+ .flowms-loader {
884
+ /* stamped front label */
54
885
  }
55
- .container #cube div span {
886
+ .flowms-loader .label-wrap {
887
+ position: absolute;
888
+ left: 50%;
889
+ top: 45%;
890
+ transform: translate(-50%, -50%) translateZ(0.1px);
891
+ backface-visibility: hidden;
892
+ pointer-events: none;
893
+ }
894
+ .flowms-loader .label {
895
+ display: inline-block;
896
+ padding: 6px 12px 5px;
897
+ border: 3px solid var(--ink);
898
+ border-radius: 3px;
899
+ font: 800 15px/1.05 "Arial Narrow", "Helvetica Neue", Arial, sans-serif;
900
+ letter-spacing: 1.5px;
901
+ text-transform: uppercase;
902
+ text-align: center;
903
+ white-space: nowrap;
904
+ color: var(--ink);
905
+ opacity: 0.72;
906
+ mix-blend-mode: multiply;
907
+ transform: rotate(-2.6deg);
908
+ filter: url(#flowms-stamp);
909
+ }
910
+ .flowms-loader {
911
+ /* positive message printed inside the rear flap */
912
+ }
913
+ .flowms-loader .flap-msg {
914
+ position: absolute;
915
+ inset: 0;
56
916
  display: flex;
57
- height: 100%;
58
- padding: 10px;
59
917
  align-items: center;
60
- text-align: left;
61
- font-size: 1rem;
62
- font-weight: bold;
63
- font-family: inherit;
64
- color: #ffffff;
918
+ justify-content: center;
919
+ text-align: center;
920
+ padding: 4px 9px;
921
+ box-sizing: border-box;
922
+ font: 800 13px/1.14 "Arial Narrow", "Helvetica Neue", Arial, sans-serif;
923
+ letter-spacing: 0.8px;
924
+ text-transform: uppercase;
925
+ color: var(--ink);
926
+ opacity: 0.8;
927
+ transform: rotate(180deg); /* reads upright on the inner face when open */
928
+ pointer-events: none;
65
929
  }
66
-
67
- @keyframes rotatecube {
68
- 0%, 10% {
69
- transform: rotateX(0deg) rotateY(360deg) rotateZ(0deg);
70
- }
71
- 10%, 100% {
72
- transform: rotateX(0deg) rotateY(360deg) rotateZ(0deg);
73
- }
74
- 35% {
75
- transform: rotateX(90deg) rotateY(270deg) rotateZ(180deg);
76
- }
77
- 60% {
78
- transform: rotateX(180deg) rotateY(180deg) rotateZ(0deg);
79
- }
80
- 85% {
81
- transform: rotateX(-90deg) rotateY(90deg) rotateZ(0deg);
82
- }
930
+ .flowms-loader {
931
+ /* Flo logo embossed on the side faces */
83
932
  }
84
- .front {
85
- background: rgba(216, 208, 199, 0.5);
933
+ .flowms-loader .flo-logo {
934
+ position: absolute;
935
+ left: 50%;
936
+ top: 50%;
937
+ height: 56%;
938
+ transform: translate(-50%, -50%) translateZ(0.1px);
939
+ backface-visibility: hidden;
940
+ pointer-events: none;
941
+ filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.42)) drop-shadow(0 -1px 0 rgba(0, 0, 0, 0.38));
86
942
  }
87
-
88
- .back {
89
- background: rgba(216, 208, 199, 0.5);
943
+ .flowms-loader .flo-logo :global(path) {
944
+ fill: rgba(64, 45, 24, 0.16);
90
945
  }
91
-
92
- .right {
93
- background: rgba(216, 208, 199, 0.35);
946
+ .flowms-loader {
947
+ /* ---- flaps ---- */
94
948
  }
95
-
96
- .left {
97
- background: rgba(216, 208, 199, 0.35);
949
+ .flowms-loader .flap-unit {
950
+ position: absolute;
951
+ top: 50%;
952
+ left: 50%;
953
+ transform-style: preserve-3d;
98
954
  }
99
-
100
- .top {
101
- background: rgba(216, 208, 199, 0.25);
955
+ .flowms-loader .flap-unit.s0 {
956
+ transform: rotateY(0deg) translateY(var(--y-outer)) translateZ(var(--hw));
102
957
  }
103
-
104
- .bottom {
105
- background: rgba(216, 208, 199, 0.75);
958
+ .flowms-loader .flap-unit.s2 {
959
+ transform: rotateY(180deg) translateY(var(--y-outer)) translateZ(var(--hw));
106
960
  }
107
-
108
- #cube .front {
109
- transform: rotateY(0deg) translateZ(50px);
961
+ .flowms-loader .flap-unit.s1 {
962
+ transform: rotateY(90deg) translateY(var(--y-inner)) translateZ(var(--hw));
110
963
  }
111
-
112
- #cube .back {
113
- transform: rotateX(180deg) translateZ(50px);
964
+ .flowms-loader .flap-unit.s3 {
965
+ transform: rotateY(270deg) translateY(var(--y-inner)) translateZ(var(--hw));
114
966
  }
115
-
116
- #cube .right {
117
- transform: rotateY(90deg) translateZ(50px);
967
+ .flowms-loader .flap-pivot {
968
+ transform-style: preserve-3d;
969
+ transform-origin: 0 0;
118
970
  }
119
-
120
- #cube .left {
121
- transform: rotateY(-90deg) translateZ(50px);
971
+ .flowms-loader .flap-panel {
972
+ position: absolute;
973
+ width: var(--w);
974
+ height: var(--hw);
975
+ margin-left: calc(var(--w) / -2);
976
+ transform-origin: 50% 0;
977
+ transform: rotateX(-90deg);
978
+ transform-style: preserve-3d;
122
979
  }
123
-
124
- #cube .top {
125
- transform: rotateX(90deg) translateZ(50px);
980
+ .flowms-loader .flap-face {
981
+ position: absolute;
982
+ inset: 0;
983
+ box-sizing: border-box;
984
+ backface-visibility: hidden;
985
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05);
986
+ }
987
+ .flowms-loader .flap-face.out {
988
+ transform: rotateY(180deg) translateZ(0.4px);
989
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.1490196078), rgba(0, 0, 0, 0.1098039216)), var(--kraft-flap);
990
+ }
991
+ .flowms-loader .flap-face.in {
992
+ transform: translateZ(0.4px);
993
+ background: linear-gradient(160deg, rgba(0, 0, 0, 0.0862745098), rgba(0, 0, 0, 0.1568627451)), var(--kraft-flap2);
994
+ }
995
+ .flowms-loader .flap-unit.outer .flap-face.out::after {
996
+ content: "";
997
+ position: absolute;
998
+ left: 0;
999
+ right: 0;
1000
+ bottom: 0;
1001
+ height: 8px;
1002
+ background: var(--seam);
1003
+ }
1004
+ .flowms-loader {
1005
+ /* eruption vent */
1006
+ }
1007
+ .flowms-loader .vent {
1008
+ position: absolute;
1009
+ top: 50%;
1010
+ left: 50%;
1011
+ transform-style: preserve-3d;
1012
+ transform: translateY(calc(var(--hh) * -1));
1013
+ }
1014
+ .flowms-loader .glow {
1015
+ position: absolute;
1016
+ left: 0;
1017
+ top: 0;
1018
+ width: 96px;
1019
+ height: 96px;
1020
+ margin-left: -48px;
1021
+ margin-top: -48px;
1022
+ border-radius: 50%;
1023
+ opacity: 0;
1024
+ background: radial-gradient(circle, rgba(127, 212, 236, 0.6666666667) 0%, rgba(37, 159, 199, 0.3333333333) 35%, rgba(37, 159, 199, 0) 70%);
1025
+ }
1026
+ .flowms-loader .fill {
1027
+ position: absolute;
1028
+ left: 0;
1029
+ top: 0;
1030
+ width: 13px;
1031
+ height: 13px;
1032
+ margin-left: -6.5px;
1033
+ margin-top: -6.5px;
1034
+ border-radius: 3px;
1035
+ opacity: 0;
1036
+ }
1037
+ .flowms-loader .fill:nth-child(5n+1) {
1038
+ background: var(--fill-1);
1039
+ }
1040
+ .flowms-loader .fill:nth-child(5n+2) {
1041
+ background: var(--fill-2);
1042
+ }
1043
+ .flowms-loader .fill:nth-child(5n+3) {
1044
+ background: var(--fill-3);
1045
+ }
1046
+ .flowms-loader .fill:nth-child(5n+4) {
1047
+ background: var(--fill-4);
1048
+ }
1049
+ .flowms-loader .fill:nth-child(5n+5) {
1050
+ background: var(--fill-5);
1051
+ }
1052
+ .flowms-loader .fill:nth-child(3n) {
1053
+ width: 9px;
1054
+ height: 9px;
1055
+ margin-left: -4.5px;
1056
+ margin-top: -4.5px;
1057
+ border-radius: 50%;
1058
+ }
1059
+ .flowms-loader {
1060
+ /* 3D word letters: own perspective stage, sits in front of the box and
1061
+ matches its resting angle. Drawn above the box, so letters land in front
1062
+ of everything — never behind or through the front flap. */
1063
+ }
1064
+ .flowms-loader .letterstage {
1065
+ position: absolute;
1066
+ left: 50%;
1067
+ top: 50%;
1068
+ width: var(--w);
1069
+ height: var(--h);
1070
+ transform: translate(-50%, -50%);
1071
+ perspective: 820px;
1072
+ perspective-origin: 50% 40%;
1073
+ pointer-events: none;
1074
+ }
1075
+ .flowms-loader .l3d {
1076
+ position: absolute;
1077
+ inset: 0;
1078
+ transform-style: preserve-3d;
1079
+ transform: rotateX(-26deg) rotateY(-32deg); /* the box's resting angle */
1080
+ }
1081
+ .flowms-loader .letter {
1082
+ position: absolute;
1083
+ left: 50%;
1084
+ top: 50%;
1085
+ transform: translate(-50%, -50%) scale(0);
1086
+ transform-origin: 50% 50%;
1087
+ transform-style: preserve-3d;
1088
+ font-family: "Arial Black", "Helvetica Neue", system-ui, sans-serif;
1089
+ font-weight: 900;
1090
+ line-height: 1;
1091
+ opacity: 0;
1092
+ }
1093
+ .flowms-loader .letter .lface,
1094
+ .flowms-loader .letter .lyr {
1095
+ position: absolute;
1096
+ left: 0;
1097
+ top: 0;
1098
+ }
1099
+ .flowms-loader {
1100
+ /* Caption pill: sits above the box and on top of every other layer. */
1101
+ }
1102
+ .flowms-loader .caption {
1103
+ position: absolute;
1104
+ left: 50%;
1105
+ top: 22px; /* above the box, clear of the eruption */
1106
+ transform: translateX(-50%);
1107
+ white-space: nowrap;
1108
+ padding: 7px 16px;
1109
+ border-radius: 999px;
1110
+ background: rgba(255, 255, 255, 0.82);
1111
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
1112
+ -webkit-backdrop-filter: blur(3px);
1113
+ backdrop-filter: blur(3px);
1114
+ font: 600 14px/1.2 system-ui, -apple-system, "Segoe UI", sans-serif;
1115
+ color: #44515a;
1116
+ letter-spacing: 0.2px;
1117
+ }
1118
+ .flowms-loader {
1119
+ /* Three always-present dots; only their opacity animates, so the pill never
1120
+ reflows or changes width as they cycle. */
1121
+ }
1122
+ .flowms-loader .caption-dots {
1123
+ display: inline-block;
1124
+ text-align: left;
1125
+ }
1126
+ .flowms-loader .caption-dots span:nth-child(2),
1127
+ .flowms-loader .caption-dots span:nth-child(3) {
1128
+ opacity: 1; /* shown when motion is reduced (static "...") */
1129
+ }
1130
+ @media (prefers-reduced-motion: no-preference) {
1131
+ .flowms-loader .caption-dots span:nth-child(2) {
1132
+ animation: flowms-dot2 1.5s infinite;
1133
+ }
1134
+ .flowms-loader .caption-dots span:nth-child(3) {
1135
+ animation: flowms-dot3 1.5s infinite;
1136
+ }
1137
+ }
1138
+ @media (prefers-reduced-motion: reduce) {
1139
+ .flowms-loader .box {
1140
+ animation: flowms-rest 2.4s ease-in-out infinite;
1141
+ }
126
1142
  }
127
1143
 
128
- #cube .bottom {
129
- transform: rotateX(-90deg) translateZ(50px);
1144
+ @keyframes flowms-shadow {
1145
+ 0%, 100% {
1146
+ transform: translate(-50%, -50%) scale(1, 1);
1147
+ opacity: 0.5;
1148
+ }
1149
+ 50% {
1150
+ transform: translate(-50%, -50%) scale(0.93, 1);
1151
+ opacity: 0.45;
1152
+ }
1153
+ }
1154
+ @keyframes flowms-rest {
1155
+ 0%, 100% {
1156
+ transform: rotateX(-22deg) rotateY(-28deg);
1157
+ }
1158
+ 50% {
1159
+ transform: rotateX(-22deg) rotateY(-22deg);
1160
+ }
1161
+ }
1162
+ /* Dots reveal in sequence (".", "..", "...") then reset — width never changes
1163
+ because all three glyphs always occupy space; only opacity toggles. */
1164
+ @keyframes flowms-dot2 {
1165
+ 0%, 32.9% {
1166
+ opacity: 0;
1167
+ }
1168
+ 33%, 100% {
1169
+ opacity: 1;
1170
+ }
1171
+ }
1172
+ @keyframes flowms-dot3 {
1173
+ 0%, 65.9% {
1174
+ opacity: 0;
1175
+ }
1176
+ 66%, 100% {
1177
+ opacity: 1;
1178
+ }
130
1179
  }</style>