@asksable/site-connector 0.1.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css CHANGED
@@ -1,20 +1,54 @@
1
+ /* Mobile-web hygiene scoped to the widget. Each rule is a known
2
+ iOS Safari / Android Chrome gotcha called out by the alvsr-mobile-
3
+ ui-review audit. -webkit-text-size-adjust stops iOS Safari from
4
+ auto-enlarging the widget's text in landscape; -webkit-tap-
5
+ highlight-color removes the default blue tap flash; touch-action:
6
+ manipulation kills the legacy 300ms double-tap delay on iOS so
7
+ buttons feel responsive. */
8
+ .bw,
9
+ .bw * {
10
+ -webkit-text-size-adjust: 100%;
11
+ -webkit-tap-highlight-color: transparent;
12
+ }
13
+
14
+ .bw button,
15
+ .bw [role='button'],
16
+ .bw a {
17
+ touch-action: manipulation;
18
+ }
19
+
1
20
  .bw {
21
+ /* Theme tokens — overridable per host site (color + font). */
2
22
  --bw-font: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3
23
  --bw-bg: #ffffff;
4
24
  --bw-text: #1a1a1a;
5
25
  --bw-text-secondary: #6b6b6b;
6
- --bw-text-muted: #999999;
26
+ /* Bumped from #999 (2.85:1, fails WCAG AA) to #737373 (~4.74:1,
27
+ passes AA for normal text). Still distinct from --bw-text-secondary
28
+ (#6b6b6b, 5.74:1) so the visual hierarchy holds. */
29
+ --bw-text-muted: #737373;
7
30
  --bw-border: #e8e8e8;
8
31
  --bw-border-light: #f0f0f0;
9
- --bw-radius: 8px;
10
- --bw-radius-lg: 12px;
11
32
  --bw-primary: #1a1a1a;
12
33
  --bw-primary-text: #ffffff;
13
34
  --bw-hover: #f8f8f8;
14
35
  --bw-error-bg: #fef2f2;
15
36
  --bw-error-text: #b91c1c;
16
- --bw-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
17
- --bw-shadow-badge: 0 1px 4px rgba(0, 0, 0, 0.12);
37
+ /* Structural tokens Sable design rhythm; tenants generally
38
+ don't override these. Mirrors the radius scale documented in
39
+ productdesign.md (cards 12/16, inputs 22, buttons 28). */
40
+ --bw-radius-xs: 4px;
41
+ --bw-radius-sm: 8px;
42
+ --bw-radius-md: 12px;
43
+ --bw-radius: 12px;
44
+ --bw-radius-lg: 16px;
45
+ --bw-radius-xl: 22px;
46
+ --bw-radius-pill: 9999px;
47
+ --bw-shadow: 0 1px 2px rgba(15, 15, 15, 0.04), 0 8px 24px -12px rgba(15, 15, 15, 0.08);
48
+ --bw-shadow-badge: 0 1px 4px rgba(15, 15, 15, 0.12);
49
+ --bw-shadow-lift: 0 1px 2px rgba(15, 15, 15, 0.06), 0 12px 32px -16px rgba(15, 15, 15, 0.12);
50
+ --bw-ease: cubic-bezier(0.16, 1, 0.3, 1);
51
+ --bw-duration: 220ms;
18
52
  position: relative;
19
53
  display: flex;
20
54
  flex-direction: column;
@@ -22,9 +56,20 @@
22
56
  background: var(--bw-bg);
23
57
  color: var(--bw-text);
24
58
  font-family: var(--bw-font);
59
+ font-feature-settings: "ss01", "cv11";
60
+ -webkit-font-smoothing: antialiased;
61
+ -moz-osx-font-smoothing: grayscale;
62
+ text-wrap: pretty;
25
63
  overflow: hidden;
26
64
  }
27
65
 
66
+ .bw h1,
67
+ .bw h2,
68
+ .bw h3,
69
+ .bw h4 {
70
+ text-wrap: balance;
71
+ }
72
+
28
73
  .bw,
29
74
  .bw * {
30
75
  box-sizing: border-box;
@@ -51,67 +96,12 @@
51
96
  animation: bw-shimmer 1.5s ease infinite;
52
97
  }
53
98
 
54
- .bw-skel--title {
55
- width: min(340px, 70%);
56
- height: 32px;
57
- }
58
-
59
- .bw-skel--text {
60
- width: 48%;
61
- height: 14px;
62
- margin-top: 14px;
63
- }
64
-
65
- .bw-skel--text-lg {
66
- width: min(520px, 85%);
67
- }
68
-
69
- .bw-skel--card {
70
- width: 100%;
71
- height: 56px;
72
- margin-bottom: 10px;
73
- }
74
-
75
- .bw-skel--calendar {
76
- width: 100%;
77
- height: 300px;
78
- }
79
-
80
- .bw-skel--input {
81
- width: 100%;
82
- height: 40px;
83
- margin-bottom: 12px;
84
- }
85
-
86
- .bw-skel--textarea {
87
- width: 100%;
88
- height: 92px;
89
- }
90
-
91
99
  .bw-skel--slot {
92
- height: 40px;
93
- }
94
-
95
- .bw-skel-list {
96
- display: flex;
97
- flex-direction: column;
98
- gap: 0;
99
- margin-top: 16px;
100
- }
101
-
102
- .bw-skel-slots {
103
- margin-top: 8px;
104
- }
105
-
106
- .bw-skel-slots-grid {
107
- display: grid;
108
- grid-template-columns: repeat(4, 1fr);
109
- gap: 10px;
110
- }
111
-
112
- .bw-skel-mobile {
113
- display: none;
114
- padding: 0 24px 24px;
100
+ /* Default mobile pill height: matches .bw-slot { padding: 12px 0;
101
+ font-size: 13px } ≈ 44px. Desktop overrides this below to match
102
+ the taller .bw-slots-desktop .bw-slot { padding: 14px 16px } pill. */
103
+ height: 44px;
104
+ border-radius: var(--bw-radius);
115
105
  }
116
106
 
117
107
  .bw-loading,
@@ -174,7 +164,11 @@
174
164
 
175
165
  .bw-body {
176
166
  display: grid;
177
- grid-template-columns: 22% 1fr 22%;
167
+ /* Left col widened from 22% to 28% so the service rows have room
168
+ to render the duration + description on their secondary line
169
+ without crunching. The middle (calendar) absorbs the difference;
170
+ the right (slots/details) stays at 22%. */
171
+ grid-template-columns: 28% 1fr 22%;
178
172
  gap: 32px;
179
173
  padding: 32px 48px 48px;
180
174
  min-height: 0;
@@ -190,6 +184,86 @@
190
184
  min-height: 0;
191
185
  }
192
186
 
187
+ /* Mobile-only review column. Desktop uses bw-details-view for review,
188
+ so this col stays hidden at all desktop widths and only the mobile
189
+ media query opts it back in on step 4. */
190
+ .bw-col--review {
191
+ display: none;
192
+ }
193
+
194
+ /* Review variant runs full-bleed inside its col: no card chrome (no
195
+ border, no rounded background), generous type. Sized for arm's-
196
+ length reading on phone — body 15px floor, total 17px, with a
197
+ thin hairline separating each group. */
198
+ .bw-summary--review {
199
+ width: 100%;
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: 20px;
203
+ padding: 0;
204
+ border: 0;
205
+ border-radius: 0;
206
+ background: transparent;
207
+ }
208
+
209
+ .bw-summary--review .bw-summary-title {
210
+ margin-bottom: 4px;
211
+ font-size: 17px;
212
+ font-weight: 600;
213
+ letter-spacing: -0.01em;
214
+ }
215
+
216
+ .bw-summary--review .bw-summary-rows {
217
+ gap: 12px;
218
+ }
219
+
220
+ .bw-summary--review .bw-summary-row {
221
+ font-size: 15px;
222
+ line-height: 1.5;
223
+ }
224
+
225
+ .bw-summary--review .bw-summary-total {
226
+ font-size: 17px;
227
+ }
228
+
229
+ .bw-summary-group {
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: 12px;
233
+ }
234
+
235
+ .bw-summary-subhead {
236
+ display: block;
237
+ font-size: 13px;
238
+ font-weight: 600;
239
+ letter-spacing: -0.005em;
240
+ color: var(--bw-text-muted);
241
+ }
242
+
243
+ .bw-summary-row--stack {
244
+ flex-direction: column;
245
+ align-items: flex-start;
246
+ gap: 4px;
247
+ }
248
+
249
+ .bw-summary-row--stack .bw-summary-val {
250
+ text-align: left;
251
+ white-space: normal;
252
+ }
253
+
254
+ .bw-summary-notes {
255
+ margin: 0;
256
+ font-size: 15px;
257
+ line-height: 1.55;
258
+ color: var(--bw-text);
259
+ white-space: pre-wrap;
260
+ }
261
+
262
+ .bw-summary-rows--total {
263
+ padding-top: 16px;
264
+ border-top: 1px solid var(--bw-border);
265
+ }
266
+
193
267
  .bw-step-1 {
194
268
  display: flex;
195
269
  flex-direction: column;
@@ -203,8 +277,8 @@
203
277
 
204
278
  .bw-label {
205
279
  display: block;
206
- margin-bottom: 10px;
207
- font-size: 12px;
280
+ margin-bottom: 14px;
281
+ font-size: 14px;
208
282
  font-weight: 500;
209
283
  color: var(--bw-text);
210
284
  }
@@ -215,6 +289,35 @@
215
289
  background: var(--bw-border);
216
290
  }
217
291
 
292
+ /* Service groups — categories. We use generous vertical rhythm
293
+ (28px gap) and a small caption-style heading instead of a hard
294
+ divider line, so the category break reads through whitespace +
295
+ typographic shift, not via a blend-in 1px border. */
296
+ .bw-svc-group {
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 2px;
300
+ }
301
+
302
+ .bw-svc-group + .bw-svc-group {
303
+ margin-top: 28px;
304
+ }
305
+
306
+ /* Category heading: smaller than the section labels (14/500) but
307
+ heavier weight so the section break still reads cleanly. The
308
+ wider top gap above (28px between groups) carries most of the
309
+ visual separation. */
310
+ .bw-svc-category {
311
+ margin: 0 0 12px;
312
+ padding: 0;
313
+ font-size: 13px;
314
+ font-weight: 600;
315
+ line-height: 1.4;
316
+ letter-spacing: 0.01em;
317
+ color: var(--bw-text-secondary);
318
+ text-transform: none;
319
+ }
320
+
218
321
  .bw-staff-dropdown {
219
322
  position: relative;
220
323
  margin-bottom: 10px;
@@ -223,20 +326,30 @@
223
326
  .bw-staff-trigger {
224
327
  display: flex;
225
328
  align-items: center;
226
- gap: 10px;
329
+ gap: 12px;
227
330
  width: 100%;
228
- padding: 9px 13px;
229
- border: 2px solid var(--bw-border);
331
+ padding: 10px 14px;
332
+ border: 1px solid var(--bw-border);
230
333
  border-radius: var(--bw-radius);
231
334
  background: transparent;
232
335
  cursor: pointer;
233
336
  text-align: left;
234
- transition: border-color 250ms ease;
337
+ transition: border-color var(--bw-duration) var(--bw-ease),
338
+ box-shadow var(--bw-duration) var(--bw-ease),
339
+ background var(--bw-duration) var(--bw-ease);
235
340
  }
236
341
 
237
- .bw-staff-trigger:hover,
238
- .bw-staff-trigger.has-value {
342
+ .bw-staff-trigger:hover {
343
+ border-color: var(--bw-text-secondary);
344
+ }
345
+
346
+ .bw-staff-trigger:focus-visible {
239
347
  border-color: var(--bw-primary);
348
+ box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.06);
349
+ }
350
+
351
+ .bw-staff-trigger[disabled] {
352
+ cursor: default;
240
353
  }
241
354
 
242
355
  .bw-staff-trigger-content,
@@ -252,6 +365,7 @@
252
365
  }
253
366
 
254
367
  .bw-staff-avatar {
368
+ position: relative;
255
369
  display: flex;
256
370
  flex-shrink: 0;
257
371
  align-items: center;
@@ -261,6 +375,15 @@
261
375
  border-radius: 999px;
262
376
  background: var(--bw-border-light);
263
377
  color: var(--bw-text-secondary);
378
+ overflow: hidden;
379
+ }
380
+
381
+ .bw-staff-avatar-img {
382
+ position: absolute;
383
+ inset: 0;
384
+ width: 100%;
385
+ height: 100%;
386
+ object-fit: cover;
264
387
  }
265
388
 
266
389
  .bw-staff-avatar svg,
@@ -299,6 +422,9 @@
299
422
  font-weight: 500;
300
423
  line-height: 1.3;
301
424
  color: var(--bw-text);
425
+ white-space: nowrap;
426
+ overflow: hidden;
427
+ text-overflow: ellipsis;
302
428
  }
303
429
 
304
430
  .bw-staff-trigger-desc,
@@ -333,8 +459,9 @@
333
459
  z-index: 9;
334
460
  display: flex;
335
461
  flex-direction: column;
462
+ gap: 2px;
336
463
  width: 100%;
337
- padding: 4px;
464
+ padding: 6px;
338
465
  border: 1px solid var(--bw-border);
339
466
  border-radius: var(--bw-radius);
340
467
  background: var(--bw-bg);
@@ -353,10 +480,11 @@
353
480
  width: 100%;
354
481
  padding: 10px 12px;
355
482
  border: 0;
356
- border-radius: 6px;
483
+ border-radius: var(--bw-radius-sm);
357
484
  background: transparent;
358
485
  cursor: pointer;
359
486
  text-align: left;
487
+ transition: background var(--bw-duration) var(--bw-ease);
360
488
  }
361
489
 
362
490
  .bw-staff-option:hover,
@@ -364,52 +492,147 @@
364
492
  background: var(--bw-hover);
365
493
  }
366
494
 
495
+ /* Provider has no slots on the selected date. Visually muted, not
496
+ clickable, with an "Unavailable" subtitle under the name (rendered
497
+ inline via .bw-staff-option-desc). */
498
+ .bw-staff-option.is-disabled {
499
+ cursor: not-allowed;
500
+ opacity: 0.5;
501
+ }
502
+
503
+ .bw-staff-option.is-disabled:hover {
504
+ background: transparent;
505
+ }
506
+
507
+ .bw-staff-option.is-disabled .bw-staff-option-desc {
508
+ color: var(--bw-muted);
509
+ }
510
+
511
+ /* When the picker is in expanded mode (full list visible), let it
512
+ stretch to fill the column so the scroll list extends down to the
513
+ bottom of the left column instead of capping at an arbitrary
514
+ height. Detected via :has() — the .bw-svc-scroll only renders in
515
+ expanded mode; in collapsed mode the picker contains only the
516
+ selected card, which sizes to its content (so the provider section
517
+ below sits flush, not pushed to the bottom). */
518
+ .bw-service-picker:has(.bw-svc-scroll) {
519
+ display: flex;
520
+ flex-direction: column;
521
+ flex: 1;
522
+ min-height: 0;
523
+ }
524
+
367
525
  .bw-svc-scroll {
368
- display: grid;
369
- grid-template-rows: 1fr;
526
+ display: flex;
527
+ flex-direction: column;
528
+ flex: 1;
529
+ min-height: 0;
370
530
  }
371
531
 
372
532
  .bw-svc-scroll-wrap {
533
+ position: relative;
534
+ flex: 1;
373
535
  min-height: 0;
374
- max-height: 320px;
375
536
  overflow-y: auto;
537
+ /* Soft fade at the top + bottom so the scroll edge looks
538
+ intentional instead of cutting service rows in half. Matches
539
+ the editorial "warm paper, not cold glass" feel — depth from
540
+ softness, not a hard line. */
541
+ -webkit-mask-image: linear-gradient(
542
+ to bottom,
543
+ transparent 0,
544
+ #000 18px,
545
+ #000 calc(100% - 18px),
546
+ transparent 100%
547
+ );
548
+ mask-image: linear-gradient(
549
+ to bottom,
550
+ transparent 0,
551
+ #000 18px,
552
+ #000 calc(100% - 18px),
553
+ transparent 100%
554
+ );
555
+ scroll-padding-block: 16px;
556
+ /* Native scrollbar tightened so it doesn't dominate the
557
+ softened edge. Webkit-only; Firefox uses default. */
558
+ scrollbar-width: thin;
559
+ scrollbar-color: var(--bw-border) transparent;
560
+ }
561
+
562
+ .bw-svc-scroll-wrap::-webkit-scrollbar {
563
+ width: 6px;
564
+ }
565
+
566
+ .bw-svc-scroll-wrap::-webkit-scrollbar-thumb {
567
+ border-radius: var(--bw-radius-pill);
568
+ background: var(--bw-border);
569
+ }
570
+
571
+ .bw-svc-scroll-wrap::-webkit-scrollbar-thumb:hover {
572
+ background: var(--bw-text-muted);
376
573
  }
377
574
 
378
575
  .bw-svc-scroll-inner {
379
576
  display: flex;
380
577
  flex-direction: column;
381
- padding-bottom: 10px;
578
+ padding-block: 12px 24px;
382
579
  }
383
580
 
581
+ /* Service row — refactored to fix the narrow-width hierarchy issues:
582
+ - Top-aligned items so price reads with the first line of the name
583
+ - Name allowed to wrap with text-wrap: pretty (no orphans)
584
+ - Description line-clamps to 2 lines instead of single-line ellipsis
585
+ - Concentric inner radius (row 12px - 14px padding around 20px check
586
+ leaves the optical center on the first line of text) */
384
587
  .bw-svc-row {
385
588
  display: flex;
386
- align-items: center;
387
- gap: 10px;
589
+ align-items: flex-start;
590
+ gap: 12px;
388
591
  width: 100%;
389
- padding: 10px 12px;
592
+ padding: 14px;
390
593
  border: 0;
391
- border-radius: 6px;
594
+ border-radius: var(--bw-radius);
392
595
  background: transparent;
393
596
  cursor: pointer;
394
597
  text-align: left;
395
- transition: background 150ms ease;
598
+ transition: background var(--bw-duration) var(--bw-ease),
599
+ box-shadow var(--bw-duration) var(--bw-ease),
600
+ transform var(--bw-duration) var(--bw-ease);
396
601
  }
397
602
 
398
603
  .bw-svc-row:hover {
399
604
  background: var(--bw-hover);
400
605
  }
401
606
 
607
+ .bw-svc-row:focus-visible {
608
+ background: var(--bw-hover);
609
+ box-shadow: inset 0 0 0 1.5px var(--bw-text);
610
+ }
611
+
612
+ .bw-svc-row:active {
613
+ transform: scale(0.98);
614
+ }
615
+
616
+ /* Checkbox: aligned to first line of name (1.4 line-height on 14px =
617
+ ~20px line, matches check height so they share a baseline). */
402
618
  .bw-check {
403
619
  display: flex;
404
620
  flex-shrink: 0;
405
621
  align-items: center;
406
622
  justify-content: center;
407
- width: 18px;
408
- height: 18px;
409
- border: 1.5px solid #d0d0d0;
410
- border-radius: 4px;
623
+ width: 20px;
624
+ height: 20px;
625
+ margin-top: 1px;
626
+ border: 1.5px solid var(--bw-border);
627
+ border-radius: var(--bw-radius-xs);
411
628
  color: transparent;
412
- transition: all 150ms ease;
629
+ transition: border-color var(--bw-duration) var(--bw-ease),
630
+ background var(--bw-duration) var(--bw-ease),
631
+ color var(--bw-duration) var(--bw-ease);
632
+ }
633
+
634
+ .bw-svc-row:hover .bw-check {
635
+ border-color: var(--bw-text-secondary);
413
636
  }
414
637
 
415
638
  .bw-check.is-checked {
@@ -419,38 +642,156 @@
419
642
  }
420
643
 
421
644
  .bw-check--lg {
422
- width: 20px;
423
- height: 20px;
424
- border-radius: 5px;
645
+ width: 22px;
646
+ height: 22px;
647
+ margin-top: 0;
648
+ border-radius: var(--bw-radius-xs);
649
+ }
650
+
651
+ /* Service thumbnail (Fresha/Square pattern). Replaces the checkbox
652
+ slot when a service has an image. Selected state shows a small
653
+ primary-colored badge in the bottom-right corner with the same
654
+ check icon — keeps the affordance consistent with image-less
655
+ rows. Concentric radius (md = 12px) matches surrounding chrome. */
656
+ .bw-svc-image {
657
+ position: relative;
658
+ flex-shrink: 0;
659
+ width: 80px;
660
+ height: 80px;
661
+ margin-top: 0;
662
+ border-radius: var(--bw-radius-md);
663
+ overflow: hidden;
664
+ background: var(--bw-border-light);
665
+ outline: 1px solid rgba(0, 0, 0, 0.06);
666
+ outline-offset: -1px;
667
+ transition: outline-color var(--bw-duration) var(--bw-ease);
668
+ }
669
+
670
+ .bw-svc-image img {
671
+ display: block;
672
+ width: 100%;
673
+ height: 100%;
674
+ object-fit: cover;
675
+ object-position: center;
676
+ }
677
+
678
+ .bw-svc-row:hover .bw-svc-image {
679
+ outline-color: rgba(0, 0, 0, 0.12);
680
+ }
681
+
682
+ .bw-svc-image.is-checked {
683
+ outline-color: var(--bw-primary);
684
+ outline-width: 1.5px;
685
+ }
686
+
687
+ .bw-svc-image-badge {
688
+ position: absolute;
689
+ right: 4px;
690
+ bottom: 4px;
691
+ display: flex;
692
+ align-items: center;
693
+ justify-content: center;
694
+ width: 18px;
695
+ height: 18px;
696
+ border-radius: var(--bw-radius-pill);
697
+ background: var(--bw-primary);
698
+ color: var(--bw-primary-text);
699
+ box-shadow: var(--bw-shadow-badge);
700
+ }
701
+
702
+ .bw-svc-image-badge svg {
703
+ width: 11px;
704
+ height: 11px;
705
+ }
706
+
707
+ .bw-svc-image--lg {
708
+ width: 96px;
709
+ height: 96px;
710
+ }
711
+
712
+ /* Meta footer row: groups duration + price on a single line under
713
+ the description with a dot separator. Replaces the old top-right
714
+ floating price so the row reads as a tidy stack instead of an
715
+ L-shape. */
716
+ .bw-svc-meta-row {
717
+ display: inline-flex;
718
+ align-items: baseline;
719
+ gap: 8px;
720
+ margin-top: 4px;
721
+ font-variant-numeric: tabular-nums;
722
+ color: var(--bw-text-secondary);
723
+ }
724
+
725
+ .bw-svc-meta-row .bw-svc-meta {
726
+ margin: 0;
727
+ }
728
+
729
+ .bw-svc-meta-row .bw-svc-price {
730
+ margin: 0;
731
+ flex-shrink: 0;
732
+ color: var(--bw-text);
733
+ }
734
+
735
+ .bw-svc-meta-dot {
736
+ color: var(--bw-text-muted);
737
+ user-select: none;
738
+ }
739
+
740
+ .bw-svc-info {
741
+ display: flex;
742
+ flex-direction: column;
743
+ gap: 4px;
744
+ min-width: 0;
745
+ flex: 1 1 auto;
425
746
  }
426
747
 
748
+ /* Name: 14px, allowed to wrap, pretty wrap to avoid one-word
749
+ orphans, hard cap at 2 lines so very long names truncate
750
+ cleanly instead of dominating the row. */
427
751
  .bw-svc-name {
428
- font-size: 12px;
429
- font-weight: 500;
430
- line-height: 1.3;
752
+ font-size: 14px;
753
+ font-weight: 600;
754
+ line-height: 1.35;
755
+ letter-spacing: -0.005em;
431
756
  color: var(--bw-text);
757
+ text-wrap: pretty;
758
+ display: -webkit-box;
759
+ -webkit-line-clamp: 2;
760
+ -webkit-box-orient: vertical;
761
+ overflow: hidden;
432
762
  }
433
763
 
764
+ /* Meta + description sit together as a soft secondary line. Meta
765
+ (duration) renders inline, then description picks up after a
766
+ middle dot. Wrapping is allowed; we line-clamp to 2 so multi-
767
+ sentence descriptions don't blow up the row height. */
434
768
  .bw-svc-meta {
435
- font-size: 11px;
436
- line-height: 1.3;
437
- color: var(--bw-text-muted);
769
+ font-size: 13px;
770
+ line-height: 1.45;
771
+ color: var(--bw-text-secondary);
772
+ font-variant-numeric: tabular-nums;
438
773
  }
439
774
 
440
775
  .bw-svc-desc {
441
- font-size: 11px;
442
- line-height: 1.4;
443
- color: var(--bw-text-muted);
444
- overflow: hidden;
445
- text-overflow: ellipsis;
446
- white-space: nowrap;
776
+ font-size: 13px;
777
+ line-height: 1.45;
778
+ color: var(--bw-text-secondary);
779
+ text-wrap: pretty;
447
780
  }
448
781
 
782
+ /* Price: same scale as name so the first-line baseline locks them
783
+ together optically. Regular weight (not 600) so price doesn't
784
+ compete for attention with the name — name leads, price reads
785
+ as a quiet endcap. */
449
786
  .bw-svc-price {
450
787
  flex-shrink: 0;
788
+ margin-top: 1px;
451
789
  font-size: 14px;
452
- font-weight: 600;
790
+ font-weight: 500;
791
+ line-height: 1.35;
453
792
  color: var(--bw-text);
793
+ font-variant-numeric: tabular-nums;
794
+ letter-spacing: -0.005em;
454
795
  }
455
796
 
456
797
  .bw-step-2-heading {
@@ -486,7 +827,7 @@
486
827
  }
487
828
 
488
829
  .bw-svc-row--full .bw-svc-meta {
489
- font-size: 12px;
830
+ font-size: 13px;
490
831
  }
491
832
 
492
833
  .bw-col--center {
@@ -503,9 +844,10 @@
503
844
  flex: 1;
504
845
  flex-direction: column;
505
846
  gap: 24px;
506
- padding: 32px;
847
+ padding: 24px 32px 32px;
507
848
  border: 1px solid var(--bw-border);
508
- border-radius: var(--bw-radius);
849
+ border-radius: var(--bw-radius-lg);
850
+ background: var(--bw-bg);
509
851
  }
510
852
 
511
853
  .bw-cal-prompt,
@@ -518,9 +860,402 @@
518
860
  color: var(--bw-text-muted);
519
861
  }
520
862
 
521
- .bw-cal-prompt {
522
- padding-top: 8px;
523
- text-align: center;
863
+ /* Transition the service-picker swap (full list <-> selected card)
864
+ and the provider section's reveal. Each branch animates in on
865
+ mount via the shared bw-slots-fade-in keyframe so the picker
866
+ feels like a settling state-machine, not a hard swap. */
867
+ .bw-svc-selected,
868
+ .bw-svc-scroll,
869
+ .bw-provider-section {
870
+ animation: bw-slots-fade-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
871
+ }
872
+
873
+ @media (prefers-reduced-motion: reduce) {
874
+ .bw-svc-selected,
875
+ .bw-svc-scroll,
876
+ .bw-provider-section {
877
+ animation: none;
878
+ }
879
+ }
880
+
881
+ /* Provider list — replaces the old dropdown so every option is
882
+ visible at a glance. Two layouts share the same DOM:
883
+ Desktop (>=1025px): vertical stack of horizontal rows
884
+ (avatar | name + desc) — reads like the old dropdown list.
885
+ Mobile (<=1024px): horizontal scrolling strip of square-ish
886
+ cards (avatar above name) so the row fits on a phone column.
887
+ Selection, hover, and disabled-state chrome is shared between
888
+ both layouts so the visual language matches the rest of the
889
+ widget. */
890
+ .bw-staff-row {
891
+ display: flex;
892
+ flex-direction: column;
893
+ gap: 4px;
894
+ padding: 0;
895
+ margin: 0;
896
+ /* Desktop list shouldn't scroll horizontally; if the column is
897
+ ever taller than the card, the surrounding scroller handles
898
+ vertical overflow. */
899
+ max-height: 320px;
900
+ overflow-y: auto;
901
+ overscroll-behavior: contain;
902
+ scrollbar-width: thin;
903
+ }
904
+
905
+ .bw-staff-row::-webkit-scrollbar {
906
+ width: 6px;
907
+ height: 6px;
908
+ }
909
+
910
+ .bw-staff-row::-webkit-scrollbar-track {
911
+ background: transparent;
912
+ }
913
+
914
+ .bw-staff-row::-webkit-scrollbar-thumb {
915
+ background: var(--bw-border);
916
+ border-radius: 3px;
917
+ }
918
+
919
+ .bw-staff-card {
920
+ display: flex;
921
+ flex-direction: row;
922
+ align-items: center;
923
+ gap: 12px;
924
+ width: 100%;
925
+ padding: 10px 12px;
926
+ border: 0;
927
+ border-radius: var(--bw-radius-sm);
928
+ background: transparent;
929
+ color: var(--bw-text);
930
+ cursor: pointer;
931
+ text-align: left;
932
+ transition:
933
+ background var(--bw-duration) var(--bw-ease),
934
+ color var(--bw-duration) var(--bw-ease);
935
+ }
936
+
937
+ .bw-staff-card:hover {
938
+ background: var(--bw-hover);
939
+ }
940
+
941
+ .bw-staff-card.is-active {
942
+ background: var(--bw-bg);
943
+ /* Inset shadow so the active state shows a crisp black ring
944
+ without shifting layout (default card has no border). */
945
+ box-shadow: inset 0 0 0 1.5px var(--bw-text);
946
+ }
947
+
948
+ .bw-staff-card.is-active:hover {
949
+ background: var(--bw-bg);
950
+ }
951
+
952
+ .bw-staff-card.is-disabled {
953
+ cursor: not-allowed;
954
+ opacity: 0.5;
955
+ }
956
+
957
+ .bw-staff-card.is-disabled:hover {
958
+ background: transparent;
959
+ }
960
+
961
+ .bw-staff-card-avatar {
962
+ position: relative;
963
+ display: inline-flex;
964
+ align-items: center;
965
+ justify-content: center;
966
+ width: 36px;
967
+ height: 36px;
968
+ border-radius: 999px;
969
+ background: var(--bw-border-light);
970
+ color: var(--bw-text-secondary);
971
+ flex-shrink: 0;
972
+ overflow: hidden;
973
+ }
974
+
975
+ .bw-staff-card-avatar svg {
976
+ width: 16px;
977
+ height: 16px;
978
+ }
979
+
980
+ .bw-staff-card-avatar .bw-staff-initials {
981
+ font-size: 12px;
982
+ font-weight: 500;
983
+ color: var(--bw-text);
984
+ }
985
+
986
+ .bw-staff-card-info {
987
+ display: flex;
988
+ flex-direction: column;
989
+ gap: 2px;
990
+ min-width: 0;
991
+ flex: 1;
992
+ }
993
+
994
+ .bw-staff-card-name {
995
+ font-size: 14px;
996
+ font-weight: 500;
997
+ line-height: 1.25;
998
+ color: var(--bw-text);
999
+ overflow: hidden;
1000
+ text-overflow: ellipsis;
1001
+ white-space: nowrap;
1002
+ }
1003
+
1004
+ .bw-staff-card-desc {
1005
+ font-size: 12px;
1006
+ line-height: 1.25;
1007
+ color: var(--bw-text-muted);
1008
+ }
1009
+
1010
+ /* Mobile uses the same vertical list as desktop — no horizontal
1011
+ scrolling chip strip. The single layout keeps the visual language
1012
+ identical across breakpoints; if the list exceeds the card's
1013
+ built-in 320px max-height, it scrolls vertically inside the
1014
+ card. */
1015
+
1016
+ /* Mobile step 2 stacks three frames in bw-col--center: the calendar
1017
+ card (existing), the provider card, then the times card. Each
1018
+ mobile-only card matches the calendar card chrome so the column
1019
+ reads as one family. Hidden on desktop where each section has
1020
+ its own column. */
1021
+ .bw-mobile-card,
1022
+ .bw-provider-section--mobile {
1023
+ display: none;
1024
+ }
1025
+
1026
+ @media (max-width: 1024px) {
1027
+ .bw-mobile-card {
1028
+ display: flex;
1029
+ flex-direction: column;
1030
+ gap: 12px;
1031
+ margin-top: 16px;
1032
+ padding: 20px 20px 24px;
1033
+ border: 1px solid var(--bw-border);
1034
+ border-radius: var(--bw-radius-lg);
1035
+ background: var(--bw-bg);
1036
+ }
1037
+
1038
+ /* Times card needs extra bottom breathing room so the slot pills
1039
+ don't sit flush against the card border. */
1040
+ .bw-mobile-card.bw-slots-mobile {
1041
+ padding-bottom: 28px;
1042
+ }
1043
+
1044
+ /* Label sits at the top of each mobile card. Its desktop margin
1045
+ would double up with the parent's flex gap, so we zero it. */
1046
+ .bw-mobile-card > .bw-label {
1047
+ margin-bottom: 0;
1048
+ }
1049
+
1050
+ /* The provider section uses .bw-mobile-card chrome — kill the
1051
+ desktop bw-provider-section animation so it doesn't replay
1052
+ when the section enters the mobile flow. */
1053
+ .bw-mobile-card.bw-provider-section--mobile {
1054
+ animation: none;
1055
+ }
1056
+ }
1057
+
1058
+ /* Service picker — collapsed selected state. Once a service is
1059
+ picked, the long list collapses to a read-only card showing just
1060
+ the selected service plus a "Change" link. Below this card the
1061
+ provider section reveals (per the service-first flow). The card
1062
+ reuses .bw-svc-row chrome but with --readonly disabling hover/cursor. */
1063
+ .bw-svc-selected {
1064
+ display: flex;
1065
+ flex-direction: column;
1066
+ gap: 12px;
1067
+ }
1068
+
1069
+ .bw-svc-selected-header {
1070
+ display: flex;
1071
+ align-items: baseline;
1072
+ justify-content: space-between;
1073
+ gap: 12px;
1074
+ }
1075
+
1076
+ .bw-svc-selected-header .bw-label {
1077
+ margin-bottom: 0;
1078
+ }
1079
+
1080
+ .bw-svc-change {
1081
+ padding: 0;
1082
+ border: 0;
1083
+ background: transparent;
1084
+ color: var(--bw-text-secondary);
1085
+ font-size: 13px;
1086
+ font-weight: 500;
1087
+ cursor: pointer;
1088
+ transition: color var(--bw-duration) var(--bw-ease);
1089
+ }
1090
+
1091
+ .bw-svc-change:hover {
1092
+ color: var(--bw-text);
1093
+ text-decoration: underline;
1094
+ text-underline-offset: 3px;
1095
+ }
1096
+
1097
+ .bw-svc-row--readonly {
1098
+ cursor: default;
1099
+ }
1100
+
1101
+ .bw-svc-row--readonly:hover {
1102
+ background: transparent;
1103
+ }
1104
+
1105
+ .bw-svc-row--readonly:hover .bw-check {
1106
+ border-color: var(--bw-primary);
1107
+ }
1108
+
1109
+ /* === Payment panel (design proposal) ===
1110
+ Renders inside the details form when paymentMode === 'full' or
1111
+ 'deposit'. Two stacks: a soft cararra-tinted summary card with
1112
+ line items + total + charge breakdown (mirrors the reference
1113
+ pattern but in Sable's tone), then a card-details slot for the
1114
+ eventual Stripe PaymentElement, then a small "secured by Stripe"
1115
+ reassurance line. Stub fields are placeholders only — real impl
1116
+ replaces .bw-pay-card-slot's children with <PaymentElement />. */
1117
+ .bw-pay {
1118
+ display: flex;
1119
+ flex-direction: column;
1120
+ gap: 18px;
1121
+ margin-top: 4px;
1122
+ }
1123
+
1124
+ .bw-pay-summary {
1125
+ display: flex;
1126
+ flex-direction: column;
1127
+ padding: 18px 20px;
1128
+ border: 1px solid var(--bw-border);
1129
+ border-radius: var(--bw-radius-lg);
1130
+ background: var(--bw-border-light);
1131
+ }
1132
+
1133
+ .bw-pay-summary-header {
1134
+ margin-bottom: 14px;
1135
+ font-size: 13px;
1136
+ font-weight: 600;
1137
+ color: var(--bw-text);
1138
+ }
1139
+
1140
+ .bw-pay-summary-row {
1141
+ display: flex;
1142
+ align-items: baseline;
1143
+ justify-content: space-between;
1144
+ gap: 16px;
1145
+ padding: 4px 0;
1146
+ font-size: 13px;
1147
+ line-height: 1.45;
1148
+ color: var(--bw-text);
1149
+ font-variant-numeric: tabular-nums;
1150
+ }
1151
+
1152
+ .bw-pay-summary-row--muted {
1153
+ color: var(--bw-text-secondary);
1154
+ }
1155
+
1156
+ .bw-pay-summary-row--total {
1157
+ font-weight: 600;
1158
+ }
1159
+
1160
+ .bw-pay-summary-row--strong {
1161
+ margin-top: 2px;
1162
+ font-size: 14px;
1163
+ font-weight: 600;
1164
+ color: var(--bw-text);
1165
+ }
1166
+
1167
+ .bw-pay-summary-val {
1168
+ flex-shrink: 0;
1169
+ }
1170
+
1171
+ .bw-pay-summary-divider {
1172
+ height: 1px;
1173
+ margin: 10px 0;
1174
+ background: var(--bw-border);
1175
+ }
1176
+
1177
+ .bw-pay-card {
1178
+ display: flex;
1179
+ flex-direction: column;
1180
+ gap: 10px;
1181
+ }
1182
+
1183
+ .bw-pay-card .bw-label {
1184
+ margin-bottom: 0;
1185
+ }
1186
+
1187
+ /* Card slot: a soft surface where Stripe's PaymentElement will
1188
+ mount. Until then, render a static stub of the fields so the
1189
+ spacing + chrome can be validated. */
1190
+ .bw-pay-card-slot {
1191
+ padding: 14px;
1192
+ border: 1px solid var(--bw-border);
1193
+ border-radius: var(--bw-radius-lg);
1194
+ background: var(--bw-bg);
1195
+ }
1196
+
1197
+ .bw-pay-card-stub {
1198
+ display: flex;
1199
+ flex-direction: column;
1200
+ gap: 12px;
1201
+ }
1202
+
1203
+ .bw-pay-card-stub-row {
1204
+ display: flex;
1205
+ flex-direction: column;
1206
+ gap: 4px;
1207
+ padding: 10px 12px;
1208
+ border: 1px solid var(--bw-border-light);
1209
+ border-radius: var(--bw-radius);
1210
+ background: var(--bw-border-light);
1211
+ }
1212
+
1213
+ .bw-pay-card-stub-label {
1214
+ font-size: 11px;
1215
+ font-weight: 500;
1216
+ color: var(--bw-text-muted);
1217
+ }
1218
+
1219
+ .bw-pay-card-stub-value {
1220
+ font-size: 13px;
1221
+ color: var(--bw-text-secondary);
1222
+ font-variant-numeric: tabular-nums;
1223
+ letter-spacing: 0.04em;
1224
+ }
1225
+
1226
+ .bw-pay-card-stub-grid {
1227
+ display: grid;
1228
+ grid-template-columns: 1fr 1fr 1fr;
1229
+ gap: 8px;
1230
+ }
1231
+
1232
+ .bw-pay-secure {
1233
+ margin: 0;
1234
+ font-size: 12px;
1235
+ color: var(--bw-text-muted);
1236
+ }
1237
+
1238
+ /* Empty-state when no service is selected. Centered in the slots
1239
+ column so the right pane reads as "waiting for input" instead
1240
+ of just blank. Used in both desktop (right column) and mobile
1241
+ (inline under the calendar) since slotsArea renders in both. */
1242
+ .bw-slots-empty {
1243
+ display: flex;
1244
+ flex: 1;
1245
+ align-items: center;
1246
+ justify-content: center;
1247
+ min-height: 120px;
1248
+ margin: 0;
1249
+ padding: 24px 12px;
1250
+ font-size: 14px;
1251
+ line-height: 1.5;
1252
+ color: var(--bw-text-secondary);
1253
+ text-align: center;
1254
+ }
1255
+
1256
+ .bw-cal-prompt {
1257
+ padding-top: 8px;
1258
+ text-align: center;
524
1259
  }
525
1260
 
526
1261
  .bw-cal-header {
@@ -609,35 +1344,42 @@
609
1344
  .bw-cal-grid {
610
1345
  display: grid;
611
1346
  grid-template-columns: repeat(7, 1fr);
1347
+ column-gap: 6px;
612
1348
  }
613
1349
 
614
1350
  .bw-cal-weekdays {
615
1351
  text-align: center;
1352
+ margin-bottom: 4px;
616
1353
  }
617
1354
 
618
1355
  .bw-cal-weekdays span {
619
1356
  padding: 6px 0;
620
- font-size: 12px;
621
- font-weight: 500;
1357
+ font-size: 11px;
1358
+ font-weight: 600;
1359
+ letter-spacing: 0.08em;
1360
+ text-transform: uppercase;
622
1361
  color: var(--bw-text-muted);
623
1362
  }
624
1363
 
625
1364
  .bw-cal-grid {
626
- gap: 6px 0;
1365
+ gap: 6px;
627
1366
  }
628
1367
 
629
1368
  .bw-cal-day {
1369
+ position: relative;
630
1370
  display: flex;
631
1371
  align-items: center;
632
1372
  justify-content: center;
633
1373
  height: 48px;
634
1374
  border: 0;
635
- border-radius: 6px;
1375
+ border-radius: var(--bw-radius-sm);
636
1376
  background: transparent;
637
1377
  color: var(--bw-text);
638
1378
  cursor: default;
639
1379
  font-size: 13px;
640
- transition: background 300ms ease, color 300ms ease;
1380
+ font-variant-numeric: tabular-nums;
1381
+ transition: background var(--bw-duration) var(--bw-ease),
1382
+ color var(--bw-duration) var(--bw-ease);
641
1383
  }
642
1384
 
643
1385
  .bw-cal-day.is-outside {
@@ -662,6 +1404,26 @@
662
1404
  font-weight: 600;
663
1405
  }
664
1406
 
1407
+ /* Dot under today's number — subtle reference indicator. Hidden when
1408
+ the day is the active selection (the filled background already
1409
+ communicates focus). */
1410
+ .bw-cal-day.is-today::after {
1411
+ content: '';
1412
+ position: absolute;
1413
+ bottom: 6px;
1414
+ left: 50%;
1415
+ width: 4px;
1416
+ height: 4px;
1417
+ border-radius: var(--bw-radius-pill);
1418
+ background: var(--bw-text-muted);
1419
+ transform: translateX(-50%);
1420
+ }
1421
+
1422
+ .bw-cal-day.is-today.is-selected::after {
1423
+ background: var(--bw-primary-text);
1424
+ opacity: 0.7;
1425
+ }
1426
+
665
1427
  .bw-cal-day.is-blocked,
666
1428
  .bw-cal-day.is-disabled {
667
1429
  color: var(--bw-border);
@@ -669,26 +1431,85 @@
669
1431
 
670
1432
  .bw-time-slots {
671
1433
  display: grid;
672
- grid-template-columns: repeat(4, 1fr);
1434
+ /* 3-column grid on mobile so each pill gets more horizontal room
1435
+ and longer locale time strings (e.g. "10:30 a. m.") don't crowd
1436
+ each other. Desktop overrides this to 1fr further down to render
1437
+ a vertical pill list. */
1438
+ grid-template-columns: repeat(3, 1fr);
673
1439
  gap: 10px;
674
1440
  margin-top: 8px;
675
1441
  }
676
1442
 
677
- .bw-slot {
678
- padding: 12px 0;
679
- border: 1px solid var(--bw-border);
680
- border-radius: var(--bw-radius);
681
- background: transparent;
682
- cursor: pointer;
1443
+ /* Wrapper that re-mounts whenever service/staff/date/loading state
1444
+ changes (key-driven from JSX). The wrapper itself fades, and each
1445
+ slot pill staggers in via --bw-slot-i set inline so the list
1446
+ reads as "settling into place" instead of popping. */
1447
+ .bw-slots-fade {
1448
+ animation: bw-slots-fade-in 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
1449
+ }
1450
+
1451
+ .bw-slots-fade .bw-slot {
1452
+ animation: bw-slot-stagger-in 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
1453
+ animation-delay: calc(var(--bw-slot-i, 0) * 22ms);
1454
+ }
1455
+
1456
+ @keyframes bw-slots-fade-in {
1457
+ from {
1458
+ opacity: 0;
1459
+ transform: translateY(4px);
1460
+ }
1461
+ to {
1462
+ opacity: 1;
1463
+ transform: translateY(0);
1464
+ }
1465
+ }
1466
+
1467
+ @keyframes bw-slot-stagger-in {
1468
+ from {
1469
+ opacity: 0;
1470
+ transform: translateY(6px) scale(0.98);
1471
+ }
1472
+ to {
1473
+ opacity: 1;
1474
+ transform: translateY(0) scale(1);
1475
+ }
1476
+ }
1477
+
1478
+ @media (prefers-reduced-motion: reduce) {
1479
+ .bw-slots-fade,
1480
+ .bw-slots-fade .bw-slot {
1481
+ animation: none;
1482
+ }
1483
+ }
1484
+
1485
+ .bw-slot {
1486
+ display: flex;
1487
+ align-items: center;
1488
+ justify-content: center;
1489
+ padding: 12px 14px;
1490
+ min-width: 0;
1491
+ border: 1px solid var(--bw-border);
1492
+ border-radius: var(--bw-radius);
1493
+ background: transparent;
1494
+ cursor: pointer;
683
1495
  text-align: center;
684
1496
  white-space: nowrap;
685
1497
  font-size: 13px;
1498
+ font-variant-numeric: tabular-nums;
686
1499
  color: var(--bw-text);
687
- transition: all 150ms ease;
1500
+ transition: border-color var(--bw-duration) var(--bw-ease),
1501
+ background var(--bw-duration) var(--bw-ease),
1502
+ color var(--bw-duration) var(--bw-ease),
1503
+ transform var(--bw-duration) var(--bw-ease);
688
1504
  }
689
1505
 
690
1506
  .bw-slot:hover {
691
- border-color: var(--bw-text);
1507
+ border-color: var(--bw-text-secondary);
1508
+ background: var(--bw-hover);
1509
+ }
1510
+
1511
+ .bw-slot:active {
1512
+ transform: scale(0.98);
692
1513
  }
693
1514
 
694
1515
  .bw-slot.is-active {
@@ -741,23 +1562,45 @@
741
1562
  .bw-field input,
742
1563
  .bw-field textarea {
743
1564
  width: 100%;
744
- padding: 10px 14px;
1565
+ padding: 11px 16px;
745
1566
  border: 1px solid var(--bw-border);
746
- border-radius: 6px;
1567
+ border-radius: var(--bw-radius-md);
747
1568
  background: transparent;
748
1569
  color: var(--bw-text);
749
1570
  font-size: 13px;
750
- transition: border-color 150ms ease;
1571
+ font-family: inherit;
1572
+ transition: border-color var(--bw-duration) var(--bw-ease),
1573
+ box-shadow var(--bw-duration) var(--bw-ease);
751
1574
  }
752
1575
 
753
1576
  .bw-field textarea {
754
- min-height: 80px;
1577
+ min-height: 96px;
755
1578
  resize: none;
756
1579
  }
757
1580
 
1581
+ .bw-field input:hover,
1582
+ .bw-field textarea:hover {
1583
+ border-color: var(--bw-text-secondary);
1584
+ }
1585
+
758
1586
  .bw-field input:focus,
759
1587
  .bw-field textarea:focus {
760
1588
  border-color: var(--bw-primary);
1589
+ box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.06);
1590
+ }
1591
+
1592
+ .bw-field.is-invalid input,
1593
+ .bw-field.is-invalid textarea,
1594
+ .bw-field.is-invalid select {
1595
+ border-color: var(--bw-error-text);
1596
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--bw-error-text) 12%, transparent);
1597
+ }
1598
+
1599
+ .bw-field-error {
1600
+ margin-top: -4px;
1601
+ color: var(--bw-error-text);
1602
+ font-size: 12px;
1603
+ line-height: 1.35;
761
1604
  }
762
1605
 
763
1606
  .bw-field input::placeholder,
@@ -765,6 +1608,208 @@
765
1608
  color: var(--bw-text-muted);
766
1609
  }
767
1610
 
1611
+ /* ===========================================================================
1612
+ Custom IntakeSelect — replaces the native <select>. Trigger matches
1613
+ the .bw-field input chrome (border, radius, padding, font). Menu sits
1614
+ absolutely below, scrolls if taller than max-height, animates in.
1615
+ =========================================================================== */
1616
+ .bw-select {
1617
+ position: relative;
1618
+ width: 100%;
1619
+ }
1620
+
1621
+ .bw-select-trigger {
1622
+ display: flex;
1623
+ align-items: center;
1624
+ justify-content: space-between;
1625
+ gap: 12px;
1626
+ width: 100%;
1627
+ padding: 11px 14px 11px 16px;
1628
+ border: 1px solid var(--bw-border);
1629
+ border-radius: var(--bw-radius-md);
1630
+ background: transparent;
1631
+ color: var(--bw-text);
1632
+ font-size: 13px;
1633
+ font-family: inherit;
1634
+ text-align: left;
1635
+ cursor: pointer;
1636
+ transition:
1637
+ border-color var(--bw-duration) var(--bw-ease),
1638
+ box-shadow var(--bw-duration) var(--bw-ease);
1639
+ }
1640
+
1641
+ .bw-select-trigger:hover {
1642
+ border-color: var(--bw-text-secondary);
1643
+ }
1644
+
1645
+ .bw-select-trigger:focus-visible,
1646
+ .bw-select-trigger.is-open {
1647
+ outline: 0;
1648
+ border-color: var(--bw-primary);
1649
+ box-shadow: 0 0 0 3px rgba(15, 15, 15, 0.05);
1650
+ }
1651
+
1652
+ .bw-select-value {
1653
+ flex: 1;
1654
+ min-width: 0;
1655
+ overflow: hidden;
1656
+ text-overflow: ellipsis;
1657
+ white-space: nowrap;
1658
+ }
1659
+
1660
+ .bw-select-value.is-placeholder {
1661
+ color: var(--bw-text-muted);
1662
+ }
1663
+
1664
+ .bw-select-chevron {
1665
+ flex-shrink: 0;
1666
+ display: inline-flex;
1667
+ align-items: center;
1668
+ justify-content: center;
1669
+ width: 16px;
1670
+ height: 16px;
1671
+ color: var(--bw-text-secondary);
1672
+ transition: transform var(--bw-duration) var(--bw-ease);
1673
+ }
1674
+
1675
+ .bw-select-trigger.is-open .bw-select-chevron {
1676
+ transform: rotate(180deg);
1677
+ }
1678
+
1679
+ .bw-select-chevron svg {
1680
+ width: 16px;
1681
+ height: 16px;
1682
+ }
1683
+
1684
+ .bw-select-menu {
1685
+ position: absolute;
1686
+ top: calc(100% + 6px);
1687
+ left: 0;
1688
+ right: 0;
1689
+ /* Sits above the widget's own footer (z-index up to 102 on mobile)
1690
+ and any nearby chrome so flip-up + flip-down both render cleanly. */
1691
+ z-index: 110;
1692
+ /* Cap menu height to whichever is smaller — the 280px design max
1693
+ or 60% of the viewport — so on short phones the dropdown never
1694
+ dominates the screen and always leaves the trigger visible. */
1695
+ max-height: min(280px, 60svh);
1696
+ overflow-y: auto;
1697
+ margin: 0;
1698
+ padding: 6px;
1699
+ list-style: none;
1700
+ background: var(--bw-bg);
1701
+ border: 1px solid var(--bw-border);
1702
+ border-radius: var(--bw-radius-md);
1703
+ box-shadow: var(--bw-shadow-lift);
1704
+ /* 250ms ease-out-expo open animation per the widget's motion
1705
+ spec. Scale + fade + 4px lift mirrors the popup-animation rule
1706
+ used elsewhere in the Sable product. */
1707
+ animation: bw-select-open 250ms cubic-bezier(0.16, 1, 0.3, 1) both;
1708
+ outline: 0;
1709
+ scrollbar-width: thin;
1710
+ scrollbar-color: var(--bw-border) transparent;
1711
+ }
1712
+
1713
+ @keyframes bw-select-open {
1714
+ from {
1715
+ opacity: 0;
1716
+ transform: translateY(-4px) scale(0.97);
1717
+ }
1718
+ to {
1719
+ opacity: 1;
1720
+ transform: translateY(0) scale(1);
1721
+ }
1722
+ }
1723
+
1724
+ /* Flip-up variant: when there's no room below the trigger (select
1725
+ sits near the bottom of the widget AND the host page has a fixed
1726
+ footer below it), the menu opens upward instead. Computed at open
1727
+ time by IntakeSelect.computePlacement. */
1728
+ .bw-select-menu.is-above {
1729
+ top: auto;
1730
+ bottom: calc(100% + 6px);
1731
+ animation-name: bw-select-open-above;
1732
+ }
1733
+
1734
+ @keyframes bw-select-open-above {
1735
+ from {
1736
+ opacity: 0;
1737
+ transform: translateY(4px) scale(0.97);
1738
+ }
1739
+ to {
1740
+ opacity: 1;
1741
+ transform: translateY(0) scale(1);
1742
+ }
1743
+ }
1744
+
1745
+ .bw-select-option {
1746
+ display: flex;
1747
+ align-items: center;
1748
+ justify-content: space-between;
1749
+ gap: 12px;
1750
+ padding: 9px 12px;
1751
+ border-radius: var(--bw-radius-sm);
1752
+ color: var(--bw-text);
1753
+ font-size: 14px;
1754
+ cursor: pointer;
1755
+ user-select: none;
1756
+ transition: background-color var(--bw-duration) var(--bw-ease);
1757
+ }
1758
+
1759
+ .bw-select-option.is-focused {
1760
+ background: var(--bw-hover);
1761
+ }
1762
+
1763
+ .bw-select-option.is-active {
1764
+ color: var(--bw-text);
1765
+ font-weight: 500;
1766
+ }
1767
+
1768
+ .bw-select-option-label {
1769
+ flex: 1;
1770
+ min-width: 0;
1771
+ overflow: hidden;
1772
+ text-overflow: ellipsis;
1773
+ white-space: nowrap;
1774
+ }
1775
+
1776
+ .bw-select-option-check {
1777
+ flex-shrink: 0;
1778
+ display: inline-flex;
1779
+ align-items: center;
1780
+ justify-content: center;
1781
+ width: 14px;
1782
+ height: 14px;
1783
+ color: var(--bw-primary);
1784
+ }
1785
+
1786
+ .bw-select-option-check svg {
1787
+ width: 14px;
1788
+ height: 14px;
1789
+ }
1790
+
1791
+ @media (prefers-reduced-motion: reduce) {
1792
+ .bw-select-menu {
1793
+ animation: none;
1794
+ }
1795
+ .bw-select-chevron {
1796
+ transition: none;
1797
+ }
1798
+ }
1799
+
1800
+ @media (max-width: 1024px) {
1801
+ /* Match the bumped 16px input font on mobile so the dropdown sits
1802
+ in the same visual family as the rest of the form fields. */
1803
+ .bw-select-trigger {
1804
+ font-size: 16px;
1805
+ padding: 12px 14px 12px 16px;
1806
+ }
1807
+ .bw-select-option {
1808
+ font-size: 16px;
1809
+ padding: 12px;
1810
+ }
1811
+ }
1812
+
768
1813
  .bw-field--notes {
769
1814
  position: relative;
770
1815
  }
@@ -777,25 +1822,55 @@
777
1822
  color: var(--bw-text-muted);
778
1823
  }
779
1824
 
1825
+ .bw-submit-help {
1826
+ border: 1px solid color-mix(in srgb, var(--bw-error-text) 24%, var(--bw-border));
1827
+ border-radius: 10px;
1828
+ background: color-mix(in srgb, var(--bw-error-text) 8%, var(--bw-bg));
1829
+ color: var(--bw-text);
1830
+ padding: 10px 12px;
1831
+ font-size: 12px;
1832
+ line-height: 1.45;
1833
+ }
1834
+
1835
+ .bw-submit-help-title {
1836
+ display: block;
1837
+ font-weight: 650;
1838
+ }
1839
+
1840
+ .bw-submit-help ul {
1841
+ margin: 6px 0 0;
1842
+ padding-left: 18px;
1843
+ }
1844
+
1845
+ .bw-submit-help li + li {
1846
+ margin-top: 2px;
1847
+ }
1848
+
780
1849
  .bw-required {
1850
+ /* 4px gap between the label and the asterisk so they don't kiss. */
1851
+ margin-left: 4px;
781
1852
  color: var(--bw-error-text);
782
1853
  font-weight: 400;
783
1854
  }
784
1855
 
785
1856
  .bw-summary {
786
- padding-top: 16px;
1857
+ padding: 18px;
1858
+ border: 1px solid var(--bw-border);
1859
+ border-radius: var(--bw-radius-lg);
1860
+ background: var(--bw-bg);
787
1861
  }
788
1862
 
789
1863
  .bw-summary-title {
790
1864
  display: block;
791
- margin-bottom: 12px;
792
- font-size: 14px;
1865
+ margin-bottom: 14px;
1866
+ font-size: 13px;
793
1867
  font-weight: 600;
1868
+ letter-spacing: -0.005em;
794
1869
  color: var(--bw-text);
795
1870
  }
796
1871
 
797
1872
  .bw-summary-rows {
798
- gap: 8px;
1873
+ gap: 10px;
799
1874
  }
800
1875
 
801
1876
  .bw-summary-row {
@@ -804,6 +1879,8 @@
804
1879
  justify-content: space-between;
805
1880
  gap: 16px;
806
1881
  font-size: 13px;
1882
+ line-height: 1.45;
1883
+ font-variant-numeric: tabular-nums;
807
1884
  }
808
1885
 
809
1886
  .bw-summary-row span:first-child {
@@ -818,6 +1895,9 @@
818
1895
  text-align: right;
819
1896
  text-overflow: ellipsis;
820
1897
  white-space: nowrap;
1898
+ /* Hold countdown + price + date numerals all benefit from tabular
1899
+ figures so they don't shift horizontally as digits change. */
1900
+ font-variant-numeric: tabular-nums;
821
1901
  }
822
1902
 
823
1903
  .bw-summary-total span:first-child,
@@ -848,6 +1928,63 @@
848
1928
  line-height: 1.4;
849
1929
  }
850
1930
 
1931
+ .bw-service-warning {
1932
+ display: flex;
1933
+ gap: 14px;
1934
+ padding: 18px;
1935
+ border: 2px solid #ef4444;
1936
+ border-radius: var(--bw-radius-lg);
1937
+ background: #fef2f2;
1938
+ color: #7f1d1d;
1939
+ }
1940
+
1941
+ .bw-service-warning-icon {
1942
+ display: flex;
1943
+ flex: 0 0 auto;
1944
+ align-items: center;
1945
+ justify-content: center;
1946
+ width: 42px;
1947
+ height: 42px;
1948
+ border-radius: 999px;
1949
+ background: #fee2e2;
1950
+ color: #b91c1c;
1951
+ }
1952
+
1953
+ .bw-service-warning-icon svg {
1954
+ width: 24px;
1955
+ height: 24px;
1956
+ }
1957
+
1958
+ .bw-service-warning-copy {
1959
+ display: flex;
1960
+ min-width: 0;
1961
+ flex-direction: column;
1962
+ gap: 6px;
1963
+ }
1964
+
1965
+ .bw-service-warning-title {
1966
+ font-size: 16px;
1967
+ font-weight: 800;
1968
+ line-height: 1.25;
1969
+ color: #7f1d1d;
1970
+ }
1971
+
1972
+ .bw-service-warning p {
1973
+ margin: 0;
1974
+ font-size: 13px;
1975
+ line-height: 1.5;
1976
+ color: #991b1b;
1977
+ }
1978
+
1979
+ .bw-service-warning-link {
1980
+ align-self: flex-start;
1981
+ font-size: 13px;
1982
+ font-weight: 800;
1983
+ color: #7f1d1d;
1984
+ text-decoration: underline;
1985
+ text-underline-offset: 3px;
1986
+ }
1987
+
851
1988
  .bw-confirm-btn,
852
1989
  .bw-footer-next,
853
1990
  .bw-btn-primary {
@@ -859,36 +1996,105 @@
859
1996
  background: var(--bw-primary);
860
1997
  color: var(--bw-primary-text);
861
1998
  cursor: pointer;
862
- transition: opacity 150ms ease;
1999
+ transition: opacity var(--bw-duration) var(--bw-ease),
2000
+ transform var(--bw-duration) var(--bw-ease),
2001
+ box-shadow var(--bw-duration) var(--bw-ease);
863
2002
  }
864
2003
 
865
2004
  .bw-confirm-btn {
866
2005
  width: 100%;
867
- height: 48px;
868
- border-radius: var(--bw-radius);
2006
+ height: 52px;
2007
+ border-radius: var(--bw-radius-pill);
869
2008
  font-size: 15px;
870
2009
  font-weight: 500;
2010
+ letter-spacing: -0.005em;
871
2011
  }
872
2012
 
873
2013
  .bw-confirm-btn:hover:not(:disabled),
874
2014
  .bw-footer-next:hover:not(:disabled),
875
2015
  .bw-btn-primary:hover {
876
- opacity: 0.9;
2016
+ opacity: 0.92;
2017
+ box-shadow: var(--bw-shadow-lift);
2018
+ }
2019
+
2020
+ .bw-confirm-btn:active:not(:disabled),
2021
+ .bw-footer-next:active:not(:disabled),
2022
+ .bw-btn-primary:active {
2023
+ transform: scale(0.99);
877
2024
  }
878
2025
 
879
2026
  .bw-confirm-btn:disabled,
880
- .bw-footer-next:disabled {
2027
+ .bw-confirm-btn.is-disabled,
2028
+ .bw-footer-next:disabled,
2029
+ .bw-footer-next.is-disabled {
881
2030
  opacity: 0.35;
882
2031
  cursor: default;
883
2032
  }
884
2033
 
885
2034
  .bw-done {
886
2035
  display: flex;
2036
+ flex: 1;
887
2037
  flex-direction: column;
888
2038
  align-items: center;
2039
+ justify-content: center;
889
2040
  gap: 16px;
2041
+ min-height: 80svh;
890
2042
  padding: 80px 20px;
891
2043
  text-align: center;
2044
+ animation: bw-done-fade-in 480ms cubic-bezier(0.16, 1, 0.3, 1) both;
2045
+ }
2046
+
2047
+ .bw-done-icon,
2048
+ .bw-done-title,
2049
+ .bw-done-text,
2050
+ .bw-done > .bw-btn-primary {
2051
+ animation: bw-done-stagger-in 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
2052
+ }
2053
+
2054
+ .bw-done-icon {
2055
+ animation-delay: 80ms;
2056
+ }
2057
+
2058
+ .bw-done-title {
2059
+ animation-delay: 180ms;
2060
+ }
2061
+
2062
+ .bw-done-text {
2063
+ animation-delay: 260ms;
2064
+ }
2065
+
2066
+ .bw-done > .bw-btn-primary {
2067
+ animation-delay: 360ms;
2068
+ }
2069
+
2070
+ @keyframes bw-done-fade-in {
2071
+ from {
2072
+ opacity: 0;
2073
+ }
2074
+ to {
2075
+ opacity: 1;
2076
+ }
2077
+ }
2078
+
2079
+ @keyframes bw-done-stagger-in {
2080
+ from {
2081
+ opacity: 0;
2082
+ transform: translateY(8px) scale(0.985);
2083
+ }
2084
+ to {
2085
+ opacity: 1;
2086
+ transform: translateY(0) scale(1);
2087
+ }
2088
+ }
2089
+
2090
+ @media (prefers-reduced-motion: reduce) {
2091
+ .bw-done,
2092
+ .bw-done-icon,
2093
+ .bw-done-title,
2094
+ .bw-done-text,
2095
+ .bw-done > .bw-btn-primary {
2096
+ animation: none;
2097
+ }
892
2098
  }
893
2099
 
894
2100
  .bw-done-icon {
@@ -903,6 +2109,24 @@
903
2109
  color: var(--bw-primary-text);
904
2110
  }
905
2111
 
2112
+ /* Cancelled-state disc: neutral background instead of the celebratory
2113
+ primary fill. Tone is informational, not affirmative — we don't
2114
+ want to "celebrate" a cancellation with a brand-color check badge. */
2115
+ .bw-done-icon--muted {
2116
+ background: var(--bw-border-light);
2117
+ color: var(--bw-text-secondary);
2118
+ }
2119
+
2120
+ /* Override the shared 16px sizing for icons rendered inside chrome
2121
+ (.bw-done-icon, .bw-staff-avatar, etc.) — the success view's check
2122
+ reads small inside the 56px disc, so bump the glyph to 24px so it
2123
+ anchors the moment cleanly. */
2124
+ .bw-done-icon svg {
2125
+ width: 24px;
2126
+ height: 24px;
2127
+ stroke-width: 2;
2128
+ }
2129
+
906
2130
  .bw-done-title {
907
2131
  margin: 0;
908
2132
  font-size: 28px;
@@ -927,16 +2151,49 @@
927
2151
  }
928
2152
 
929
2153
  @media (max-width: 1024px) {
2154
+ /* Lock the frame to a single height across all steps so the widget
2155
+ doesn't bounce as the user moves between services / date / etc.
2156
+ Sized to fit the tallest step (date + time) with a little
2157
+ breathing room above and below. Body scrolls internally when a
2158
+ step exceeds it. */
930
2159
  .bw {
931
- min-height: 80vh;
2160
+ /* svh (small viewport height) doesn't jump when iOS Safari's URL
2161
+ bar toggles, unlike vh which uses the largest possible height
2162
+ and overflows when bars become visible. */
2163
+ min-height: 82svh;
2164
+ max-height: 82svh;
932
2165
  }
933
2166
 
934
- .bw-skel-desktop {
935
- display: none;
2167
+ /* Lock header/footer in the column so they never shrink under
2168
+ content pressure. Flex children default to flex-shrink: 1, which
2169
+ would let a tall body squeeze the title/buttons. Force them to
2170
+ size to their content and stay put — the body in between is the
2171
+ only thing that scrolls. */
2172
+ .bw-mobile-header,
2173
+ .bw-header,
2174
+ .bw-footer {
2175
+ flex: 0 0 auto;
936
2176
  }
937
2177
 
938
- .bw-skel-mobile {
939
- display: block;
2178
+ .bw-body {
2179
+ overflow-y: auto;
2180
+ -webkit-overflow-scrolling: touch;
2181
+ overscroll-behavior: contain;
2182
+ /* Top fade only; the bottom taper is rendered as a gradient
2183
+ overlay above .bw-footer so content fades into the footer
2184
+ edge, not into the body frame's own bottom. */
2185
+ -webkit-mask-image: linear-gradient(
2186
+ to bottom,
2187
+ transparent 0,
2188
+ #000 24px,
2189
+ #000 100%
2190
+ );
2191
+ mask-image: linear-gradient(
2192
+ to bottom,
2193
+ transparent 0,
2194
+ #000 24px,
2195
+ #000 100%
2196
+ );
940
2197
  }
941
2198
 
942
2199
  .bw-mobile-header {
@@ -953,11 +2210,12 @@
953
2210
 
954
2211
  .bw-header {
955
2212
  padding: 20px 24px 20px;
2213
+ border-bottom: 1px solid var(--bw-border-light);
956
2214
  }
957
2215
 
958
2216
  .bw-header-row {
959
2217
  display: flex;
960
- align-items: center;
2218
+ align-items: flex-start;
961
2219
  justify-content: space-between;
962
2220
  gap: 16px;
963
2221
  }
@@ -969,15 +2227,21 @@
969
2227
  justify-content: flex-end;
970
2228
  gap: 8px;
971
2229
  width: 116px;
2230
+ /* Align dots with the title's first cap-height when the title
2231
+ wraps to multiple lines (e.g. "Reschedule Eric Lan's
2232
+ appointment"). Without this the dots would sit on the row's
2233
+ top edge above the title's optical line. */
2234
+ margin-top: 10px;
972
2235
  }
973
2236
 
974
2237
  .bw-dot {
975
2238
  flex: 1 1 0;
976
2239
  width: auto;
977
2240
  min-width: 20px;
978
- max-width: 28px;
979
- height: 3px;
980
- border-radius: 1.5px;
2241
+ max-width: 32px;
2242
+ /* Bumped from 3px to 4px for a stronger active-step signal. */
2243
+ height: 4px;
2244
+ border-radius: 2px;
981
2245
  background: var(--bw-border);
982
2246
  transition: background 200ms ease;
983
2247
  }
@@ -987,7 +2251,9 @@
987
2251
  }
988
2252
 
989
2253
  .bw-title {
990
- font-size: 24px;
2254
+ /* iOS Title 1 = 28pt. Was 24px (no-man's land between Title 1
2255
+ and Title 2). 28px gives better scale separation from body. */
2256
+ font-size: 28px;
991
2257
  letter-spacing: -0.5px;
992
2258
  }
993
2259
 
@@ -1011,50 +2277,56 @@
1011
2277
  flex-direction: column;
1012
2278
  flex: 1;
1013
2279
  min-height: 0;
1014
- padding: 0 24px;
2280
+ padding: 0 24px 24px;
1015
2281
  }
1016
2282
 
1017
2283
  .bw-col {
1018
2284
  display: none;
1019
2285
  }
1020
2286
 
2287
+ /* Mobile 4-step flow:
2288
+ Step 1: services (bw-col--left, bw-step-2 only)
2289
+ Step 2: calendar + provider chips + slots (bw-col--center)
2290
+ Step 3: form (bw-col--right, form fields visible)
2291
+ Step 4: review (bw-col--review with the full summary card,
2292
+ scrollable; footer only carries back + confirm buttons).
2293
+ Provider lives inside bw-cal-card on mobile so the visual
2294
+ order reads date → provider → time. The desktop dropdown in
2295
+ bw-step-1 is hidden on mobile (we use chips here instead). */
1021
2296
  .bw-body[data-mobile-step="1"] .bw-col--left,
1022
- .bw-body[data-mobile-step="2"] .bw-col--left,
1023
- .bw-body[data-mobile-step="3"] .bw-col--center,
1024
- .bw-body[data-mobile-step="4"] .bw-col--right {
2297
+ .bw-body[data-mobile-step="2"] .bw-col--center,
2298
+ .bw-body[data-mobile-step="3"] .bw-col--right,
2299
+ .bw-body[data-mobile-step="4"] .bw-col--review {
1025
2300
  display: flex;
2301
+ padding-top: 16px;
2302
+ padding-bottom: 24px;
1026
2303
  }
1027
2304
 
1028
- .bw-body[data-mobile-step="4"] .bw-right-inner {
2305
+ .bw-body[data-mobile-step="3"] .bw-right-inner {
1029
2306
  padding-bottom: 20px;
1030
2307
  }
1031
2308
 
2309
+ /* Step 1: hide the desktop service-picker (inside bw-step-1) and
2310
+ hide the entire bw-step-1 wrapper. Show bw-step-2 (full-width
2311
+ services list). */
1032
2312
  .bw-body[data-mobile-step="1"] .bw-step-1 {
1033
- display: flex;
1034
- }
1035
-
1036
- .bw-body[data-mobile-step="1"] .bw-step-2 {
1037
- display: none;
1038
- }
1039
-
1040
- .bw-body[data-mobile-step="1"] .bw-service-picker {
1041
2313
  display: none;
1042
2314
  }
1043
2315
 
1044
- .bw-body[data-mobile-step="2"] .bw-step-1 {
1045
- display: none;
1046
- }
1047
-
1048
- .bw-body[data-mobile-step="2"] .bw-step-2 {
2316
+ .bw-body[data-mobile-step="1"] .bw-step-2 {
1049
2317
  display: flex;
1050
2318
  flex-direction: column;
1051
2319
  }
1052
2320
 
1053
- .bw-body[data-mobile-step="2"] .bw-step-2-heading,
1054
- .bw-body[data-mobile-step="2"] .bw-step-2-divider {
2321
+ .bw-body[data-mobile-step="1"] .bw-step-2-heading,
2322
+ .bw-body[data-mobile-step="1"] .bw-step-2-divider {
1055
2323
  display: none;
1056
2324
  }
1057
2325
 
2326
+ /* Desktop dropdown lives in bw-col--left's bw-step-1; we don't show
2327
+ bw-col--left at all on mobile step 2, so this is implicitly
2328
+ hidden. The chip strip below handles provider selection. */
2329
+
1058
2330
  .bw-svc-scroll {
1059
2331
  display: none;
1060
2332
  }
@@ -1078,31 +2350,105 @@
1078
2350
  margin: 4px 0;
1079
2351
  }
1080
2352
 
1081
- .bw-form .bw-summary,
1082
- .bw-confirm-btn {
1083
- display: none;
2353
+ /* Mobile typography bumps. Per productdesign.md typography scale
2354
+ mobile column: list-row primary reads as body-s (13px) at the
2355
+ smallest, body-m (15px) for drawer-style titles, caption-l (12px)
2356
+ for list-row secondary. The widget is essentially a one-screen
2357
+ booking surface, so primary read targets get a one-step bump on
2358
+ mobile so they stay accessible at arm's length. */
2359
+ .bw-svc-name,
2360
+ .bw-svc-row--full .bw-svc-name,
2361
+ .bw-svc-price {
2362
+ font-size: 15px;
1084
2363
  }
1085
2364
 
1086
- .bw-footer {
1087
- display: flex;
1088
- flex-direction: column;
1089
- gap: 20px;
1090
- margin-top: auto;
1091
- padding: 16px 24px 34px;
1092
- border-top: 1px solid var(--bw-border-light);
2365
+ .bw-svc-meta,
2366
+ .bw-svc-row--full .bw-svc-meta,
2367
+ .bw-svc-desc {
2368
+ font-size: 15px;
2369
+ line-height: 1.5;
1093
2370
  }
1094
2371
 
1095
- .bw-footer-summary {
1096
- display: flex;
2372
+ .bw-svc-category {
2373
+ font-size: 14px;
2374
+ }
2375
+
2376
+ .bw-staff-trigger-name,
2377
+ .bw-staff-option-name {
2378
+ font-size: 15px;
2379
+ }
2380
+
2381
+ .bw-staff-trigger-desc,
2382
+ .bw-staff-option-desc {
2383
+ font-size: 12px;
2384
+ }
2385
+
2386
+ .bw-staff-initials {
2387
+ font-size: 13px;
2388
+ }
2389
+
2390
+ /* Mobile thumbnails: bump both the inline (left col) and the
2391
+ full-width (mobile step 1) variants up one notch. The mobile
2392
+ full-width row has more horizontal breathing room and the
2393
+ thumbnail gets to anchor the row visually. */
2394
+ .bw-svc-image {
2395
+ width: 88px;
2396
+ height: 88px;
2397
+ }
2398
+
2399
+ .bw-svc-image--lg {
2400
+ width: 104px;
2401
+ height: 104px;
2402
+ }
2403
+
2404
+ .bw-svc-image-badge {
2405
+ width: 20px;
2406
+ height: 20px;
2407
+ }
2408
+
2409
+ .bw-svc-image-badge svg {
2410
+ width: 12px;
2411
+ height: 12px;
2412
+ }
2413
+
2414
+ .bw-form .bw-summary,
2415
+ .bw-confirm-btn {
2416
+ display: none;
2417
+ }
2418
+
2419
+ .bw-footer {
2420
+ position: relative;
2421
+ display: flex;
1097
2422
  flex-direction: column;
1098
- gap: 12px;
1099
- padding: 16px;
1100
- border: 1px solid var(--bw-border);
1101
- border-radius: var(--bw-radius);
2423
+ gap: 20px;
2424
+ /* .bw-footer is a sibling of .bw-content, not .bw-body, so the
2425
+ frame-to-frame mobile gap has to live on the footer flex item.
2426
+ Bottom padding uses env(safe-area-inset-bottom) so notched iOS
2427
+ devices clear the home indicator naturally, while Touch ID
2428
+ devices + Android don't waste 34px of space. Floors at 24px so
2429
+ there's always real breathing room below the Confirm button.
2430
+ Host site MUST include `viewport-fit=cover` in its viewport
2431
+ meta for env(safe-area-inset-*) to be non-zero on iOS. */
2432
+ margin-top: 16px;
2433
+ padding: 16px 24px
2434
+ max(24px, calc(env(safe-area-inset-bottom) + 16px));
2435
+ border-top: 1px solid var(--bw-border-light);
1102
2436
  }
1103
2437
 
1104
- .bw-footer-summary .bw-summary-title {
1105
- margin-bottom: 0;
2438
+ /* Gradient overlay sits above the footer (in the 16px gap and a
2439
+ touch into the body's frame above it) so scrolling content fades
2440
+ into the footer edge instead of slicing or tapering inside the
2441
+ body's own frame. */
2442
+ .bw-footer::before {
2443
+ content: '';
2444
+ position: absolute;
2445
+ inset: -32px 0 100%;
2446
+ pointer-events: none;
2447
+ background: linear-gradient(
2448
+ to bottom,
2449
+ rgba(255, 255, 255, 0) 0%,
2450
+ var(--bw-bg) 100%
2451
+ );
1106
2452
  }
1107
2453
 
1108
2454
  .bw-footer-btns {
@@ -1132,6 +2478,25 @@
1132
2478
  font-size: 15px;
1133
2479
  font-weight: 500;
1134
2480
  }
2481
+
2482
+ .bw-footer-payment-slot {
2483
+ display: flex;
2484
+ flex: 1;
2485
+ min-width: 0;
2486
+ }
2487
+
2488
+ .bw-footer-payment-slot:empty {
2489
+ display: none;
2490
+ }
2491
+
2492
+ .bw-footer-payment-slot .bw-confirm-btn {
2493
+ display: flex;
2494
+ flex: 1;
2495
+ height: 48px;
2496
+ border-radius: var(--bw-radius-lg);
2497
+ font-size: 15px;
2498
+ font-weight: 500;
2499
+ }
1135
2500
  }
1136
2501
 
1137
2502
  @media (max-width: 480px) {
@@ -1144,7 +2509,7 @@
1144
2509
  gap: 6px;
1145
2510
  }
1146
2511
 
1147
- .bw-body[data-mobile-step="4"] .bw-right-inner {
2512
+ .bw-body[data-mobile-step="3"] .bw-right-inner {
1148
2513
  padding-bottom: 16px;
1149
2514
  }
1150
2515
 
@@ -1153,7 +2518,7 @@
1153
2518
  }
1154
2519
 
1155
2520
  .bw-body {
1156
- padding: 0 20px;
2521
+ padding: 0 20px 24px;
1157
2522
  }
1158
2523
 
1159
2524
  .bw-footer {
@@ -1180,16 +2545,633 @@
1180
2545
 
1181
2546
  .bw-slot {
1182
2547
  min-height: 40px;
1183
- padding: 12px 0;
2548
+ padding: 12px 12px;
1184
2549
  font-size: 12px;
1185
2550
  }
1186
2551
 
1187
2552
  .bw-field input,
1188
2553
  .bw-field textarea {
1189
- font-size: 14px;
2554
+ /* 16px on mobile prevents iOS Safari's auto-zoom-on-focus,
2555
+ which otherwise leaves the form stuck zoomed-in mid-flow. */
2556
+ font-size: 16px;
2557
+ }
2558
+
2559
+ .bw-field label {
2560
+ /* Bumped from 12px to 13px so labels read clearly without
2561
+ crowding the 16px input below them. */
2562
+ font-size: 13px;
1190
2563
  }
1191
2564
 
1192
2565
  .bw-summary-row {
2566
+ font-size: 13px;
2567
+ }
2568
+
2569
+ .bw-slot {
2570
+ /* Time pills are a primary tap target — bump from 13px
2571
+ (footnote-tier) to 15px (subheadline). 3-col mobile grid
2572
+ still fits "10:30 a. m." comfortably at 15px. */
2573
+ font-size: 15px;
2574
+ padding: 13px 12px;
2575
+ }
2576
+
2577
+ .bw-cal-weekdays span {
2578
+ /* Day-of-week header bumped from 12px to 13px for readability. */
2579
+ font-size: 13px;
2580
+ }
2581
+ }
2582
+
2583
+ /* ===========================================================================
2584
+ Desktop-only right-pane swap (slots → details) and sizing fixes.
2585
+ Mobile is intentionally untouched here — the pane wrappers exist in the
2586
+ DOM on mobile but receive no styling, so they collapse into transparent
2587
+ block containers and the form / cal-card / step-1 keep their original
2588
+ mobile flex behavior from commit 6ca438f.
2589
+ =========================================================================== */
2590
+
2591
+ /* Slot pill structural styling that's safe on both breakpoints because
2592
+ the .bw-slots-desktop wrapper is hidden on mobile (display: none below)
2593
+ and the .bw-slots-mobile wrapper is the only path that renders the
2594
+ inline 4-up grid on mobile via display: contents. */
2595
+ .bw-slots-mobile {
2596
+ display: none;
2597
+ }
2598
+
2599
+ .bw-slots-heading {
2600
+ display: flex;
2601
+ align-items: baseline;
2602
+ justify-content: space-between;
2603
+ margin-bottom: 14px;
2604
+ font-size: 14px;
2605
+ font-weight: 500;
2606
+ color: var(--bw-text);
2607
+ }
2608
+
2609
+ .bw-slots-count {
2610
+ font-size: 12px;
2611
+ color: var(--bw-text-muted);
2612
+ font-variant-numeric: tabular-nums;
2613
+ }
2614
+
2615
+ .bw-back-btn {
2616
+ display: inline-flex;
2617
+ align-items: center;
2618
+ gap: 6px;
2619
+ margin-bottom: 16px;
2620
+ padding: 6px 0;
2621
+ background: transparent;
2622
+ border: 0;
2623
+ font-size: 12px;
2624
+ color: var(--bw-text-muted);
2625
+ cursor: pointer;
2626
+ transition: color var(--bw-duration) var(--bw-ease);
2627
+ }
2628
+
2629
+ .bw-back-btn:hover {
2630
+ color: var(--bw-text);
2631
+ }
2632
+
2633
+ .bw-back-btn svg {
2634
+ width: 14px;
2635
+ height: 14px;
2636
+ }
2637
+
2638
+ @media (min-width: 1025px) {
2639
+ /* Lock the desktop frame to a stable height so it doesn't snap to a
2640
+ shorter fit-content size when the widget swaps into the success
2641
+ view (.bw-done has less content than the normal flow). Picked to
2642
+ comfortably fit the calendar + slots + form layout. */
2643
+ .bw {
2644
+ min-height: 720px;
2645
+ }
2646
+
2647
+ /* Desktop body grid: cols sit at the top of their cell instead of
2648
+ stretching to the row's max content height — the visual empty
2649
+ space below the cal-card and right-pane goes away. */
2650
+ .bw-body {
2651
+ align-items: start;
2652
+ }
2653
+
2654
+ /* Desktop cal-card hugs its grid + timezone instead of stretching
2655
+ to fill the row, and the timezone sits flush below the day grid
2656
+ (no auto margin pushing it down). */
2657
+ .bw-cal-card {
2658
+ flex: 0 0 auto;
2659
+ }
2660
+
2661
+ .bw-timezone {
2662
+ margin-top: 0;
2663
+ }
2664
+
2665
+ /* Slots column on desktop: vertical pill list above the form, not
2666
+ a 4-up grid below the calendar. */
2667
+ .bw-slots-desktop .bw-time-slots {
2668
+ grid-template-columns: 1fr;
2669
+ gap: 8px;
2670
+ margin-top: 0;
2671
+ }
2672
+
2673
+ .bw-slots-desktop .bw-slot {
2674
+ padding: 14px 16px;
2675
+ }
2676
+
2677
+ /* Match the skeleton pill dimensions to the real desktop pill so
2678
+ the load → loaded swap doesn't reflow the column. Real pill =
2679
+ 14px padding + 13px font + line-height ≈ 50px tall. */
2680
+ .bw-slots-desktop .bw-skel--slot {
2681
+ height: 50px;
2682
+ }
2683
+
2684
+ /* --bw-cal-h is set on the .bw root via ResizeObserver in the
2685
+ widget — it tracks the live height of the .bw-cal-card in the
2686
+ middle column. Use it to cap the services list (left) and the
2687
+ slots list (right) so all three columns share the same visual
2688
+ height. Both internal scrollers fade their top + bottom edges
2689
+ so cut-off content reads as scrollable, not clipped. */
2690
+ .bw-svc-scroll-wrap {
2691
+ max-height: var(--bw-cal-h, 480px);
2692
+ }
2693
+
2694
+ .bw-pane--slots,
2695
+ .bw-pane--details {
2696
+ /* Lock both panes in the body's right column to the same max
2697
+ height as the calendar so the body row's intrinsic height
2698
+ doesn't change between viewState 'slots' and 'details'. Without
2699
+ this, the form pane grows taller than the slots pane and
2700
+ .bw-content's grid-stack cell jumps in height on step 2. */
2701
+ max-height: var(--bw-cal-h, 480px);
2702
+ overflow: hidden;
2703
+ display: flex;
2704
+ flex-direction: column;
2705
+ }
2706
+
2707
+ /* Sticky "Available times" heading on desktop. Lives as a sibling
2708
+ of .bw-slots-desktop (the scroller) so it doesn't scroll with
2709
+ the time pills. Sits flush at the top of the right column. */
2710
+ .bw-pane--slots .bw-slots-heading--sticky {
2711
+ flex: 0 0 auto;
2712
+ margin-bottom: 4px;
2713
+ padding-bottom: 8px;
2714
+ }
2715
+
2716
+ .bw-pane--slots .bw-slots-desktop {
2717
+ flex: 1;
2718
+ min-height: 0;
2719
+ overflow-y: auto;
2720
+ -webkit-mask-image: linear-gradient(
2721
+ to bottom,
2722
+ transparent 0,
2723
+ #000 18px,
2724
+ #000 calc(100% - 18px),
2725
+ transparent 100%
2726
+ );
2727
+ mask-image: linear-gradient(
2728
+ to bottom,
2729
+ transparent 0,
2730
+ #000 18px,
2731
+ #000 calc(100% - 18px),
2732
+ transparent 100%
2733
+ );
2734
+ scrollbar-width: thin;
2735
+ scrollbar-color: var(--bw-border) transparent;
2736
+ /* Reserve space for the scrollbar so it never overlays the
2737
+ pills' right edge. Combined with the right padding below this
2738
+ gives the bar its own lane regardless of whether content
2739
+ overflows. */
2740
+ scrollbar-gutter: stable;
2741
+ /* Padding so first/last pills sit inside the fade zone, matching
2742
+ the services scroller pattern. Right padding gives the pills
2743
+ breathing room from the scrollbar lane. */
2744
+ padding-block: 12px 16px;
2745
+ padding-right: 6px;
2746
+ }
2747
+
2748
+ .bw-pane--slots .bw-slots-desktop::-webkit-scrollbar {
2749
+ width: 6px;
2750
+ }
2751
+
2752
+ .bw-pane--slots .bw-slots-desktop::-webkit-scrollbar-thumb {
2753
+ border-radius: var(--bw-radius-pill);
2754
+ background: var(--bw-border);
2755
+ }
2756
+
2757
+ .bw-pane--slots .bw-slots-desktop::-webkit-scrollbar-thumb:hover {
2758
+ background: var(--bw-text-muted);
2759
+ }
2760
+
2761
+ /* Right column two-step stage: slots pane → (crossfade) → details
2762
+ pane. Both panes share the stage area; the hidden one is taken
2763
+ out of normal flow so the column sizes only to the active pane.
2764
+ Animation timing matches Sable's productdesign popup rule:
2765
+ 250ms expo ease-out on enter, 150ms standard ease-out on exit. */
2766
+ .bw-right-stage {
2767
+ position: relative;
2768
+ display: flex;
2769
+ flex-direction: column;
2770
+ min-height: 0;
2771
+ }
2772
+
2773
+ .bw-pane {
2774
+ transition:
2775
+ opacity 250ms cubic-bezier(0.16, 1, 0.3, 1),
2776
+ transform 250ms cubic-bezier(0.16, 1, 0.3, 1);
2777
+ will-change: opacity, transform;
2778
+ }
2779
+
2780
+ .bw-pane.is-active {
2781
+ position: relative;
2782
+ opacity: 1;
2783
+ transform: scale(1);
2784
+ pointer-events: auto;
2785
+ z-index: 1;
2786
+ }
2787
+
2788
+ .bw-pane.is-hidden {
2789
+ position: absolute;
2790
+ inset: 0;
2791
+ opacity: 0;
2792
+ transform: scale(0.97);
2793
+ pointer-events: none;
2794
+ transition-duration: 150ms;
2795
+ transition-timing-function: ease-out;
2796
+ }
2797
+ }
2798
+
2799
+ /* Mobile-only flips for the new pane wrappers: only enough to hide
2800
+ the desktop slots column copy and force the slots-mobile wrapper
2801
+ to render its children inline (the original 4-up grid below the
2802
+ calendar). The pane--details wrapper has no styling on mobile, so
2803
+ it's a transparent block container and the form renders exactly
2804
+ like it did pre-pane-restructure. */
2805
+ @media (max-width: 1024px) {
2806
+ .bw-pane--slots {
2807
+ display: none;
2808
+ }
2809
+
2810
+ .bw-back-btn {
2811
+ display: none;
2812
+ }
2813
+
2814
+ /* Slots-mobile is now its own card frame on step 2 (combined with
2815
+ .bw-mobile-card class). Override default display: none so the
2816
+ card chrome shows; .bw-mobile-card already sets flex column. */
2817
+ .bw-slots-mobile {
2818
+ display: flex;
2819
+ }
2820
+
2821
+ /* Details view is desktop-only; mobile uses the existing step-4
2822
+ right column for the form. */
2823
+ .bw-details-view {
2824
+ display: none;
2825
+ }
2826
+
2827
+ /* Mobile step transitions: when a step becomes active, the relevant
2828
+ content fades + slides up subtly. Same 250ms expo ease-out timing
2829
+ as desktop transitions so the product feels consistent across
2830
+ breakpoints. We target the step-specific content (not the col
2831
+ wrapper for steps 1/2 since col--left stays visible across
2832
+ them) so animations don't re-fire when the active col is shared
2833
+ between adjacent steps. */
2834
+ /* 4-step flow. Step 1 shares bw-col--left with no other step.
2835
+ Steps 3 + 4 both live in bw-col--right but differ in content,
2836
+ so we re-fire the enter animation on data-mobile-step changes
2837
+ between them. */
2838
+ .bw-body[data-mobile-step='1'] .bw-col--left,
2839
+ .bw-body[data-mobile-step='2'] .bw-col--center,
2840
+ .bw-body[data-mobile-step='3'] .bw-col--right,
2841
+ .bw-body[data-mobile-step='4'] .bw-col--review {
2842
+ animation: bw-mobile-step-in 250ms cubic-bezier(0.16, 1, 0.3, 1);
2843
+ }
2844
+
2845
+ @keyframes bw-mobile-step-in {
2846
+ from {
2847
+ opacity: 0;
2848
+ transform: translateY(6px);
2849
+ }
2850
+ to {
2851
+ opacity: 1;
2852
+ transform: translateY(0);
2853
+ }
2854
+ }
2855
+ }
2856
+
2857
+ /* Desktop-only details view: 2-column summary | form layout shown
2858
+ when viewState === 'details'. Mobile keeps the step-based flow
2859
+ (bw-details-view is display: none on mobile, set above). */
2860
+ .bw-details-view {
2861
+ display: none;
2862
+ }
2863
+
2864
+ /* The .bw-content wrapper holds bw-body and bw-details-view. On
2865
+ mobile it's a transparent flex column passthrough. On desktop it
2866
+ becomes a positioning context so the inactive view can overlay
2867
+ during the crossfade without contributing its height. */
2868
+ .bw-content {
2869
+ display: flex;
2870
+ flex-direction: column;
2871
+ flex: 1;
2872
+ min-height: 0;
2873
+ }
2874
+
2875
+ @media (min-width: 1025px) {
2876
+ /* Stack the two panes in the same grid cell so they share space,
2877
+ but keep the inactive pane out of normal flow. Otherwise the
2878
+ services/calendar step reserves the much taller details form
2879
+ before the user has selected a time. */
2880
+ .bw-content {
2881
+ position: relative;
2882
+ display: grid;
2883
+ grid-template-areas: 'stack';
2884
+ align-items: start;
2885
+ flex: 0 1 auto;
2886
+ }
2887
+
2888
+ .bw-body,
2889
+ .bw-details-view {
2890
+ grid-area: stack;
2891
+ transition:
2892
+ opacity 250ms cubic-bezier(0.16, 1, 0.3, 1),
2893
+ transform 250ms cubic-bezier(0.16, 1, 0.3, 1);
2894
+ will-change: opacity, transform;
2895
+ }
2896
+
2897
+ .bw-body {
2898
+ position: relative;
2899
+ }
2900
+
2901
+ .bw-details-view {
2902
+ position: absolute;
2903
+ inset: 0;
2904
+ display: grid;
2905
+ grid-template-columns: 5fr 6fr;
2906
+ gap: 56px;
2907
+ padding: 32px 48px 48px;
2908
+ max-width: 1440px;
2909
+ width: 100%;
2910
+ margin: 0 auto;
2911
+ /* Default hidden state — overlaid on top of body. */
2912
+ opacity: 0;
2913
+ transform: scale(0.99);
2914
+ pointer-events: none;
2915
+ }
2916
+
2917
+ .bw[data-view-state='details'] .bw-body {
2918
+ position: absolute;
2919
+ inset: 0;
2920
+ opacity: 0;
2921
+ transform: scale(0.99);
2922
+ pointer-events: none;
2923
+ }
2924
+
2925
+ .bw[data-view-state='details'] .bw-details-view {
2926
+ position: relative;
2927
+ inset: auto;
2928
+ opacity: 1;
2929
+ transform: scale(1);
2930
+ pointer-events: auto;
2931
+ }
2932
+
2933
+ .bw-details-summary {
2934
+ display: flex;
2935
+ flex-direction: column;
2936
+ gap: 16px;
2937
+ padding-right: 8px;
2938
+ /* Sticky so the booking summary stays alongside the form when
2939
+ it gets long (e.g. paid services with payment block, or many
2940
+ intake fields). `align-self: start` keeps the grid cell from
2941
+ stretching to row height, which would block sticky behavior.
2942
+ Host sites can override --bw-sticky-summary-top to clear a
2943
+ fixed top nav — encomiendas' nav is 72px so a host override
2944
+ to ~96px makes sense; the default 24px works for hosts with
2945
+ no fixed nav. */
2946
+ position: sticky;
2947
+ top: var(--bw-sticky-summary-top, 24px);
2948
+ align-self: start;
2949
+ /* Cap height so very-tall summaries (long service description,
2950
+ all reschedule rows visible) don't overflow the viewport;
2951
+ internal scroll if needed. */
2952
+ max-height: calc(100svh - var(--bw-sticky-summary-top, 24px) - 24px);
2953
+ overflow-y: auto;
2954
+ scrollbar-width: thin;
2955
+ scrollbar-color: var(--bw-border) transparent;
2956
+ }
2957
+
2958
+ .bw-details-eyebrow {
2959
+ margin: 0;
1193
2960
  font-size: 12px;
2961
+ font-weight: 500;
2962
+ letter-spacing: 0.04em;
2963
+ color: var(--bw-text-muted);
2964
+ }
2965
+
2966
+ .bw-details-service {
2967
+ margin: 0;
2968
+ font-size: 24px;
2969
+ font-weight: 500;
2970
+ letter-spacing: -0.4px;
2971
+ line-height: 1.2;
2972
+ color: var(--bw-text);
2973
+ }
2974
+
2975
+ .bw-details-desc {
2976
+ margin: 0;
2977
+ font-size: 13px;
2978
+ line-height: 1.55;
2979
+ color: var(--bw-text-secondary);
2980
+ }
2981
+
2982
+ .bw-details-meta {
2983
+ display: flex;
2984
+ flex-direction: column;
2985
+ gap: 14px;
2986
+ margin-top: 8px;
2987
+ padding-top: 20px;
2988
+ border-top: 1px solid var(--bw-border-light);
2989
+ }
2990
+
2991
+ .bw-details-meta-row {
2992
+ display: flex;
2993
+ align-items: flex-start;
2994
+ gap: 12px;
2995
+ font-size: 13px;
2996
+ color: var(--bw-text);
2997
+ }
2998
+
2999
+ .bw-details-meta-icon {
3000
+ display: inline-flex;
3001
+ flex-shrink: 0;
3002
+ align-items: center;
3003
+ justify-content: center;
3004
+ width: 18px;
3005
+ height: 18px;
3006
+ color: var(--bw-text-muted);
1194
3007
  }
3008
+
3009
+ .bw-details-meta-icon svg {
3010
+ width: 14px;
3011
+ height: 14px;
3012
+ }
3013
+
3014
+ .bw-details-meta-text {
3015
+ display: flex;
3016
+ flex-direction: column;
3017
+ gap: 2px;
3018
+ min-width: 0;
3019
+ }
3020
+
3021
+ .bw-details-meta-label {
3022
+ font-size: 11px;
3023
+ color: var(--bw-text-muted);
3024
+ }
3025
+
3026
+ .bw-details-meta-value {
3027
+ font-size: 13px;
3028
+ color: var(--bw-text);
3029
+ font-variant-numeric: tabular-nums;
3030
+ }
3031
+
3032
+ .bw-details-meta-row--strike .bw-details-meta-value {
3033
+ text-decoration: line-through;
3034
+ opacity: 0.55;
3035
+ }
3036
+
3037
+ .bw-details-hold {
3038
+ color: var(--bw-text);
3039
+ }
3040
+
3041
+ .bw-details-form {
3042
+ display: flex;
3043
+ flex-direction: column;
3044
+ gap: 16px;
3045
+ }
3046
+
3047
+ /* Back link sits at the top of the summary pane, in the eyebrow
3048
+ position — replacing the previous static "Reschedule" / "Booking"
3049
+ label as the first thing the user reads on the left. The static
3050
+ eyebrow still renders just below as the section label for the
3051
+ summary stack. */
3052
+ .bw-details-summary .bw-back-btn--details {
3053
+ align-self: flex-start;
3054
+ margin-bottom: 16px;
3055
+ padding: 0;
3056
+ color: var(--bw-text-secondary);
3057
+ font-size: 14px;
3058
+ }
3059
+
3060
+ .bw-details-summary .bw-back-btn--details:hover {
3061
+ color: var(--bw-text);
3062
+ }
3063
+
3064
+ .bw-form--details {
3065
+ gap: 18px;
3066
+ }
3067
+
3068
+ /* Confirm sits alone at the bottom-right of the form column. */
3069
+ .bw-form--details .bw-confirm-btn {
3070
+ align-self: flex-end;
3071
+ margin-top: 8px;
3072
+ }
3073
+ }
3074
+
3075
+ .bw-intake {
3076
+ display: flex;
3077
+ flex-direction: column;
3078
+ gap: 14px;
3079
+ }
3080
+
3081
+ .bw-intake-section {
3082
+ display: flex;
3083
+ flex-direction: column;
3084
+ gap: 10px;
3085
+ }
3086
+
3087
+ .bw-intake-title {
3088
+ font-size: 12px;
3089
+ font-weight: 700;
3090
+ color: var(--bw-text);
3091
+ text-transform: uppercase;
3092
+ letter-spacing: 0.08em;
3093
+ }
3094
+
3095
+ .bw-field--wide {
3096
+ grid-column: 1 / -1;
3097
+ }
3098
+
3099
+ .bw .bw-field select {
3100
+ width: 100%;
3101
+ min-height: 40px;
3102
+ border: 1px solid var(--bw-border);
3103
+ border-radius: 10px;
3104
+ background: var(--bw-bg);
3105
+ color: var(--bw-text);
3106
+ padding: 0 12px;
3107
+ font: inherit;
3108
+ }
3109
+
3110
+ .bw-help {
3111
+ margin: 4px 0 0;
3112
+ font-size: 12px;
3113
+ color: var(--bw-text-muted);
3114
+ }
3115
+
3116
+ .bw-checkbox-field label {
3117
+ display: flex;
3118
+ align-items: flex-start;
3119
+ gap: 8px;
3120
+ font-size: 13px;
3121
+ line-height: 1.4;
3122
+ }
3123
+
3124
+ .bw-checkbox-field input {
3125
+ width: 16px;
3126
+ height: 16px;
3127
+ min-width: 16px;
3128
+ flex: 0 0 16px;
3129
+ margin-top: 1px;
3130
+ padding: 0;
3131
+ accent-color: var(--bw-primary);
3132
+ }
3133
+
3134
+ .bw-repeatable {
3135
+ display: flex;
3136
+ flex-direction: column;
3137
+ gap: 10px;
3138
+ }
3139
+
3140
+ .bw-repeatable-item {
3141
+ display: flex;
3142
+ flex-direction: column;
3143
+ gap: 10px;
3144
+ border: 1px solid var(--bw-border);
3145
+ border-radius: 12px;
3146
+ padding: 12px;
3147
+ background: color-mix(in srgb, var(--bw-bg) 85%, transparent);
3148
+ }
3149
+
3150
+ .bw-repeatable-head {
3151
+ display: flex;
3152
+ align-items: center;
3153
+ justify-content: space-between;
3154
+ gap: 10px;
3155
+ font-size: 13px;
3156
+ font-weight: 700;
3157
+ color: var(--bw-text);
3158
+ }
3159
+
3160
+ .bw-link-btn,
3161
+ .bw-secondary-btn {
3162
+ border: 0;
3163
+ background: transparent;
3164
+ color: var(--bw-primary);
3165
+ font: inherit;
3166
+ font-size: 13px;
3167
+ font-weight: 700;
3168
+ cursor: pointer;
3169
+ }
3170
+
3171
+ .bw-secondary-btn {
3172
+ align-self: flex-start;
3173
+ border: 1px solid var(--bw-border);
3174
+ border-radius: 999px;
3175
+ padding: 8px 12px;
3176
+ background: var(--bw-bg);
1195
3177
  }