-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathglobals.css
More file actions
4992 lines (4447 loc) · 174 KB
/
Copy pathglobals.css
File metadata and controls
4992 lines (4447 loc) · 174 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
@import "tailwindcss";
/* Design-scratch routes have their own route-scoped utility sheet. Keeping
their rapidly growing utility vocabulary out of every production page cuts
the shared render-blocking stylesheet without changing mockup rendering. */
@source not "./mockups";
@source not "../components/**/*mockup*";
/* Opt-in v2 design-system token layer. Every rule is scoped to `.ckb-v2`, so
importing it cannot change any surface that does not carry the class. */
@import "./ckb-v2-tokens.css";
@custom-variant dark (&:where(.dark, .dark *));
/*
* Motion variants are redefined so the in-app Motion preference is authoritative
* in BOTH directions, without touching the ~164 `motion-reduce:`/`motion-safe:`
* call sites.
*
* data-motion absent -> "system": follow prefers-reduced-motion (unchanged)
* data-motion="reduced" -> explicit opt-OUT: suppress regardless of the OS
* data-motion="full" -> explicit opt-IN: animate even when the OS asks to reduce
*
* The opt-in exists because iOS "Reduce Motion" is commonly switched on to stop
* app-launch zoom rather than out of vestibular sensitivity, and it was silently
* freezing this app's answer-progress feedback on a physical iPhone (both Safari
* and the installed PWA) with no in-app way to get it back.
*/
@custom-variant motion-reduce {
@media (prefers-reduced-motion: reduce) {
html:not([data-motion="full"]) & {
@slot;
}
}
html[data-motion="reduced"] & {
@slot;
}
}
@custom-variant motion-safe {
@media (prefers-reduced-motion: no-preference) {
html:not([data-motion="reduced"]) & {
@slot;
}
}
@media (prefers-reduced-motion: reduce) {
html[data-motion="full"] & {
@slot;
}
}
}
/*
* Design tokens that generate Tailwind utilities.
* Colour ramps and per-theme values live in :root / .dark below and are
* consumed directly via var(), e.g. text-[color:var(--text-muted)], so
* .dark overrides cascade to every call site without a generated colour bridge.
*
* Radius usage rules:
* rounded-md chips, inner elements, metadata pills
* rounded-lg controls, inputs, buttons, cards, panels, drawers
* rounded-xl bottom sheets, dialogs, large shells
*
* Z-index ladder (Tailwind v4 has no z theme namespace; arbitrary values are
* the idiom — pick an existing rung, never invent a new number):
* 0–40 in-page layering: sticky headers, badges, count bubbles
* 60 app chrome: master search header, mode menus
* 80–85 document overlays: viewer fullscreen (80), table fullscreen (85)
* 95 popovers that must beat overlays (source preview)
* 100 modal layer — the Sheet primitive — and the skip link
* max mockup-only diagnostics; never in live routes
* New overlays go through components/ui/sheet.tsx rather than a new layer.
*/
@theme {
/* Named responsive breakpoint tokens (Task #336) */
--breakpoint-phone: 640px;
--breakpoint-tablet: 768px;
--breakpoint-desktop: 1024px;
/* The one narrow phone interval where the shared filter wordmark would make
a single-line control band wrap. The upper token is exclusive, so this is
exactly 414–429px. */
--breakpoint-filter-label-collapse: 414px;
--breakpoint-filter-label-restore: 430px;
/* Radius ladder: 4 · 6 · 10 · 12 · 16 · 20 — the 4px grid with two deliberate
half-steps. `sm` is 6px, for chips and pills that read as too heavy at 8.
`md` is 10px because it is the control rung, and it is the one value the v2
layer and the live layer had to agree on: `ckb-v2-tokens.css` sizes
controls, inputs and buttons at 10px, and leaving the live ladder at 8
meant every adopted surface silently changed shape (SPEC §4.6). Moving it
here instead moves every `rounded-md` at once, deliberately, with one
visual diff. `lg` used to be 10px, which collided with `xl` when it moved
to 12, so the whole upper ladder shifted a rung rather than collapsing two
names onto one value. */
--radius-xs: 0.25rem;
--radius-sm: 0.375rem;
--radius-md: 0.625rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.25rem;
--radius-phone-frame: 1.85rem;
/* Fully-round role (chips, pills, avatars). Call sites carried a bare
`, 9999px` fallback before this was declared. */
--radius-pill: 9999px;
/* Minimum interactive target (WCAG 2.5.5 / 2.5.8) — 48px, and the design
system's ONE tap knob (SPEC §4.10, DECISIONS §C2). It is declared here in
@theme because this is the layer that generates min-h-tap / h-tap / w-tap /
min-w-tap / size-tap; the v2 layer's --tap-min is a pure alias of it and
never carries its own value, so the app can never render a 44/48 split.
Semantic intent: "this is a touch target", not a pixel count.
NB: the min-h-12 sites now match the token rather than exceeding it — leave
them alone; no production target is ever reduced. A grid track sized to hold
a tap target uses var(--spacing-tap), never a copy of the number, or the
track and its child desync the next time this value moves. The one
deliberate exception is the phone composer, pinned to 44px below 431px so
the edge-to-edge dock keeps its height (see the max-width: 430px block,
which covers the composer input as well as the icon/send buttons). */
--spacing-tap: 3rem;
--spacing-phone-frame: 52.75rem;
/* Shared reading measure, wide mockup study canvas, and Factsheets phone geometry. */
--container-reading: 64rem;
--container-mockup-wide: 92rem;
--container-phone-frame: 24.375rem;
/* Specifier-map layout roles: jump target height, numbered step column, and
wide-screen detail rail. These stay together so the page cannot drift back
to unrelated one-off dimensions. */
--spacing-specifier-map-jump: 5.25rem;
--spacing-specifier-map-step-number: 2.25rem;
--spacing-specifier-map-aside: 22rem;
/* Mode-home composer slots reserve their settled portal geometry before the
client host adopts them. Without this structural reserve, Therapy moved the
entire action stack when hydration relocated the composer (CLS 0.248).
These are layout tokens, not a second v2 colour/theme source. */
/* Re-measured 2026-08-19 at 390px across six mode homes (therapy-compass,
dsm, forms, factsheets, specifiers, and the shared `/` home): every one
settles at 162px, not the 106px this reserved. The 56px difference is the
smart-search hint, which `UniversalSearchCommandSurface` renders INSIDE
this slot above the composer: a 48px hint row plus the 8px flex gap that
separates it from the composer (`gap-2` on `.universal-command-surface` —
the `gap-1` in that same expression is the bottom-dock placement, not this
slot). The earlier measurement counted only the composer pill plus the
privacy notice, before that hint existed. Under-reserving it meant the slot
grew on portal adoption and pushed the action and pill stacks down on every
ticket-bearing mode home (measured CLS: specifiers 0.055, dsm 0.047,
therapy 0.042, `/` 0.020, forms 0.019, factsheets 0.017). Both the hint row
and its gap are fixed-height on phone, so 162px is exact: the settled
layout is unchanged and only the pre-adoption reserve grows.
The default wide value remains 88px. Current production attribution at
1280–1440 measured the resulting initial shift below the 0.01 decision
threshold, so those profiles stay unchanged. The 640–1279px band has a
separate measured reserve and one-line prompt rail below: the old wrapping
surface grew 88→199px and produced 0.021676 CLS at 800px and 0.019870 at
1025px. */
--spacing-mode-home-composer-phone: 10.125rem;
--spacing-mode-home-composer-wide: 5.5rem;
/* Hero medallion. Scales continuously with the viewport the way --text-hero
does, instead of stepping 48→48→56 at sm/lg: the old steps served the
768–1023px tablet band the phone size while the heading beside it had
already grown to 34.56px. The 3rem floor is load-bearing — it must still
resolve to exactly 48px at 390px, where ui-tools.spec.ts pins the tile
across seven mode homes — and it holds that floor through 430px. The cap
is reached at 800px. */
--spacing-hero-medallion: clamp(3rem, 1.5rem + 4vw, 3.5rem);
/* Icon glyph size scale. Named spacing tokens so icon sizing is a documented
scale (size-icon-md is the 16px default) instead of raw `h-4 w-4` literals,
and so a step can carry a responsive variant: `size-icon-md sm:size-icon-lg`.
Generates size-icon-*, h-icon-*, w-icon-* exactly like --spacing-tap generates
size-tap. The `4.5` (18px) half-step these retire snaps to sm/md/lg per site.
NB: container tiles (iconTile h-9, empty-state tile h-10) are NOT glyphs and
stay on the integer spacing scale — this token is for the icon glyph itself. */
--spacing-icon-xs: 0.75rem; /* 12px — inline markers, dense metadata */
--spacing-icon-sm: 0.875rem; /* 14px — badge / chip / metadata icons */
--spacing-icon-md: 1rem; /* 16px — default: buttons, nav, fields */
--spacing-icon-lg: 1.25rem; /* 20px — header / primary controls */
--spacing-icon-xl: 1.5rem; /* 24px — hero / composer emphasis */
/* Active-filter count bubble on the search band. 17px is deliberately off
both the 4px grid and the icon scale above: it is a text-bearing box sized
to its own --text-2xs numeral plus the tinted ring, not a glyph, so
snapping it to 16px clips the digit and 20px reads as the solid alert disc
the pill is written to avoid. Held as a token because the raw value has
already leaked once — it reached mode-nav and a since-deleted therapy
filter sheet before the shared control absorbed those call sites (#275). */
--spacing-search-band-badge: 1.0625rem;
/* Type scale — sub-`xs` micro steps plus the 13–22px "between" steps the
dense clinical UI reaches for. These are intentionally SIZE-ONLY: unlike
Tailwind's default text-* tokens they carry no `--line-height` /
`--letter-spacing` companion, so `text-3xs` etc. set font-size alone and
drop in cleanly for the `text-[Npx]` arbitrary values they retire without
re-leading or re-tracking. This UI sets its own `leading-*` / `tracking-*`
per context (pills, tables, legends, eyebrow labels); a baked line-height
would be overridden at most call sites and silently shift the few without
one. Set leading/tracking explicitly where a step needs it — see
`eyebrowText` in ui-primitives.tsx for the uppercase eyebrow treatment
(`--text-2xs` used to bake that tracking in; it is now the neutral 11px
step and the tracking lives at the recipe/call site instead).
3xs 10px micro floor: dense metadata, legend labels, count
bubbles. An 8px `4xs` step existed here and is retired
— unreadable at any density in a clinical product, so
nothing below 10px is a legal step. ≤9px snaps here.
2xs 11px Chip `compact` / secondary labels (most common small size)
(xs 12 · sm 14 · base 16 come from Tailwind's default scale —
Chip `standard` uses xs / 12px; see docs/design-system/TOKENS.md)
sm-minus 13px one notch under sm — dense body/labels bigger than xs
base-minus 15px one notch under base — comfortable body just under 16
(lg 18 · xl 20 · 2xl 24 come from Tailwind's default scale)
lg-minus 17px one notch under lg — display titles a shade under 18
2xl-minus 22px one notch under 2xl — wide-screen nowrap headings */
--text-3xs: 0.625rem;
--text-2xs: 0.6875rem;
--text-sm-minus: 0.8125rem;
--text-base-minus: 0.9375rem;
--text-lg-minus: 1.0625rem;
--text-2xl-minus: 1.375rem;
--text-3xl-minus: 1.625rem;
/* Fluid display heading for the hero / mode-home titles (hybrid-fluid type:
the large display heading scales continuously; body / UI text stays on the
stepped scale above). The floor is deliberately 1.45rem so the phone
baseline (23.2px at ≤~393px) is unchanged, then it ramps to the 2.15rem cap
by ~1033px — no breakpoint jumps. Generates the `text-hero` utility.
clamp() font sizes are not flagged by scripts/check-type-scale.mjs. */
--text-hero: clamp(1.75rem, 1.2rem + 2vw, 2.25rem);
/* Companion line-height marks --text-hero as a font-SIZE token (Tailwind v4
`--text-<name>--line-height` convention) so downstream tooling never
misreads the clamp() value as a font-family list. Call sites that set an
explicit leading-* utility still win over this default. */
--text-hero--line-height: 1.12;
/* Named leading steps for the two contexts the stepped scale could not
express, so neither has to be spelled as an arbitrary value. Tailwind owns
--leading-tight/-snug/-normal/-relaxed; these do NOT redefine those (that
would silently retune every existing leading-tight/leading-snug call site).
leading-display 1.05 large display headings — hero/mode-home/detail h1
leading-prose 1.6 the max-w-[68ch] body measure */
--leading-display: 1.05;
--leading-prose: 1.65;
/* ---- Letterspacing scale -------------------------------------------------
Measured 7 Aug 2026, production only (`*-mockups.tsx` and `mockups/` are
design scratch and gate-exempt): 110 arbitrary `tracking-[…]` call sites
across 18 distinct values, against one named token. Counting mockups gives
372/31 — that is the number to quote about the *sprawl*, not about the debt
a gate can act on.
The real defect is sharper than sprawl. 57 of the 110 sites are a single
value, 0.06em — and `--tracking-eyebrow` is declared at 0.08em, so the one
named token does not match the value its own call sites overwhelmingly use.
That is why it has almost no consumers: the token was named for a number
nobody writes.
Named roles, with the measured production counts each one absorbs:
--tracking-display -0.02em folds: -0.008 (1), -0.01 (3), -0.035 (1), -0.04 (1)
--tracking-normal 0 folds: 0.01 (1), 0.02 (2)
--tracking-label 0.06em folds: 0.04 (3), 0.05 (6), 0.06 (57) <- the mass
--tracking-eyebrow 0.08em folds: 0.08 (8), 0.09 (1) [pre-existing, value unchanged]
--tracking-kicker 0.12em folds: 0.1 (7), 0.11 (1), 0.12 (14), 0.16 (1)
Five roles, not seven: production does not justify a second wide step or a
separate tight-display step — the negative cluster is six sites total. No
fold moves a call site by more than 0.04em, and most by 0.01–0.02em.
`--tracking-eyebrow` keeps 0.08em deliberately. Retuning it to 0.06em would
silently move every existing consumer of the token; that is a different
change from naming the scale, and wants its own measured step.
The `arbitraryTracking` contract ratchet holds the family at its current
per-path count (68 statically-resolvable sites across 40 files) so it cannot
regrow while the call-site migration waits on the open answer-shell and
doc-truth PRs. `tracking-[var(--…)]` is the sanctioned token form and is not
counted, exactly as `text-[color:var(--…)]` is exempt from the type-scale
check. */
--tracking-display: -0.02em;
--tracking-normal: 0em;
--tracking-label: 0.06em;
--tracking-eyebrow: 0.08em;
--tracking-kicker: 0.12em;
/* Font families: bind Tailwind's font-sans / font-mono to the loaded Geist
faces (variables set on <html> by next/font). font-mono is used for
clinical codes and IDs (guideline refs, chunk/page numbers, versions). */
--font-sans:
var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, monospace;
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
--ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
--animate-fade-up: fade-up 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-overlay-in: overlay-in 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-sheet-up: sheet-up var(--swipe-duration, 250ms) cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-sheet-left: sheet-left var(--swipe-duration, 220ms) cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-pop-in: pop-in 180ms cubic-bezier(0.34, 1.3, 0.64, 1) both;
--animate-dialog-rise: dialog-rise 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-action-tray-in: action-tray-in var(--swipe-duration, 160ms) cubic-bezier(0.22, 1, 0.36, 1) both;
--animate-shimmer: shimmer 1.4s linear infinite;
--animate-answer-ecg: answer-ecg-scroll 3.2s linear infinite;
--animate-answer-ecg-compact: answer-ecg-scroll 2.6s linear infinite;
}
/* Theme tokens */
:root {
/* Named responsive breakpoint tokens (Task #336) */
--bp-phone: 640px;
--bp-tablet: 768px;
--bp-desktop: 1024px;
/* Layout constraints */
--content-max-width: 1440px;
--rail-width: 480px;
--content-width-catalogue: 76rem;
/* Clinical White: true-neutral graphite-to-ash ramp (de-blued). */
--neutral-0: #ffffff;
--neutral-50: #f7f8fa;
--neutral-100: #f1f4f6;
--neutral-200: #e5e7eb;
--neutral-300: #cdd5df;
--neutral-400: #98a2b3;
--neutral-500: #667085;
--neutral-600: #475467;
--neutral-700: #344054;
--neutral-800: #1d2939;
--neutral-900: #101418;
--neutral-950: #080b0f;
/* Clinical accent (Clinical Sky): cool blue clinical identity ramp. The whole
ramp is derived from --primary-500 #1d6fb8 (5.2:1 on white, so it is legal
as both a text colour and a button fill) — never swap one step in isolation
or the tints, borders and hover states stop agreeing with each other. */
--primary-50: #eff5fc;
--primary-100: #d9e8f8;
--primary-200: #c6dcf2;
--primary-300: #8ebde6;
--primary-400: #4a91d2;
--primary-500: #1d6fb8;
--primary-600: #1a66a8;
--primary-700: #185c99;
--primary-800: #164a7a;
--primary-900: #143c62;
/* Surfaces are an ordered five-step scale: inset < wash < subtle < surface <
raised. `raised` must stay the lightest plane or cards read as recesses. */
--background: #f1f4f8;
--app-shell: #101d2e;
--surface: #fcfdfe;
--surface-chrome: #f7f9fc;
--surface-raised: #ffffff;
--surface-subtle: #f7f9fc;
--surface-inset: #eaeef4;
--surface-glass: rgb(255 255 255 / 82%);
--surface-highlight: color-mix(in srgb, var(--neutral-0) 64%, transparent);
--surface-lux: #ffffff;
--surface-wash: #f1f4f8;
--text: var(--neutral-900);
--text-heading: #080b0f;
--text-muted: var(--neutral-600);
--text-soft: var(--neutral-500);
--decoration-soft: var(--neutral-500);
/* Placeholder is still text (≥4.5:1). Same ink as muted until a distinct step
is needed; also declared on the v2 layer for opt-in adoption. */
--text-placeholder: var(--neutral-600);
--border: #e3e8ef;
--border-strong: #c8d2de;
/* Tracks --border-strong at 55% — re-derive both together. */
--border-lux: rgb(200 210 222 / 55%);
/* Highlight rings on lux surfaces (panel, mobile FAB menu, PWA notice). These
exist so the ring carries its own per-theme value and the call site does not
need a `dark:` override — the prohibition GATES §3 lists, now gated at zero.
Values are byte-equivalent to the utilities they replace: Tailwind emits
`color-mix(in oklab, <colour> N%, transparent)` for `ring-<colour>/N`, and
mixing any colour with `transparent` yields that colour at alpha N
regardless of space, so the glass ring is written in the plainer rgb() form
the .dark --surface-highlight already uses.
--ring-highlight tracks --border-strong, 20% light / 10% dark
--ring-glass literal white, 35% light / 10% dark — deliberately NOT
--neutral-0, which inverts to #060708 in dark */
--ring-highlight: color-mix(in oklab, var(--border-strong) 20%, transparent);
--ring-glass: rgb(255 255 255 / 35%);
--primary: var(--primary-500);
--primary-strong: var(--primary-700);
--primary-soft: #eff5fc;
--primary-contrast: #ffffff;
/* Command (graphite): primary actions — New chat, primary CTAs. */
--command: #111827;
--command-hover: #0b1220;
--command-contrast: #ffffff;
/* Clinical accent role tokens (Clinical Sky). Selected · evidence · send ·
focus. Hover is a full ramp step (not a 4% nudge) and :active settles back
to rest rather than pushing below the resting baseline. */
--clinical-accent: var(--primary-500);
--clinical-accent-hover: #185c99;
--clinical-accent-active: #14507f;
--clinical-accent-strong: var(--primary-700);
--clinical-accent-soft: var(--primary-soft);
--clinical-accent-border: #c6dcf2;
--clinical-accent-contrast: #ffffff;
/* Tinted filter-count pill on the results band — quieter than a solid disc. */
--search-band-badge-bg: color-mix(in oklab, var(--clinical-accent) 16%, transparent);
/* Bespoke chat surfaces with no role-token equivalent: warm sand callouts
and table headers. Per-theme values, not aliases. */
--clinical-chat-sand: #f7f1e6;
--clinical-chat-sand-border: #eadcc7;
--clinical-chat-sand-border-strong: #ddc7a8;
/* These two are plain surface steps, not bespoke values — alias them so they
cannot drift off the scale again when the surface ladder is re-derived. */
--clinical-chat-document: var(--surface-inset);
--clinical-chat-table-header: var(--surface-subtle);
--clinical-chat-ready: var(--success);
/* Favourite type chips — categorical, not semantic status. Chroma is capped
and lightness matched across the six so an identity chip can never
out-shout a status badge: status always wins the attention contest. */
--type-document: #3a5a94;
--type-document-soft: #f0f4fa;
--type-document-border: #d3dfef;
--type-table: #2f6b57;
--type-table-soft: #eef5f2;
--type-table-border: #cfe3da;
--type-search: #4a5568;
--type-search-soft: #f4f5f7;
--type-search-border: #dfe2e8;
--type-source: #634f8f;
--type-source-soft: #f4f1fa;
--type-source-border: #ded6ee;
--type-service: #2b6474;
--type-service-soft: #eaf4f7;
--type-service-border: #c4dfe7;
--type-form: #7d5a2c;
--type-form-soft: #f8f4ec;
--type-form-border: #e8dbc4;
/* Semantic triads (text / background / border), on-white for light mode.
These all sat in one 4.6–5.2:1 band, so nothing read as more urgent than
anything else. Every -text step now clears 5.5:1 on its own -bg and danger
is clearly the highest of the four. */
--info-text: #1c4fbf;
--info-bg: #eff4ff;
--info-border: #b2ccff;
--success-text: #0c6b41;
--success-bg: #ecfdf3;
--success-border: #abefc6;
--warning-text: #8a4d05;
--warning-bg: #fef7ec;
--warning-border: #f5d9a8;
--danger-text: #a3190f;
--danger-bg: #fef3f2;
--danger-border: #fda29b;
--danger-solid: #a3190f;
--danger-solid-hover: #85140d;
--danger-solid-active: #6f120c;
--danger-solid-contrast: #ffffff;
--print-furniture-gap: 4mm;
--info: var(--info-text);
--info-soft: var(--info-bg);
--success: var(--success-text);
--success-soft: var(--success-bg);
--warning: var(--warning-text);
--warning-soft: var(--warning-bg);
--danger: var(--danger-text);
--danger-soft: var(--danger-bg);
/* Categorical tone scale — taxonomy/pathway dots only. NEVER semantic: kept
visually distinct from the role tokens (success/danger/warning/info) and
the brand accent so green/blue/sky keep their meanings. Each tone ships a
complete text/-soft/-border triad so chips never hand-roll their own wash,
and chroma is capped to match the --type-* identity set. */
--tone-purple: #634f8f;
--tone-purple-soft: #f4f1fa;
--tone-purple-border: #ded6ee;
--tone-indigo: #43508f;
--tone-indigo-soft: #f0f2fa;
--tone-indigo-border: #d6dcef;
--tone-rose: #8a4a7d;
--tone-rose-soft: #f9f0f6;
--tone-rose-border: #eed6e6;
--tone-slate: #4a5568;
--tone-slate-soft: #f4f5f7;
--tone-slate-border: #dfe2e8;
/* WCAG exempts disabled controls from contrast, but a clinician still has to
read WHICH action is unavailable — so this clears 3:1 rather than 2.5:1. */
--disabled: #8794a8;
--focus: var(--clinical-accent);
--overlay-backdrop: rgb(4 8 14 / 56%);
--z-overlay: 80;
--z-popover: 95;
--z-modal: 100;
--z-toast: 110;
--panel-gloss: color-mix(in srgb, var(--surface-lux) 82%, transparent);
/* Selection emphasis: crisp accent ring + shadow tinted to the same hue as
the elevation ladder (these were the last flat-grey shadows in the set). */
--glow-primary: 0 0 0 1px color-mix(in srgb, var(--clinical-accent) 40%, transparent), 0 8px 20px rgb(11 42 56 / 10%);
--glow-soft: 0 0 0 1px color-mix(in srgb, var(--clinical-accent) 28%, transparent), 0 4px 12px rgb(11 42 56 / 8%);
/* Elevation ladder. --e0 … --e4 is the vocabulary: one monotonic sequence
that sorts by name, hue-tinted rather than flat grey, with negative spread
so a shadow pulls inward instead of bleeding past its own box. Reach for a
tier, never a hand-rolled `shadow-[0_…]`.
e0 flush e1 resting hairline e2 cards, popovers
e3 hover / lifted chrome e4 modals, sheets, drawers */
--e0: none;
--e1: 0 1px 2px rgb(11 42 56 / 7%);
--e2: 0 1px 2px rgb(11 42 56 / 5%), 0 4px 10px -2px rgb(11 42 56 / 8%);
--e3: 0 2px 4px rgb(11 42 56 / 5%), 0 10px 24px -6px rgb(11 42 56 / 11%);
--e4: 0 4px 8px rgb(11 42 56 / 5%), 0 24px 48px -12px rgb(11 42 56 / 15%);
/* Role aliases onto the ladder. Keep these pointing AT a tier — the previous
hand-tuned values produced five effects that did not sort by name. The
resting-hairline role is gone: its call sites reach for --e1 directly. */
--shadow-card: var(--e2);
--shadow-soft: var(--e2);
--shadow-hover: var(--e3);
--shadow-elevated: var(--e4);
/* Floating overlay surfaces (menus, popovers). Aliased to the elevated tier
because that is what the existing production popovers already use. */
--shadow-overlay: var(--e4);
--shadow-lux: inset 0 1px 0 rgb(255 255 255 / 90%), var(--e4);
--shadow-inset: inset 0 1px 0 rgb(255 255 255 / 58%);
/* Active left-rail marker (nav/list "current" indicator). One token so the
accent rail is a consistent 2px everywhere instead of hand-inlined at 2px/3px. */
--shadow-rail-active: inset 2px 0 0 var(--clinical-accent);
/* Mode-nav collapsed control: a surface lifted off the header. */
--shadow-lift: 0 1px 2px rgb(12 24 34 / 8%), 0 2px 6px rgb(12 24 34 / 5%);
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-right: env(safe-area-inset-right, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-left: env(safe-area-inset-left, 0px);
/* Phone/sm header chrome inset; matches mode-home px-4. Referenced by both
the layered .edge-glass-header base and the unlayered max-width:639px
guard so the values cannot drift apart. lg overrides to 1.5rem. */
--header-edge-pad: 1rem;
/* Distance from `.mode-nav__bar`'s box edge to the first tab's INK: the
bar's own `px-1` plus the slot's `px-3`. It happens to equal the base
header gutter, which is why phone alignment was already correct. The mode
nav subtracts it from its gutter and adds it back to its max-width so the
ink — not the box — lands on the header's content edge at every width. */
--mode-nav-ink-offset: 1rem;
/* Borderless app-shell/header height (the min-h-14 bar). Page shells fill the
viewport below it via calc(100dvh - var(--shell-header-h)); one token so the
header height and those page-fill floors cannot silently drift apart. */
--shell-header-h: 4rem;
/* Server-stable default for the phone overlay header's content clearance.
`usePhoneOverlayChromeReserve` refines this to the measured stack height in
a layout effect, but the property must never be absent: the overlay header
is out of flow, so an unset value paints the first content underneath the
chrome and then shifts it down at hydration — the cold-load jump the
overlay switch exists to remove.
The top term must be `max(0.5rem, …)`, not the bare inset. An overlay
header pads its top with `pt-[max(0.5rem,var(--safe-area-top))]`
(master-search-header.tsx) while `--shell-header-h` covers the inner
`min-h-14` bar plus its `pb-2`. Seeding the bare inset therefore
under-reserves by `max(0, 0.5rem - safe-area-top)` — nothing on a notched
iPhone, but a full 8px on any phone reporting no top inset (Android, and
Playwright's default emulation), which the layout effect then corrects
after paint. That is the same cold-load shift in miniature.
Exact for routes without a `header-collapse-addon` row; addon routes
refine by that row's height. */
--phone-overlay-chrome-h: calc(max(0.5rem, var(--safe-area-top)) + var(--shell-header-h));
/* Keep in sync with mobile-composer-reserve.ts (phone dock clearance). */
--keyboard-height: 0px;
--phone-dock-hidden-pad: 0rem;
--phone-dock-differentials-compare-clearance: 12.5rem;
--phone-dock-differentials-compare-compact-clearance: 12.25rem;
/* Patient details is a single ~3rem row + 0.5rem gap above the pill, so it
clears roughly 3.5rem less than the Compare bar. */
--phone-dock-patient-details-clearance: 9rem;
--phone-dock-patient-details-compact-clearance: 8.75rem;
--patient-details-fab-gap: 0.5rem;
--patient-details-fab-padding: 0.3125rem 0.5rem 0.3125rem 1rem;
--patient-details-fab-count-size: 1.625rem;
--patient-details-fab-count-padding: 0.375rem;
/* Radius tokens are the single source of truth in @theme above (they also
generate the rounded-* utilities); do not redefine them here or var() and
the utilities drift apart. */
/* Motion ladder — exact observed production timings (Wave 4 Arch).
Phone-chrome hide=240ms / reveal=200ms must stay exact (search-chrome-behaviour). */
--duration-instant: 100ms;
--duration-fast: 120ms;
--duration-quick: 150ms;
--duration-base: 180ms;
--duration-moderate: 200ms;
--duration-slow: 240ms;
--duration-deliberate: 300ms;
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
--ease-emphasized: cubic-bezier(0.2, 0.8, 0.2, 1);
/* CSS keyword ease-out equivalent — do not conflate with chrome-reveal / soft.
Named --ease-out-keyword (not --ease-out): Tailwind v4 owns --ease-out inside
@layer theme for the ease-out utility and --default-transition-timing-function;
an unlayered :root --ease-out would silently retarget every bare transition. */
--ease-out-keyword: cubic-bezier(0, 0, 0.58, 1);
/* Phone-chrome hide vs reveal: two tokens, never one (search-chrome-behaviour). */
--ease-chrome-hide: cubic-bezier(0.4, 0, 0.2, 1);
--ease-chrome-reveal: cubic-bezier(0.22, 1, 0.36, 1);
color-scheme: light;
}
.dark {
/* Dark neutral ramp (inverted): black-polish canvas to near-white ink. */
--neutral-0: #060708;
--neutral-50: #0b0d0f;
--neutral-100: #101214;
--neutral-200: #171a1d;
--neutral-300: #23282c;
--neutral-400: #3b454b;
--neutral-500: #7c858f;
--neutral-600: #a4adb7;
--neutral-700: #cbd1d8;
--neutral-800: #e5e8ec;
--neutral-900: #f4f6f8;
--neutral-950: #fbfcfd;
/* Clinical accent (Clinical Sky), brightened for dark surfaces. Dark holds
the same hue as light rather than drifting toward cyan-green, so the accent
reads as one colour across themes. */
--primary-50: #08203a;
--primary-100: #0d2b48;
--primary-200: #123556;
--primary-300: #1c4f79;
--primary-400: #3a80c0;
--primary-500: #74bdf0;
--primary-600: #92cdf5;
--primary-700: #a9d8f8;
--primary-800: #c4e5fb;
--primary-900: #ddf0fd;
/* Same ordered five-step surface scale as light, inverted. */
--background: #060708;
--app-shell: #090a0c;
--surface: #101315;
--surface-chrome: #0b0d0f;
--surface-raised: #171b1e;
--surface-subtle: #0d0f11;
--surface-inset: #08090b;
--surface-glass: rgb(6 7 8 / 78%);
--surface-highlight: rgb(255 255 255 / 5%);
--surface-lux: #1c2124;
--surface-wash: #0a0c0e;
--text: var(--neutral-900);
--text-heading: var(--neutral-950);
--text-muted: var(--neutral-600);
--text-soft: var(--neutral-500);
--decoration-soft: var(--neutral-500);
--text-placeholder: var(--neutral-600);
/* Per-theme values, not neutral-ramp aliases: tracking --neutral-300 left the
dark border at ~1.35:1 against --surface, i.e. effectively invisible. */
--border: #2b3136;
--border-strong: #3b444b;
--border-lux: rgba(255, 255, 255, 0.1);
/* Dark halves both highlight rings — the exact values the retired
`dark:ring-*` overrides carried. See the :root declarations. */
--ring-highlight: color-mix(in oklab, var(--border-strong) 10%, transparent);
--ring-glass: rgb(255 255 255 / 10%);
--primary: var(--primary-500);
--primary-strong: var(--primary-700);
--primary-soft: #123556;
--primary-contrast: #08203a;
--command: #f5f7f7;
--command-hover: #e6e9e8;
--command-contrast: #060708;
--clinical-accent: var(--primary-500);
--clinical-accent-hover: var(--primary-600);
--clinical-accent-active: var(--primary-700);
--clinical-accent-strong: var(--primary-700);
--clinical-accent-soft: var(--primary-soft);
--clinical-accent-border: #235a7d;
--clinical-accent-contrast: #082e4d;
--search-band-badge-bg: color-mix(in oklab, var(--clinical-accent) 16%, transparent);
--clinical-chat-sand: #2d2418;
--clinical-chat-sand-border: #5a4427;
--clinical-chat-sand-border-strong: #806034;
--clinical-chat-document: var(--surface-inset);
--clinical-chat-table-header: var(--surface-subtle);
--clinical-chat-ready: var(--success);
/* Identity anchors capped to match light, and their washes re-derived under
the capped anchors — the old raw-Tailwind -soft/-border pairs were more
saturated than the text sitting on them. */
--type-document: #9db6dc;
--type-document-soft: #1a2438;
--type-document-border: #2b3d5e;
--type-table: #93c7b3;
--type-table-soft: #152820;
--type-table-border: #2a4d3d;
--type-search: #c3c9d4;
--type-search-soft: #1e222a;
--type-search-border: #3a4150;
--type-source: #bdb0da;
--type-source-soft: #241f33;
--type-source-border: #3c3352;
--type-service: #96c6d4;
--type-service-soft: #16262c;
--type-service-border: #2b4650;
--type-form: #d6bd8a;
--type-form-soft: #2b2317;
--type-form-border: #4d4028;
--info-text: #89c0f0;
--info-bg: #122c4c;
--info-border: #2c537f;
--success-text: #7de0a3;
--success-bg: #153f2a;
--success-border: #24794f;
--warning-text: #f2c45a;
--warning-bg: #3d2f14;
--warning-border: #725e23;
--danger-text: #ff9ca4;
--danger-bg: #401c24;
--danger-border: #7f2e36;
--danger-solid: #b42318;
--danger-solid-hover: #d92d20;
--danger-solid-active: #912018;
--danger-solid-contrast: #ffffff;
--info: var(--info-text);
--info-soft: var(--info-bg);
--success: var(--success-text);
--success-soft: var(--success-bg);
--warning: var(--warning-text);
--warning-soft: var(--warning-bg);
--danger: var(--danger-text);
--danger-soft: var(--danger-bg);
/* Categorical tone scale, brightened for dark surfaces (categorical only).
Complete triads, chroma capped to match the --type-* identity set. */
--tone-purple: #b0a0d8;
--tone-purple-soft: #2a2540;
--tone-purple-border: #3d3557;
--tone-indigo: #9aa5dd;
--tone-indigo-soft: #232842;
--tone-indigo-border: #343b5a;
--tone-rose: #d99cc8;
--tone-rose-soft: #3a2434;
--tone-rose-border: #52344a;
--tone-slate: #9aa4b4;
--tone-slate-soft: #22262d;
--tone-slate-border: #333a44;
--disabled: #626c7a;
--focus: var(--clinical-accent);
--overlay-backdrop: rgb(0 0 0 / 72%);
--panel-gloss: color-mix(in srgb, var(--surface-lux) 82%, transparent);
--glow-primary: 0 0 0 1px color-mix(in srgb, var(--clinical-accent) 45%, transparent), 0 8px 24px rgb(0 0 0 / 45%);
--glow-soft: 0 0 0 1px color-mix(in srgb, var(--clinical-accent) 32%, transparent), 0 6px 16px rgb(0 0 0 / 42%);
/* Same ladder as light. Dark lifts with a top highlight rather than more
black — piling on opacity reads as a smudge, not as elevation. */
--e0: none;
--e1: inset 0 1px 0 rgb(255 255 255 / 4%), 0 1px 2px rgb(0 0 0 / 40%);
--e2: inset 0 1px 0 rgb(255 255 255 / 5%), 0 4px 12px -2px rgb(0 0 0 / 45%);
--e3: inset 0 1px 0 rgb(255 255 255 / 6%), 0 8px 20px -6px rgb(0 0 0 / 55%);
--e4: inset 0 1px 0 rgb(255 255 255 / 7%), 0 20px 40px -12px rgb(0 0 0 / 62%);
--shadow-card: var(--e2);
--shadow-soft: var(--e2);
--shadow-hover: var(--e3);
--shadow-elevated: var(--e4);
/* Floating overlay surfaces (menus, popovers). Aliased to the elevated tier
because that is what the existing production popovers already use. */
--shadow-overlay: var(--e4);
--shadow-lux: var(--e4);
--shadow-inset: inset 0 1px 0 rgb(255 255 255 / 4%);
/* A drop shadow does not read on a near-black ground, so the lifted control
is described by an inner top highlight instead. */
--shadow-lift: inset 0 1px 0 rgb(255 255 255 / 7%), 0 2px 6px rgb(0 0 0 / 45%);
color-scheme: dark;
}
/* Reset and base document styles */
* {
box-sizing: border-box;
}
html {
min-width: 320px;
min-height: 100%;
min-height: 100svh;
min-height: 100dvh;
background-color: var(--background);
overflow-x: clip;
overscroll-behavior-x: none;
scroll-padding-top: calc(4rem + env(safe-area-inset-top));
}
/*
* Interface density scales the rem baseline so every rem-based size shifts
* together. "Comfortable" is the browser default (16px) and sets no attribute,
* so the untouched default experience is unchanged. The attribute is applied on
* <html> before first paint (layout.tsx inline script) and kept in sync by
* useAppPreferences. The unlayered 16px mobile input floor above still wins for
* form controls so iOS never zooms.
*/
html[data-density="compact"] {
font-size: 15px;
}
html[data-density="spacious"] {
font-size: 17px;
}
body {
min-height: 100%;
min-height: 100svh;
min-height: 100dvh;
background: var(--background);
color: var(--text);
font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
font-feature-settings:
"liga" 1,
"calt" 1,
"ss01" 1;
text-rendering: optimizeLegibility;
overflow-x: clip;
overscroll-behavior-x: none;
}
/* Category accent delivery.
------------------------------------------------------------------
A card names its category with `data-category-accent`, and reads the colour
back through --cat-accent / --cat-soft / --cat-border. Three reasons this is
an attribute→variable indirection rather than per-category utility classes:
1. Tailwind's scanner only sees class strings that appear literally in the
source, so `bg-[color:var(--type-${accent}-soft)]` silently produces no
CSS. Every consumer here writes the literal `var(--cat-soft)` instead.
2. Light, dark AND forced-colors come for free: each value aliases an
existing triad that all three themes already remap, so a new category
never needs a fourth declaration site to stay legible in high contrast.
3. It replaces the inline `style={{ backgroundColor: theme.soft }}` the
factsheet cards used, which bypassed the class contract entirely.
Every accent below resolves to a NON-semantic triad. Nothing here may point
at --danger / --warning / --success / --info: those belong to the six-tone
badge system, where the colour is the meaning. A category is not a status.
Held by tests/design-token-contract.test.ts. */
[data-category-accent="document"] {
--cat-accent: var(--type-document);
--cat-soft: var(--type-document-soft);
--cat-border: var(--type-document-border);
}
[data-category-accent="table"] {
--cat-accent: var(--type-table);
--cat-soft: var(--type-table-soft);
--cat-border: var(--type-table-border);
}
[data-category-accent="search"] {
--cat-accent: var(--type-search);
--cat-soft: var(--type-search-soft);
--cat-border: var(--type-search-border);
}
[data-category-accent="source"] {
--cat-accent: var(--type-source);
--cat-soft: var(--type-source-soft);
--cat-border: var(--type-source-border);
}
[data-category-accent="service"] {
--cat-accent: var(--type-service);
--cat-soft: var(--type-service-soft);
--cat-border: var(--type-service-border);
}
[data-category-accent="form"] {
--cat-accent: var(--type-form);
--cat-soft: var(--type-form-soft);
--cat-border: var(--type-form-border);
}
[data-category-accent="purple"] {
--cat-accent: var(--tone-purple);
--cat-soft: var(--tone-purple-soft);
--cat-border: var(--tone-purple-border);
}
[data-category-accent="indigo"] {
--cat-accent: var(--tone-indigo);
--cat-soft: var(--tone-indigo-soft);
--cat-border: var(--tone-indigo-border);
}
[data-category-accent="rose"] {
--cat-accent: var(--tone-rose);
--cat-soft: var(--tone-rose-soft);
--cat-border: var(--tone-rose-border);
}
[data-category-accent="slate"] {
--cat-accent: var(--tone-slate);
--cat-soft: var(--tone-slate-soft);
--cat-border: var(--tone-slate-border);
}
[data-category-accent="clinical"] {
--cat-accent: var(--clinical-accent);
--cat-soft: var(--clinical-accent-soft);
--cat-border: var(--clinical-accent-border);
}
/* Fallback for a surface that opts into the card recipe without naming a
category — the accent collapses to the product accent rather than to an
unresolved variable, so an omitted attribute degrades to today's look. */
:root {
--cat-accent: var(--clinical-accent);
--cat-soft: var(--clinical-accent-soft);
--cat-border: var(--clinical-accent-border);
}
@layer base {
/* Interactive element defaults */
button,
input,
textarea,
select {
font: inherit;
}
}
/*
* iOS Safari zooms the viewport when a focused form control renders below 16px.
* This rule is intentionally unlayered so it wins over Tailwind's layered
* text-* utilities, giving every control a 16px floor on small screens
* (search field, scope filter, PDF page input) without per-call-site edits.
*/
@media (max-width: 640px) {
input,
select,
textarea {
font-size: 16px;
}
}
button,
a,
summary,
input,
textarea,
select,
.touch-card {
outline: none;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* One focus owner per interaction. Buttons and links use a single external
outline; form fields draw the same outline inside their own box so scroll
containers and sheets cannot clip it at an edge. Keeping these unlayered is
intentional: it prevents component-level `focus:ring-*` utilities from
stacking a second halo on top of the shared treatment. */
/* Deliberately outline-only: no companion box-shadow ring. The reviewed design
system proposed adding a 2px `--surface` spacer shadow here, on the grounds
that `--focus` IS the accent and the ring therefore sat accent-on-accent.
That does not reproduce in this codebase — `outline-offset` paints OUTSIDE
the border box, so the gap shows the ancestor's background (a surface), not
the control's own accent fill. Adding the shadow only stacked a second
affordance, which the rule above exists to prevent, and flattened the
control's resting elevation while focused. See the "focus is singular"
assertion in tests/ui-smoke.spec.ts. */
:where(button, a, summary, input[type="checkbox"], input[type="radio"], input[type="range"]):focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
/* Controls that sit flush inside clipped sheets keep the shared focus outline
inside their own border box so no edge of the ring is cut off. */
.focus-ring-contained:focus-visible {
outline-offset: -2px;
}
:where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), textarea, select):focus-visible {
/* Hairline on the field edge, not an inset accent rectangle. The previous
inset offset plus solid `--focus` read as a default browser ring and, on
transparent inputs nested in a rounded search shell, drew a sharp box
around the text while the icon sat outside it. */
box-shadow: var(--shadow-inset);
outline: 1px solid color-mix(in srgb, var(--clinical-accent) 32%, var(--border-strong));
outline-offset: 0;