@asksable/site-connector 0.2.0 → 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,3 +1,22 @@
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 {
2
21
  /* Theme tokens — overridable per host site (color + font). */
3
22
  --bw-font: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -77,43 +96,6 @@
77
96
  animation: bw-shimmer 1.5s ease infinite;
78
97
  }
79
98
 
80
- .bw-skel--title {
81
- width: min(340px, 70%);
82
- height: 32px;
83
- }
84
-
85
- .bw-skel--text {
86
- width: 48%;
87
- height: 14px;
88
- margin-top: 14px;
89
- }
90
-
91
- .bw-skel--text-lg {
92
- width: min(520px, 85%);
93
- }
94
-
95
- .bw-skel--card {
96
- width: 100%;
97
- height: 56px;
98
- margin-bottom: 10px;
99
- }
100
-
101
- .bw-skel--calendar {
102
- width: 100%;
103
- height: 300px;
104
- }
105
-
106
- .bw-skel--input {
107
- width: 100%;
108
- height: 40px;
109
- margin-bottom: 12px;
110
- }
111
-
112
- .bw-skel--textarea {
113
- width: 100%;
114
- height: 92px;
115
- }
116
-
117
99
  .bw-skel--slot {
118
100
  /* Default mobile pill height: matches .bw-slot { padding: 12px 0;
119
101
  font-size: 13px } ≈ 44px. Desktop overrides this below to match
@@ -122,198 +104,6 @@
122
104
  border-radius: var(--bw-radius);
123
105
  }
124
106
 
125
- .bw-skel-list {
126
- display: flex;
127
- flex-direction: column;
128
- gap: 0;
129
- margin-top: 16px;
130
- }
131
-
132
- .bw-skel-slots {
133
- margin-top: 8px;
134
- }
135
-
136
- .bw-skel-slots-grid {
137
- display: grid;
138
- grid-template-columns: repeat(4, 1fr);
139
- gap: 10px;
140
- }
141
-
142
- .bw-skel-mobile {
143
- display: none;
144
- padding: 0 24px 24px;
145
- }
146
-
147
- /* === Initial-load skeleton chrome ===
148
- Each skeleton primitive mirrors a real widget element so the
149
- chrome doesn't reflow when actual content arrives. */
150
-
151
- .bw-skel--label {
152
- width: 110px;
153
- height: 18px;
154
- margin-bottom: 16px;
155
- border-radius: var(--bw-radius-sm);
156
- }
157
-
158
- /* Service group spacing matches .bw-svc-group + .bw-svc-group rules */
159
- .bw-skel-svc-group {
160
- display: flex;
161
- flex-direction: column;
162
- gap: 14px;
163
- }
164
-
165
- .bw-skel-svc-group + .bw-skel-svc-group {
166
- margin-top: 28px;
167
- }
168
-
169
- .bw-skel--svc-category {
170
- width: 90px;
171
- height: 14px;
172
- border-radius: var(--bw-radius-sm);
173
- }
174
-
175
- /* Service row: 80x80 image + flex column with name/desc/meta lines.
176
- Mirrors the rendered .bw-svc-row layout exactly. */
177
- .bw-skel-svc-row {
178
- display: flex;
179
- align-items: flex-start;
180
- gap: 16px;
181
- padding: 8px 0;
182
- }
183
-
184
- .bw-skel--svc-image {
185
- flex-shrink: 0;
186
- width: 80px;
187
- height: 80px;
188
- border-radius: var(--bw-radius-md);
189
- }
190
-
191
- .bw-skel-svc-info {
192
- display: flex;
193
- flex: 1;
194
- min-width: 0;
195
- flex-direction: column;
196
- gap: 8px;
197
- padding-top: 2px;
198
- }
199
-
200
- .bw-skel--svc-name {
201
- width: 70%;
202
- height: 16px;
203
- border-radius: var(--bw-radius-sm);
204
- }
205
-
206
- .bw-skel--svc-desc {
207
- width: 100%;
208
- height: 14px;
209
- border-radius: var(--bw-radius-sm);
210
- }
211
-
212
- .bw-skel--svc-meta {
213
- width: 50%;
214
- height: 14px;
215
- margin-top: 4px;
216
- border-radius: var(--bw-radius-sm);
217
- }
218
-
219
- /* Calendar card: month header row, 7-col day-of-week strip, 6-row
220
- day grid (42 cells), timezone chip below. */
221
- .bw-skel-cal-card {
222
- /* The real .bw-cal-card supplies its own padding/border/radius;
223
- just lay the skeleton primitives inside it. */
224
- display: flex;
225
- flex-direction: column;
226
- gap: 12px;
227
- }
228
-
229
- .bw-skel-cal-header {
230
- display: flex;
231
- align-items: center;
232
- justify-content: space-between;
233
- gap: 16px;
234
- margin-bottom: 4px;
235
- }
236
-
237
- .bw-skel--cal-month {
238
- width: 130px;
239
- height: 22px;
240
- border-radius: var(--bw-radius-sm);
241
- }
242
-
243
- .bw-skel-cal-navs {
244
- display: flex;
245
- gap: 8px;
246
- }
247
-
248
- .bw-skel--cal-nav {
249
- width: 32px;
250
- height: 32px;
251
- border-radius: var(--bw-radius-pill);
252
- }
253
-
254
- .bw-skel-cal-dows {
255
- display: grid;
256
- grid-template-columns: repeat(7, 1fr);
257
- gap: 4px;
258
- }
259
-
260
- .bw-skel--cal-dow {
261
- height: 14px;
262
- border-radius: var(--bw-radius-sm);
263
- }
264
-
265
- .bw-skel-cal-grid {
266
- display: grid;
267
- grid-template-columns: repeat(7, 1fr);
268
- gap: 4px;
269
- }
270
-
271
- .bw-skel--cal-day {
272
- height: 44px;
273
- border-radius: var(--bw-radius-sm);
274
- }
275
-
276
- .bw-skel--cal-tz {
277
- width: 140px;
278
- height: 16px;
279
- margin: 12px auto 0;
280
- border-radius: var(--bw-radius-sm);
281
- }
282
-
283
- /* Slots column heading + pills. */
284
- .bw-skel--slots-heading {
285
- width: 60%;
286
- height: 18px;
287
- margin-bottom: 16px;
288
- border-radius: var(--bw-radius-sm);
289
- }
290
-
291
- /* Skeleton body uses the same grid as the real .bw-body so column
292
- widths match without reflow. */
293
- .bw-skel-body {
294
- display: grid;
295
- grid-template-columns: 28% 1fr 22%;
296
- gap: 32px;
297
- padding: 32px 48px 48px;
298
- align-items: start;
299
- }
300
-
301
- @media (max-width: 1024px) {
302
- .bw-skel-body {
303
- display: flex;
304
- flex-direction: column;
305
- padding: 16px 24px 24px;
306
- gap: 24px;
307
- }
308
-
309
- /* On mobile only show the services column at first paint —
310
- subsequent steps reveal as user advances. */
311
- .bw-skel-body .bw-col--center,
312
- .bw-skel-body .bw-col--right {
313
- display: none;
314
- }
315
- }
316
-
317
107
  .bw-loading,
318
108
  .bw-empty {
319
109
  display: flex;
@@ -394,6 +184,86 @@
394
184
  min-height: 0;
395
185
  }
396
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
+
397
267
  .bw-step-1 {
398
268
  display: flex;
399
269
  flex-direction: column;
@@ -622,6 +492,22 @@
622
492
  background: var(--bw-hover);
623
493
  }
624
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
+
625
511
  /* When the picker is in expanded mode (full list visible), let it
626
512
  stretch to fill the column so the scroll list extends down to the
627
513
  bottom of the left column instead of capping at an arbitrary
@@ -984,10 +870,187 @@
984
870
  animation: bw-slots-fade-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
985
871
  }
986
872
 
987
- @media (prefers-reduced-motion: reduce) {
988
- .bw-svc-selected,
989
- .bw-svc-scroll,
990
- .bw-provider-section {
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 {
991
1054
  animation: none;
992
1055
  }
993
1056
  }
@@ -1368,7 +1431,11 @@
1368
1431
 
1369
1432
  .bw-time-slots {
1370
1433
  display: grid;
1371
- 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);
1372
1439
  gap: 10px;
1373
1440
  margin-top: 8px;
1374
1441
  }
@@ -1416,7 +1483,11 @@
1416
1483
  }
1417
1484
 
1418
1485
  .bw-slot {
1419
- padding: 12px 0;
1486
+ display: flex;
1487
+ align-items: center;
1488
+ justify-content: center;
1489
+ padding: 12px 14px;
1490
+ min-width: 0;
1420
1491
  border: 1px solid var(--bw-border);
1421
1492
  border-radius: var(--bw-radius);
1422
1493
  background: transparent;
@@ -1537,6 +1608,208 @@
1537
1608
  color: var(--bw-text-muted);
1538
1609
  }
1539
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
+
1540
1813
  .bw-field--notes {
1541
1814
  position: relative;
1542
1815
  }
@@ -1574,6 +1847,8 @@
1574
1847
  }
1575
1848
 
1576
1849
  .bw-required {
1850
+ /* 4px gap between the label and the asterisk so they don't kiss. */
1851
+ margin-left: 4px;
1577
1852
  color: var(--bw-error-text);
1578
1853
  font-weight: 400;
1579
1854
  }
@@ -1620,6 +1895,9 @@
1620
1895
  text-align: right;
1621
1896
  text-overflow: ellipsis;
1622
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;
1623
1901
  }
1624
1902
 
1625
1903
  .bw-summary-total span:first-child,
@@ -1746,18 +2024,77 @@
1746
2024
  }
1747
2025
 
1748
2026
  .bw-confirm-btn:disabled,
1749
- .bw-footer-next:disabled {
2027
+ .bw-confirm-btn.is-disabled,
2028
+ .bw-footer-next:disabled,
2029
+ .bw-footer-next.is-disabled {
1750
2030
  opacity: 0.35;
1751
2031
  cursor: default;
1752
2032
  }
1753
2033
 
1754
2034
  .bw-done {
1755
2035
  display: flex;
2036
+ flex: 1;
1756
2037
  flex-direction: column;
1757
2038
  align-items: center;
2039
+ justify-content: center;
1758
2040
  gap: 16px;
2041
+ min-height: 80svh;
1759
2042
  padding: 80px 20px;
1760
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
+ }
1761
2098
  }
1762
2099
 
1763
2100
  .bw-done-icon {
@@ -1814,17 +2151,17 @@
1814
2151
  }
1815
2152
 
1816
2153
  @media (max-width: 1024px) {
1817
- /* Lock mobile widget to a fixed 90vh height so the frame doesn't
1818
- bounce between steps. Step 2 (calendar + slot list) is the
1819
- tallest natural step and fits at 90vh on typical phones; step 1
1820
- and step 3 fill the same height with the active col content at
1821
- top + footer pinned at bottom. The body becomes the scroll
1822
- container below — content longer than the body height (rare:
1823
- long service list on step 1, busy slot day on step 2) scrolls
1824
- internally instead of growing the widget. */
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. */
1825
2159
  .bw {
1826
- min-height: 90vh;
1827
- max-height: 90vh;
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;
1828
2165
  }
1829
2166
 
1830
2167
  /* Lock header/footer in the column so they never shrink under
@@ -1842,34 +2179,23 @@
1842
2179
  overflow-y: auto;
1843
2180
  -webkit-overflow-scrolling: touch;
1844
2181
  overscroll-behavior: contain;
1845
- /* Soft fade at both edges of the scroll area so content reads as
1846
- scrollable past the header above and the footer below instead
1847
- of cut off. Padding inside the body keeps the first/last rows
1848
- from sitting right at the fade line. */
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. */
1849
2185
  -webkit-mask-image: linear-gradient(
1850
2186
  to bottom,
1851
2187
  transparent 0,
1852
2188
  #000 24px,
1853
- #000 calc(100% - 24px),
1854
- transparent 100%
2189
+ #000 100%
1855
2190
  );
1856
2191
  mask-image: linear-gradient(
1857
2192
  to bottom,
1858
2193
  transparent 0,
1859
2194
  #000 24px,
1860
- #000 calc(100% - 24px),
1861
- transparent 100%
2195
+ #000 100%
1862
2196
  );
1863
2197
  }
1864
2198
 
1865
- .bw-skel-desktop {
1866
- display: none;
1867
- }
1868
-
1869
- .bw-skel-mobile {
1870
- display: block;
1871
- }
1872
-
1873
2199
  .bw-mobile-header {
1874
2200
  display: block;
1875
2201
  position: relative;
@@ -1912,9 +2238,10 @@
1912
2238
  flex: 1 1 0;
1913
2239
  width: auto;
1914
2240
  min-width: 20px;
1915
- max-width: 28px;
1916
- height: 3px;
1917
- 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;
1918
2245
  background: var(--bw-border);
1919
2246
  transition: background 200ms ease;
1920
2247
  }
@@ -1924,7 +2251,9 @@
1924
2251
  }
1925
2252
 
1926
2253
  .bw-title {
1927
- 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;
1928
2257
  letter-spacing: -0.5px;
1929
2258
  }
1930
2259
 
@@ -1948,7 +2277,7 @@
1948
2277
  flex-direction: column;
1949
2278
  flex: 1;
1950
2279
  min-height: 0;
1951
- padding: 0 24px;
2280
+ padding: 0 24px 24px;
1952
2281
  }
1953
2282
 
1954
2283
  .bw-col {
@@ -1956,20 +2285,24 @@
1956
2285
  }
1957
2286
 
1958
2287
  /* Mobile 4-step flow:
1959
- Step 1: services (bw-col--left, bw-step-2 only — provider hidden)
1960
- Step 2: provider (bw-col--left, bw-step-1 only — services hidden)
1961
- Step 3: calendar + slots (bw-col--center)
1962
- Step 4: form (bw-col--right) */
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). */
1963
2296
  .bw-body[data-mobile-step="1"] .bw-col--left,
1964
- .bw-body[data-mobile-step="2"] .bw-col--left,
1965
- .bw-body[data-mobile-step="3"] .bw-col--center,
1966
- .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 {
1967
2300
  display: flex;
1968
2301
  padding-top: 16px;
1969
- padding-bottom: 20px;
2302
+ padding-bottom: 24px;
1970
2303
  }
1971
2304
 
1972
- .bw-body[data-mobile-step="4"] .bw-right-inner {
2305
+ .bw-body[data-mobile-step="3"] .bw-right-inner {
1973
2306
  padding-bottom: 20px;
1974
2307
  }
1975
2308
 
@@ -1990,27 +2323,9 @@
1990
2323
  display: none;
1991
2324
  }
1992
2325
 
1993
- /* Step 2: provider only. Show bw-step-1 (which renders the
1994
- provider section conditional on selectedService guaranteed
1995
- true at step 2). Hide bw-step-2 services + the desktop
1996
- service-picker stub inside bw-step-1. */
1997
- .bw-body[data-mobile-step="2"] .bw-step-1 {
1998
- display: flex;
1999
- flex: 0 0 auto;
2000
- }
2001
-
2002
- .bw-body[data-mobile-step="2"] .bw-step-2 {
2003
- display: none;
2004
- }
2005
-
2006
- .bw-body[data-mobile-step="2"] .bw-step-1 .bw-service-picker {
2007
- display: none;
2008
- }
2009
-
2010
- .bw-body[data-mobile-step="2"] .bw-step-1 .bw-provider-section .bw-section-divider {
2011
- /* Mobile rhythm uses whitespace, not 1px lines */
2012
- display: none;
2013
- }
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. */
2014
2329
 
2015
2330
  .bw-svc-scroll {
2016
2331
  display: none;
@@ -2102,25 +2417,38 @@
2102
2417
  }
2103
2418
 
2104
2419
  .bw-footer {
2420
+ position: relative;
2105
2421
  display: flex;
2106
2422
  flex-direction: column;
2107
2423
  gap: 20px;
2108
- margin-top: auto;
2109
- padding: 16px 24px 34px;
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));
2110
2435
  border-top: 1px solid var(--bw-border-light);
2111
2436
  }
2112
2437
 
2113
- .bw-footer-summary {
2114
- display: flex;
2115
- flex-direction: column;
2116
- gap: 12px;
2117
- padding: 16px;
2118
- border: 1px solid var(--bw-border);
2119
- border-radius: var(--bw-radius);
2120
- }
2121
-
2122
- .bw-footer-summary .bw-summary-title {
2123
- 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
+ );
2124
2452
  }
2125
2453
 
2126
2454
  .bw-footer-btns {
@@ -2190,7 +2518,7 @@
2190
2518
  }
2191
2519
 
2192
2520
  .bw-body {
2193
- padding: 0 20px;
2521
+ padding: 0 20px 24px;
2194
2522
  }
2195
2523
 
2196
2524
  .bw-footer {
@@ -2217,17 +2545,38 @@
2217
2545
 
2218
2546
  .bw-slot {
2219
2547
  min-height: 40px;
2220
- padding: 12px 0;
2548
+ padding: 12px 12px;
2221
2549
  font-size: 12px;
2222
2550
  }
2223
2551
 
2224
2552
  .bw-field input,
2225
2553
  .bw-field textarea {
2226
- 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;
2227
2563
  }
2228
2564
 
2229
2565
  .bw-summary-row {
2230
- font-size: 12px;
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;
2231
2580
  }
2232
2581
  }
2233
2582
 
@@ -2287,6 +2636,14 @@
2287
2636
  }
2288
2637
 
2289
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
+
2290
2647
  /* Desktop body grid: cols sit at the top of their cell instead of
2291
2648
  stretching to the row's max content height — the visual empty
2292
2649
  space below the cal-card and right-pane goes away. */
@@ -2334,13 +2691,28 @@
2334
2691
  max-height: var(--bw-cal-h, 480px);
2335
2692
  }
2336
2693
 
2337
- .bw-pane--slots {
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. */
2338
2701
  max-height: var(--bw-cal-h, 480px);
2339
2702
  overflow: hidden;
2340
2703
  display: flex;
2341
2704
  flex-direction: column;
2342
2705
  }
2343
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
+
2344
2716
  .bw-pane--slots .bw-slots-desktop {
2345
2717
  flex: 1;
2346
2718
  min-height: 0;
@@ -2361,9 +2733,16 @@
2361
2733
  );
2362
2734
  scrollbar-width: thin;
2363
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;
2364
2741
  /* Padding so first/last pills sit inside the fade zone, matching
2365
- the services scroller pattern. */
2742
+ the services scroller pattern. Right padding gives the pills
2743
+ breathing room from the scrollbar lane. */
2366
2744
  padding-block: 12px 16px;
2745
+ padding-right: 6px;
2367
2746
  }
2368
2747
 
2369
2748
  .bw-pane--slots .bw-slots-desktop::-webkit-scrollbar {
@@ -2432,8 +2811,11 @@
2432
2811
  display: none;
2433
2812
  }
2434
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. */
2435
2817
  .bw-slots-mobile {
2436
- display: contents;
2818
+ display: flex;
2437
2819
  }
2438
2820
 
2439
2821
  /* Details view is desktop-only; mobile uses the existing step-4
@@ -2449,14 +2831,14 @@
2449
2831
  wrapper for steps 1/2 since col--left stays visible across
2450
2832
  them) so animations don't re-fire when the active col is shared
2451
2833
  between adjacent steps. */
2452
- /* 4-step flow: steps 1+2 both share .bw-col--left, so we target
2453
- the inner content (bw-step-2 for services, bw-step-1 for
2454
- provider) so each step transitions independently. Steps 3+4
2455
- have dedicated cols. */
2456
- .bw-body[data-mobile-step='1'] .bw-step-2,
2457
- .bw-body[data-mobile-step='2'] .bw-step-1,
2458
- .bw-body[data-mobile-step='3'] .bw-col--center,
2459
- .bw-body[data-mobile-step='4'] .bw-col--right {
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 {
2460
2842
  animation: bw-mobile-step-in 250ms cubic-bezier(0.16, 1, 0.3, 1);
2461
2843
  }
2462
2844
 
@@ -2553,6 +2935,24 @@
2553
2935
  flex-direction: column;
2554
2936
  gap: 16px;
2555
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;
2556
2956
  }
2557
2957
 
2558
2958
  .bw-details-eyebrow {