@lavalogic/scoria 0.40.9 → 0.40.11

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