@wix/agent-skills 1.8.0 → 1.9.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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +2 -1
  3. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +14 -0
  4. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +11 -6
  5. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +3 -17
  6. package/skills/wix-app/references/editor-react-component/SSR.md +119 -0
  7. package/skills/wix-design-system/SKILL.md +1 -1
  8. package/skills/wix-headless/references/BUILD-astro.md +5 -7
  9. package/skills/wix-headless/references/SETUP.md +1 -1
  10. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
  11. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +58 -428
  12. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +4 -7
  13. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +158 -143
  14. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
  15. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +17 -13
  16. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +16 -78
  17. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
  18. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +122 -179
  19. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +61 -9
  20. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +10 -2
  21. package/skills/wix-headless/references/bookings/FLOW.md +185 -0
  22. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +60 -76
  23. package/skills/wix-headless/references/custom/bookings/WIRING.md +123 -65
  24. package/skills/wix-headless/references/verticals/bookings.md +6 -8
  25. package/skills/wix-manage/SKILL.md +12 -0
  26. package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
  27. package/skills/wix-manage/references/bookings/create-appointment-service.md +266 -0
  28. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
  29. package/skills/wix-manage/references/bookings/create-class-service.md +245 -0
  30. package/skills/wix-manage/references/bookings/create-course-service.md +249 -0
  31. package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
  32. package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
  33. package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
  34. package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
  35. package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
  36. package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
  37. package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
@@ -0,0 +1,245 @@
1
+ // bookingDriver.ts — the booking sequence in plain @wix SDK calls.
2
+ // Framework-agnostic: no React, no signals, no UI. Any framework's booking step
3
+ // imports book()/navigateToCheckout() and drives them; APPOINTMENT and CLASS
4
+ // share the same sequence.
5
+ //
6
+ // Mechanism = ecom Cart V2 on every path: createBooking leaves the booking
7
+ // CREATED, then the cart holds the seat — a paid service hands off to the
8
+ // Wix-hosted checkout, a free / pay-in-person service places the order directly.
9
+ //
10
+ // On @wix/astro the SDK calls run ambiently (the visitor client is provided by
11
+ // @wix/essentials, like the ecom CartView island) — no createClient/OAuthStrategy
12
+ // needed. On an own/own-build SPA, acquire a visitor client with
13
+ // createClient({ modules, auth: OAuthStrategy({ clientId }) }) and call the same
14
+ // functions through it. Keep the SDK calls, payload shapes, and the sequence;
15
+ // adapt only brand copy/styling in the UI that drives this module.
16
+
17
+ import { bookings } from "@wix/bookings";
18
+ import { createCart, calculateCart, placeOrder } from "@wix/auto_sdk_ecom_cart-v-2";
19
+ import { redirects } from "@wix/redirects";
20
+
21
+ // ── Constants (C:src/services/constants.ts) ────────────────────────────────
22
+ export const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
23
+ export const STAFF_MEMBER_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155";
24
+
25
+ // ── Types ───────────────────────────────────────────────────────────────────
26
+ // FormValues = the submitted booking-form values, keyed by each field's `target`
27
+ // (NOT hardcoded field names). The default booking form's targets are snake_case
28
+ // `first_name`/`last_name`/`email`/`phone`; read them from the form schema and key
29
+ // the values by `target` — never hardcode the keys here.
30
+ export type FormValues = Record<string, unknown>;
31
+
32
+ export interface SelectedSlot {
33
+ serviceType: "APPOINTMENT" | "CLASS";
34
+ serviceId: string;
35
+ localStartDate: string; // local "YYYY-MM-DDTHH:mm:ss" (NOT UTC) — maps to slot.startDate
36
+ localEndDate: string; // " " → slot.endDate
37
+ timezone: string;
38
+ // APPOINTMENT:
39
+ scheduleId?: string;
40
+ locationId?: string; // from slot.location._id (the id field is `_id`, not `.id`)
41
+ locationType?: string; // "BUSINESS" | "CUSTOMER" | "CUSTOM"
42
+ // CLASS:
43
+ eventId?: string;
44
+ // Resource chosen on the slot (flow 5). When absent the driver emits the
45
+ // ANY_RESOURCE fallback so Wix auto-assigns a bookable staff member.
46
+ resource?: { _id: string; name?: string };
47
+ }
48
+
49
+ export interface BookParams {
50
+ service: any; // the @wix/bookings Service (read payment.options + bookingPolicy)
51
+ slot: SelectedSlot;
52
+ formSubmission: FormValues; // values from the @wix/forms Form, keyed by target
53
+ timezone: string;
54
+ totalParticipants?: number;
55
+ /** consumer override; when omitted, derived from service.payment.options */
56
+ selectedPaymentOption?: "ONLINE" | "OFFLINE";
57
+ }
58
+
59
+ export const BookResultType = {
60
+ CheckoutRequired: "checkout_required",
61
+ CheckoutSkipped: "checkout_skipped",
62
+ } as const;
63
+ export type BookResult =
64
+ | { type: typeof BookResultType.CheckoutRequired; cartId: string }
65
+ | { type: typeof BookResultType.CheckoutSkipped; orderId: string };
66
+
67
+ // ── Payment derivation ───────────────────────────────────────────────────────
68
+ function deriveSelectedPaymentOption(service: any): "ONLINE" | "OFFLINE" {
69
+ const options = service?.payment?.options;
70
+ if (options?.online && !options?.inPerson) return "ONLINE";
71
+ if (!options?.online && options?.inPerson) return "OFFLINE";
72
+ return "ONLINE"; // both / neither → ONLINE (consumer override wins, see below)
73
+ }
74
+
75
+ // LocationType maps from the availability slot's type → the booking endpoint enum.
76
+ function mapLocationType(slotType?: string): string {
77
+ switch (slotType) {
78
+ case "BUSINESS":
79
+ return "OWNER_BUSINESS";
80
+ case "CUSTOMER":
81
+ return "CUSTOM";
82
+ case "CUSTOM":
83
+ return "OWNER_CUSTOM";
84
+ default:
85
+ return "OWNER_BUSINESS";
86
+ }
87
+ }
88
+
89
+ // ── buildBookingRequest ───────────────────────────────────────────────────────
90
+ function buildBookingRequest(params: BookParams) {
91
+ const { service, slot, formSubmission, timezone } = params;
92
+ const resource = slot.resource; // prefer the slot's chosen resource when present
93
+
94
+ return {
95
+ booking: {
96
+ // Consumer override wins over the service-config heuristic.
97
+ selectedPaymentOption:
98
+ params.selectedPaymentOption ?? deriveSelectedPaymentOption(service),
99
+ totalParticipants: params.totalParticipants || 1,
100
+ bookedEntity: {
101
+ slot: {
102
+ serviceId: slot.serviceId,
103
+ // APPOINTMENT carries scheduleId; CLASS carries eventId (Wix derives the rest).
104
+ scheduleId: slot.scheduleId ?? undefined,
105
+ startDate: slot.localStartDate,
106
+ endDate: slot.localEndDate,
107
+ timezone,
108
+ eventId: slot.eventId ?? undefined,
109
+ // Resource: use the chosen one, else the ANY_RESOURCE fallback so Wix
110
+ // auto-assigns a bookable staff member. Appointment availability slots
111
+ // return availableResources:[] yet book fine via ANY_RESOURCE.
112
+ ...(resource
113
+ ? { resource: { _id: resource._id, name: resource.name } }
114
+ : {
115
+ resourceSelections: [
116
+ {
117
+ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID,
118
+ selectionMethod: "ANY_RESOURCE",
119
+ },
120
+ ],
121
+ }),
122
+ location:
123
+ slot.locationId || slot.locationType
124
+ ? {
125
+ ...(slot.locationId ? { _id: slot.locationId } : {}),
126
+ locationType: mapLocationType(slot.locationType),
127
+ }
128
+ : { locationType: "OWNER_BUSINESS" },
129
+ },
130
+ },
131
+ },
132
+ participantNotification: {
133
+ metadata: { channels: "EMAIL,SMS" },
134
+ notifyParticipants: true,
135
+ },
136
+ sendSmsReminder: true,
137
+ formSubmission,
138
+ };
139
+ }
140
+
141
+ // ── buildCartRequest ──────────────────────────────────────────────────────────
142
+ function buildCartRequest(args: {
143
+ bookingIds: string[];
144
+ contactDetails?: any;
145
+ businessLocationId?: string | null;
146
+ }) {
147
+ const cart: any = { source: { channelType: "WEB" } };
148
+ if (args.businessLocationId)
149
+ cart.businessInfo = { locationId: args.businessLocationId };
150
+ if (args.contactDetails) {
151
+ cart.customerInfo = args.contactDetails.email
152
+ ? { email: args.contactDetails.email }
153
+ : {};
154
+ if (args.contactDetails.fullAddress?.country) {
155
+ cart.deliveryInfo = { address: { ...args.contactDetails.fullAddress } };
156
+ cart.paymentInfo = {
157
+ billingAddress: { ...args.contactDetails.fullAddress },
158
+ };
159
+ }
160
+ }
161
+ return {
162
+ catalogItems: args.bookingIds.map((id) => ({
163
+ quantity: 1,
164
+ catalogReference: { catalogItemId: id, appId: BOOKING_APP_ID },
165
+ })),
166
+ cart,
167
+ };
168
+ }
169
+
170
+ // ── isCheckoutRequired ────────────────────────────────────────────────────────
171
+ function isCheckoutRequired(cart: any, summary: any, service: any): boolean {
172
+ if (service?.bookingPolicy?.cancellationFeePolicy?.enabled) return true;
173
+ const total = Number(summary?.priceSummary?.total?.amount ?? 0);
174
+ if (total === 0) return false;
175
+ if (cart?.lineItems?.[0]?.paymentConfig?.paymentOption === "FULL_PAYMENT_OFFLINE")
176
+ return false;
177
+ return true;
178
+ }
179
+
180
+ // ── canBook ───────────────────────────────────────────────────────────────────
181
+ function canBook(params: BookParams): boolean {
182
+ const slotOk =
183
+ !!params.slot &&
184
+ !!params.slot.localStartDate &&
185
+ (params.slot.serviceType === "CLASS"
186
+ ? !!params.slot.eventId
187
+ : !!params.slot.scheduleId);
188
+ return slotOk && params.formSubmission != null;
189
+ }
190
+
191
+ // ── book — the full sequence ──────────────────────────────────────────────────
192
+ export async function book(params: BookParams): Promise<BookResult> {
193
+ if (!canBook(params))
194
+ throw new Error("Cannot book: missing slot or form submission");
195
+
196
+ // 1. createBooking → CREATED (the cart holds the seat)
197
+ const req = buildBookingRequest(params);
198
+ const created = await bookings.createBooking(req.booking as any, {
199
+ participantNotification: req.participantNotification,
200
+ sendSmsReminder: req.sendSmsReminder,
201
+ formSubmission: req.formSubmission as any,
202
+ });
203
+ const bookingId = created?.booking?._id;
204
+ if (!bookingId) throw new Error("Failed to create booking");
205
+ const contactDetails = created?.booking?.contactDetails;
206
+
207
+ // 2. createCart — one catalog item per booking id (channel WEB, bookings appId)
208
+ const businessLocationId = params.slot.locationId ?? undefined;
209
+ const cart = await createCart(
210
+ buildCartRequest({ bookingIds: [bookingId], contactDetails, businessLocationId }),
211
+ );
212
+ const cartId = cart?._id;
213
+ if (!cartId) throw new Error("Failed to create cart");
214
+
215
+ // 3. calculateCart → totals (not stored on the Cart V2 entity)
216
+ const { cart: calculatedCart, summary } = await calculateCart(cartId);
217
+ if (!calculatedCart || !summary) throw new Error("Failed to calculate cart");
218
+
219
+ // 4. checkout required? → redirect (paid) ; else placeOrder (free/offline)
220
+ if (isCheckoutRequired(calculatedCart, summary, params.service)) {
221
+ return { type: BookResultType.CheckoutRequired, cartId };
222
+ }
223
+ const order = await placeOrder(cartId);
224
+ const orderId = order?.orderId;
225
+ if (!orderId) throw new Error("Failed to place order");
226
+ return { type: BookResultType.CheckoutSkipped, orderId };
227
+ }
228
+
229
+ // ── navigateToCheckout ────────────────────────────────────────────────────────
230
+ // Paid services: hand the cart to the Wix-hosted ecom checkout. Same shape on
231
+ // every path — ecomCheckout.checkoutId = the cartId.
232
+ export async function navigateToCheckout(
233
+ cartId: string,
234
+ postFlowUrl: string,
235
+ ): Promise<void> {
236
+ const { redirectSession } = await redirects.createRedirectSession({
237
+ ecomCheckout: { checkoutId: cartId },
238
+ callbacks: { postFlowUrl },
239
+ });
240
+ if (redirectSession?.fullUrl) {
241
+ window.location.href = redirectSession.fullUrl;
242
+ } else {
243
+ throw new Error("Failed to create redirect session");
244
+ }
245
+ }
@@ -1,39 +1,23 @@
1
1
  /* src/styles/components-bookings.css
2
2
  *
3
- * Bookings-pack component CSS. Pre-copied by the orchestrator at Step 4.5
4
- * before Phase 3 Components dispatches. Do NOT write or modify this file
5
- * from a Phase 3 or Phase 4 subagent — it is already on disk.
3
+ * Bookings-pack component CSS. Pre-copied by the orchestrator before the build
4
+ * wave (it is brand-agnostic var(--token) CSS). The designer's global.css does
5
+ * NOT publish these classes — they live here. See references/shared/STYLING.md
6
+ * § "Component-specific CSS is owned by the component, not the designer".
6
7
  *
7
- * This file is the SINGLE source of truth for bookings-component classes.
8
- * The designer's global.css does NOT publish .service-card, .service-grid,
9
- * .availability-calendar, .time-slot, or .booking-form — those all live
10
- * here. See references/shared/STYLING.md § "Component-specific CSS is
11
- * owned by the component, not the designer".
8
+ * Class groups:
9
+ * - .service-card / .service-grid / .service-card-* → ServiceCard.astro, services/index.astro
10
+ * - .availability-* / .time-slot* → AvailabilityCalendar.tsx (week calendar + slots)
11
+ * - .booking-* → BookingForm.tsx (schema-driven form)
12
12
  *
13
- * Class groups in this file:
14
- * - .service-card / .service-grid / .service-card-* → ServiceCard.astro,
15
- * services/index.astro, index.astro home teaser
16
- * - .availability-calendar / .time-slot / .time-slot-* / .availability-*
17
- * → AvailabilityCalendar.tsx date picker + slot grid
18
- * - .booking-form / .booking-field / .booking-* → BookingForm.tsx
19
- * details form + submit button
13
+ * Primary actions (a selected slot, the confirm button) use var(--color-accent)
14
+ * so the flow matches the site's primary-action styling.
20
15
  *
21
- * Rules use direct CSS (`var(--token)`) only — no `@apply`. Tailwind v4
22
- * `@apply` in a non-entrypoint file only resolves utilities whose theme keys
23
- * the designer's `@theme` block declares. Direct `var(--color-ink)` /
24
- * `var(--spacing-sm)` calls work against any designer-published token set as
25
- * long as the core scales exist (they always do). Use fallback chains
26
- * (`var(--color-paper-warm, var(--color-paper, var(--color-cream)))`) for
27
- * sometimes-published tokens.
16
+ * Direct var(--token) only (no @apply). Fallback chains for sometimes-published
17
+ * tokens, e.g. var(--color-paper-warm, var(--color-paper, var(--color-cream))).
28
18
  */
29
19
 
30
- /* =========================================================================
31
- Service grid + card — owned here, NOT in global.css.
32
- .service-card uses overflow: hidden + border-radius, so inner padding
33
- is the responsibility of THIS file (whoever sets the radius owns the
34
- inset). The image is clipped by the card's overflow; content needs
35
- padding-inline so text doesn't sit flush against the rounded edge.
36
- ========================================================================= */
20
+ /* ── Service grid + card ────────────────────────────────────────────────────── */
37
21
  .service-grid {
38
22
  display: grid;
39
23
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
@@ -75,9 +59,6 @@ body[data-navigating="true"] .service-grid {
75
59
  object-fit: cover;
76
60
  transition: transform 600ms ease;
77
61
  }
78
- .service-card-image--placeholder {
79
- /* fallback when no image is provided */
80
- }
81
62
 
82
63
  .service-card-meta {
83
64
  display: flex;
@@ -86,7 +67,6 @@ body[data-navigating="true"] .service-grid {
86
67
  padding: var(--spacing-md) var(--spacing-md) var(--spacing-lg);
87
68
  flex: 1;
88
69
  }
89
-
90
70
  .service-card-name {
91
71
  margin: 0;
92
72
  font-family: var(--font-display);
@@ -94,7 +74,6 @@ body[data-navigating="true"] .service-grid {
94
74
  line-height: 1.25;
95
75
  color: var(--color-ink);
96
76
  }
97
-
98
77
  .service-card-tagline {
99
78
  margin: 0;
100
79
  font-family: var(--font-body);
@@ -102,7 +81,6 @@ body[data-navigating="true"] .service-grid {
102
81
  color: var(--color-ink-soft, var(--color-mute));
103
82
  line-height: 1.45;
104
83
  }
105
-
106
84
  .service-card-price {
107
85
  display: flex;
108
86
  align-items: center;
@@ -119,17 +97,14 @@ body[data-navigating="true"] .service-grid {
119
97
  color: var(--color-rule);
120
98
  }
121
99
 
122
- /* =========================================================================
123
- Availability calendar — date navigator + slot grid.
124
- Owned here, NOT in global.css.
125
- ========================================================================= */
100
+ /* ── Availability — week calendar ───────────────────────────────────────────── */
126
101
  .availability-calendar {
127
102
  display: flex;
128
103
  flex-direction: column;
129
104
  gap: var(--spacing-lg);
130
105
  }
131
106
 
132
- .availability-date-nav {
107
+ .availability-week-nav {
133
108
  display: flex;
134
109
  align-items: center;
135
110
  justify-content: space-between;
@@ -137,8 +112,7 @@ body[data-navigating="true"] .service-grid {
137
112
  padding: var(--spacing-sm) 0;
138
113
  border-bottom: 1px solid var(--color-rule);
139
114
  }
140
-
141
- .availability-date-label {
115
+ .availability-week-label {
142
116
  font-family: var(--font-display);
143
117
  font-size: 1rem;
144
118
  color: var(--color-ink);
@@ -166,12 +140,57 @@ body[data-navigating="true"] .service-grid {
166
140
  cursor: not-allowed;
167
141
  }
168
142
 
169
- .availability-slots {
143
+ /* The 7-day strip. */
144
+ .availability-day-strip {
170
145
  display: grid;
171
- grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
146
+ grid-template-columns: repeat(7, 1fr);
172
147
  gap: var(--spacing-sm);
173
148
  }
149
+ .availability-day {
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ gap: 2px;
154
+ padding: var(--spacing-sm) 0;
155
+ border: 1px solid var(--color-rule);
156
+ border-radius: var(--radius-sm);
157
+ background: var(--color-paper, var(--color-cream));
158
+ color: var(--color-ink);
159
+ font-family: var(--font-body);
160
+ cursor: pointer;
161
+ transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
162
+ }
163
+ .availability-day-name {
164
+ font-size: 0.7rem;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.04em;
167
+ color: var(--color-mute);
168
+ }
169
+ .availability-day-num {
170
+ font-size: 1rem;
171
+ font-weight: 600;
172
+ }
173
+ .availability-day--has-slots:hover {
174
+ border-color: var(--color-accent);
175
+ }
176
+ .availability-day--empty {
177
+ opacity: 0.4;
178
+ cursor: not-allowed;
179
+ }
180
+ .availability-day--selected {
181
+ background: var(--color-accent);
182
+ border-color: var(--color-accent);
183
+ color: var(--color-paper);
184
+ }
185
+ .availability-day--selected .availability-day-name {
186
+ color: var(--color-paper);
187
+ }
174
188
 
189
+ .availability-slots {
190
+ display: grid;
191
+ grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
192
+ gap: var(--spacing-sm);
193
+ }
175
194
  .availability-loading,
176
195
  .availability-empty,
177
196
  .availability-error {
@@ -181,14 +200,17 @@ body[data-navigating="true"] .service-grid {
181
200
  padding: var(--spacing-lg) 0;
182
201
  text-align: center;
183
202
  }
203
+ .availability-empty {
204
+ display: flex;
205
+ flex-direction: column;
206
+ align-items: center;
207
+ gap: var(--spacing-md);
208
+ }
184
209
  .availability-error {
185
210
  color: var(--color-error, var(--color-accent));
186
211
  }
187
212
 
188
- /* =========================================================================
189
- Time slot buttons — individual slot items in the grid.
190
- Three states: available (default), selected, unavailable.
191
- ========================================================================= */
213
+ /* ── Time-slot buttons ──────────────────────────────────────────────────────── */
192
214
  .time-slot {
193
215
  padding: var(--spacing-sm) var(--spacing-md);
194
216
  border: 1px solid var(--color-rule);
@@ -201,29 +223,25 @@ body[data-navigating="true"] .service-grid {
201
223
  cursor: pointer;
202
224
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
203
225
  }
226
+ .time-slot-time {
227
+ font-weight: 500;
228
+ }
204
229
  .time-slot--available {
205
230
  border-color: var(--color-ink);
206
231
  }
207
232
  .time-slot--available:hover {
208
- background: var(--color-ink);
233
+ background: var(--color-accent);
234
+ border-color: var(--color-accent);
209
235
  color: var(--color-paper);
210
236
  }
211
237
  .time-slot--selected {
212
- background: var(--color-ink);
238
+ background: var(--color-accent);
239
+ border-color: var(--color-accent);
213
240
  color: var(--color-paper);
214
- border-color: var(--color-ink);
215
241
  font-weight: 600;
216
242
  }
217
- .time-slot--unavailable {
218
- opacity: 0.3;
219
- cursor: not-allowed;
220
- border-color: var(--color-rule);
221
- }
222
243
 
223
- /* =========================================================================
224
- Booking form — rendered after a slot is selected.
225
- Owned here, NOT in global.css.
226
- ========================================================================= */
244
+ /* ── Booking form (schema-driven) ───────────────────────────────────────────── */
227
245
  .booking-form {
228
246
  display: flex;
229
247
  flex-direction: column;
@@ -234,22 +252,37 @@ body[data-navigating="true"] .service-grid {
234
252
  background: var(--color-paper-warm, var(--color-paper, var(--color-cream)));
235
253
  }
236
254
 
237
- .booking-form-header {
255
+ .booking-form-summary {
238
256
  display: flex;
239
257
  flex-direction: column;
240
258
  gap: var(--spacing-xs);
259
+ padding-bottom: var(--spacing-md);
260
+ border-bottom: 1px solid var(--color-rule);
241
261
  }
242
- .booking-form-title {
262
+ .booking-form-summary strong {
243
263
  font-family: var(--font-display);
244
- font-size: 1.25rem;
264
+ font-size: 1.125rem;
245
265
  color: var(--color-ink);
246
- margin: 0;
247
266
  }
248
- .booking-form-subtitle {
267
+ .booking-form-summary span {
249
268
  font-family: var(--font-body);
250
- font-size: 0.875rem;
269
+ font-size: 0.9375rem;
251
270
  color: var(--color-ink-soft, var(--color-mute));
252
- margin: 0;
271
+ }
272
+ .booking-form-change {
273
+ align-self: flex-start;
274
+ margin-top: var(--spacing-xs);
275
+ background: none;
276
+ border: none;
277
+ padding: 0;
278
+ font-family: var(--font-body);
279
+ font-size: 0.8125rem;
280
+ color: var(--color-mute);
281
+ text-decoration: underline;
282
+ cursor: pointer;
283
+ }
284
+ .booking-form-change:hover {
285
+ color: var(--color-ink);
253
286
  }
254
287
 
255
288
  .booking-field {
@@ -257,17 +290,6 @@ body[data-navigating="true"] .service-grid {
257
290
  flex-direction: column;
258
291
  gap: var(--spacing-xs);
259
292
  }
260
- .booking-field-row {
261
- display: grid;
262
- grid-template-columns: 1fr 1fr;
263
- gap: var(--spacing-md);
264
- }
265
- @media (max-width: 480px) {
266
- .booking-field-row {
267
- grid-template-columns: 1fr;
268
- }
269
- }
270
-
271
293
  .booking-label {
272
294
  font-family: var(--font-display);
273
295
  font-size: 0.75rem;
@@ -275,13 +297,11 @@ body[data-navigating="true"] .service-grid {
275
297
  letter-spacing: 0.05em;
276
298
  color: var(--color-ink-soft, var(--color-mute));
277
299
  }
278
- .booking-label--required::after {
279
- content: " *";
300
+ .booking-required {
280
301
  color: var(--color-accent);
281
302
  }
282
303
 
283
- .booking-input,
284
- .booking-textarea {
304
+ .booking-input {
285
305
  width: 100%;
286
306
  padding: var(--spacing-sm) var(--spacing-md);
287
307
  font-family: var(--font-body);
@@ -293,121 +313,44 @@ body[data-navigating="true"] .service-grid {
293
313
  transition: border-color 150ms ease;
294
314
  box-sizing: border-box;
295
315
  }
296
- .booking-input:focus,
297
- .booking-textarea:focus {
316
+ .booking-input:focus {
298
317
  outline: none;
299
- border-color: var(--color-ink);
318
+ border-color: var(--color-accent);
300
319
  }
301
- .booking-textarea {
320
+ textarea.booking-input {
302
321
  resize: vertical;
303
322
  min-height: 6rem;
304
323
  }
305
324
 
306
- .booking-submit {
325
+ .booking-error {
326
+ font-family: var(--font-body);
327
+ font-size: 0.875rem;
328
+ color: var(--color-error, var(--color-accent));
329
+ padding: var(--spacing-sm) var(--spacing-md);
330
+ border: 1px solid var(--color-error, var(--color-accent));
331
+ border-radius: var(--radius-sm);
332
+ }
333
+
334
+ .booking-cta {
307
335
  display: inline-flex;
308
336
  align-items: center;
309
337
  justify-content: center;
338
+ width: 100%;
310
339
  padding: var(--spacing-md) var(--spacing-xl);
311
340
  font-family: var(--font-display);
312
341
  font-size: 0.9375rem;
313
342
  font-weight: 500;
314
- background: var(--color-ink);
343
+ background: var(--color-accent);
315
344
  color: var(--color-paper);
316
- border: 1px solid var(--color-ink);
345
+ border: 1px solid var(--color-accent);
317
346
  border-radius: var(--radius-sm);
318
347
  cursor: pointer;
319
- width: 100%;
320
- margin-top: var(--spacing-sm);
321
- transition: background-color 150ms ease, box-shadow 150ms ease;
348
+ transition: opacity 150ms ease;
322
349
  }
323
- .booking-submit:hover:not(:disabled) {
324
- background: var(--color-ink-soft, var(--color-ink));
350
+ .booking-cta:hover:not(:disabled) {
351
+ opacity: 0.9;
325
352
  }
326
- .booking-submit:disabled {
353
+ .booking-cta:disabled {
327
354
  opacity: 0.5;
328
355
  cursor: not-allowed;
329
356
  }
330
-
331
- .booking-cancel {
332
- display: block;
333
- text-align: center;
334
- font-family: var(--font-body);
335
- font-size: 0.875rem;
336
- color: var(--color-mute);
337
- cursor: pointer;
338
- background: none;
339
- border: none;
340
- text-decoration: underline;
341
- padding: 0;
342
- margin-top: var(--spacing-xs);
343
- }
344
- .booking-cancel:hover {
345
- color: var(--color-ink);
346
- }
347
-
348
- .booking-error {
349
- font-family: var(--font-body);
350
- font-size: 0.875rem;
351
- color: var(--color-error, var(--color-accent));
352
- padding: var(--spacing-sm) var(--spacing-md);
353
- border: 1px solid var(--color-error, var(--color-accent));
354
- border-radius: var(--radius-sm);
355
- background: rgba(255, 0, 0, 0.04);
356
- }
357
-
358
- /* ── Slot capacity (CLASS) + full→waitlist ─────────────────────────────────── */
359
- .time-slot {
360
- display: flex;
361
- flex-direction: column;
362
- align-items: center;
363
- gap: 2px;
364
- line-height: 1.2;
365
- }
366
- .time-slot-time {
367
- font-weight: 500;
368
- }
369
- .time-slot-capacity {
370
- font-size: 0.7rem;
371
- letter-spacing: 0.04em;
372
- text-transform: uppercase;
373
- color: var(--color-mute, var(--color-secondary));
374
- }
375
- .time-slot--full {
376
- opacity: 0.85;
377
- border-style: dashed;
378
- }
379
- .time-slot--full .time-slot-capacity {
380
- color: var(--color-accent);
381
- }
382
- .booking-form-note {
383
- font-family: var(--font-body);
384
- font-size: 0.875rem;
385
- color: var(--color-mute, var(--color-secondary));
386
- margin-top: var(--spacing-xs);
387
- }
388
-
389
- /* ── Manage booking page ─────────────────────────────────────────────────────── */
390
- .manage-booking {
391
- display: flex;
392
- flex-direction: column;
393
- gap: var(--spacing-lg);
394
- align-items: flex-start;
395
- }
396
- .manage-booking-summary,
397
- .manage-booking-status {
398
- font-family: var(--font-body);
399
- font-size: 1.05rem;
400
- color: var(--color-ink, var(--color-primary));
401
- }
402
- .manage-booking-note {
403
- font-family: var(--font-body);
404
- font-size: 0.875rem;
405
- color: var(--color-mute, var(--color-secondary));
406
- }
407
- .manage-booking-link {
408
- color: var(--color-ink, var(--color-primary));
409
- text-decoration: underline;
410
- }
411
- .manage-booking-link:hover {
412
- color: var(--color-accent);
413
- }