@patientos/website-kit 0.1.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.
@@ -0,0 +1,3580 @@
1
+ /* ============================================================================
2
+ * @patientos/website-kit — compiled stylesheet
3
+ *
4
+ * Self-contained. NO Tailwind, NO shadcn, NO dashboard app.css. Hand-authored
5
+ * so the generated clinic websites are fully styleable from the kit alone (D2).
6
+ * Every rule reads `--sk-*` tokens with inline fallbacks, so the kit looks
7
+ * reasonable even before <ThemeProvider>/<WebsiteShell> injects a theme.
8
+ * ==========================================================================*/
9
+
10
+ /* ── token slots + fallbacks ──────────────────────────────────────────────*/
11
+ :where([data-sk-theme]),
12
+ :root {
13
+ --sk-brand: #1f6feb;
14
+ --sk-brand-foreground: #ffffff;
15
+ --sk-neutral: #f4f6f8;
16
+ --sk-neutral-foreground: #1b2733;
17
+ --sk-accent: #0fb39a;
18
+ --sk-background: #ffffff;
19
+ --sk-foreground: #1b2733;
20
+ --sk-muted: #5b6b7a;
21
+ /* Destructive intent (cancel an appointment, a failed write). NOT clinic-supplied:
22
+ * `patientSurfaceTheme` allow-lists brand/accent/radius/font only, and "the colour
23
+ * that means danger" is not a branding decision on a patient surface. */
24
+ --sk-danger: #b3261e;
25
+
26
+ --sk-font: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
27
+ sans-serif;
28
+ --sk-font-heading: var(--sk-font);
29
+ --sk-font-size-base: 1rem;
30
+ --sk-font-size-sm: 0.875rem;
31
+ --sk-font-size-lg: 1.25rem;
32
+ --sk-font-size-h2: 2rem;
33
+ --sk-font-size-h1: 3rem;
34
+
35
+ --sk-space: 0.5rem;
36
+ --sk-radius: 0.625rem;
37
+ }
38
+
39
+ /* ── base ──────────────────────────────────────────────────────────────────*/
40
+ [data-sk-theme] {
41
+ color: var(--sk-foreground);
42
+ background: var(--sk-background);
43
+ font-family: var(--sk-font);
44
+ font-size: var(--sk-font-size-base);
45
+ line-height: 1.55;
46
+ -webkit-font-smoothing: antialiased;
47
+ }
48
+
49
+ /* `.sk-flow` is listed alongside the theme root because an ISLAND does not require one:
50
+ the funnel mounts into a clinic-authored page, and the Pocket Doc funnel page has no
51
+ `[data-sk-theme]` wrapper at all. It looked fine only because that site imports
52
+ Tailwind, whose preflight sets this globally — i.e. the island's layout depended on a
53
+ reset the host happened to ship. A clinic page without one had every padded input
54
+ overflow its grid column. An island pays for its own reset. */
55
+ [data-sk-theme] *,
56
+ [data-sk-theme] *::before,
57
+ [data-sk-theme] *::after,
58
+ .sk-flow,
59
+ .sk-flow *,
60
+ .sk-flow *::before,
61
+ .sk-flow *::after {
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ [data-sk-theme] h1,
66
+ [data-sk-theme] h2,
67
+ [data-sk-theme] h3,
68
+ .sk-flow h1,
69
+ .sk-flow h2,
70
+ .sk-flow h3 {
71
+ font-family: var(--sk-font-heading);
72
+ line-height: 1.15;
73
+ margin: 0;
74
+ }
75
+
76
+ /* The rest of the island's element reset, for the same reason as `box-sizing` above:
77
+ `[data-sk-theme]` never reaches an island, so the funnel inherited the UA's
78
+ `line-height: normal` (≈1.21 against the kit's own 1.55) and every `<p>` and `<h2>`
79
+ kept its UA `margin-block`. That unmanaged margin — not the declared gaps — is what
80
+ set the spacing between the heading, its subheading and the consent paragraphs, so
81
+ the vertical rhythm was the browser's, not ours. */
82
+ .sk-flow {
83
+ font-family: var(--sk-font);
84
+ font-size: var(--sk-font-size-base);
85
+ line-height: 1.55;
86
+ }
87
+ /* ANCHOR the funnel's column. The step heading was landing anywhere from 203px to 389px
88
+ down the page depending on how tall the step happened to be — walking identity→intake
89
+ it dropped 186px and then climbed back, which reads as the page breaking rather than
90
+ advancing. `.sk-flow` never centred anything itself; the clinic's page was centring a
91
+ short island. The kit cannot edit clinic markup (islands are themed, not
92
+ restructurable), so the lever it does have is to stop being a short box.
93
+ SCOPED to the funnel's own mount point on purpose: `.sk-flow` is shared with
94
+ `<BookingBlock/>`, which is embedded mid-page and must NOT become viewport-tall. */
95
+ .sk-certificate-funnel .sk-flow {
96
+ /* All-AUTO rows, overriding the shell's `auto 1fr auto`. That `1fr` body track and the
97
+ `min-block-size` below were in direct conflict: an `fr` track eats the column's free
98
+ space during TRACK SIZING, before `align-content` ever runs, so `start` was a no-op
99
+ and the whole slack of a viewport-tall column opened up between the last field and
100
+ the CTA — ~280px on the medicare step, ~420px on the confirmation, where the primary
101
+ link was exiled to the bottom of an otherwise blank screen. With auto rows the tracks
102
+ pack to the top and the min-height goes back to doing only its stated job above.
103
+ THREE explicit tracks, not `none`: the railed rules below place the rail by line
104
+ number and need an explicit grid to resolve against. */
105
+ grid-template-rows: auto auto auto;
106
+ align-content: start;
107
+ min-block-size: calc(100dvh - 8rem);
108
+ /* 2×, not the 6× this shipped with. MEASURED: on a 390×844 phone the step heading's
109
+ first ink sat at 179px, of which the clinic's own 000 bar was 62 — the other 117 was
110
+ this inset (48), the back/progress row (36), the shell's row gap (16) and the eyebrow
111
+ line (17), i.e. a sixth of the fold spent before the screen says what it is asking.
112
+ The 000 bar directly above already carries its own 14px of air and a hairline, so 16px
113
+ here still reads as a gap rather than a collision, and every one of the fourteen
114
+ screens starts 32px higher. The BOTTOM stays 3× — that edge is against the page, not
115
+ against a bar. */
116
+ padding-block: calc(var(--sk-space) * 2) calc(var(--sk-space) * 3);
117
+
118
+ /* The HORIZONTAL twin of the `min-block-size` above, and the same root cause. This
119
+ column lives inside a host box that is SHRINK-TO-FIT — PocketDoc's FunnelShell
120
+ centres the island with `md:items-center` on a column flex box, and any wrapper in
121
+ between (a React root, a `.container` div) is shrink-to-fit for the same reason.
122
+ Against such a parent `max-width` only CAPS a width the CONTENT picks, so every step
123
+ sized itself to its own content. Measured at 1440, walking forward: identity 848 (reading
124
+ column 496) → confirm-identity 822 (470) → intake 777 (425) → slot 764 (412), then back
125
+ to 848 on medicare — the card's left edge sliding 296→309→332→338 and the column
126
+ narrowing 17% mid-intake, which reads as the page breaking rather than advancing.
127
+ A DEFINITE inline-size is the only lever that survives shrink-to-fit: it raises the
128
+ max-content contribution, so the host box resolves to this width and the step's own
129
+ content can no longer set it. `max-inline-size: 100%` is what keeps that safe — it
130
+ out-specifies the `max-width` caps below (logical and physical cascade as one
131
+ property), so a host column NARROWER than the cap still wins and the island shrinks
132
+ to fit it rather than overflowing into a horizontal scrollbar.
133
+ NOT on `.sk-flow` itself: `<BookingBlock/>` is embedded mid-page and must stay
134
+ shrink-to-fit. NOT on `.sk-certificate-funnel`: widening the mount point also widens
135
+ the full-bleed escalate screen, whose pixels are fixed. */
136
+ inline-size: 34rem;
137
+ max-inline-size: 100%;
138
+ }
139
+ .sk-flow p,
140
+ .sk-flow ul,
141
+ .sk-flow ol,
142
+ .sk-flow dl,
143
+ .sk-flow dd,
144
+ .sk-flow figure {
145
+ /* TRAP, read before adding a rule below: this is (0,1,1) — HIGHER than a single
146
+ component class, and `[data-sk-theme] p` at the same strength backs it up. Any block
147
+ margin authored on a <p>/<ul>/<dl> in this flow must therefore be written as
148
+ `.sk-flow .sk-whatever { … }` or it is silently dead. That is not hypothetical: a
149
+ whole round of section spacing (`.sk-identity__section`, `.sk-cf__body`, …) was
150
+ zeroed by this rule, which is how "Residential address" ended up captioning the
151
+ wrong field and the exclusion list ended up welded to the prose around it. */
152
+ margin: 0;
153
+ }
154
+ .sk-flow ul,
155
+ .sk-flow ol {
156
+ padding-inline-start: 0;
157
+ list-style: none;
158
+ }
159
+ /* `[hidden]` is only a UA `display: none`, so ANY author `display` beats it. That is not
160
+ hypothetical here: `.sk-cf__consent-full` sets `display: grid`, so the "Read the full
161
+ consent text" disclosure never hid anything — the full legal text was always on screen
162
+ while its own button offered to reveal it. One guard, so no future disclosure in the
163
+ kit can be defeated the same way. */
164
+ [hidden] {
165
+ display: none !important;
166
+ }
167
+
168
+ [data-sk-theme] p {
169
+ margin: 0;
170
+ }
171
+
172
+ [data-sk-theme] img {
173
+ max-width: 100%;
174
+ display: block;
175
+ }
176
+
177
+ [data-sk-theme] a {
178
+ color: var(--sk-brand);
179
+ }
180
+
181
+ /* ── layout primitives ─────────────────────────────────────────────────────*/
182
+ .sk-container {
183
+ width: 100%;
184
+ max-width: 72rem;
185
+ margin-inline: auto;
186
+ padding-inline: calc(var(--sk-space) * 3);
187
+ }
188
+
189
+ .sk-row {
190
+ display: flex;
191
+ flex-wrap: wrap;
192
+ gap: calc(var(--sk-space) * 3);
193
+ }
194
+
195
+ .sk-stack {
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: calc(var(--sk-space) * 3);
199
+ }
200
+
201
+ /* ── Hero ───────────────────────────────────────────────────────────────────*/
202
+ .sk-hero {
203
+ padding: calc(var(--sk-space) * 12) calc(var(--sk-space) * 3);
204
+ background: var(--sk-neutral);
205
+ color: var(--sk-neutral-foreground);
206
+ text-align: center;
207
+ }
208
+
209
+ .sk-hero--image {
210
+ background-image:
211
+ linear-gradient(rgba(10, 18, 28, 0.55), rgba(10, 18, 28, 0.55)),
212
+ var(--sk-hero-image);
213
+ background-size: cover;
214
+ background-position: center;
215
+ color: #ffffff;
216
+ }
217
+
218
+ .sk-hero__inner {
219
+ max-width: 48rem;
220
+ margin-inline: auto;
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: calc(var(--sk-space) * 3);
224
+ align-items: center;
225
+ }
226
+
227
+ .sk-hero__heading {
228
+ font-size: var(--sk-font-size-h1);
229
+ font-weight: 800;
230
+ letter-spacing: -0.02em;
231
+ }
232
+
233
+ .sk-hero__subheading {
234
+ font-size: var(--sk-font-size-lg);
235
+ max-width: 38rem;
236
+ opacity: 0.92;
237
+ }
238
+
239
+ .sk-hero__cta {
240
+ display: flex;
241
+ flex-wrap: wrap;
242
+ gap: calc(var(--sk-space) * 2);
243
+ justify-content: center;
244
+ }
245
+
246
+ /* ── Section ────────────────────────────────────────────────────────────────*/
247
+ .sk-section {
248
+ padding: calc(var(--sk-space) * 10) calc(var(--sk-space) * 3);
249
+ }
250
+
251
+ .sk-section__inner {
252
+ max-width: 72rem;
253
+ margin-inline: auto;
254
+ }
255
+
256
+ .sk-section__title {
257
+ font-size: var(--sk-font-size-h2);
258
+ font-weight: 700;
259
+ letter-spacing: -0.01em;
260
+ }
261
+
262
+ .sk-section__intro {
263
+ color: var(--sk-muted);
264
+ font-size: var(--sk-font-size-lg);
265
+ margin-top: calc(var(--sk-space) * 2);
266
+ max-width: 44rem;
267
+ }
268
+
269
+ .sk-section__title + .sk-service-grid,
270
+ .sk-section__title + .sk-team-grid,
271
+ .sk-section__intro + .sk-service-grid,
272
+ .sk-section__intro + .sk-team-grid,
273
+ .sk-section__intro + .sk-faq,
274
+ .sk-section__title + .sk-faq {
275
+ margin-top: calc(var(--sk-space) * 6);
276
+ }
277
+
278
+ /* ── ServiceGrid ────────────────────────────────────────────────────────────*/
279
+ .sk-service-grid {
280
+ list-style: none;
281
+ margin: 0;
282
+ padding: 0;
283
+ display: grid;
284
+ grid-template-columns: repeat(3, minmax(0, 1fr));
285
+ gap: calc(var(--sk-space) * 4);
286
+ }
287
+
288
+ .sk-service-grid__item {
289
+ background: var(--sk-neutral);
290
+ color: var(--sk-neutral-foreground);
291
+ border-radius: var(--sk-radius);
292
+ padding: calc(var(--sk-space) * 4);
293
+ display: flex;
294
+ flex-direction: column;
295
+ gap: calc(var(--sk-space) * 1.5);
296
+ }
297
+
298
+ .sk-service-grid__icon {
299
+ width: calc(var(--sk-space) * 6);
300
+ height: calc(var(--sk-space) * 6);
301
+ object-fit: contain;
302
+ }
303
+
304
+ .sk-service-grid__title {
305
+ font-size: var(--sk-font-size-lg);
306
+ font-weight: 650;
307
+ }
308
+
309
+ .sk-service-grid__desc {
310
+ color: var(--sk-muted);
311
+ font-size: var(--sk-font-size-sm);
312
+ }
313
+
314
+ /* ── TeamGrid ───────────────────────────────────────────────────────────────*/
315
+ .sk-team-grid {
316
+ list-style: none;
317
+ margin: 0;
318
+ padding: 0;
319
+ display: grid;
320
+ grid-template-columns: repeat(4, minmax(0, 1fr));
321
+ gap: calc(var(--sk-space) * 4);
322
+ }
323
+
324
+ .sk-team-grid__item {
325
+ text-align: center;
326
+ display: flex;
327
+ flex-direction: column;
328
+ gap: calc(var(--sk-space) * 1);
329
+ }
330
+
331
+ .sk-team-grid__photo {
332
+ width: 100%;
333
+ aspect-ratio: 1 / 1;
334
+ object-fit: cover;
335
+ border-radius: var(--sk-radius);
336
+ background: var(--sk-neutral);
337
+ }
338
+
339
+ .sk-team-grid__photo--placeholder {
340
+ background: linear-gradient(135deg, var(--sk-neutral), #e2e8ee);
341
+ }
342
+
343
+ .sk-team-grid__name {
344
+ font-weight: 650;
345
+ }
346
+
347
+ .sk-team-grid__role {
348
+ color: var(--sk-muted);
349
+ font-size: var(--sk-font-size-sm);
350
+ }
351
+
352
+ /* ── Hours ──────────────────────────────────────────────────────────────────*/
353
+ .sk-hours {
354
+ margin: 0;
355
+ display: flex;
356
+ flex-direction: column;
357
+ max-width: 32rem;
358
+ }
359
+
360
+ .sk-hours__row {
361
+ display: flex;
362
+ justify-content: space-between;
363
+ gap: calc(var(--sk-space) * 4);
364
+ padding: calc(var(--sk-space) * 1.5) 0;
365
+ border-bottom: 1px solid color-mix(in srgb, var(--sk-muted) 20%, transparent);
366
+ }
367
+
368
+ .sk-hours__row:last-child {
369
+ border-bottom: 0;
370
+ }
371
+
372
+ .sk-hours__day {
373
+ font-weight: 600;
374
+ margin: 0;
375
+ }
376
+
377
+ .sk-hours__time {
378
+ color: var(--sk-muted);
379
+ margin: 0;
380
+ }
381
+
382
+ /* ── MapEmbed ───────────────────────────────────────────────────────────────*/
383
+ .sk-map {
384
+ width: 100%;
385
+ height: var(--sk-map-height, 320px);
386
+ border-radius: var(--sk-radius);
387
+ overflow: hidden;
388
+ background: var(--sk-neutral);
389
+ }
390
+
391
+ .sk-map__frame {
392
+ width: 100%;
393
+ height: 100%;
394
+ border: 0;
395
+ display: block;
396
+ }
397
+
398
+ /* ── FAQ ────────────────────────────────────────────────────────────────────*/
399
+ .sk-faq {
400
+ display: flex;
401
+ flex-direction: column;
402
+ max-width: 48rem;
403
+ }
404
+
405
+ .sk-faq__item {
406
+ border-bottom: 1px solid color-mix(in srgb, var(--sk-muted) 20%, transparent);
407
+ }
408
+
409
+ .sk-faq__q {
410
+ cursor: pointer;
411
+ font-weight: 600;
412
+ padding: calc(var(--sk-space) * 2.5) 0;
413
+ list-style: none;
414
+ }
415
+
416
+ .sk-faq__q::-webkit-details-marker {
417
+ display: none;
418
+ }
419
+
420
+ .sk-faq__q::after {
421
+ content: "+";
422
+ float: right;
423
+ color: var(--sk-muted);
424
+ font-weight: 400;
425
+ }
426
+
427
+ .sk-faq__item[open] .sk-faq__q::after {
428
+ content: "\2212"; /* minus */
429
+ }
430
+
431
+ .sk-faq__a {
432
+ color: var(--sk-muted);
433
+ padding: 0 0 calc(var(--sk-space) * 2.5);
434
+ }
435
+
436
+ /* ── Button ─────────────────────────────────────────────────────────────────*/
437
+ .sk-button {
438
+ display: inline-flex;
439
+ align-items: center;
440
+ justify-content: center;
441
+ gap: calc(var(--sk-space) * 1);
442
+ padding: calc(var(--sk-space) * 1.5) calc(var(--sk-space) * 3);
443
+ border-radius: var(--sk-radius);
444
+ font: inherit;
445
+ font-weight: 600;
446
+ line-height: 1;
447
+ text-decoration: none;
448
+ cursor: pointer;
449
+ border: 1px solid transparent;
450
+ transition: filter 0.15s ease, background 0.15s ease, color 0.15s ease;
451
+ }
452
+
453
+ .sk-button:focus-visible {
454
+ outline: 2px solid var(--sk-brand);
455
+ outline-offset: 2px;
456
+ }
457
+
458
+ .sk-button--solid {
459
+ background: var(--sk-brand);
460
+ color: var(--sk-brand-foreground);
461
+ }
462
+
463
+ .sk-button--solid:hover {
464
+ filter: brightness(0.94);
465
+ }
466
+
467
+ .sk-button--outline {
468
+ background: transparent;
469
+ color: var(--sk-brand);
470
+ border-color: var(--sk-brand);
471
+ }
472
+
473
+ .sk-button--outline:hover {
474
+ background: color-mix(in srgb, var(--sk-brand) 8%, transparent);
475
+ }
476
+
477
+ .sk-button--ghost {
478
+ background: transparent;
479
+ color: var(--sk-foreground);
480
+ }
481
+
482
+ .sk-button--ghost:hover {
483
+ background: color-mix(in srgb, var(--sk-muted) 12%, transparent);
484
+ }
485
+
486
+ /* ── BookingBlock placeholder (D8 seam, inert in v1) ────────────────────────*/
487
+ .sk-booking {
488
+ border: 1px dashed color-mix(in srgb, var(--sk-muted) 40%, transparent);
489
+ border-radius: var(--sk-radius);
490
+ padding: calc(var(--sk-space) * 4);
491
+ text-align: center;
492
+ }
493
+
494
+ .sk-booking__placeholder {
495
+ color: var(--sk-muted);
496
+ font-size: var(--sk-font-size-sm);
497
+ }
498
+
499
+ /* ── responsive ─────────────────────────────────────────────────────────────*/
500
+ @media (max-width: 900px) {
501
+ .sk-service-grid {
502
+ grid-template-columns: repeat(2, minmax(0, 1fr));
503
+ }
504
+ .sk-team-grid {
505
+ grid-template-columns: repeat(3, minmax(0, 1fr));
506
+ }
507
+ :where([data-sk-theme]),
508
+ :root {
509
+ --sk-font-size-h1: 2.25rem;
510
+ --sk-font-size-h2: 1.625rem;
511
+ }
512
+ }
513
+
514
+ @media (max-width: 560px) {
515
+ .sk-service-grid {
516
+ grid-template-columns: minmax(0, 1fr);
517
+ }
518
+ .sk-team-grid {
519
+ grid-template-columns: repeat(2, minmax(0, 1fr));
520
+ }
521
+ .sk-hero {
522
+ padding-block: calc(var(--sk-space) * 8);
523
+ }
524
+ .sk-section {
525
+ padding-block: calc(var(--sk-space) * 7);
526
+ }
527
+ }
528
+
529
+ /* ── ISLAND STYLES (PAT-774) ─────────────────────────────────────────────────
530
+ * One section per island, so the four parallel workstreams filling in the
531
+ * implementations append inside their OWN block and never touch each other's.
532
+ * Each island's CSS reads the patient-surface theme vars off its own mount point
533
+ * (see `patient-surface-theme.ts`) — brand/accent/radius/font only; neutrals stay
534
+ * on the kit's tuned values.
535
+ * ------------------------------------------------------------------------- */
536
+
537
+ /* ── booking (PAT-765) ───────────────────────────────────────────────────── */
538
+ .sk-booking {
539
+ /* The inert dashed placeholder is gone — this is a real flow surface now. */
540
+ border: 0;
541
+ padding: 0;
542
+ text-align: left;
543
+ }
544
+ .sk-booking__heading {
545
+ font-size: var(--sk-font-size-h2);
546
+ line-height: 1.2;
547
+ }
548
+ .sk-booking__list {
549
+ list-style: none;
550
+ padding: 0;
551
+ margin: 0;
552
+ display: grid;
553
+ gap: calc(var(--sk-space) * 1.5);
554
+ }
555
+ .sk-booking__option {
556
+ display: flex;
557
+ align-items: baseline;
558
+ justify-content: space-between;
559
+ gap: calc(var(--sk-space) * 2);
560
+ width: 100%;
561
+ padding: calc(var(--sk-space) * 2) calc(var(--sk-space) * 2.5);
562
+ border: 1px solid color-mix(in srgb, var(--sk-muted) 30%, transparent);
563
+ border-radius: var(--sk-island-radius);
564
+ background: none;
565
+ /* `font: inherit` is not enough: a <button> does NOT inherit colour, so a bare
566
+ transparent one falls back to the UA's `buttontext` (near-black) and vanishes on
567
+ a dark clinic canvas. Every see-through control in this sheet must say so. */
568
+ color: inherit;
569
+ font: inherit;
570
+ text-align: left;
571
+ cursor: pointer;
572
+ }
573
+ .sk-booking__option:hover {
574
+ border-color: var(--sk-island-primary);
575
+ }
576
+ .sk-booking__option-label {
577
+ font-weight: 600;
578
+ }
579
+ .sk-booking__option-meta,
580
+ .sk-booking__empty,
581
+ .sk-booking__body,
582
+ .sk-booking__aside {
583
+ color: var(--sk-muted);
584
+ font-size: var(--sk-font-size-sm);
585
+ }
586
+ .sk-booking__link {
587
+ color: var(--sk-island-primary);
588
+ }
589
+
590
+ /* NOTE: `.sk-booking` + `.sk-booking__placeholder` predate this split and live
591
+ * with the other primitives above. */
592
+
593
+ /* ── certificate-funnel (PAT-764) ────────────────────────────────────────── */
594
+ /* The shared FLOW frame (shell, buttons, fields) + the funnel's own screens. Neutral
595
+ * by default; the four allow-listed clinic values arrive as `--sk-island-*` custom
596
+ * properties on the island's OWN mount point (see `patient-surface-theme.ts`), so a
597
+ * clinic brand reaches these components without the clinic being able to restyle them. */
598
+ .sk-certificate-funnel,
599
+ .sk-booking {
600
+ /* The kit's OWN default, deliberately NOT `var(--sk-primary, …)`. `--sk-primary` is a
601
+ value the clinic's site stylesheet publishes, and reading it here handed a raw,
602
+ unchecked brand colour to the island under the hardcoded white below — the one route
603
+ in that skips `derivePrimaryPair` entirely, so the CTA's label could land at any
604
+ ratio at all (PocketDoc's own #e92095 is 4.11:1 under white). The two tokens are a
605
+ PAIR: `themeStyle()` overrides both inline together after deriving the foreground,
606
+ or neither moves. #0f766e is 5.47:1 under #ffffff. */
607
+ --sk-island-primary: #0f766e;
608
+ --sk-island-primary-fg: #ffffff;
609
+ --sk-island-accent: var(--sk-island-primary);
610
+ --sk-island-radius: var(--sk-radius, 10px);
611
+ --sk-island-font: inherit;
612
+ font-family: var(--sk-island-font);
613
+ }
614
+
615
+ /* Screen-reader-only text. Used for required markers + hidden labels. */
616
+ .sk-sr-only {
617
+ position: absolute;
618
+ width: 1px;
619
+ height: 1px;
620
+ padding: 0;
621
+ margin: -1px;
622
+ overflow: hidden;
623
+ clip: rect(0 0 0 0);
624
+ white-space: nowrap;
625
+ border: 0;
626
+ }
627
+
628
+ /* ── flow shell ─────────────────────────────────────────────────────────────*/
629
+ .sk-flow {
630
+ display: grid;
631
+ grid-template-rows: auto 1fr auto;
632
+ gap: calc(var(--sk-space) * 2);
633
+ max-width: 34rem;
634
+ margin-inline: auto;
635
+ /* The island pays for its OWN inset. It used to inherit one from the host page, so a
636
+ site that padded its funnel canvas only from `md:` up shipped a mobile flow whose
637
+ heading and cards touched the left edge of the phone (PAT-853). An island that is
638
+ unreadable unless its host remembers to pad it is a contract nobody can keep. */
639
+ padding-inline: calc(var(--sk-space) * 3);
640
+ padding-block: calc(var(--sk-space) * 3);
641
+ text-align: left;
642
+
643
+ /* ── Ink DERIVED from the surface, not taken from the token bag ────────────────
644
+ `--sk-muted` is one absolute colour, but this island mounts on a background the
645
+ CLINIC chose: PocketDoc's funnel canvas is #121936, where the bridged muted
646
+ (#626884) lands at 3.1:1 and fails AA on every subheading, Back link and help
647
+ line. Mixing the surface's OWN `currentColor` instead means secondary ink is
648
+ always a softened version of whatever ink already reads there — ~8:1 on that dark
649
+ canvas, ~7:1 on white — without the kit needing to know which it is. Same rule the
650
+ island's foreground already follows (`deriveForeground`), same lesson as the
651
+ slots-confirm bar (PAT-834/838): pair the ink with the surface it sits on. */
652
+ --sk-flow-muted: color-mix(in srgb, currentColor 68%, transparent);
653
+ /* Hairlines and card edges, same derivation. A 30%-of-mid-grey border was ~1.2:1 on
654
+ the dark canvas — a card you can only see if you already know it is there. */
655
+ --sk-flow-line: color-mix(in srgb, currentColor 24%, transparent);
656
+ /* A barely-there raised surface for cards, again relative to the page. */
657
+ --sk-flow-raised: color-mix(in srgb, currentColor 6%, transparent);
658
+
659
+ /* Brand colour as TEXT ink, pulled toward the surface's own readable ink until it
660
+ clears AA. The raw brand is tuned to be a BUTTON FILL (where the kit derives a
661
+ contrasting foreground for it), not small text on a canvas the clinic also chose:
662
+ PocketDoc's #e92095 on #121936 is 4.14:1, which fails for the 11px uppercase
663
+ eyebrow and the inline policy links. Mixing `currentColor` in pulls it back toward
664
+ whatever the canvas is, in one declaration, without the kit knowing which it is.
665
+ Fills are untouched — only ink.
666
+
667
+ 44%, not the 64% this shipped with. MEASURED, not estimated: the policy links on the
668
+ preflight screen sit inside a 6%-white card, and against that real background 64%
669
+ gave 4.07:1 — already under AA before the CTA was AA-floored, and 3.76:1 after. The
670
+ probe that reported "all passes" was comparing them to the page canvas instead of the
671
+ card they sit on. 44% measures 4.55:1 over the card and 5.17:1 on the bare canvas. */
672
+ --sk-flow-accent-ink: color-mix(in srgb, var(--sk-island-primary) 44%, currentColor);
673
+
674
+ /* Error INK, same derivation. `#b3261e` is tuned as a red on white — on PocketDoc's
675
+ #121936 canvas it is 2.64:1, so "Enter your date of birth" is a message the patient
676
+ literally cannot read on the screen that is blocking them. Pulling a brighter red
677
+ toward the surface's own ink gives 6.58:1 there and 5.94:1 on white: still
678
+ unmistakably red, readable either way. Text AND the `[aria-invalid]` field border —
679
+ the field no longer paints its own white fill, so that border sits on the clinic's
680
+ canvas too and needs the same derivation. */
681
+ --sk-flow-danger: color-mix(in srgb, #ef4444 70%, currentColor);
682
+ }
683
+ .sk-flow__bar {
684
+ display: flex;
685
+ align-items: center;
686
+ justify-content: space-between;
687
+ gap: calc(var(--sk-space) * 2);
688
+ }
689
+ .sk-flow__back,
690
+ .sk-flow__back-spacer {
691
+ min-height: 2.25rem;
692
+ }
693
+ .sk-flow__back {
694
+ background: none;
695
+ border: 0;
696
+ padding: 0;
697
+ color: var(--sk-flow-muted, var(--sk-muted));
698
+ font: inherit;
699
+ font-size: var(--sk-font-size-sm);
700
+ cursor: pointer;
701
+ }
702
+ .sk-flow__back:hover {
703
+ /* Transparent over the page → full-strength means the PAGE's text colour. */
704
+ color: inherit;
705
+ }
706
+ /* The position group: the words and the dots, on the bar's own row (PAT-853 fold pass).
707
+ `min-width: 0` + a nowrap label so a long eyebrow ("Question 6 of 6") shrinks the group
708
+ rather than wrapping the bar onto a second line — a bar that grows is the opposite of
709
+ what this move was for. */
710
+ .sk-flow__place {
711
+ display: flex;
712
+ align-items: center;
713
+ gap: calc(var(--sk-space) * 1.5);
714
+ min-width: 0;
715
+ }
716
+ .sk-flow__progress {
717
+ display: flex;
718
+ align-items: center;
719
+ gap: 4px;
720
+ }
721
+ .sk-flow__dot {
722
+ width: 6px;
723
+ height: 6px;
724
+ border-radius: 999px;
725
+ background: var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 35%, transparent));
726
+ }
727
+ .sk-flow__dot--done {
728
+ background: color-mix(in srgb, var(--sk-island-primary) 45%, transparent);
729
+ }
730
+ .sk-flow__dot--current {
731
+ width: 20px;
732
+ background: var(--sk-island-primary);
733
+ }
734
+ .sk-flow__head {
735
+ margin-bottom: calc(var(--sk-space) * 2);
736
+ }
737
+ /* Lives in the bar beside the dots now, not above the heading — see the note in
738
+ `flow-shell.tsx`. `nowrap` because it shares a 342px row on a phone with the Back
739
+ control and up to eight dots. */
740
+ .sk-flow__eyebrow {
741
+ white-space: nowrap;
742
+ font-size: 0.6875rem;
743
+ font-weight: 600;
744
+ letter-spacing: 0.14em;
745
+ text-transform: uppercase;
746
+ color: var(--sk-flow-accent-ink, var(--sk-island-accent));
747
+ }
748
+ .sk-flow__heading {
749
+ margin-top: calc(var(--sk-space) * 0.5);
750
+ font-size: var(--sk-font-size-h2);
751
+ /* WEIGHT 500, not the UA's bold 700. The heading was never given a weight, so every
752
+ step title rendered at the browser's default — heavy and untracked, which is what
753
+ made the funnel read as an unstyled document rather than an authored one. */
754
+ font-weight: 500;
755
+ line-height: 1.1;
756
+ letter-spacing: -0.025em;
757
+ }
758
+ /* NO ring on the step heading. Earlier reasoning here was that "the heading is focused
759
+ on every step change, so a visible ring is the point" — but the heading is
760
+ `tabindex="-1"`, so no keyboard user can ever Tab to it. The focus is purely
761
+ programmatic, to make assistive tech announce the new step; the ring therefore marks
762
+ something nobody navigated to, and Chrome still matches `:focus-visible` after a
763
+ click-advance. At desktop it painted a 508×90 outlined rectangle around the question —
764
+ which reads as an empty text input sitting above the real one. The focus move and its
765
+ announcement are unchanged; only the paint is suppressed. */
766
+ .sk-flow__heading:focus {
767
+ outline: none;
768
+ }
769
+ /* READING copy, so it sits at the base size. 14px is for subordinate metadata — a
770
+ back link, a slot's "1 left" — not for the sentence explaining what this step is. */
771
+ .sk-flow__sub {
772
+ margin-top: calc(var(--sk-space) * 1.5);
773
+ color: var(--sk-flow-muted, var(--sk-muted));
774
+ }
775
+ /* Announced, never shown — the heading already carries it visually. */
776
+ .sk-flow__status {
777
+ position: absolute;
778
+ width: 1px;
779
+ height: 1px;
780
+ overflow: hidden;
781
+ clip: rect(0 0 0 0);
782
+ white-space: nowrap;
783
+ }
784
+ /* 1×, not 2×: the shell's own grid `gap` already puts 2× between the body and this row, so
785
+ the declared 2× here was a second helping of the same air — 32px between the last field
786
+ and the button the patient came to press, on every step that has one. */
787
+ .sk-flow__footer {
788
+ padding-top: calc(var(--sk-space));
789
+ }
790
+ /* NO PINNED CTA BAR (PAT-867 — this REPLACES the PAT-853/865 sticky footer).
791
+ The fold problem was real; bottom-sticky was the wrong answer to it. A pinned bar is a
792
+ floating rectangle over a scrolling column, so it needs a veil to occlude what passes
793
+ behind — and a veil an island is allowed to paint can only be derived from
794
+ `currentColor` (the clinic's canvas is unknowable here, PAT-834/838), which means it is
795
+ TRANSLUCENT by construction. The preflight step's own 000 link read straight through
796
+ the glass, under the secondary button, as one garbled line. Two sentences sharing a
797
+ rectangle is a worse defect than a button one scroll away.
798
+ So the footer sits in the column like every other row, and a step that does not fit is
799
+ made to FIT — see `.sk-pf__statements` for the density preflight now runs at. Where a
800
+ step genuinely cannot (identity's nine fields), scrolling is the honest answer.
801
+ The slot picker's `.sk-slots__confirm` is a different case and stays: it pins a
802
+ selection summary over a LIST that is unbounded by nature, and it paints no veil
803
+ because its content is one opaque full-width fill. */
804
+ .sk-flow__btn {
805
+ display: inline-flex;
806
+ align-items: center;
807
+ justify-content: center;
808
+ gap: calc(var(--sk-space));
809
+ width: 100%;
810
+ min-height: 2.75rem;
811
+ padding-inline: calc(var(--sk-space) * 3);
812
+ border: 1px solid transparent;
813
+ border-radius: var(--sk-island-radius);
814
+ font: inherit;
815
+ font-weight: 600;
816
+ cursor: pointer;
817
+ /* Declared HERE, not inherited: the `box-sizing` reset above is scoped to
818
+ `[data-sk-theme]`, and an island's mount point need not sit inside one. A `<button>`
819
+ gets border-box from the UA stylesheet and looked fine anyway — an `<a>` styled as a
820
+ button did not, and overflowed the viewport by its own padding. */
821
+ box-sizing: border-box;
822
+ text-align: center;
823
+ text-decoration: none;
824
+ }
825
+ /* A disabled button must not wear the accent. Two reasons, and the second is measurable:
826
+ the accent is the single signal telling the patient which control is the one to press,
827
+ so spending it on something inert lies about affordance; and `opacity: .55` over the
828
+ primary fill dragged the label to 3.25:1 — under AA on the very screens where the
829
+ patient is trying to work out why they cannot continue. Repainted as a neutral surface
830
+ derived from the canvas, so it tracks whatever clinic colour the island landed on. */
831
+ .sk-flow__btn[disabled] {
832
+ cursor: not-allowed;
833
+ background: color-mix(in srgb, currentColor 12%, transparent);
834
+ border-color: transparent;
835
+ color: color-mix(in srgb, currentColor 62%, transparent);
836
+ }
837
+ .sk-flow__btn--primary {
838
+ background: var(--sk-island-primary);
839
+ color: var(--sk-island-primary-fg);
840
+ }
841
+ .sk-flow__btn--secondary {
842
+ background: transparent;
843
+ border-color: var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 40%, transparent));
844
+ color: inherit;
845
+ }
846
+ .sk-flow__btn--quiet {
847
+ background: none;
848
+ border: 0;
849
+ color: var(--sk-flow-muted, var(--sk-muted));
850
+ font-weight: 500;
851
+ }
852
+ /* A standalone quiet action has no adjacent button to align against, so its label follows
853
+ the left-set reading column. Keep the full-width 44px row as the phone tap target; slot
854
+ cancellation and the submitted confirmation pair are not only children and stay centred. */
855
+ .sk-flow__footer > .sk-flow__btn--quiet:only-child,
856
+ .sk-cf__after > .sk-flow__btn--quiet:only-child {
857
+ justify-content: flex-start;
858
+ padding-inline: 0;
859
+ text-align: start;
860
+ }
861
+ .sk-flow__btn:focus-visible {
862
+ outline: 2px solid var(--sk-island-accent);
863
+ outline-offset: 2px;
864
+ }
865
+ .sk-flow__error {
866
+ margin-top: calc(var(--sk-space));
867
+ color: var(--sk-flow-danger, #b3261e);
868
+ font-size: var(--sk-font-size-sm);
869
+ }
870
+ .sk-flow__loading {
871
+ color: var(--sk-flow-muted, var(--sk-muted));
872
+ font-size: var(--sk-font-size-sm);
873
+ }
874
+
875
+ /* ── trust rail (PAT-853) ───────────────────────────────────────────────────
876
+ Standing context — who reviews the request, who regulates them, what it costs, where
877
+ the data lives. LAST in the DOM, so on a phone it reads after the CTA; the desktop
878
+ rule below moves it into the empty column beside the flow with grid placement only,
879
+ never by reordering the markup. */
880
+ .sk-flow__rail {
881
+ display: grid;
882
+ gap: calc(var(--sk-space) * 1.5);
883
+ /* A grid item defaults to `min-width: auto`, so one unbreakable string (a URL a clinic
884
+ pastes into its policy label) would widen the whole track and give the phone a
885
+ horizontal scrollbar. The island's own inset cannot save a track that is too wide. */
886
+ min-width: 0;
887
+ overflow-wrap: break-word;
888
+ margin-top: calc(var(--sk-space) * 3);
889
+ padding-top: calc(var(--sk-space) * 3);
890
+ border-top: 1px solid var(--sk-flow-line, color-mix(in srgb, currentColor 24%, transparent));
891
+ font-size: var(--sk-font-size-sm);
892
+ }
893
+ /* Ink on the CHILDREN, not on the rail itself, so `currentColor` in this mix is still the
894
+ PAGE's ink rather than an already-softened copy of it — nesting the same mix twice is
895
+ how "quiet" becomes "unreadable". 74% of the surface's own ink is ~7:1 on PocketDoc's
896
+ near-black canvas and ~6:1 on white; below ~65% neither clears AA.
897
+ NOT `--sk-flow-accent-ink`: the accent marks the one thing the patient is meant to
898
+ press, and a rail that borrows it competes with the button for that meaning. */
899
+ .sk-flow__rail > * {
900
+ color: color-mix(in srgb, currentColor 74%, transparent);
901
+ }
902
+ .sk-flow__rail-clinic {
903
+ font-weight: 600;
904
+ }
905
+ .sk-flow__rail-policies {
906
+ display: flex;
907
+ flex-wrap: wrap;
908
+ gap: calc(var(--sk-space) * 2);
909
+ }
910
+ .sk-flow__rail-link {
911
+ color: inherit;
912
+ text-decoration: underline;
913
+ text-underline-offset: 3px;
914
+ }
915
+ .sk-flow__rail-link:hover {
916
+ text-decoration-thickness: 2px;
917
+ }
918
+ .sk-flow__rail-link:focus-visible {
919
+ outline: 2px solid currentColor;
920
+ outline-offset: 2px;
921
+ }
922
+ /* Reusing the sheet's existing breakpoint scale (900/720/600/560/480), not a new one.
923
+ Below this the rail stays where the markup puts it: under the step. */
924
+ @media (min-width: 900px) {
925
+ /* 53rem = the flow's own 34rem (border-box: 31rem of content inside its 1.5rem insets)
926
+ + a 2rem gutter + the 17rem rail. The content column is therefore EXACTLY as wide as
927
+ it is today — only the empty canvas beside it is claimed. The three shell rows are
928
+ pinned to column 1 because grid auto-placement fills ACROSS first, which would
929
+ otherwise drop the step body into the rail's column. */
930
+ .sk-flow--railed {
931
+ position: relative;
932
+ --sk-rail-w: 17rem;
933
+ grid-template-columns: minmax(0, 1fr) var(--sk-rail-w);
934
+ column-gap: calc(var(--sk-space) * 4);
935
+ max-width: 53rem;
936
+ }
937
+ /* The railed width, said as a DEFINITE size for the reason spelt out on
938
+ `.sk-certificate-funnel .sk-flow` above: a `max-width` alone lets each step shrink to
939
+ its own content, so the reading column changed width and left edge between steps.
940
+ `max-inline-size: 100%` up there still caps this against a narrow host. */
941
+ .sk-certificate-funnel .sk-flow--railed {
942
+ inline-size: 53rem;
943
+ }
944
+ .sk-flow--railed > .sk-flow__bar,
945
+ .sk-flow--railed > .sk-flow__body,
946
+ .sk-flow--railed > .sk-flow__footer {
947
+ grid-column: 1;
948
+ }
949
+ /* Starts in row 2, level with the heading. A spanning in-flow grid item still feeds its
950
+ height into every intrinsic track it crosses, so the rail's leftover height used to
951
+ set the body/footer rhythm. An absolutely-positioned grid child keeps this grid area
952
+ as its anchor but contributes nothing to track sizing. The explicit inline start and
953
+ width exclude the container's inline-end padding from the rail's resolved box. */
954
+ .sk-flow--railed > .sk-flow__rail {
955
+ grid-column: 2;
956
+ grid-row: 2 / -1;
957
+ position: absolute;
958
+ inset-block-start: 0;
959
+ inset-inline-start: 0;
960
+ inline-size: var(--sk-rail-w);
961
+ margin-top: 0;
962
+ padding-top: 0;
963
+ border-top: 0;
964
+ padding-inline-start: calc(var(--sk-space) * 4);
965
+ border-inline-start: 1px solid
966
+ var(--sk-flow-line, color-mix(in srgb, currentColor 24%, transparent));
967
+ }
968
+ }
969
+
970
+ /* ── questions ──────────────────────────────────────────────────────────────*/
971
+ .sk-q {
972
+ margin-block: calc(var(--sk-space) * 2);
973
+ border: 0;
974
+ padding: 0;
975
+ }
976
+ .sk-q__label,
977
+ .sk-q__legend {
978
+ display: block;
979
+ margin-bottom: calc(var(--sk-space));
980
+ font-size: var(--sk-font-size-sm);
981
+ font-weight: 600;
982
+ }
983
+ .sk-q__help {
984
+ margin-bottom: calc(var(--sk-space));
985
+ color: var(--sk-flow-muted, var(--sk-muted));
986
+ font-size: var(--sk-font-size-sm);
987
+ }
988
+ /* ── quick-pick dates (PAT-873) ──────────────────────────────────────────────
989
+ Chips over real date inputs. The chip row is the shortcut; the inputs below are
990
+ the control, and stay visible so the patient can SEE the date a chip chose and
991
+ adjust it. Sized for a thumb (44px min target) and wrapping, because five chips
992
+ never fit one phone line. Surfaces derive from `currentColor` (the clinic canvas
993
+ is unknowable to an island — PAT-834/838); the pressed chip is the ONE place the
994
+ clinic's own accent is spent, so "which did I pick" reads at a glance. */
995
+ .sk-qd__chips {
996
+ display: flex;
997
+ flex-wrap: wrap;
998
+ gap: calc(var(--sk-space));
999
+ margin-bottom: calc(var(--sk-space) * 1.5);
1000
+ }
1001
+ .sk-qd__chip {
1002
+ min-height: 2.75rem;
1003
+ padding-inline: calc(var(--sk-space) * 2);
1004
+ border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
1005
+ border-radius: 999px;
1006
+ background: color-mix(in srgb, currentColor 6%, transparent);
1007
+ color: inherit;
1008
+ font: inherit;
1009
+ font-size: var(--sk-font-size-sm);
1010
+ font-weight: 600;
1011
+ cursor: pointer;
1012
+ }
1013
+ .sk-qd__chip:hover:not(:disabled) {
1014
+ border-color: var(--sk-island-primary);
1015
+ }
1016
+ /* `aria-pressed` IS the state — styling it means the visual and the accessible
1017
+ answer can never disagree, which a separate `.is-selected` class routinely does. */
1018
+ .sk-qd__chip[aria-pressed='true'] {
1019
+ border-color: var(--sk-island-primary);
1020
+ background: var(--sk-island-primary);
1021
+ color: var(--sk-island-primary-fg);
1022
+ }
1023
+ .sk-qd__chip:focus-visible {
1024
+ outline: 2px solid var(--sk-island-accent);
1025
+ outline-offset: 2px;
1026
+ }
1027
+ .sk-qd__chip:disabled {
1028
+ opacity: 0.55;
1029
+ cursor: not-allowed;
1030
+ }
1031
+ /* Two columns for a range, one for a single date — `auto-fit` does both with no
1032
+ modifier class, and collapses to one column on a narrow phone. */
1033
+ .sk-qd__fields {
1034
+ display: grid;
1035
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
1036
+ gap: calc(var(--sk-space) * 1.5);
1037
+ }
1038
+ .sk-flow .sk-qd__sublabel {
1039
+ display: block;
1040
+ margin-bottom: calc(var(--sk-space) * 0.5);
1041
+ color: var(--sk-flow-muted, var(--sk-muted));
1042
+ font-size: 0.8125rem;
1043
+ font-weight: 500;
1044
+ }
1045
+ .sk-q__options {
1046
+ display: flex;
1047
+ flex-wrap: wrap;
1048
+ gap: calc(var(--sk-space) * 1.5);
1049
+ }
1050
+ .sk-q__option {
1051
+ display: inline-flex;
1052
+ align-items: center;
1053
+ gap: calc(var(--sk-space));
1054
+ padding: calc(var(--sk-space) * 1.5) calc(var(--sk-space) * 2.5);
1055
+ border: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 35%, transparent));
1056
+ border-radius: var(--sk-island-radius);
1057
+ cursor: pointer;
1058
+ }
1059
+ .sk-q__option:has(input:checked) {
1060
+ border-color: var(--sk-island-primary);
1061
+ background: color-mix(in srgb, var(--sk-island-primary) 8%, transparent);
1062
+ }
1063
+ /* The funnel's last two UA widgets — the intake radios and the consent tick. `accent-color`
1064
+ tints only the CHECKED box; unchecked, with no `color-scheme` to go on, the UA paints its
1065
+ light-scheme default: a solid #ffffff square with a grey hairline and a ~3px radius. On
1066
+ PocketDoc's #121936 that made the single brightest object on the screen where the patient
1067
+ signs a legal consent an unstyled browser control — a larger continuous white mass than
1068
+ the 32px heading's strokes, wearing a corner radius that contradicts every other box on
1069
+ the step. The kit CANNOT answer this with `color-scheme`: the canvas belongs to the
1070
+ clinic and the island must never guess it. So the same recipe the fields took at
1071
+ `.sk-q__input` below — derive the resting state from the SURFACE — and pay for the
1072
+ checked state with the `--sk-island-primary` / `-fg` PAIR the primary CTA already ships,
1073
+ so the tick's contrast is guaranteed by the same derivation that guarantees the button's
1074
+ label rather than by `accent-color`'s per-spec glyph. These stay real `<input>`s: the
1075
+ checked state, the label association and Space/arrow-key operation are still the
1076
+ platform's, and the focus ring still lives on the card (`:has(input:focus-visible)`). */
1077
+ .sk-cf__check input,
1078
+ .sk-q__option input[type='radio'] {
1079
+ appearance: none;
1080
+ -webkit-appearance: none;
1081
+ inline-size: 1.25rem;
1082
+ block-size: 1.25rem;
1083
+ flex: none;
1084
+ margin: 0;
1085
+ display: grid;
1086
+ place-content: center;
1087
+ /* NOT `--sk-flow-line` (currentColor 24%) — that is a hairline BETWEEN cards, and 24%
1088
+ measures 2.2:1 on that dark canvas, under the 3:1 a UI component's boundary owes
1089
+ (WCAG 1.4.11). 55% of the surface's own ink clears it on a dark canvas and on white. */
1090
+ border: 1.5px solid color-mix(in srgb, currentColor 55%, transparent);
1091
+ /* 6px, deliberately NOT `--sk-island-radius`: 10-12px on a 20px box renders as a circle,
1092
+ which is the radio idiom, and the two would stop being tellable apart. */
1093
+ border-radius: 0.375rem;
1094
+ background: color-mix(in srgb, currentColor 8%, transparent);
1095
+ color: inherit;
1096
+ cursor: pointer;
1097
+ }
1098
+ .sk-q__option input[type='radio'] {
1099
+ border-radius: 50%;
1100
+ }
1101
+ .sk-cf__check input:checked,
1102
+ .sk-q__option input[type='radio']:checked {
1103
+ background: var(--sk-island-primary);
1104
+ border-color: var(--sk-island-primary);
1105
+ }
1106
+ .sk-cf__check input:checked::before {
1107
+ content: '';
1108
+ inline-size: 0.7rem;
1109
+ block-size: 0.7rem;
1110
+ background: var(--sk-island-primary-fg);
1111
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
1112
+ }
1113
+ .sk-q__option input[type='radio']:checked::before {
1114
+ content: '';
1115
+ inline-size: 0.5rem;
1116
+ block-size: 0.5rem;
1117
+ border-radius: 50%;
1118
+ background: var(--sk-island-primary-fg);
1119
+ }
1120
+ /* `appearance: none` also drops the UA's disabled greying, and both controls are disabled
1121
+ while a step's write is in flight. Same 0.55 the fields use. */
1122
+ .sk-cf__check input:disabled,
1123
+ .sk-q__option input[type='radio']:disabled {
1124
+ opacity: 0.55;
1125
+ }
1126
+ /* …and it erases the control ENTIRELY under a forced-colours OS theme, where the native
1127
+ widget is the only thing the user can see. Hand it straight back there: a consent tick
1128
+ nobody can find is a worse defect than a white one. */
1129
+ @media (forced-colors: active) {
1130
+ .sk-cf__check input,
1131
+ .sk-q__option input[type='radio'] {
1132
+ appearance: auto;
1133
+ }
1134
+ .sk-cf__check input:checked::before,
1135
+ .sk-q__option input[type='radio']:checked::before {
1136
+ content: none;
1137
+ }
1138
+ }
1139
+ .sk-q__input,
1140
+ .sk-identity__input,
1141
+ .sk-cf__input,
1142
+ .sk-pay__input {
1143
+ width: 100%;
1144
+ /* A DEFINITE box, not a `min-height`. A minimum let `<input type=text>`, `<input
1145
+ type=date>` and `<select>` each resolve their own intrinsic height, so one row of the
1146
+ identity form shipped three of them: Suburb 43px beside State 40px, Phone 43px beside
1147
+ Date of birth 45px — ragged bottom edges on the screen that asks for name, DOB and
1148
+ home address, which is exactly where a patient decides whether this is a real clinic.
1149
+ 2.75rem rather than 2.5 also lifts every control to the 44px touch-target floor
1150
+ (WCAG 2.2 SC 2.5.8) on mobile. Safe as a fixed size because none of these four classes
1151
+ is ever on a `<textarea>` — every consumer is a single-line input or a select. The
1152
+ explicit `line-height` is the other half: without it a date input's UA line box, not
1153
+ ours, sets where the text sits inside the box. */
1154
+ block-size: 2.75rem;
1155
+ line-height: 1.2;
1156
+ padding: calc(var(--sk-space)) calc(var(--sk-space) * 1.5);
1157
+ /* Same surface-derived boundary recipe as the unchecked checkbox/radio above. Fields
1158
+ need a 3:1 component edge; the shared `--sk-flow-line` remains a decorative hairline. */
1159
+ border: 1px solid color-mix(in srgb, currentColor 55%, transparent);
1160
+ border-radius: var(--sk-island-radius);
1161
+ /* Derived from the SURFACE, not taken from the token bag — same rule as
1162
+ `--sk-flow-raised` above and the slots-confirm scrim below (PAT-834/838).
1163
+ `--sk-background` is the kit's own LIGHT token and says nothing about the canvas the
1164
+ island was dropped on: painting it here laid NINE opaque white slabs down the dark
1165
+ identity step, so the loudest thing on the screen where the patient hands over their
1166
+ name, DOB and address was a wall of empty rectangles, out-competing both the question
1167
+ and the CTA. A translucent tint of the page's own ink darkens a light canvas and
1168
+ lightens a dark one, and `color: inherit` means the fill and the ink can never split. */
1169
+ background: color-mix(in srgb, currentColor 8%, transparent);
1170
+ color: inherit;
1171
+ font: inherit;
1172
+ }
1173
+ /* A <select>'s options are their own painting surface on Firefox and on Windows
1174
+ Chrome — declaring the ink without the fill there hands the popup back to the
1175
+ system colours. Same token pair as the field, so they can never diverge. */
1176
+ .sk-q__input option,
1177
+ .sk-identity__input option,
1178
+ .sk-cf__input option,
1179
+ .sk-pay__input option {
1180
+ background: var(--sk-background, #fff);
1181
+ color: var(--sk-foreground, #1b2733);
1182
+ }
1183
+ .sk-q__input[aria-invalid='true'],
1184
+ .sk-identity__input[aria-invalid='true'],
1185
+ .sk-cf__input[aria-invalid='true'],
1186
+ .sk-pay__input[aria-invalid='true'] {
1187
+ /* Derived, not absolute: the field no longer paints its own white fill, so this
1188
+ border now sits on the clinic's canvas, where `#b3261e` is 2.64:1 on PocketDoc's
1189
+ #121936 — an "this one is wrong" marker you cannot see. */
1190
+ border-color: var(--sk-flow-danger, #b3261e);
1191
+ }
1192
+ .sk-q__input:focus-visible,
1193
+ .sk-identity__input:focus-visible,
1194
+ .sk-cf__input:focus-visible,
1195
+ .sk-pay__input:focus-visible {
1196
+ /* Same ring as `.sk-flow__btn:focus-visible` — and load-bearing now, because a
1197
+ see-through field has no opaque fill left to mark "you are typing here". */
1198
+ outline: 2px solid var(--sk-island-accent);
1199
+ outline-offset: 2px;
1200
+ border-color: var(--sk-island-accent);
1201
+ }
1202
+ .sk-q__input::placeholder,
1203
+ .sk-identity__input::placeholder,
1204
+ .sk-cf__input::placeholder,
1205
+ .sk-pay__input::placeholder {
1206
+ /* The UA's placeholder grey is tuned for a white field. Say it in the surface's own
1207
+ ink instead, or "MM"/"YYYY" go near-invisible on a dark canvas. */
1208
+ color: var(--sk-flow-muted, var(--sk-muted));
1209
+ opacity: 1;
1210
+ }
1211
+ /* `::placeholder` above CANNOT reach `input[type=date]` — no engine matches it there. The
1212
+ UA paints the empty `dd/mm/yyyy` through its own datetime sub-fields, which inherit our
1213
+ `color: inherit`, so an UNANSWERED date was rendered in full-strength value ink: on the
1214
+ identity step the blank DOB read as already filled while the fields beside it read blank.
1215
+ Colour the PSEUDO, not the input — the field's fill is `currentColor 8%`, so recolouring
1216
+ the control itself would also re-tint the box away from its neighbours in the same grid.
1217
+ `:not(:focus)` hands full ink back the moment the patient starts typing, so a
1218
+ half-entered date is never mistaken for placeholder text. */
1219
+ .sk-q__input[type='date'][data-empty]:not(:focus)::-webkit-datetime-edit,
1220
+ .sk-identity__input[type='date'][data-empty]:not(:focus)::-webkit-datetime-edit {
1221
+ color: var(--sk-flow-muted, var(--sk-muted));
1222
+ }
1223
+ /* Firefox has no `::-webkit-datetime-edit` and paints the same placeholder, so it needs the
1224
+ colour on the control. Scoped so Chrome/Safari never take this branch and never shift the
1225
+ fill; Safari without `selector()` support falls through to the pseudo rule above. */
1226
+ @supports not selector(::-webkit-datetime-edit) {
1227
+ .sk-q__input[type='date'][data-empty]:not(:focus),
1228
+ .sk-identity__input[type='date'][data-empty]:not(:focus) {
1229
+ color: var(--sk-flow-muted, var(--sk-muted));
1230
+ }
1231
+ }
1232
+ /* A `<select>` still on its empty option is an UNANSWERED question, not a chosen value. */
1233
+ select.sk-identity__input[data-empty] {
1234
+ color: var(--sk-flow-muted, var(--sk-muted));
1235
+ }
1236
+ /* The UA calendar glyph is a fixed dark SVG — legible on the old white fill, all but
1237
+ invisible once the field is a tint of a dark page. `invert(0.5)` flattens ANY source
1238
+ luminance to mid grey, so it reads on both without the island guessing the canvas. */
1239
+ .sk-q__input::-webkit-calendar-picker-indicator,
1240
+ .sk-identity__input::-webkit-calendar-picker-indicator {
1241
+ filter: invert(0.5);
1242
+ }
1243
+ /* Chrome repaints an autofilled field with an opaque pale slab, which would put the
1244
+ white rectangles straight back on the name, address and card fields. There is no
1245
+ opaque colour we could hand it, so defer its paint indefinitely and keep our own.
1246
+
1247
+ THE INK COMES FROM `--sk-flow-ink`, WHICH ONLY SCRIPT CAN SUPPLY. This rule shipped
1248
+ as `currentColor`, which is self-referential: `currentColor` resolves against the
1249
+ element the value lands on, and that element is the one the UA has just recoloured
1250
+ for the autofill paint. So the override dutifully re-read the UA's black and painted
1251
+ the patient's autofilled name, email, phone and street address in it, on a dark
1252
+ canvas. The declared colour on the field stays `rgb(255,255,255)` throughout, which
1253
+ is why this survived every contrast probe — nothing in the cascade is wrong, the
1254
+ value is just read off the wrong element.
1255
+
1256
+ Every CSS way of saying "the colour around me" fails the same way here, measured in
1257
+ Chromium and WebKit rather than assumed: `inherit` inherits the KEYWORD and
1258
+ re-resolves it, an unregistered custom property substitutes it as a token, an
1259
+ `@property <color>` one keeps it late-resolved, and `color-mix(… currentColor …)`
1260
+ yields `color(srgb 0 0 0)`. Only a concrete `rgb(…)` survives, so `useResolvedInk`
1261
+ in `flow-shell.tsx` reads the flow root's own computed colour and publishes it here.
1262
+ The fallback is the old behaviour, for a form that has not been autofilled yet.
1263
+
1264
+ Two blocks, not one selector list: `:autofill` is the standard and `:-webkit-autofill`
1265
+ the legacy alias, and a selector list is discarded WHOLE by any engine that fails to
1266
+ parse one member. Split, each engine keeps the rule it understands — which is also
1267
+ what finally covers Firefox, whose autofill paints its own black-on-yellow and which
1268
+ never matched the `-webkit-` selector at all. */
1269
+ .sk-q__input:-webkit-autofill,
1270
+ .sk-identity__input:-webkit-autofill,
1271
+ .sk-cf__input:-webkit-autofill,
1272
+ .sk-pay__input:-webkit-autofill {
1273
+ -webkit-text-fill-color: var(--sk-flow-ink, currentColor);
1274
+ caret-color: var(--sk-flow-ink, currentColor);
1275
+ transition: background-color 600000s 0s;
1276
+ }
1277
+ .sk-q__input:autofill,
1278
+ .sk-identity__input:autofill,
1279
+ .sk-cf__input:autofill,
1280
+ .sk-pay__input:autofill {
1281
+ -webkit-text-fill-color: var(--sk-flow-ink, currentColor);
1282
+ caret-color: var(--sk-flow-ink, currentColor);
1283
+ transition: background-color 600000s 0s;
1284
+ }
1285
+ /* Payment disables its fields while the pre-auth is in flight; with an author-set fill
1286
+ the UA no longer greys them out, so the disabled state has to be said explicitly. */
1287
+ .sk-q__input:disabled,
1288
+ .sk-identity__input:disabled,
1289
+ .sk-cf__input:disabled,
1290
+ .sk-pay__input:disabled {
1291
+ opacity: 0.55;
1292
+ }
1293
+ /* Fallback kept: these classes can render outside a `.sk-flow` subtree, where the
1294
+ derived token is undefined. */
1295
+ .sk-q__error {
1296
+ margin-top: calc(var(--sk-space));
1297
+ color: var(--sk-flow-danger, #b3261e);
1298
+ font-size: var(--sk-font-size-sm);
1299
+ }
1300
+ .sk-q__required {
1301
+ color: var(--sk-flow-danger, #b3261e);
1302
+ }
1303
+
1304
+ /* ── identity ───────────────────────────────────────────────────────────────*/
1305
+ .sk-identity__grid {
1306
+ display: grid;
1307
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1308
+ gap: calc(var(--sk-space) * 2);
1309
+ }
1310
+ .sk-identity__field--wide {
1311
+ grid-column: 1 / -1;
1312
+ }
1313
+ .sk-identity__label {
1314
+ display: block;
1315
+ margin-bottom: calc(var(--sk-space) * 0.75);
1316
+ font-size: var(--sk-font-size-sm);
1317
+ font-weight: 500;
1318
+ }
1319
+ /* `.sk-flow`-prefixed, here and below, purely to out-specify the block-margin reset at
1320
+ the top of this sheet — see the TRAP note there. Without it these three margins were
1321
+ dead and identity read as one undifferentiated column: the RESIDENTIAL ADDRESS eyebrow
1322
+ sat flush against the date-of-birth hint above it and the "Street address" label below,
1323
+ so a group heading appeared to caption a field it does not belong to, and the sign-in
1324
+ shortcut looked like part of the first-name field. Every one of these classes only ever
1325
+ renders inside FlowShell's `.sk-flow` root, so the prefix always matches. */
1326
+ .sk-flow .sk-identity__help {
1327
+ margin-top: calc(var(--sk-space) * 0.5);
1328
+ color: var(--sk-flow-muted, var(--sk-muted));
1329
+ font-size: 0.75rem;
1330
+ }
1331
+ .sk-flow .sk-identity__section {
1332
+ margin-top: calc(var(--sk-space) * 3);
1333
+ margin-bottom: calc(var(--sk-space));
1334
+ font-size: 0.6875rem;
1335
+ font-weight: 600;
1336
+ letter-spacing: 0.12em;
1337
+ text-transform: uppercase;
1338
+ color: var(--sk-flow-muted, var(--sk-muted));
1339
+ }
1340
+ /* Caption for the group heading above, NOT a field hint — so it does not reuse
1341
+ `.sk-identity__help`, whose id is an aria-describedby contract. `.sk-flow`-prefixed for
1342
+ the same reason as the rules above it: the `.sk-flow p` reset near the top of this sheet
1343
+ is (0,1,1) and silently zeroes any bare block margin here. The negative top margin
1344
+ collapses against the eyebrow's 1×space bottom margin to ~0.25rem, so the note reads as
1345
+ the eyebrow's caption rather than the first field's hint. */
1346
+ .sk-flow .sk-identity__section-note {
1347
+ margin-top: calc(var(--sk-space) * -0.5);
1348
+ margin-bottom: calc(var(--sk-space) * 2);
1349
+ max-inline-size: 44ch;
1350
+ color: var(--sk-flow-muted, var(--sk-muted));
1351
+ font-size: 0.8125rem;
1352
+ line-height: 1.45;
1353
+ }
1354
+ .sk-flow .sk-identity__ramp {
1355
+ margin-bottom: calc(var(--sk-space) * 2);
1356
+ font-size: var(--sk-font-size-sm);
1357
+ }
1358
+ .sk-identity__ramp-link {
1359
+ background: none;
1360
+ border: 0;
1361
+ padding: 0;
1362
+ color: var(--sk-flow-accent-ink, var(--sk-island-primary));
1363
+ font: inherit;
1364
+ text-decoration: underline;
1365
+ text-underline-offset: 3px;
1366
+ cursor: pointer;
1367
+ }
1368
+ .sk-identity__ramp--open {
1369
+ display: grid;
1370
+ gap: calc(var(--sk-space));
1371
+ padding: calc(var(--sk-space) * 2);
1372
+ border: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 30%, transparent));
1373
+ border-radius: var(--sk-island-radius);
1374
+ }
1375
+ .sk-identity__confirm {
1376
+ padding: calc(var(--sk-space) * 2.5);
1377
+ border: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 30%, transparent));
1378
+ border-radius: var(--sk-island-radius);
1379
+ }
1380
+ .sk-flow .sk-identity__confirm-title {
1381
+ font-weight: 600;
1382
+ margin-bottom: calc(var(--sk-space) * 1.5);
1383
+ }
1384
+ /* The RETURNING-patient screen, so the one that must feel effortless — and it was the
1385
+ only screen in the funnel that scrolled sideways. A `<dd>` carries a UA
1386
+ `margin-inline-start: 40px`, and neither cell could shrink, so an ordinary
1387
+ 24-character email pushed the card to 442px inside a 390px phone and took the only
1388
+ button on the screen off the right edge with it. */
1389
+ .sk-identity__confirm-row {
1390
+ display: flex;
1391
+ gap: calc(var(--sk-space) * 2);
1392
+ padding-block: calc(var(--sk-space) * 0.5);
1393
+ }
1394
+ .sk-identity__confirm-row dt {
1395
+ flex: 0 0 6.5rem;
1396
+ min-width: 0;
1397
+ color: var(--sk-flow-muted, var(--sk-muted));
1398
+ }
1399
+ .sk-identity__confirm-row dd {
1400
+ min-width: 0;
1401
+ /* An email has no break opportunity, so without this it sets the card's width. */
1402
+ overflow-wrap: anywhere;
1403
+ }
1404
+ .sk-identity__confirm-actions {
1405
+ display: grid;
1406
+ gap: calc(var(--sk-space));
1407
+ margin-top: calc(var(--sk-space) * 2.5);
1408
+ }
1409
+
1410
+ /* ── funnel screens ─────────────────────────────────────────────────────────*/
1411
+ .sk-cf__services,
1412
+ .sk-cf__list,
1413
+ .sk-cf__links,
1414
+ .sk-cf__consent-summary {
1415
+ list-style: none;
1416
+ padding: 0;
1417
+ margin: 0;
1418
+ display: grid;
1419
+ /* 1× not 1.5×: on the confirm screen these five bullets sit between the review block
1420
+ and the button, and the extra half-space pushed the button below the fold. The
1421
+ bullets are still separated by more than their own line-height, so they read as a
1422
+ list rather than a paragraph. */
1423
+ gap: var(--sk-space);
1424
+ }
1425
+ /* These two lists FOLLOW reading copy, unlike `.sk-cf__services` / `.sk-cf__consent-summary`
1426
+ which lead their screens — and with the reset zeroing them they sat flush against the
1427
+ sentence that introduces them. On `unavailable` the tightest gap on the page fell
1428
+ exactly on the list's boundary, so the first exclusion read as the next line of the
1429
+ paragraph; on `blocked` "Nothing has been charged." ran straight into the 000 line.
1430
+ `.sk-flow` prefix is load-bearing — see the TRAP note on the reset. Outer air is
1431
+ deliberately WIDER than the 12px item gap, so the set groups against the prose. */
1432
+ .sk-flow .sk-cf__list,
1433
+ .sk-flow .sk-cf__links {
1434
+ margin-top: calc(var(--sk-space) * 2.5);
1435
+ }
1436
+ .sk-flow .sk-cf__list {
1437
+ /* Only `.sk-cf__list` has copy after it (the "see your regular GP" close), which
1438
+ otherwise read as a sixth exclusion. */
1439
+ margin-bottom: calc(var(--sk-space) * 2.5);
1440
+ }
1441
+ /* A marker, because `.sk-flow ul { list-style: none }` outranks any `list-style` we
1442
+ could set here — a pseudo-dot is the only route. NEVER `--sk-island-accent`: accent is
1443
+ a press signal and a list marker is not pressable. The consent button deliberately
1444
+ stays neutral until the patient ticks and signs, so its bullets must not out-shout it.
1445
+ Deliberately NOT on `.sk-cf__links`, whose first item is `.sk-cf__emergency` —
1446
+ decoration beside the 000 affordance competes with the one thing that must shout. */
1447
+ .sk-cf__list li {
1448
+ position: relative;
1449
+ padding-left: calc(var(--sk-space) * 2.5);
1450
+ }
1451
+ .sk-cf__list li::before {
1452
+ content: '';
1453
+ position: absolute;
1454
+ left: 0;
1455
+ top: 0.55em;
1456
+ width: 6px;
1457
+ height: 6px;
1458
+ border-radius: 999px;
1459
+ background: color-mix(in srgb, currentColor 55%, transparent);
1460
+ }
1461
+ .sk-cf__service {
1462
+ display: flex;
1463
+ align-items: center;
1464
+ justify-content: space-between;
1465
+ gap: calc(var(--sk-space) * 2);
1466
+ width: 100%;
1467
+ padding: calc(var(--sk-space) * 2) calc(var(--sk-space) * 2.5);
1468
+ border: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 30%, transparent));
1469
+ border-radius: var(--sk-island-radius);
1470
+ background: var(--sk-flow-raised, none);
1471
+ color: inherit;
1472
+ font: inherit;
1473
+ text-align: left;
1474
+ cursor: pointer;
1475
+ }
1476
+ .sk-cf__service:hover:not(:disabled) {
1477
+ border-color: var(--sk-island-primary);
1478
+ }
1479
+ .sk-cf__service:disabled {
1480
+ cursor: not-allowed;
1481
+ opacity: 0.6;
1482
+ }
1483
+ .sk-cf__service:focus-visible {
1484
+ outline: 2px solid var(--sk-island-accent);
1485
+ outline-offset: 2px;
1486
+ }
1487
+ /* WHAT was booked, on the screen that says it was booked (PAT-853). */
1488
+ .sk-flow .sk-cf__booked {
1489
+ display: grid;
1490
+ gap: calc(var(--sk-space) * 0.5);
1491
+ margin-top: calc(var(--sk-space) * 2);
1492
+ padding: calc(var(--sk-space) * 2);
1493
+ border: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 30%, transparent));
1494
+ border-radius: var(--sk-island-radius);
1495
+ background: var(--sk-flow-raised, none);
1496
+ }
1497
+ .sk-cf__booked-label {
1498
+ color: var(--sk-flow-muted, var(--sk-muted));
1499
+ font-size: 0.6875rem;
1500
+ font-weight: 600;
1501
+ letter-spacing: 0.14em;
1502
+ text-transform: uppercase;
1503
+ }
1504
+ .sk-cf__booked-when {
1505
+ font-size: 1.125rem;
1506
+ font-weight: 600;
1507
+ }
1508
+ /* The call the patient is waiting for. Same block as `.sk-cf__booked` so the two read
1509
+ as one pair of facts, but carrying the island accent on its leading edge — this is
1510
+ the next thing that HAPPENS, and it was previously losing the page to an email
1511
+ address in a bordered box. Accent only on the rule, never a fill: the primary button
1512
+ owns the filled accent, and a confirmation screen must not have two of them. */
1513
+ .sk-flow .sk-cf__call {
1514
+ margin-top: var(--sk-space);
1515
+ border-left: 3px solid var(--sk-island-primary);
1516
+ }
1517
+
1518
+ /* The review rows on the confirm screen — label above value, PAIRED into two columns
1519
+ so six facts cost three rows rather than six. That is the difference between the
1520
+ "Agree and confirm" button sitting above the fold and 175px below it, which is the
1521
+ whole point of a confirmation screen: you should be able to see what you are
1522
+ confirming AND the button that confirms it without scrolling.
1523
+
1524
+ `auto-fit` + a 10rem floor collapses to ONE column on a narrow phone, where two
1525
+ columns of truncated text would be worse than a longer scroll. The natural field
1526
+ order already pairs well — Name|Date of birth, Email|Phone, Request|Fee — so no
1527
+ row needs to span and no hole opens up. `dl`/`dt`/`dd` are reset because the kit
1528
+ ships no UA-margin reset for them. */
1529
+ .sk-flow .sk-cf__review {
1530
+ display: grid;
1531
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
1532
+ gap: var(--sk-space) calc(var(--sk-space) * 1.5);
1533
+ margin: 0 0 calc(var(--sk-space) * 1.5);
1534
+ padding: calc(var(--sk-space) * 1.25) calc(var(--sk-space) * 1.5);
1535
+ border: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 30%, transparent));
1536
+ border-radius: var(--sk-island-radius);
1537
+ background: var(--sk-flow-raised, none);
1538
+ }
1539
+ .sk-flow .sk-cf__review-row {
1540
+ display: grid;
1541
+ gap: calc(var(--sk-space) * 0.25);
1542
+ }
1543
+ .sk-flow .sk-cf__review-label {
1544
+ margin: 0;
1545
+ color: var(--sk-flow-muted, var(--sk-muted));
1546
+ font-size: 0.6875rem;
1547
+ font-weight: 600;
1548
+ letter-spacing: 0.14em;
1549
+ text-transform: uppercase;
1550
+ }
1551
+ .sk-flow .sk-cf__review-value {
1552
+ margin: 0;
1553
+ font-size: 1rem;
1554
+ /* An email or a long service label must wrap rather than widen the flow column. */
1555
+ overflow-wrap: anywhere;
1556
+ }
1557
+
1558
+ /* The sentence that says what the button means. Sits WITH the button, so it is read at
1559
+ the moment of pressing rather than remembered from further up the page. */
1560
+ .sk-flow .sk-cf__agree-note {
1561
+ margin: var(--sk-space) 0 0;
1562
+ color: var(--sk-flow-muted, var(--sk-muted));
1563
+ font-size: 0.875rem;
1564
+ line-height: 1.45;
1565
+ }
1566
+
1567
+ /* Quiet actions under the content — deliberately not a row of equal-weight buttons. */
1568
+ .sk-cf__after {
1569
+ display: grid;
1570
+ gap: calc(var(--sk-space) * 0.5);
1571
+ margin-top: calc(var(--sk-space) * 3);
1572
+ }
1573
+ .sk-cf__service-body {
1574
+ display: grid;
1575
+ gap: calc(var(--sk-space) * 0.5);
1576
+ min-width: 0;
1577
+ }
1578
+ .sk-cf__service-label {
1579
+ font-weight: 600;
1580
+ }
1581
+ /* The price on the picker (PAT-853). Muted, NOT accent-coloured: the accent is the
1582
+ primary action's alone, and a price that shouts reads as a sales page rather than a
1583
+ fact. Muted here still clears AA — it derives from the same currentColor ramp. */
1584
+ .sk-cf__service-delivery,
1585
+ .sk-cf__service-fee {
1586
+ color: var(--sk-flow-muted, var(--sk-muted));
1587
+ font-size: var(--sk-font-size-sm);
1588
+ }
1589
+ /* THE accent on this screen, and the only one. The four cards are what the patient is
1590
+ meant to press, and they were painted entirely in neutral ink while the "I need a
1591
+ different type of certificate" escape hatch below them carried the clinic's accent and
1592
+ an underline — so the loudest thing on the first screen of the funnel was the way out.
1593
+ Marking the chevron is enough to claim the affordance without four accent-filled slabs
1594
+ competing with each other. Scoped INSIDE `.sk-cf__service`: the same chevron class is
1595
+ reused by the preflight decline list, which is a different screen's decision. Accent
1596
+ INK, not the raw brand — the derived mix is what clears AA on a clinic's own canvas. */
1597
+ .sk-cf__service .sk-cf__service-chevron {
1598
+ color: var(--sk-flow-accent-ink, var(--sk-island-accent));
1599
+ }
1600
+ /* The hold rule, said ONCE for the whole priced list (PAT-853). It replaces the same
1601
+ 12-word sentence repeated verbatim on every paid row, so it belongs to the list rather
1602
+ than to the copy below it: 1× gap above (against the list's own 1.5× between rows) and
1603
+ the same muted ink as the prices it footnotes. `.sk-flow` prefix is load-bearing — see
1604
+ the TRAP note on the `<p>` margin reset. */
1605
+ .sk-flow .sk-cf__services-note {
1606
+ margin-top: calc(var(--sk-space) * 1.5);
1607
+ color: var(--sk-flow-muted, var(--sk-muted));
1608
+ font-size: var(--sk-font-size-sm);
1609
+ }
1610
+ /* An emergency affordance on a NON-emergency screen (the blocked exit). Absolute
1611
+ colours for the same reason `.sk-pf__emergency` uses them — a way to call an
1612
+ ambulance must not change appearance with the clinic's brand. */
1613
+ .sk-cf__emergency {
1614
+ display: inline-flex;
1615
+ align-items: center;
1616
+ min-height: 2.75rem;
1617
+ color: color-mix(in srgb, #ef4444 70%, currentColor);
1618
+ font-weight: 700;
1619
+ text-decoration: underline;
1620
+ text-underline-offset: 3px;
1621
+ }
1622
+ .sk-cf__emergency:focus-visible {
1623
+ outline: 2px solid #ef4444;
1624
+ outline-offset: 2px;
1625
+ }
1626
+ .sk-cf__row {
1627
+ display: grid;
1628
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1629
+ gap: calc(var(--sk-space) * 2);
1630
+ }
1631
+ .sk-cf__field {
1632
+ margin-block: calc(var(--sk-space) * 1.5);
1633
+ }
1634
+ .sk-cf__label {
1635
+ display: block;
1636
+ margin-bottom: calc(var(--sk-space) * 0.75);
1637
+ font-size: var(--sk-font-size-sm);
1638
+ font-weight: 500;
1639
+ }
1640
+ .sk-flow .sk-cf__help {
1641
+ margin-top: calc(var(--sk-space) * 0.5);
1642
+ color: var(--sk-flow-muted, var(--sk-muted));
1643
+ font-size: 0.75rem;
1644
+ }
1645
+ .sk-cf__expiry {
1646
+ display: flex;
1647
+ gap: calc(var(--sk-space));
1648
+ border: 0;
1649
+ padding: 0;
1650
+ margin-inline: 0;
1651
+ min-inline-size: 0;
1652
+ }
1653
+ .sk-cf__expiry > legend {
1654
+ padding-inline: 0;
1655
+ }
1656
+ .sk-cf__actions {
1657
+ display: grid;
1658
+ gap: calc(var(--sk-space));
1659
+ }
1660
+ .sk-cf__link {
1661
+ background: none;
1662
+ border: 0;
1663
+ padding: 0;
1664
+ color: var(--sk-flow-accent-ink, var(--sk-island-primary));
1665
+ font: inherit;
1666
+ text-decoration: underline;
1667
+ text-underline-offset: 3px;
1668
+ cursor: pointer;
1669
+ }
1670
+ .sk-flow .sk-cf__aside,
1671
+ /* Also reading copy — and on the exit screens it is the ONLY copy, telling a blocked
1672
+ patient what to do next. It was the smallest text on the screen. */
1673
+ .sk-flow .sk-cf__body,
1674
+ .sk-flow .sk-cf__notice {
1675
+ margin-top: calc(var(--sk-space) * 2);
1676
+ }
1677
+ /* The disclosure is a <button>, so no reset ever touched it — it simply never had any
1678
+ spacing, and sat welded to the last consent bullet above and the "I agree" card below,
1679
+ reading as a fifth bullet on the screen that captures binding consent. */
1680
+ .sk-cf__consent-summary + .sk-cf__link {
1681
+ display: block;
1682
+ margin-block: calc(var(--sk-space) * 2);
1683
+ }
1684
+ .sk-cf__consent-summary li {
1685
+ padding-left: calc(var(--sk-space) * 2.5);
1686
+ position: relative;
1687
+ }
1688
+ .sk-cf__consent-summary li::before {
1689
+ content: '';
1690
+ position: absolute;
1691
+ left: 0;
1692
+ top: 0.55em;
1693
+ width: 6px;
1694
+ height: 6px;
1695
+ border-radius: 999px;
1696
+ background: color-mix(in srgb, currentColor 55%, transparent);
1697
+ }
1698
+ .sk-cf__consent-full {
1699
+ margin-top: calc(var(--sk-space) * 2);
1700
+ display: grid;
1701
+ gap: calc(var(--sk-space) * 1.5);
1702
+ font-size: var(--sk-font-size-sm);
1703
+ color: var(--sk-flow-muted, var(--sk-muted));
1704
+ }
1705
+ .sk-cf__consent-form {
1706
+ margin-top: calc(var(--sk-space) * 3);
1707
+ /* The tick and the signature are one act. `.sk-cf__field`'s own 12px margin left them
1708
+ as two loose rows under the now-taller consent card. */
1709
+ display: grid;
1710
+ gap: calc(var(--sk-space) * 2.5);
1711
+ }
1712
+ .sk-cf__consent-form .sk-cf__field {
1713
+ margin-block: 0;
1714
+ }
1715
+ /* The one control on this island that legally BINDS the patient, and it was the only
1716
+ browser-default thing on the screen: a ~13px UA checkbox with a 14px label — smaller
1717
+ than the bullets it agrees to — inside a ~20px hit target. Same authored card recipe
1718
+ as `.sk-q__option`, so the whole 44px row is the target (WCAG 2.2 SC 2.5.8). */
1719
+ .sk-cf__check {
1720
+ display: flex;
1721
+ align-items: center;
1722
+ gap: calc(var(--sk-space) * 1.5);
1723
+ min-height: 2.75rem;
1724
+ padding: calc(var(--sk-space) * 1.5) calc(var(--sk-space) * 2);
1725
+ border: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 35%, transparent));
1726
+ border-radius: var(--sk-island-radius);
1727
+ font-size: var(--sk-font-size-base);
1728
+ cursor: pointer;
1729
+ }
1730
+ /* The box itself is authored up with `.sk-q__option`'s radio — one recipe for both, so the
1731
+ island cannot ship two "you ticked this" idioms. */
1732
+ /* The same selected-state language as `.sk-q__option:has(input:checked)` — two "you
1733
+ picked this" idioms on one island is how an authored surface starts to look assembled. */
1734
+ .sk-cf__check:has(input:checked) {
1735
+ border-color: var(--sk-island-primary);
1736
+ background: color-mix(in srgb, var(--sk-island-primary) 8%, transparent);
1737
+ }
1738
+ /* The card is the control now, so the ring belongs on the card, not on a 20px box
1739
+ floating inside it. */
1740
+ .sk-cf__check:has(input:focus-visible) {
1741
+ outline: 2px solid var(--sk-island-accent);
1742
+ outline-offset: 2px;
1743
+ }
1744
+ .sk-cf__check input:focus-visible {
1745
+ outline: none;
1746
+ }
1747
+ .sk-cf__signature {
1748
+ font-style: italic;
1749
+ }
1750
+ .sk-cf__cancel {
1751
+ margin-top: calc(var(--sk-space) * 3);
1752
+ padding-top: calc(var(--sk-space) * 3);
1753
+ border-top: 1px solid var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 25%, transparent));
1754
+ }
1755
+
1756
+ /* ── preflight (the safety check) ───────────────────────────────────────────*/
1757
+ /* THE DENSITY THAT REPLACED THE PINNED BAR (PAT-867).
1758
+ This step's job is to be READ, and it was losing that job twice over: five cards at
1759
+ card-comfortable padding ran ~180px past the fold on a laptop, so the two buttons that
1760
+ answer them were off screen — and the fix for that (a sticky footer) put a translucent
1761
+ bar over the step's own 000 link. The step is the thing that had to give. Every number
1762
+ below is tighter than the kit's default card rhythm on purpose; the READING measures
1763
+ (`line-height`, font size) are untouched, because compressing those would trade a
1764
+ layout problem for a comprehension one on the only screen in the funnel that decides
1765
+ whether an online consult is safe. */
1766
+ .sk-pf__statements {
1767
+ list-style: none;
1768
+ padding: 0;
1769
+ margin: 0;
1770
+ display: grid;
1771
+ gap: var(--sk-space);
1772
+ }
1773
+ /* Each statement is a card, not a bullet: it is a thing being ATTESTED to, and it
1774
+ should carry the visual weight of one. */
1775
+ .sk-pf__statement {
1776
+ display: flex;
1777
+ align-items: flex-start;
1778
+ gap: calc(var(--sk-space) * 1.5);
1779
+ padding: calc(var(--sk-space) * 1.25) calc(var(--sk-space) * 1.5);
1780
+ border: 1px solid var(--sk-flow-line);
1781
+ border-radius: var(--sk-island-radius);
1782
+ background: var(--sk-flow-raised);
1783
+ }
1784
+ .sk-pf__num {
1785
+ flex: none;
1786
+ display: grid;
1787
+ place-items: center;
1788
+ width: 1.5rem;
1789
+ height: 1.5rem;
1790
+ margin-top: 0.1em;
1791
+ border-radius: 999px;
1792
+ background: color-mix(in srgb, var(--sk-island-primary) 22%, transparent);
1793
+ color: inherit;
1794
+ font-size: 0.75rem;
1795
+ font-weight: 700;
1796
+ font-variant-numeric: tabular-nums;
1797
+ }
1798
+ .sk-pf__statement-body {
1799
+ min-width: 0;
1800
+ }
1801
+ .sk-pf__statement-title {
1802
+ font-weight: 600;
1803
+ line-height: 1.35;
1804
+ }
1805
+ .sk-flow .sk-pf__statement-detail {
1806
+ margin-top: calc(var(--sk-space) * 0.375);
1807
+ color: var(--sk-flow-muted);
1808
+ font-size: var(--sk-font-size-sm);
1809
+ line-height: 1.5;
1810
+ }
1811
+
1812
+ .sk-pf__declines {
1813
+ list-style: none;
1814
+ padding: 0;
1815
+ margin: 0;
1816
+ display: grid;
1817
+ gap: calc(var(--sk-space) * 1.5);
1818
+ }
1819
+ .sk-pf__decline {
1820
+ display: flex;
1821
+ align-items: center;
1822
+ justify-content: space-between;
1823
+ gap: calc(var(--sk-space) * 2);
1824
+ width: 100%;
1825
+ padding: calc(var(--sk-space) * 2) calc(var(--sk-space) * 2.5);
1826
+ border: 1px solid var(--sk-flow-line);
1827
+ border-radius: var(--sk-island-radius);
1828
+ background: var(--sk-flow-raised);
1829
+ color: inherit;
1830
+ font: inherit;
1831
+ font-weight: 600;
1832
+ text-align: left;
1833
+ cursor: pointer;
1834
+ }
1835
+ .sk-pf__decline:hover {
1836
+ border-color: var(--sk-island-primary);
1837
+ }
1838
+ .sk-pf__decline:focus-visible {
1839
+ outline: 2px solid var(--sk-island-accent);
1840
+ outline-offset: 2px;
1841
+ }
1842
+
1843
+ /* The 000 escape hatch. Danger colours are ABSOLUTE, never derived: an emergency
1844
+ affordance must look the same on every clinic's canvas, and "reads as urgent" is
1845
+ not something to hand to a brand token. It is a link, not a button, because the
1846
+ only correct outcome is a phone call. */
1847
+ .sk-pf__emergency {
1848
+ display: flex;
1849
+ align-items: center;
1850
+ justify-content: center;
1851
+ /* 44px — the touch-target floor, matching `.sk-flow__btn`. The one control on this
1852
+ screen a patient may be reaching for while unwell must not be the smallest. */
1853
+ min-height: 2.75rem;
1854
+ margin-top: calc(var(--sk-space) * 1.5);
1855
+ padding-inline: calc(var(--sk-space) * 2);
1856
+ border: 1px solid color-mix(in srgb, #ef4444 55%, transparent);
1857
+ border-radius: var(--sk-island-radius);
1858
+ background: color-mix(in srgb, #ef4444 12%, transparent);
1859
+ color: inherit;
1860
+ font-size: var(--sk-font-size-sm);
1861
+ font-weight: 700;
1862
+ text-align: center;
1863
+ text-decoration: none;
1864
+ }
1865
+ .sk-pf__emergency:hover {
1866
+ background: color-mix(in srgb, #ef4444 20%, transparent);
1867
+ }
1868
+ .sk-pf__emergency:focus-visible {
1869
+ outline: 2px solid #ef4444;
1870
+ outline-offset: 2px;
1871
+ }
1872
+ /* The prerendered no-JS fallback: real content, always visible. Never opacity-0. */
1873
+ .sk-certificate-funnel__services,
1874
+ .sk-booking__types {
1875
+ list-style: none;
1876
+ padding: 0;
1877
+ margin: calc(var(--sk-space) * 2) 0;
1878
+ display: grid;
1879
+ gap: calc(var(--sk-space));
1880
+ font-size: var(--sk-font-size-sm);
1881
+ }
1882
+
1883
+ /* ── the escalation alert — full-bleed and unmissable by design ─────────────*/
1884
+ .sk-cf__escalate {
1885
+ position: fixed;
1886
+ inset: 0;
1887
+ z-index: 9999;
1888
+ display: flex;
1889
+ flex-direction: column;
1890
+ align-items: center;
1891
+ justify-content: center;
1892
+ gap: calc(var(--sk-space) * 2);
1893
+ padding: calc(var(--sk-space) * 6);
1894
+ overflow-y: auto;
1895
+ /* Self-painted overlay: pair the ink with the surface (a dark site would
1896
+ otherwise inherit white onto white — unacceptable on a safety surface). */
1897
+ background: var(--sk-background, #fff);
1898
+ color: var(--sk-foreground, #1b2733);
1899
+ text-align: center;
1900
+ }
1901
+ /* This surface is a bare <div> OUTSIDE `.sk-flow`, so the UA heading/paragraph margins
1902
+ the funnel resets everywhere else survive here — the one screen where they must not.
1903
+ With a flat 16px gap plus an h2's 0.83em UA margin, "EMERGENCY" sat ~70px above the
1904
+ headline it labels, further from "Call 000" than "Call 000" was from the instruction,
1905
+ so three fragments of ONE instruction read as three unrelated things. Zero the UA
1906
+ margins, then author the rhythm: eyebrow tight to its heading, one step of air before
1907
+ the body. */
1908
+ .sk-cf__escalate > * {
1909
+ margin: 0;
1910
+ }
1911
+ .sk-cf__escalate-eyebrow {
1912
+ margin-bottom: calc(var(--sk-space) * -1);
1913
+ }
1914
+ .sk-cf__escalate-eyebrow {
1915
+ font-size: 0.6875rem;
1916
+ font-weight: 700;
1917
+ letter-spacing: 0.2em;
1918
+ text-transform: uppercase;
1919
+ color: #b3261e;
1920
+ }
1921
+ .sk-cf__escalate-heading {
1922
+ font-size: clamp(2.5rem, 8vw, 4rem);
1923
+ line-height: 1;
1924
+ }
1925
+ /* Same programmatic focus as the step heading, and this screen especially must never
1926
+ show a browser default: it was painting a flat UA-BLUE box around "Call 000" on the
1927
+ one canvas whose colours are absolute and authored on purpose. */
1928
+ .sk-cf__escalate-heading:focus {
1929
+ outline: none;
1930
+ }
1931
+ .sk-cf__escalate-body,
1932
+ .sk-cf__escalate-note {
1933
+ max-width: 28rem;
1934
+ color: var(--sk-flow-muted, var(--sk-muted));
1935
+ }
1936
+ .sk-cf__escalate-cta {
1937
+ display: inline-flex;
1938
+ align-items: center;
1939
+ justify-content: center;
1940
+ min-height: 3.5rem;
1941
+ padding-inline: calc(var(--sk-space) * 5);
1942
+ border-radius: 999px;
1943
+ background: #b3261e;
1944
+ color: #fff;
1945
+ font-size: 1.25rem;
1946
+ font-weight: 700;
1947
+ text-decoration: none;
1948
+ }
1949
+ .sk-cf__escalate-alt {
1950
+ color: inherit;
1951
+ font-size: var(--sk-font-size-sm);
1952
+ }
1953
+
1954
+ /* ── payment ────────────────────────────────────────────────────────────────*/
1955
+ .sk-pay__form {
1956
+ display: grid;
1957
+ gap: calc(var(--sk-space) * 1.5);
1958
+ }
1959
+ .sk-pay__row {
1960
+ display: grid;
1961
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1962
+ gap: calc(var(--sk-space) * 1.5);
1963
+ }
1964
+ .sk-pay__label {
1965
+ display: block;
1966
+ margin-bottom: calc(var(--sk-space) * 0.75);
1967
+ font-size: var(--sk-font-size-sm);
1968
+ font-weight: 500;
1969
+ }
1970
+ .sk-pay__mock {
1971
+ padding: calc(var(--sk-space) * 2);
1972
+ border: 1px dashed var(--sk-flow-line, color-mix(in srgb, var(--sk-muted) 40%, transparent));
1973
+ border-radius: var(--sk-island-radius);
1974
+ font-size: var(--sk-font-size-sm);
1975
+ color: var(--sk-flow-muted, var(--sk-muted));
1976
+ }
1977
+ /* The "who takes your card" line (PAT-853). No `margin-top`: it is a `.sk-pay__form`
1978
+ grid child and already sits on the field gap — adding one double-spaced it. Muted via
1979
+ `--sk-flow-muted` rather than the flat `--sk-muted` the status line uses, because this
1980
+ sits on the CLINIC's canvas and a fixed grey is unreadable on half of them (8.47:1 at
1981
+ 14px on the dark canvas, measured). */
1982
+ /* The timezone qualifier on the slot day heading (PAT-853). Its own rule so it does not
1983
+ inherit the heading's 600 — the DATE is the heading, the zone is the fine print that
1984
+ stops a Perth patient reading "09:00" as their own 09:00. */
1985
+ .sk-slots__heading-tz {
1986
+ font-weight: 500;
1987
+ color: var(--sk-flow-muted, var(--sk-muted));
1988
+ font-size: var(--sk-font-size-sm);
1989
+ }
1990
+ .sk-pay__secure {
1991
+ display: flex;
1992
+ align-items: flex-start;
1993
+ gap: calc(var(--sk-space) * 0.75);
1994
+ font-size: var(--sk-font-size-sm);
1995
+ line-height: 1.45;
1996
+ color: var(--sk-flow-muted, var(--sk-muted));
1997
+ }
1998
+ .sk-pay__secure-icon {
1999
+ flex: none;
2000
+ margin-top: 0.2em;
2001
+ }
2002
+ .sk-pay__status,
2003
+ .sk-pay__loading {
2004
+ margin-top: calc(var(--sk-space));
2005
+ font-size: 0.75rem;
2006
+ /* `--sk-muted` is a FIXED grey (#5b6b7a) — 3.14:1 on the PocketDoc canvas, i.e. the
2007
+ "Placing hold…" microcopy on the money screen was under AA. Derived like every other
2008
+ secondary ink in the flow so it tracks whatever canvas the island landed on. */
2009
+ color: var(--sk-flow-muted, var(--sk-muted));
2010
+ }
2011
+ .sk-pay__actions {
2012
+ margin-top: calc(var(--sk-space) * 2);
2013
+ }
2014
+ .sk-turnstile {
2015
+ margin-top: calc(var(--sk-space) * 2);
2016
+ }
2017
+
2018
+ /* ── slots ──────────────────────────────────────────────────────────────────*/
2019
+ .sk-slots__days {
2020
+ display: flex;
2021
+ gap: calc(var(--sk-space));
2022
+ overflow-x: auto;
2023
+ padding-bottom: calc(var(--sk-space));
2024
+ }
2025
+ .sk-slots__day {
2026
+ flex-shrink: 0;
2027
+ padding: calc(var(--sk-space)) calc(var(--sk-space) * 2);
2028
+ border: 1px solid color-mix(in srgb, var(--sk-muted) 35%, transparent);
2029
+ border-radius: 999px;
2030
+ background: none;
2031
+ /* See `.sk-booking__option` — without this the day chips render `buttontext` on the
2032
+ clinic's canvas, i.e. invisible on a dark site, on the screen where the patient
2033
+ picks their appointment. */
2034
+ color: inherit;
2035
+ font: inherit;
2036
+ font-size: var(--sk-font-size-sm);
2037
+ font-weight: 600;
2038
+ white-space: nowrap;
2039
+ cursor: pointer;
2040
+ }
2041
+ .sk-slots__day--active {
2042
+ border-color: var(--sk-island-primary);
2043
+ background: color-mix(in srgb, var(--sk-island-primary) 10%, transparent);
2044
+ }
2045
+ .sk-slots__heading {
2046
+ margin-top: calc(var(--sk-space) * 2);
2047
+ font-size: var(--sk-font-size-sm);
2048
+ font-weight: 600;
2049
+ }
2050
+ .sk-slots__grid {
2051
+ margin-top: calc(var(--sk-space) * 1.5);
2052
+ display: grid;
2053
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2054
+ gap: calc(var(--sk-space));
2055
+ }
2056
+ .sk-slots__time {
2057
+ padding: calc(var(--sk-space) * 1.5);
2058
+ border: 1px solid color-mix(in srgb, var(--sk-muted) 30%, transparent);
2059
+ border-radius: var(--sk-island-radius);
2060
+ background: none;
2061
+ /* Same as `.sk-slots__day`: every time label inherits from here. */
2062
+ color: inherit;
2063
+ font: inherit;
2064
+ cursor: pointer;
2065
+ }
2066
+ .sk-slots__time--selected {
2067
+ border-color: var(--sk-island-primary);
2068
+ background: color-mix(in srgb, var(--sk-island-primary) 12%, transparent);
2069
+ }
2070
+ /* An advisory hold: dimmed as a hint, still clickable — the booking call decides. */
2071
+ .sk-slots__time--held {
2072
+ opacity: 0.5;
2073
+ }
2074
+ .sk-slots__time-label {
2075
+ display: block;
2076
+ font-weight: 600;
2077
+ font-variant-numeric: tabular-nums;
2078
+ }
2079
+ .sk-slots__time-note {
2080
+ display: block;
2081
+ margin-top: 2px;
2082
+ font-size: 0.6875rem;
2083
+ color: var(--sk-flow-muted, var(--sk-muted));
2084
+ }
2085
+ .sk-slots__confirm {
2086
+ /* Sticky at EVERY width: a long diary (10+ times = 4+ grid rows) pushes this CTA
2087
+ below the fold on a 900px-tall desktop viewport too. What is width-conditional is
2088
+ the SCRIM, below — at 1440 the whole step fits, the bar is never stuck, and the
2089
+ veil painted a square-cornered slab across the column with an 8px halo round the
2090
+ button on the last screen before the patient commits a time and a card hold. When
2091
+ it IS stuck at desktop width the full-width opaque `.sk-flow__btn--primary` fill
2092
+ does the occluding on its own. */
2093
+ position: sticky;
2094
+ bottom: 0;
2095
+ margin-top: calc(var(--sk-space) * 2);
2096
+ padding-block: calc(var(--sk-space) * 1.5);
2097
+ color: inherit;
2098
+ }
2099
+ /* Below the rail breakpoint (`@media (min-width: 900px)`, line 868) the rail is stacked
2100
+ under the step, so the bar owns the full column and is a genuine bottom bar: it bleeds
2101
+ to the flow's own 1.5rem inset (`.sk-flow { padding-inline: calc(var(--sk-space) * 3) }`,
2102
+ line ~628 — `.sk-flow__body` adds none, so this lands exactly on the flow's border box)
2103
+ and carries a hairline so its top edge reads as an edge rather than a seam. NOT above
2104
+ 900px: the flow is railed there (`minmax(0, 1fr) 17rem` + a 2rem gap), so the same bleed
2105
+ would poke into the gutter and stop ~8px short of the rail's divider.
2106
+ The veil is still derived from `currentColor`, never from `--sk-background`: the island
2107
+ cannot know the clinic's canvas (PAT-834/838). */
2108
+ @media (max-width: 899.98px) {
2109
+ .sk-slots__confirm {
2110
+ margin-inline: calc(var(--sk-space) * -3);
2111
+ padding-inline: calc(var(--sk-space) * 3);
2112
+ border-top: 1px solid
2113
+ var(--sk-flow-line, color-mix(in srgb, currentColor 24%, transparent));
2114
+ background: color-mix(in srgb, currentColor 8%, transparent);
2115
+ backdrop-filter: blur(12px);
2116
+ -webkit-backdrop-filter: blur(12px);
2117
+ }
2118
+ }
2119
+ .sk-slots--empty {
2120
+ padding: calc(var(--sk-space) * 4);
2121
+ border: 1px solid color-mix(in srgb, var(--sk-muted) 25%, transparent);
2122
+ border-radius: var(--sk-island-radius);
2123
+ text-align: center;
2124
+ }
2125
+ /* The escapes on an empty diary (PAT-853). Stacked, not a row: on a 390px screen a row
2126
+ of three would each be too narrow to read, and they are not equal-weight choices. */
2127
+ .sk-slots__empty-actions {
2128
+ display: grid;
2129
+ gap: calc(var(--sk-space));
2130
+ margin-top: calc(var(--sk-space) * 2);
2131
+ text-align: left;
2132
+ }
2133
+ .sk-slots__empty-note {
2134
+ color: var(--sk-flow-muted, var(--sk-muted));
2135
+ font-size: var(--sk-font-size-sm);
2136
+ }
2137
+ .sk-slots__empty-title {
2138
+ font-weight: 600;
2139
+ }
2140
+ .sk-slots__empty-body {
2141
+ margin-top: calc(var(--sk-space));
2142
+ color: var(--sk-muted);
2143
+ font-size: var(--sk-font-size-sm);
2144
+ }
2145
+
2146
+ @media (max-width: 560px) {
2147
+ .sk-identity__grid,
2148
+ .sk-cf__row,
2149
+ .sk-pay__row {
2150
+ grid-template-columns: minmax(0, 1fr);
2151
+ }
2152
+ .sk-identity__field--wide {
2153
+ grid-column: auto;
2154
+ }
2155
+ /* Stacked, the row's 16px row-gap ADDS to the 12px `margin-block` each `.sk-cf__field`
2156
+ already carries (grid item margins never collapse), so the Expiry group sat 40px below
2157
+ the Reference number group against 24px everywhere else in the form. One column needs
2158
+ no row-gap — the fields' own margins are the rhythm. Scoped to `.sk-cf__row`, not the
2159
+ group above: `.sk-identity__grid` / `.sk-pay__row` children space themselves differently. */
2160
+ .sk-cf__row {
2161
+ row-gap: 0;
2162
+ }
2163
+ /* Stack the label above the value: at phone width a 104px label column leaves too
2164
+ little for an address line, and a side-by-side row is what forced the overflow. */
2165
+ .sk-identity__confirm-row {
2166
+ display: grid;
2167
+ grid-template-columns: minmax(0, 1fr);
2168
+ gap: 2px;
2169
+ }
2170
+ }
2171
+
2172
+
2173
+ /* ── portal controls, shared by portal-panel + portal-account (PAT-783) ───── */
2174
+ /* ONE label / input / button style across both portal islands. They render the
2175
+ * same kinds of control on the same origin, and two sets of styles is how a
2176
+ * patient starts to feel the seam between "the clinic's site" and "the portal" —
2177
+ * which is the exact seam one-origin exists to remove. */
2178
+ .sk-portal-label {
2179
+ font-size: var(--sk-font-size-sm, 0.875rem);
2180
+ font-weight: 600;
2181
+ }
2182
+
2183
+ .sk-portal-input {
2184
+ font: inherit;
2185
+ width: 100%;
2186
+ padding: var(--sk-space, 0.5rem);
2187
+ border: 1px solid color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 20%, transparent);
2188
+ border-radius: var(--sk-radius, 0.625rem);
2189
+ background: var(--sk-background, #ffffff);
2190
+ color: var(--sk-foreground, #1b2733);
2191
+ }
2192
+
2193
+ .sk-portal-button {
2194
+ display: inline-block;
2195
+ font: inherit;
2196
+ font-weight: 600;
2197
+ text-align: center;
2198
+ text-decoration: none;
2199
+ cursor: pointer;
2200
+ padding: var(--sk-space, 0.5rem) calc(var(--sk-space, 0.5rem) * 2);
2201
+ border: 1px solid transparent;
2202
+ border-radius: var(--sk-radius, 0.625rem);
2203
+ /* `--sk-brand-foreground` is DERIVED by contrast (`resolveTheme`), never taken
2204
+ * from the clinic — see contrast.ts. */
2205
+ background: var(--sk-brand, #1f6feb);
2206
+ color: var(--sk-brand-foreground, #ffffff);
2207
+ }
2208
+
2209
+ .sk-portal-button--ghost {
2210
+ background: transparent;
2211
+ color: var(--sk-brand, #1f6feb);
2212
+ border-color: color-mix(in srgb, var(--sk-brand, #1f6feb) 40%, transparent);
2213
+ }
2214
+
2215
+ .sk-portal-button--danger {
2216
+ background: var(--sk-danger, #b3261e);
2217
+ color: #ffffff;
2218
+ border-color: transparent;
2219
+ }
2220
+
2221
+ .sk-portal-button[disabled] {
2222
+ opacity: 0.6;
2223
+ cursor: default;
2224
+ }
2225
+
2226
+ .sk-portal-link {
2227
+ color: var(--sk-brand, #1f6feb);
2228
+ font-weight: 600;
2229
+ text-decoration: underline;
2230
+ }
2231
+
2232
+ .sk-portal-error {
2233
+ margin: var(--sk-space, 0.5rem) 0;
2234
+ font-size: var(--sk-font-size-sm, 0.875rem);
2235
+ color: var(--sk-danger, #b3261e);
2236
+ }
2237
+
2238
+ /* The sign-in ramp. One field + one button — anything more belongs in the portal. */
2239
+ .sk-portal-ramp {
2240
+ display: flex;
2241
+ flex-direction: column;
2242
+ gap: var(--sk-space, 0.5rem);
2243
+ }
2244
+
2245
+ .sk-portal-ramp__intro,
2246
+ .sk-portal-ramp__sent {
2247
+ margin: 0;
2248
+ font-size: var(--sk-font-size-sm, 0.875rem);
2249
+ color: var(--sk-muted, #5b6b7a);
2250
+ }
2251
+
2252
+ /* ── portal-panel (PAT-767) ──────────────────────────────────────────────── */
2253
+ .sk-portal-panel {
2254
+ background: var(--sk-neutral, #f4f6f8);
2255
+ border-radius: var(--sk-radius, 0.625rem);
2256
+ padding: calc(var(--sk-space, 0.5rem) * 3);
2257
+ font-family: var(--sk-font, system-ui, sans-serif);
2258
+ color: var(--sk-neutral-foreground, #1b2733);
2259
+ }
2260
+
2261
+ .sk-portal-panel__heading {
2262
+ font-family: var(--sk-font-heading, var(--sk-font, system-ui, sans-serif));
2263
+ font-size: var(--sk-font-size-lg, 1.25rem);
2264
+ line-height: 1.3;
2265
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2266
+ }
2267
+
2268
+ .sk-portal-panel__placeholder,
2269
+ .sk-portal-panel__empty {
2270
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2271
+ font-size: var(--sk-font-size-sm, 0.875rem);
2272
+ color: var(--sk-muted, #5b6b7a);
2273
+ }
2274
+
2275
+ .sk-portal-panel__link {
2276
+ color: var(--sk-brand, #1f6feb);
2277
+ font-weight: 600;
2278
+ text-decoration: underline;
2279
+ }
2280
+
2281
+ /* The summary list: metadata only (title · date · status). Never bytes, never
2282
+ * clinical narrative — the panel is a teaser that links into the real portal. */
2283
+ .sk-portal-panel__list {
2284
+ list-style: none;
2285
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2286
+ padding: 0;
2287
+ display: flex;
2288
+ flex-direction: column;
2289
+ gap: var(--sk-space, 0.5rem);
2290
+ }
2291
+
2292
+ .sk-portal-panel__item {
2293
+ display: flex;
2294
+ flex-wrap: wrap;
2295
+ align-items: baseline;
2296
+ gap: calc(var(--sk-space, 0.5rem) * 0.75);
2297
+ padding-bottom: var(--sk-space, 0.5rem);
2298
+ border-bottom: 1px solid
2299
+ color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 12%, transparent);
2300
+ }
2301
+
2302
+ .sk-portal-panel__item:last-child {
2303
+ border-bottom: 0;
2304
+ padding-bottom: 0;
2305
+ }
2306
+
2307
+ .sk-portal-panel__item-title {
2308
+ font-weight: 600;
2309
+ }
2310
+
2311
+ .sk-portal-panel__item-meta {
2312
+ font-size: var(--sk-font-size-sm, 0.875rem);
2313
+ color: var(--sk-muted, #5b6b7a);
2314
+ }
2315
+
2316
+ .sk-portal-panel__item-status {
2317
+ margin-left: auto;
2318
+ font-size: var(--sk-font-size-sm, 0.875rem);
2319
+ text-transform: capitalize;
2320
+ color: var(--sk-muted, #5b6b7a);
2321
+ }
2322
+
2323
+ /* ── portal-account (PAT-783) ────────────────────────────────────────────── */
2324
+ /* The FULL account surface, living inside the clinic's own page. Calm and dense:
2325
+ * this is a patient managing their care, not a marketing band, so it reads as a
2326
+ * quiet card rather than competing with the page around it. Controls come from
2327
+ * the shared `.sk-portal-*` primitives above. */
2328
+ .sk-portal-account {
2329
+ background: var(--sk-neutral, #f4f6f8);
2330
+ border-radius: var(--sk-radius, 0.625rem);
2331
+ padding: calc(var(--sk-space, 0.5rem) * 3);
2332
+ font-family: var(--sk-font, system-ui, sans-serif);
2333
+ color: var(--sk-neutral-foreground, #1b2733);
2334
+ }
2335
+
2336
+ .sk-portal-account__heading {
2337
+ font-family: var(--sk-font-heading, var(--sk-font, system-ui, sans-serif));
2338
+ font-size: var(--sk-font-size-lg, 1.25rem);
2339
+ line-height: 1.3;
2340
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2341
+ }
2342
+
2343
+ .sk-portal-account__placeholder,
2344
+ .sk-portal-account__empty,
2345
+ .sk-portal-account__note,
2346
+ .sk-portal-account__hint {
2347
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2348
+ font-size: var(--sk-font-size-sm, 0.875rem);
2349
+ color: var(--sk-muted, #5b6b7a);
2350
+ }
2351
+
2352
+ /* ── the /account/book launcher (PAT-899) ───────────────────────────────────
2353
+ * Rows that LEAVE for the funnel, so they are anchors and styled as such. They live in
2354
+ * the light account panel and must not borrow the funnel's dark canvas — the two
2355
+ * surfaces are deliberately different places, which is the whole reason the funnel was
2356
+ * not mounted here. Colours come from the same `--sk-*` slots as the rest of the panel,
2357
+ * so a clinic's theme reaches them without a second vocabulary. */
2358
+ .sk-portal-launcher {
2359
+ list-style: none;
2360
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2361
+ padding: 0;
2362
+ display: flex;
2363
+ flex-direction: column;
2364
+ gap: var(--sk-space, 0.5rem);
2365
+ }
2366
+
2367
+ .sk-portal-launcher__service {
2368
+ display: flex;
2369
+ align-items: center;
2370
+ justify-content: space-between;
2371
+ gap: calc(var(--sk-space, 0.5rem) * 2);
2372
+ width: 100%;
2373
+ padding: calc(var(--sk-space, 0.5rem) * 2);
2374
+ border: 1px solid var(--sk-border, #dce3ea);
2375
+ border-radius: var(--sk-radius, 0.625rem);
2376
+ background: var(--sk-surface, #fff);
2377
+ color: inherit;
2378
+ text-decoration: none;
2379
+ }
2380
+
2381
+ .sk-portal-launcher__service:hover,
2382
+ .sk-portal-launcher__service:focus-visible {
2383
+ border-color: var(--sk-primary, #0f766e);
2384
+ }
2385
+
2386
+ .sk-portal-launcher__body {
2387
+ display: flex;
2388
+ flex-direction: column;
2389
+ gap: calc(var(--sk-space, 0.5rem) * 0.5);
2390
+ /* The row can hold a long service name; let it wrap rather than push the chevron out. */
2391
+ min-width: 0;
2392
+ }
2393
+
2394
+ .sk-portal-launcher__label {
2395
+ font-weight: 600;
2396
+ }
2397
+
2398
+ .sk-portal-launcher__fee {
2399
+ font-size: var(--sk-font-size-sm, 0.875rem);
2400
+ color: var(--sk-muted, #5b6b7a);
2401
+ }
2402
+
2403
+ .sk-portal-launcher__chevron {
2404
+ flex: none;
2405
+ color: var(--sk-muted, #5b6b7a);
2406
+ }
2407
+
2408
+ /* The "already signed in on the last page" placeholder (PAT-797) — inert bars in the
2409
+ * shape of the rows about to land, shown INSTEAD of "Checking your sign-in…" when this
2410
+ * tab already knows. Deliberately UNANIMATED: a pulse would need a reduced-motion guard
2411
+ * to earn its keep, and it buys nothing here — this is on screen for a few hundred ms.
2412
+ * This is NOT the no-JS fallback below; that one is real content and must stay real. */
2413
+ .sk-portal-account__skeleton {
2414
+ display: flex;
2415
+ flex-direction: column;
2416
+ gap: var(--sk-space, 0.5rem);
2417
+ margin-bottom: calc(var(--sk-space, 0.5rem) * 2);
2418
+ }
2419
+
2420
+ .sk-portal-account__skeleton-row {
2421
+ height: 2.75rem;
2422
+ border-radius: var(--sk-radius, 0.625rem);
2423
+ background: color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 7%, transparent);
2424
+ }
2425
+
2426
+ /* The build-time no-JS fallback: REAL visible content, never a hidden skeleton. */
2427
+ .sk-portal-account__signin-card {
2428
+ display: flex;
2429
+ flex-direction: column;
2430
+ align-items: flex-start;
2431
+ gap: var(--sk-space, 0.5rem);
2432
+ padding: calc(var(--sk-space, 0.5rem) * 2);
2433
+ border: 1px solid color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 12%, transparent);
2434
+ border-radius: var(--sk-radius, 0.625rem);
2435
+ background: var(--sk-background, #ffffff);
2436
+ }
2437
+
2438
+ .sk-portal-account__signin-card .sk-portal-account__placeholder {
2439
+ margin: 0;
2440
+ }
2441
+
2442
+ /* Who you are + the way out, on one line. Always visible while signed in: a
2443
+ * patient looking at their own records on a clinic's marketing page must never
2444
+ * have to wonder whose session they are in. */
2445
+ .sk-portal-account__header {
2446
+ display: flex;
2447
+ flex-wrap: wrap;
2448
+ align-items: center;
2449
+ justify-content: space-between;
2450
+ gap: var(--sk-space, 0.5rem);
2451
+ margin-bottom: calc(var(--sk-space, 0.5rem) * 2);
2452
+ padding-bottom: var(--sk-space, 0.5rem);
2453
+ border-bottom: 1px solid
2454
+ color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 12%, transparent);
2455
+ }
2456
+
2457
+ .sk-portal-account__who {
2458
+ font-size: var(--sk-font-size-sm, 0.875rem);
2459
+ color: var(--sk-muted, #5b6b7a);
2460
+ }
2461
+
2462
+ .sk-portal-account__group {
2463
+ margin-bottom: calc(var(--sk-space, 0.5rem) * 3);
2464
+ }
2465
+
2466
+ .sk-portal-account__group-title {
2467
+ font-size: var(--sk-font-size-sm, 0.875rem);
2468
+ font-weight: 700;
2469
+ text-transform: uppercase;
2470
+ letter-spacing: 0.04em;
2471
+ color: var(--sk-muted, #5b6b7a);
2472
+ margin: 0 0 var(--sk-space, 0.5rem);
2473
+ }
2474
+
2475
+ .sk-portal-account__list {
2476
+ list-style: none;
2477
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2478
+ padding: 0;
2479
+ display: flex;
2480
+ flex-direction: column;
2481
+ gap: calc(var(--sk-space, 0.5rem) * 1.5);
2482
+ }
2483
+
2484
+ .sk-portal-account__item {
2485
+ display: flex;
2486
+ flex-direction: column;
2487
+ gap: calc(var(--sk-space, 0.5rem) * 0.5);
2488
+ padding: calc(var(--sk-space, 0.5rem) * 1.5);
2489
+ border: 1px solid color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 12%, transparent);
2490
+ border-radius: var(--sk-radius, 0.625rem);
2491
+ background: var(--sk-background, #ffffff);
2492
+ }
2493
+
2494
+ .sk-portal-account__item-head {
2495
+ display: flex;
2496
+ flex-wrap: wrap;
2497
+ align-items: baseline;
2498
+ gap: var(--sk-space, 0.5rem);
2499
+ }
2500
+
2501
+ /* An order row is a disclosure control, so it is a real <button>: keyboard and
2502
+ * screen-reader reachable, styled back down to look like the row it is. */
2503
+ .sk-portal-account__item-toggle {
2504
+ display: flex;
2505
+ flex-wrap: wrap;
2506
+ align-items: baseline;
2507
+ gap: var(--sk-space, 0.5rem);
2508
+ width: 100%;
2509
+ font: inherit;
2510
+ text-align: left;
2511
+ cursor: pointer;
2512
+ padding: 0;
2513
+ border: 0;
2514
+ background: none;
2515
+ color: inherit;
2516
+ }
2517
+
2518
+ .sk-portal-account__item-title {
2519
+ font-weight: 600;
2520
+ }
2521
+
2522
+ .sk-portal-account__item-meta {
2523
+ font-size: var(--sk-font-size-sm, 0.875rem);
2524
+ color: var(--sk-muted, #5b6b7a);
2525
+ }
2526
+
2527
+ .sk-portal-account__status {
2528
+ margin-left: auto;
2529
+ font-size: var(--sk-font-size-sm, 0.875rem);
2530
+ text-transform: capitalize;
2531
+ padding: 0 calc(var(--sk-space, 0.5rem) * 0.75);
2532
+ border-radius: 999px;
2533
+ background: color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 8%, transparent);
2534
+ color: var(--sk-muted, #5b6b7a);
2535
+ }
2536
+
2537
+ .sk-portal-account__actions {
2538
+ display: flex;
2539
+ flex-wrap: wrap;
2540
+ gap: var(--sk-space, 0.5rem);
2541
+ margin-top: var(--sk-space, 0.5rem);
2542
+ }
2543
+
2544
+ .sk-portal-account__actions .sk-portal-button {
2545
+ font-size: var(--sk-font-size-sm, 0.875rem);
2546
+ padding: calc(var(--sk-space, 0.5rem) * 0.5) var(--sk-space, 0.5rem);
2547
+ }
2548
+
2549
+ /* The inline cancel confirm. Expands in place rather than opening a dialog: the
2550
+ * patient is on the clinic's own page and a modal over a marketing layout is a
2551
+ * shape we cannot control. */
2552
+ .sk-portal-account__confirm {
2553
+ display: flex;
2554
+ flex-direction: column;
2555
+ gap: var(--sk-space, 0.5rem);
2556
+ margin-top: var(--sk-space, 0.5rem);
2557
+ padding: calc(var(--sk-space, 0.5rem) * 1.5);
2558
+ border-radius: var(--sk-radius, 0.625rem);
2559
+ border: 1px solid color-mix(in srgb, var(--sk-danger, #b3261e) 30%, transparent);
2560
+ background: color-mix(in srgb, var(--sk-danger, #b3261e) 5%, transparent);
2561
+ }
2562
+
2563
+ /* The SAME expanding block, without the danger tint: booking a time and moving a
2564
+ * time are ordinary things a patient does, and a red-edged card would read as a
2565
+ * warning about a decision that has nothing to warn about (PAT-789). */
2566
+ .sk-portal-account__confirm--neutral {
2567
+ border-color: color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 15%, transparent);
2568
+ background: var(--sk-neutral, #f4f6f8);
2569
+ }
2570
+
2571
+ .sk-portal-account__confirm-title {
2572
+ margin: 0;
2573
+ font-weight: 600;
2574
+ }
2575
+
2576
+ /* Back + what you are booking, on one line above the slot grid. The step label is
2577
+ * the answer to "a time for what?", which is otherwise off the top of the screen
2578
+ * once the grid scrolls. */
2579
+ .sk-portal-account__step-head {
2580
+ display: flex;
2581
+ flex-wrap: wrap;
2582
+ align-items: center;
2583
+ gap: var(--sk-space, 0.5rem);
2584
+ margin-bottom: var(--sk-space, 0.5rem);
2585
+ }
2586
+
2587
+ /* "No times available" and the like: a bordered card, not a bare grey line — an
2588
+ * empty grid is a dead end and should look like a considered answer. */
2589
+ .sk-portal-account__note-card {
2590
+ padding: calc(var(--sk-space, 0.5rem) * 2);
2591
+ border: 1px solid color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 12%, transparent);
2592
+ border-radius: var(--sk-radius, 0.625rem);
2593
+ background: var(--sk-background, #ffffff);
2594
+ text-align: center;
2595
+ }
2596
+
2597
+ .sk-portal-account__note-card .sk-portal-account__empty {
2598
+ margin: var(--sk-space, 0.5rem) 0 0;
2599
+ }
2600
+
2601
+ /* The booked confirmation. It carries `role="status"`, so it is announced as well
2602
+ * as shown — a patient using a screen reader must not have to hunt for whether
2603
+ * the booking actually happened. */
2604
+ .sk-portal-account__done {
2605
+ display: flex;
2606
+ flex-direction: column;
2607
+ gap: var(--sk-space, 0.5rem);
2608
+ padding: calc(var(--sk-space, 0.5rem) * 2);
2609
+ border: 1px solid color-mix(in srgb, var(--sk-brand, #1f6feb) 35%, transparent);
2610
+ border-radius: var(--sk-radius, 0.625rem);
2611
+ background: color-mix(in srgb, var(--sk-brand, #1f6feb) 6%, transparent);
2612
+ }
2613
+
2614
+ .sk-portal-account__done .sk-portal-account__fields,
2615
+ .sk-portal-account__done .sk-portal-account__empty {
2616
+ margin-bottom: 0;
2617
+ }
2618
+
2619
+ /* The slot grid inside a portal card. The board's own `.sk-slots__*` rules do the
2620
+ * work; this only gives it room and stops the sticky confirm bar of the funnel
2621
+ * variant (which the portal does not render) from being missed. */
2622
+ .sk-portal-account__confirm .sk-slots,
2623
+ .sk-portal-account__body .sk-slots {
2624
+ margin: var(--sk-space, 0.5rem) 0;
2625
+ }
2626
+
2627
+ .sk-portal-account__reasons {
2628
+ display: flex;
2629
+ flex-direction: column;
2630
+ gap: calc(var(--sk-space, 0.5rem) * 0.5);
2631
+ margin: 0;
2632
+ padding: 0;
2633
+ border: 0;
2634
+ }
2635
+
2636
+ .sk-portal-account__reason {
2637
+ display: flex;
2638
+ align-items: center;
2639
+ gap: calc(var(--sk-space, 0.5rem) * 0.75);
2640
+ font-size: var(--sk-font-size-sm, 0.875rem);
2641
+ }
2642
+
2643
+ .sk-portal-account__confirm-actions {
2644
+ display: flex;
2645
+ flex-wrap: wrap;
2646
+ gap: var(--sk-space, 0.5rem);
2647
+ }
2648
+
2649
+ .sk-portal-account__detail {
2650
+ margin-top: var(--sk-space, 0.5rem);
2651
+ padding-top: var(--sk-space, 0.5rem);
2652
+ border-top: 1px dashed
2653
+ color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 15%, transparent);
2654
+ }
2655
+
2656
+ .sk-portal-account__lines {
2657
+ list-style: none;
2658
+ margin: 0 0 var(--sk-space, 0.5rem);
2659
+ padding: 0;
2660
+ display: flex;
2661
+ flex-direction: column;
2662
+ gap: calc(var(--sk-space, 0.5rem) * 0.5);
2663
+ }
2664
+
2665
+ .sk-portal-account__line {
2666
+ display: flex;
2667
+ justify-content: space-between;
2668
+ gap: var(--sk-space, 0.5rem);
2669
+ font-size: var(--sk-font-size-sm, 0.875rem);
2670
+ }
2671
+
2672
+ .sk-portal-account__total {
2673
+ margin: 0;
2674
+ font-weight: 700;
2675
+ text-align: right;
2676
+ }
2677
+
2678
+ /* The profile read view. A description list, so the label→value pairing is real
2679
+ * structure a screen reader can use rather than two spans that look aligned. */
2680
+ .sk-portal-account__fields {
2681
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 2);
2682
+ padding: 0;
2683
+ display: flex;
2684
+ flex-direction: column;
2685
+ gap: var(--sk-space, 0.5rem);
2686
+ }
2687
+
2688
+ .sk-portal-account__field {
2689
+ display: grid;
2690
+ grid-template-columns: minmax(8rem, 12rem) minmax(0, 1fr);
2691
+ gap: var(--sk-space, 0.5rem);
2692
+ }
2693
+
2694
+ .sk-portal-account__field-label {
2695
+ font-size: var(--sk-font-size-sm, 0.875rem);
2696
+ color: var(--sk-muted, #5b6b7a);
2697
+ }
2698
+
2699
+ .sk-portal-account__field-value {
2700
+ margin: 0;
2701
+ }
2702
+
2703
+ .sk-portal-account__form {
2704
+ display: flex;
2705
+ flex-direction: column;
2706
+ gap: var(--sk-space, 0.5rem);
2707
+ }
2708
+
2709
+ @media (max-width: 560px) {
2710
+ .sk-portal-account__field {
2711
+ grid-template-columns: minmax(0, 1fr);
2712
+ gap: 0;
2713
+ }
2714
+ }
2715
+
2716
+ /* ── store (PAT-766) ─────────────────────────────────────────────────────── */
2717
+ /* The app-served store shell (`/store` · `/cart` · `/checkout`). It lives in the
2718
+ * kit, not the dashboard's app.css, so a shopper crossing from the clinic's own
2719
+ * brochure into a page we serve does not feel like they left the clinic's site. */
2720
+ .sk-store-shell {
2721
+ min-height: 100vh;
2722
+ padding: calc(var(--sk-space, 0.5rem) * 3);
2723
+ }
2724
+ .sk-store-shell__header {
2725
+ display: flex;
2726
+ flex-wrap: wrap;
2727
+ align-items: center;
2728
+ justify-content: space-between;
2729
+ gap: calc(var(--sk-space, 0.5rem) * 2);
2730
+ max-width: 68rem;
2731
+ margin: 0 auto calc(var(--sk-space, 0.5rem) * 4);
2732
+ padding-bottom: calc(var(--sk-space, 0.5rem) * 2);
2733
+ border-bottom: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2734
+ }
2735
+ .sk-store-shell__brand {
2736
+ display: inline-flex;
2737
+ align-items: center;
2738
+ gap: calc(var(--sk-space, 0.5rem) * 1.5);
2739
+ color: inherit;
2740
+ font-weight: 700;
2741
+ text-decoration: none;
2742
+ }
2743
+ .sk-store-shell__logo {
2744
+ height: 2rem;
2745
+ width: auto;
2746
+ }
2747
+ .sk-store-shell__nav {
2748
+ display: flex;
2749
+ gap: calc(var(--sk-space, 0.5rem) * 3);
2750
+ }
2751
+ .sk-store-shell__nav a {
2752
+ color: var(--sk-brand, #1f6feb);
2753
+ text-decoration: none;
2754
+ }
2755
+ .sk-store-shell__nav a:hover {
2756
+ text-decoration: underline;
2757
+ }
2758
+ .sk-store-shell__main {
2759
+ max-width: 68rem;
2760
+ margin: 0 auto;
2761
+ }
2762
+ .sk-store__breadcrumb {
2763
+ margin-bottom: calc(var(--sk-space, 0.5rem) * 2);
2764
+ font-size: 0.875rem;
2765
+ }
2766
+ .sk-store__breadcrumb a {
2767
+ color: var(--sk-brand, #1f6feb);
2768
+ text-decoration: none;
2769
+ }
2770
+ .sk-store__header {
2771
+ display: flex;
2772
+ align-items: baseline;
2773
+ justify-content: space-between;
2774
+ gap: calc(var(--sk-space, 0.5rem) * 2);
2775
+ margin-bottom: calc(var(--sk-space, 0.5rem) * 3);
2776
+ }
2777
+ .sk-store__cart-link {
2778
+ color: var(--sk-brand, #1f6feb);
2779
+ font-weight: 600;
2780
+ text-decoration: none;
2781
+ white-space: nowrap;
2782
+ }
2783
+ .sk-store__cart-link:hover {
2784
+ text-decoration: underline;
2785
+ }
2786
+ .sk-store__grid {
2787
+ display: grid;
2788
+ gap: calc(var(--sk-space, 0.5rem) * 3);
2789
+ grid-template-columns: repeat(var(--sk-store-columns, 3), minmax(0, 1fr));
2790
+ list-style: none;
2791
+ margin: 0;
2792
+ padding: 0;
2793
+ }
2794
+ .sk-store__card {
2795
+ display: flex;
2796
+ flex-direction: column;
2797
+ gap: calc(var(--sk-space, 0.5rem) * 1.5);
2798
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2799
+ border-radius: var(--sk-radius, 0.625rem);
2800
+ padding: calc(var(--sk-space, 0.5rem) * 2);
2801
+ }
2802
+ .sk-store__card-link {
2803
+ display: flex;
2804
+ flex-direction: column;
2805
+ gap: calc(var(--sk-space, 0.5rem) * 1.5);
2806
+ color: inherit;
2807
+ text-decoration: none;
2808
+ }
2809
+ .sk-store__image {
2810
+ aspect-ratio: 1 / 1;
2811
+ width: 100%;
2812
+ border-radius: var(--sk-radius, 0.625rem);
2813
+ object-fit: cover;
2814
+ background: color-mix(in oklab, color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent) 40%, transparent);
2815
+ }
2816
+ .sk-store__image--empty {
2817
+ display: block;
2818
+ }
2819
+ .sk-store__title {
2820
+ font-weight: 600;
2821
+ line-height: 1.3;
2822
+ }
2823
+ .sk-store__price {
2824
+ font-variant-numeric: tabular-nums;
2825
+ font-weight: 600;
2826
+ }
2827
+ .sk-store__gst {
2828
+ font-size: 0.8125rem;
2829
+ font-weight: 400;
2830
+ opacity: 0.7;
2831
+ }
2832
+ .sk-store__variant {
2833
+ display: flex;
2834
+ flex-direction: column;
2835
+ gap: calc(var(--sk-space, 0.5rem) * 0.5);
2836
+ font-size: 0.875rem;
2837
+ }
2838
+ .sk-store__variant select {
2839
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2840
+ border-radius: var(--sk-radius, 0.625rem);
2841
+ padding: calc(var(--sk-space, 0.5rem) * 1);
2842
+ font: inherit;
2843
+ background: transparent;
2844
+ /* Form controls do not inherit colour either — a transparent one must name its ink
2845
+ or it renders the UA's `fieldtext` over whatever the theme painted behind it. */
2846
+ color: inherit;
2847
+ }
2848
+ .sk-store__add {
2849
+ margin-top: auto;
2850
+ border: 0;
2851
+ border-radius: var(--sk-radius, 0.625rem);
2852
+ background: var(--sk-brand, #1f6feb);
2853
+ color: var(--sk-brand-foreground, #fff);
2854
+ padding: calc(var(--sk-space, 0.5rem) * 1.25) calc(var(--sk-space, 0.5rem) * 2);
2855
+ font: inherit;
2856
+ font-weight: 600;
2857
+ cursor: pointer;
2858
+ }
2859
+ .sk-store__add:disabled {
2860
+ cursor: not-allowed;
2861
+ opacity: 0.5;
2862
+ }
2863
+ @media (max-width: 720px) {
2864
+ .sk-store__grid {
2865
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2866
+ }
2867
+ }
2868
+ @media (max-width: 480px) {
2869
+ .sk-store__grid {
2870
+ grid-template-columns: minmax(0, 1fr);
2871
+ }
2872
+ }
2873
+
2874
+
2875
+ /* ── cart (PAT-766) ──────────────────────────────────────────────────────── */
2876
+ .sk-cart__problems {
2877
+ margin: 0 0 calc(var(--sk-space, 0.5rem) * 3);
2878
+ padding: calc(var(--sk-space, 0.5rem) * 1.5) calc(var(--sk-space, 0.5rem) * 2)
2879
+ calc(var(--sk-space, 0.5rem) * 1.5) calc(var(--sk-space, 0.5rem) * 4);
2880
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2881
+ border-left-width: 3px;
2882
+ border-radius: var(--sk-radius, 0.625rem);
2883
+ font-size: 0.9375rem;
2884
+ }
2885
+ .sk-cart__lines {
2886
+ list-style: none;
2887
+ margin: 0;
2888
+ padding: 0;
2889
+ }
2890
+ .sk-cart__line {
2891
+ display: grid;
2892
+ grid-template-columns: 1fr auto auto auto;
2893
+ align-items: center;
2894
+ gap: calc(var(--sk-space, 0.5rem) * 2);
2895
+ padding: calc(var(--sk-space, 0.5rem) * 2) 0;
2896
+ border-bottom: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2897
+ }
2898
+ .sk-cart__line-title {
2899
+ color: inherit;
2900
+ font-weight: 600;
2901
+ text-decoration: none;
2902
+ }
2903
+ .sk-cart__line-unit {
2904
+ font-size: 0.875rem;
2905
+ opacity: 0.75;
2906
+ }
2907
+ .sk-cart__qty {
2908
+ display: inline-flex;
2909
+ align-items: center;
2910
+ gap: calc(var(--sk-space, 0.5rem) * 1);
2911
+ }
2912
+ .sk-cart__qty button {
2913
+ min-width: 2rem;
2914
+ min-height: 2rem;
2915
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2916
+ border-radius: var(--sk-radius, 0.625rem);
2917
+ background: transparent;
2918
+ /* See `.sk-booking__option`: a see-through <button> must name its ink. */
2919
+ color: inherit;
2920
+ font: inherit;
2921
+ cursor: pointer;
2922
+ }
2923
+ .sk-cart__line-total {
2924
+ font-variant-numeric: tabular-nums;
2925
+ font-weight: 600;
2926
+ }
2927
+ .sk-cart__remove {
2928
+ border: 0;
2929
+ background: none;
2930
+ color: inherit;
2931
+ font: inherit;
2932
+ font-size: 0.875rem;
2933
+ opacity: 0.75;
2934
+ cursor: pointer;
2935
+ text-decoration: underline;
2936
+ }
2937
+ .sk-cart__totals {
2938
+ margin: calc(var(--sk-space, 0.5rem) * 3) 0 0;
2939
+ margin-left: auto;
2940
+ max-width: 22rem;
2941
+ }
2942
+ .sk-cart__totals > div {
2943
+ display: flex;
2944
+ justify-content: space-between;
2945
+ gap: calc(var(--sk-space, 0.5rem) * 3);
2946
+ padding: calc(var(--sk-space, 0.5rem) * 0.75) 0;
2947
+ }
2948
+ .sk-cart__totals dt,
2949
+ .sk-cart__totals dd {
2950
+ margin: 0;
2951
+ }
2952
+ .sk-cart__totals dd {
2953
+ font-variant-numeric: tabular-nums;
2954
+ }
2955
+ .sk-cart__gst-row {
2956
+ font-size: 0.875rem;
2957
+ opacity: 0.75;
2958
+ }
2959
+ .sk-cart__total-row {
2960
+ border-top: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2961
+ font-size: 1.125rem;
2962
+ font-weight: 700;
2963
+ }
2964
+ .sk-cart__note {
2965
+ font-size: 0.875rem;
2966
+ opacity: 0.75;
2967
+ }
2968
+ .sk-cart__actions {
2969
+ display: flex;
2970
+ flex-wrap: wrap;
2971
+ align-items: center;
2972
+ justify-content: space-between;
2973
+ gap: calc(var(--sk-space, 0.5rem) * 2);
2974
+ margin-top: calc(var(--sk-space, 0.5rem) * 3);
2975
+ }
2976
+ .sk-cart__continue {
2977
+ color: var(--sk-brand, #1f6feb);
2978
+ text-decoration: none;
2979
+ }
2980
+ .sk-cart__continue:hover {
2981
+ text-decoration: underline;
2982
+ }
2983
+ .sk-cart__checkout {
2984
+ border-radius: var(--sk-radius, 0.625rem);
2985
+ background: var(--sk-brand, #1f6feb);
2986
+ color: var(--sk-brand-foreground, #fff);
2987
+ padding: calc(var(--sk-space, 0.5rem) * 1.5) calc(var(--sk-space, 0.5rem) * 3);
2988
+ font-weight: 600;
2989
+ text-decoration: none;
2990
+ }
2991
+ @media (max-width: 600px) {
2992
+ .sk-cart__line {
2993
+ grid-template-columns: 1fr auto;
2994
+ }
2995
+ }
2996
+
2997
+
2998
+ /* ── checkout (PAT-766) ──────────────────────────────────────────────────── */
2999
+ .sk-checkout__summary {
3000
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
3001
+ border-radius: var(--sk-radius, 0.625rem);
3002
+ padding: calc(var(--sk-space, 0.5rem) * 2);
3003
+ margin-bottom: calc(var(--sk-space, 0.5rem) * 3);
3004
+ }
3005
+ .sk-checkout__lines {
3006
+ list-style: none;
3007
+ margin: 0;
3008
+ padding: 0;
3009
+ }
3010
+ .sk-checkout__lines li {
3011
+ display: flex;
3012
+ justify-content: space-between;
3013
+ gap: calc(var(--sk-space, 0.5rem) * 2);
3014
+ padding: calc(var(--sk-space, 0.5rem) * 0.75) 0;
3015
+ }
3016
+ .sk-checkout__totals {
3017
+ margin: calc(var(--sk-space, 0.5rem) * 2) 0 0;
3018
+ border-top: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
3019
+ padding-top: calc(var(--sk-space, 0.5rem) * 1.5);
3020
+ }
3021
+ .sk-checkout__totals > div {
3022
+ display: flex;
3023
+ justify-content: space-between;
3024
+ gap: calc(var(--sk-space, 0.5rem) * 2);
3025
+ padding: calc(var(--sk-space, 0.5rem) * 0.5) 0;
3026
+ }
3027
+ .sk-checkout__totals dt,
3028
+ .sk-checkout__totals dd {
3029
+ margin: 0;
3030
+ }
3031
+ .sk-checkout__totals dd {
3032
+ font-variant-numeric: tabular-nums;
3033
+ }
3034
+ .sk-checkout__total-row {
3035
+ font-size: 1.125rem;
3036
+ font-weight: 700;
3037
+ }
3038
+ .sk-checkout__form {
3039
+ display: flex;
3040
+ flex-direction: column;
3041
+ gap: calc(var(--sk-space, 0.5rem) * 3);
3042
+ }
3043
+ .sk-checkout__fieldset {
3044
+ display: flex;
3045
+ flex-direction: column;
3046
+ gap: calc(var(--sk-space, 0.5rem) * 1.5);
3047
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
3048
+ border-radius: var(--sk-radius, 0.625rem);
3049
+ padding: calc(var(--sk-space, 0.5rem) * 2);
3050
+ }
3051
+ .sk-checkout__fieldset legend {
3052
+ font-weight: 600;
3053
+ padding-inline: calc(var(--sk-space, 0.5rem) * 0.5);
3054
+ }
3055
+ .sk-checkout__fieldset label {
3056
+ display: flex;
3057
+ flex-direction: column;
3058
+ gap: calc(var(--sk-space, 0.5rem) * 0.5);
3059
+ font-size: 0.9375rem;
3060
+ }
3061
+ .sk-checkout__fieldset input[type='text'],
3062
+ .sk-checkout__fieldset input:not([type]),
3063
+ .sk-checkout__fieldset select {
3064
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
3065
+ border-radius: var(--sk-radius, 0.625rem);
3066
+ padding: calc(var(--sk-space, 0.5rem) * 1.25);
3067
+ font: inherit;
3068
+ background: transparent;
3069
+ /* See `.sk-store__variant select`. */
3070
+ color: inherit;
3071
+ }
3072
+ .sk-checkout__ship {
3073
+ display: flex;
3074
+ flex-direction: column;
3075
+ gap: calc(var(--sk-space, 0.5rem) * 1.5);
3076
+ padding-top: calc(var(--sk-space, 0.5rem) * 1);
3077
+ }
3078
+ .sk-checkout__pay {
3079
+ border: 0;
3080
+ border-radius: var(--sk-radius, 0.625rem);
3081
+ background: var(--sk-brand, #1f6feb);
3082
+ color: var(--sk-brand-foreground, #fff);
3083
+ padding: calc(var(--sk-space, 0.5rem) * 1.75) calc(var(--sk-space, 0.5rem) * 3);
3084
+ font: inherit;
3085
+ font-size: 1.0625rem;
3086
+ font-weight: 700;
3087
+ cursor: pointer;
3088
+ }
3089
+ .sk-checkout__pay:disabled {
3090
+ cursor: progress;
3091
+ opacity: 0.6;
3092
+ }
3093
+ .sk-checkout__error {
3094
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
3095
+ border-left: 3px solid currentColor;
3096
+ border-radius: var(--sk-radius, 0.625rem);
3097
+ padding: calc(var(--sk-space, 0.5rem) * 1.5) calc(var(--sk-space, 0.5rem) * 2);
3098
+ margin-bottom: calc(var(--sk-space, 0.5rem) * 2);
3099
+ }
3100
+ .sk-checkout__note {
3101
+ font-size: 0.875rem;
3102
+ opacity: 0.8;
3103
+ }
3104
+ .sk-checkout__link,
3105
+ .sk-checkout__signin {
3106
+ display: inline-block;
3107
+ margin-top: calc(var(--sk-space, 0.5rem) * 2);
3108
+ color: var(--sk-brand, #1f6feb);
3109
+ font-weight: 600;
3110
+ text-decoration: none;
3111
+ }
3112
+ .sk-checkout__link:hover,
3113
+ .sk-checkout__signin:hover {
3114
+ text-decoration: underline;
3115
+ }
3116
+ /* The 3DS challenge frame. Fixed minimum height because the issuer's page is
3117
+ arbitrary and cross-origin — we cannot measure it, and a collapsed frame is a
3118
+ payment the shopper cannot complete. */
3119
+ .sk-checkout__3ds {
3120
+ width: 100%;
3121
+ min-height: 26rem;
3122
+ border: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
3123
+ border-radius: var(--sk-radius, 0.625rem);
3124
+ }
3125
+
3126
+ /* ── the telehealth consult room (PAT-792) ────────────────────────────────── */
3127
+ /* Styled HERE rather than in the lazy chunk, deliberately: the room's markup ships
3128
+ * in a separately-fetched bundle, but a patient must never watch an unstyled video
3129
+ * call assemble itself while that bundle lands. These rules are already in the
3130
+ * site's compiled `/app.css`, so the panel is dressed the instant it opens.
3131
+ *
3132
+ * The stage deliberately does NOT read `--sk-background`/`--sk-foreground`: it sits
3133
+ * over arbitrary video, and a clinic that themed its site light-on-light would make
3134
+ * the controls disappear. It is a fixed dark surface with white controls — the same
3135
+ * call the app's room makes on the dashboard's dark scrim. */
3136
+ .sk-consult {
3137
+ display: flex;
3138
+ flex-direction: column;
3139
+ gap: var(--sk-space, 0.5rem);
3140
+ width: 100%;
3141
+ margin-top: var(--sk-space, 0.5rem);
3142
+ padding: calc(var(--sk-space, 0.5rem) * 1.5);
3143
+ border: 1px solid color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 12%, transparent);
3144
+ border-radius: var(--sk-radius, 0.625rem);
3145
+ background: var(--sk-neutral, #f4f6f8);
3146
+ }
3147
+
3148
+ .sk-consult__head {
3149
+ display: flex;
3150
+ align-items: center;
3151
+ justify-content: space-between;
3152
+ gap: var(--sk-space, 0.5rem);
3153
+ }
3154
+
3155
+ .sk-consult__title {
3156
+ font-weight: 600;
3157
+ }
3158
+
3159
+ .sk-consult__head .sk-portal-button {
3160
+ font-size: var(--sk-font-size-sm, 0.875rem);
3161
+ padding: calc(var(--sk-space, 0.5rem) * 0.5) var(--sk-space, 0.5rem);
3162
+ }
3163
+
3164
+ /* The chunk's own React root mounts here. Sized by its contents. */
3165
+ .sk-consult__host:empty {
3166
+ display: none;
3167
+ }
3168
+
3169
+ .sk-consult__room {
3170
+ display: flex;
3171
+ flex-direction: column;
3172
+ }
3173
+
3174
+ .sk-consult__notice {
3175
+ display: flex;
3176
+ flex-direction: column;
3177
+ align-items: flex-start;
3178
+ gap: var(--sk-space, 0.5rem);
3179
+ padding: calc(var(--sk-space, 0.5rem) * 2) 0;
3180
+ }
3181
+
3182
+ .sk-consult__placeholder {
3183
+ color: var(--sk-muted, #5b6b7a);
3184
+ font-size: var(--sk-font-size-sm, 0.875rem);
3185
+ }
3186
+
3187
+ .sk-consult__body {
3188
+ position: relative;
3189
+ display: flex;
3190
+ flex-direction: column;
3191
+ }
3192
+
3193
+ /* The video area. `aspect-ratio` (not a fixed height) so it stays sane from a phone
3194
+ * to a desktop; `max-height: 70vh` so a wide viewport can't push the controls below
3195
+ * the fold on a page whose header we do not control. */
3196
+ .sk-consult__stage {
3197
+ position: relative;
3198
+ width: 100%;
3199
+ aspect-ratio: 16 / 9;
3200
+ max-height: 70vh;
3201
+ overflow: hidden;
3202
+ border-radius: var(--sk-radius, 0.625rem);
3203
+ background: #10161d;
3204
+ color: #ffffff;
3205
+ }
3206
+
3207
+ .sk-consult__stage .sk-consult__placeholder {
3208
+ display: flex;
3209
+ align-items: center;
3210
+ justify-content: center;
3211
+ height: 100%;
3212
+ padding: 0 calc(var(--sk-space, 0.5rem) * 2);
3213
+ text-align: center;
3214
+ color: rgba(255, 255, 255, 0.75);
3215
+ }
3216
+
3217
+ .sk-consult__video {
3218
+ width: 100%;
3219
+ height: 100%;
3220
+ object-fit: cover;
3221
+ }
3222
+
3223
+ .sk-consult__video--mirrored {
3224
+ transform: scaleX(-1);
3225
+ }
3226
+
3227
+ .sk-consult__remotes {
3228
+ display: grid;
3229
+ grid-template-columns: 1fr;
3230
+ gap: 2px;
3231
+ width: 100%;
3232
+ height: 100%;
3233
+ }
3234
+
3235
+ .sk-consult__remotes--many {
3236
+ grid-template-columns: 1fr 1fr;
3237
+ }
3238
+
3239
+ .sk-consult__remote {
3240
+ position: relative;
3241
+ min-height: 0;
3242
+ overflow: hidden;
3243
+ background: #10161d;
3244
+ }
3245
+
3246
+ /* The mirrored self-view. Small, out of the way, and labelled — an unlabelled second
3247
+ * tile reads as another person in the room. */
3248
+ .sk-consult__self {
3249
+ position: absolute;
3250
+ right: calc(var(--sk-space, 0.5rem) * 1.5);
3251
+ bottom: calc(var(--sk-space, 0.5rem) * 7);
3252
+ z-index: 10;
3253
+ display: flex;
3254
+ align-items: center;
3255
+ justify-content: center;
3256
+ width: 7rem;
3257
+ aspect-ratio: 16 / 9;
3258
+ overflow: hidden;
3259
+ border: 1px solid rgba(255, 255, 255, 0.25);
3260
+ border-radius: calc(var(--sk-radius, 0.625rem) * 0.6);
3261
+ background: #10161d;
3262
+ }
3263
+
3264
+ .sk-consult__self-starting {
3265
+ font-size: 0.6875rem;
3266
+ color: rgba(255, 255, 255, 0.7);
3267
+ }
3268
+
3269
+ .sk-consult__self-label {
3270
+ position: absolute;
3271
+ left: 0.25rem;
3272
+ bottom: 0.25rem;
3273
+ padding: 0 0.25rem;
3274
+ border-radius: 0.1875rem;
3275
+ background: rgba(0, 0, 0, 0.55);
3276
+ font-size: 0.6875rem;
3277
+ font-weight: 500;
3278
+ line-height: 1.4;
3279
+ color: #ffffff;
3280
+ }
3281
+
3282
+ /* "Waiting for your clinician to join…" over the patient's own camera. */
3283
+ .sk-consult__pill {
3284
+ position: absolute;
3285
+ left: var(--sk-space, 0.5rem);
3286
+ top: var(--sk-space, 0.5rem);
3287
+ z-index: 10;
3288
+ padding: 0.25rem 0.625rem;
3289
+ border-radius: 999px;
3290
+ background: rgba(0, 0, 0, 0.55);
3291
+ font-size: var(--sk-font-size-sm, 0.875rem);
3292
+ color: #ffffff;
3293
+ }
3294
+
3295
+ /* A fixed amber, not a token: this chip sits over arbitrary video where a themed
3296
+ * colour can land at any contrast. Colour is never alone — the word carries it. */
3297
+ .sk-consult__reconnecting {
3298
+ position: absolute;
3299
+ left: 50%;
3300
+ top: var(--sk-space, 0.5rem);
3301
+ z-index: 30;
3302
+ transform: translateX(-50%);
3303
+ padding: 0.25rem 0.625rem;
3304
+ border-radius: 999px;
3305
+ background: #f0a202;
3306
+ font-size: var(--sk-font-size-sm, 0.875rem);
3307
+ font-weight: 600;
3308
+ color: #1b2733;
3309
+ }
3310
+
3311
+ /* ALWAYS visible, unlike the app's hover-reveal bar: this is a patient on a touch
3312
+ * device who has never seen this surface before, and a control they must discover by
3313
+ * hovering is a control they do not have. */
3314
+ .sk-consult__controls {
3315
+ position: absolute;
3316
+ inset-inline: 0;
3317
+ bottom: 0;
3318
+ z-index: 20;
3319
+ display: flex;
3320
+ align-items: center;
3321
+ gap: 0.25rem;
3322
+ padding: calc(var(--sk-space, 0.5rem) * 3) var(--sk-space, 0.5rem) var(--sk-space, 0.5rem);
3323
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.25) 45%, transparent);
3324
+ }
3325
+
3326
+ .sk-consult__control {
3327
+ display: inline-flex;
3328
+ align-items: center;
3329
+ justify-content: center;
3330
+ width: 2.5rem;
3331
+ height: 2.5rem;
3332
+ padding: 0;
3333
+ border: 0;
3334
+ border-radius: calc(var(--sk-radius, 0.625rem) * 0.6);
3335
+ background: rgba(255, 255, 255, 0.12);
3336
+ color: rgba(255, 255, 255, 0.95);
3337
+ cursor: pointer;
3338
+ }
3339
+
3340
+ .sk-consult__control:hover {
3341
+ background: rgba(255, 255, 255, 0.24);
3342
+ }
3343
+
3344
+ .sk-consult__control:focus-visible {
3345
+ outline: 2px solid rgba(255, 255, 255, 0.85);
3346
+ outline-offset: 2px;
3347
+ }
3348
+
3349
+ /* Muted / camera-off. The GLYPH changes too — never colour alone. */
3350
+ .sk-consult__control.is-off {
3351
+ color: #ffb4ab;
3352
+ }
3353
+
3354
+ .sk-consult__control--leave {
3355
+ margin-inline-start: auto;
3356
+ background: var(--sk-danger, #b3261e);
3357
+ color: #ffffff;
3358
+ }
3359
+
3360
+ .sk-consult__control--leave:hover {
3361
+ background: color-mix(in srgb, var(--sk-danger, #b3261e) 85%, #000000);
3362
+ }
3363
+
3364
+ /* The device menu — a plain absolutely-positioned panel inside the stage, never a
3365
+ * portal: a portal renders into the clinic page's own <body>, escaping the island's
3366
+ * subtree and landing under whatever z-index that template happens to use. */
3367
+ .sk-consult__devices {
3368
+ position: absolute;
3369
+ left: var(--sk-space, 0.5rem);
3370
+ bottom: calc(var(--sk-space, 0.5rem) * 7);
3371
+ z-index: 30;
3372
+ width: 15rem;
3373
+ max-height: 70%;
3374
+ overflow-y: auto;
3375
+ padding: 0.25rem;
3376
+ border: 1px solid rgba(255, 255, 255, 0.15);
3377
+ border-radius: calc(var(--sk-radius, 0.625rem) * 0.6);
3378
+ background: rgba(0, 0, 0, 0.85);
3379
+ color: #ffffff;
3380
+ }
3381
+
3382
+ .sk-consult__device-group {
3383
+ padding: 0.25rem 0;
3384
+ }
3385
+
3386
+ .sk-consult__device-label {
3387
+ padding: 0 0.5rem 0.125rem;
3388
+ font-size: 0.625rem;
3389
+ font-weight: 700;
3390
+ letter-spacing: 0.04em;
3391
+ text-transform: uppercase;
3392
+ color: rgba(255, 255, 255, 0.5);
3393
+ }
3394
+
3395
+ .sk-consult__device-empty {
3396
+ padding: 0.25rem 0.5rem;
3397
+ font-size: 0.75rem;
3398
+ color: rgba(255, 255, 255, 0.5);
3399
+ }
3400
+
3401
+ .sk-consult__device {
3402
+ display: flex;
3403
+ align-items: center;
3404
+ gap: 0.375rem;
3405
+ width: 100%;
3406
+ padding: 0.25rem 0.5rem;
3407
+ border: 0;
3408
+ border-radius: 0.25rem;
3409
+ background: transparent;
3410
+ font: inherit;
3411
+ font-size: 0.75rem;
3412
+ text-align: start;
3413
+ color: rgba(255, 255, 255, 0.9);
3414
+ cursor: pointer;
3415
+ }
3416
+
3417
+ .sk-consult__device:hover,
3418
+ .sk-consult__device:focus-visible {
3419
+ background: rgba(255, 255, 255, 0.15);
3420
+ outline: none;
3421
+ }
3422
+
3423
+ .sk-consult__device-tick {
3424
+ flex: none;
3425
+ width: 0.75rem;
3426
+ height: 0.75rem;
3427
+ }
3428
+
3429
+ .sk-consult__device-tick.is-off {
3430
+ opacity: 0;
3431
+ }
3432
+
3433
+ .sk-consult__device-name {
3434
+ overflow: hidden;
3435
+ text-overflow: ellipsis;
3436
+ white-space: nowrap;
3437
+ }
3438
+
3439
+ /* Autoplay blocked the remote audio (a patient who arrived from a magic link gave the
3440
+ * document no gesture). One tap, centred over the stage. */
3441
+ .sk-consult__unblock {
3442
+ position: absolute;
3443
+ inset: 0;
3444
+ z-index: 30;
3445
+ display: flex;
3446
+ align-items: center;
3447
+ justify-content: center;
3448
+ pointer-events: none;
3449
+ }
3450
+
3451
+ .sk-consult__unblock .sk-portal-button {
3452
+ pointer-events: auto;
3453
+ }
3454
+
3455
+ /* ── Address picker (PAT-850) ─────────────────────────────────────────────────
3456
+ Address entry is Google-only: the visitor searches and CHOOSES, there are no
3457
+ street/suburb/state/postcode inputs left to style. Deliberately built from the
3458
+ same tokens as .sk-identity__* so a picker sitting inside the identity grid or
3459
+ the checkout form reads as one of its fields rather than a bolted-on widget. */
3460
+ .sk-address {
3461
+ position: relative;
3462
+ grid-column: 1 / -1;
3463
+ }
3464
+ .sk-address__label {
3465
+ display: block;
3466
+ margin-bottom: calc(var(--sk-space) * 0.75);
3467
+ font-size: var(--sk-font-size-sm);
3468
+ font-weight: 500;
3469
+ }
3470
+ .sk-address__input {
3471
+ width: 100%;
3472
+ min-height: 2.5rem;
3473
+ padding: calc(var(--sk-space)) calc(var(--sk-space) * 1.5);
3474
+ border: 1px solid color-mix(in srgb, var(--sk-muted) 40%, transparent);
3475
+ border-radius: var(--sk-island-radius);
3476
+ background: var(--sk-background, #fff);
3477
+ color: var(--sk-foreground, inherit);
3478
+ font: inherit;
3479
+ }
3480
+ .sk-address__input[aria-invalid='true'] {
3481
+ border-color: #b3261e;
3482
+ }
3483
+ /* The popup is positioned against the input's own box, not the field group. */
3484
+ .sk-address__box {
3485
+ position: relative;
3486
+ }
3487
+ /* The suggestion list overlays following fields rather than shoving them down —
3488
+ the form must not reflow under the visitor mid-type. */
3489
+ .sk-address__list {
3490
+ position: absolute;
3491
+ z-index: 20;
3492
+ inset-inline: 0;
3493
+ top: calc(100% + var(--sk-space) * 0.5);
3494
+ max-height: 15rem;
3495
+ overflow-y: auto;
3496
+ list-style: none;
3497
+ margin: 0;
3498
+ padding: calc(var(--sk-space) * 0.5);
3499
+ border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
3500
+ border-radius: var(--sk-island-radius);
3501
+ /* OPAQUE, in two layers, and this is the whole point of the rule (PAT-867).
3502
+ `--sk-flow-canvas` is the page's own backdrop, resolved to a concrete colour by
3503
+ `useResolvedSurface` in `flow-shell.tsx` — the island is still reading the clinic's
3504
+ canvas rather than inventing one, and it is NOT `--sk-background` (the kit's own
3505
+ light-theme token, which would paint white down a dark page — PAT-834/838). The
3506
+ `currentColor` tint on top is what lifts the popup off that canvas, and it is
3507
+ layered ONTO an opaque base rather than being the base: as the base it was
3508
+ translucent by construction, and the content underneath read straight through the
3509
+ list. A popup covers unrelated content — it may not share it. `white` is the
3510
+ fallback for a no-JS render, where no popup exists yet anyway. */
3511
+ background:
3512
+ linear-gradient(
3513
+ color-mix(in srgb, currentColor 10%, transparent),
3514
+ color-mix(in srgb, currentColor 10%, transparent)
3515
+ ),
3516
+ var(--sk-flow-canvas, white);
3517
+ box-shadow: 0 8px 24px color-mix(in srgb, currentColor 25%, transparent);
3518
+ }
3519
+ .sk-address__option {
3520
+ padding: calc(var(--sk-space) * 1.25) calc(var(--sk-space) * 1.5);
3521
+ border-radius: calc(var(--sk-island-radius) * 0.75);
3522
+ font-size: var(--sk-font-size-sm);
3523
+ line-height: 1.4;
3524
+ cursor: pointer;
3525
+ }
3526
+ /* Keyboard and pointer land on the SAME visual state — `aria-activedescendant` moves
3527
+ no real focus, so without this the arrow keys would move an invisible cursor. */
3528
+ .sk-address__option--active,
3529
+ .sk-address__option:hover {
3530
+ background: color-mix(in srgb, currentColor 16%, transparent);
3531
+ }
3532
+ /* Reading copy, so it takes the muted ink rather than a dimmer of the field's. */
3533
+ .sk-flow .sk-address__status {
3534
+ margin-top: calc(var(--sk-space) * 0.75);
3535
+ min-height: 1.2em;
3536
+ color: var(--sk-flow-muted, var(--sk-muted));
3537
+ font-size: 0.8125rem;
3538
+ line-height: 1.2;
3539
+ }
3540
+ .sk-address__empty {
3541
+ margin-top: calc(var(--sk-space) * 0.75);
3542
+ font-size: var(--sk-font-size-sm);
3543
+ }
3544
+ .sk-address__check {
3545
+ margin-top: calc(var(--sk-space) * 0.5);
3546
+ padding: calc(var(--sk-space) * 0.75) calc(var(--sk-space) * 1.5);
3547
+ border: 1px solid color-mix(in srgb, var(--sk-muted) 40%, transparent);
3548
+ border-radius: var(--sk-island-radius);
3549
+ background: transparent;
3550
+ color: inherit;
3551
+ font: inherit;
3552
+ cursor: pointer;
3553
+ }
3554
+ /* A picked address is settled text, not a field — the visual difference between
3555
+ "chosen" and "still editable" is the whole point of the control. */
3556
+ .sk-address--picked {
3557
+ padding: calc(var(--sk-space)) calc(var(--sk-space) * 1.5);
3558
+ border: 1px solid color-mix(in srgb, var(--sk-muted) 30%, transparent);
3559
+ border-radius: var(--sk-island-radius);
3560
+ background: color-mix(in srgb, var(--sk-muted) 8%, transparent);
3561
+ }
3562
+ .sk-address__chosen {
3563
+ margin: 0;
3564
+ overflow-wrap: anywhere;
3565
+ }
3566
+ .sk-address__change {
3567
+ margin-top: calc(var(--sk-space) * 0.5);
3568
+ padding: 0;
3569
+ border: 0;
3570
+ background: transparent;
3571
+ color: var(--sk-primary, inherit);
3572
+ font: inherit;
3573
+ text-decoration: underline;
3574
+ cursor: pointer;
3575
+ }
3576
+ .sk-address__error {
3577
+ margin-top: calc(var(--sk-space));
3578
+ color: #b3261e;
3579
+ font-size: var(--sk-font-size-sm);
3580
+ }