@lilydesignsystem/themes 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.
Files changed (48) hide show
  1. package/LICENSE.md +64 -0
  2. package/dist/abyss.css +3174 -0
  3. package/dist/acid.css +3174 -0
  4. package/dist/adobe-spectrum.css +3174 -0
  5. package/dist/aqua.css +3174 -0
  6. package/dist/autumn.css +3174 -0
  7. package/dist/black.css +3174 -0
  8. package/dist/bumblebee.css +3174 -0
  9. package/dist/business.css +3174 -0
  10. package/dist/caramellatte.css +3174 -0
  11. package/dist/cmyk.css +3174 -0
  12. package/dist/coffee.css +3174 -0
  13. package/dist/corporate.css +3174 -0
  14. package/dist/cupcake.css +3174 -0
  15. package/dist/cyberpunk.css +3174 -0
  16. package/dist/dark.css +3175 -0
  17. package/dist/dim.css +3174 -0
  18. package/dist/dracula.css +3174 -0
  19. package/dist/emerald.css +3174 -0
  20. package/dist/fantasy.css +3174 -0
  21. package/dist/forest.css +3174 -0
  22. package/dist/garden.css +3174 -0
  23. package/dist/halloween.css +3174 -0
  24. package/dist/lemonade.css +3174 -0
  25. package/dist/light.css +3174 -0
  26. package/dist/lofi.css +3174 -0
  27. package/dist/luxury.css +3174 -0
  28. package/dist/mozilla-protocol.css +3174 -0
  29. package/dist/night.css +3174 -0
  30. package/dist/nord.css +3174 -0
  31. package/dist/pastel.css +3174 -0
  32. package/dist/retro.css +3174 -0
  33. package/dist/silk.css +3174 -0
  34. package/dist/sunset.css +3174 -0
  35. package/dist/synthwave.css +3174 -0
  36. package/dist/united-kingdom-government-digital-service.css +3179 -0
  37. package/dist/united-kingdom-national-health-service-england-for-patients.css +3215 -0
  38. package/dist/united-kingdom-national-health-service-england-for-practitioners.css +3308 -0
  39. package/dist/united-kingdom-national-health-service-scotland-for-patients.css +3318 -0
  40. package/dist/united-kingdom-national-health-service-scotland-for-practitioners.css +3404 -0
  41. package/dist/united-kingdom-national-health-service-wales-for-patients.css +4211 -0
  42. package/dist/united-kingdom-national-health-service-wales-for-practitioners.css +4387 -0
  43. package/dist/united-states-web-design-system.css +3175 -0
  44. package/dist/valentine.css +3175 -0
  45. package/dist/winter.css +3175 -0
  46. package/dist/wireframe.css +3175 -0
  47. package/index.md +95 -0
  48. package/package.json +43 -0
@@ -0,0 +1,4387 @@
1
+ /* ============================================================================
2
+ * Theme: united-kingdom-national-health-service-wales-practitioners
3
+ * Brand: NHS Wales / GIG Cymru (Gwasanaeth Iechyd Gwladol Cymru) — practitioners
4
+ * Format: Lily standalone theme
5
+ *
6
+ *
7
+ * Upstream tracked: DHCW / NHS Wales design system (2026 — navy primary,
8
+ * gold rail accent, yellow focus). Independent implementation against Lily
9
+ * class hooks; not affiliated with or endorsed by NHS Wales / DHCW.
10
+ * --------------------------------------------------------------------------
11
+ * Practitioners variant
12
+ * --------------------------------------------------------------------------
13
+ * This is the *practitioners* / high-density variant of the NHS Wales
14
+ * theme. It shares the NHS Wales colour palette, brand intent, and
15
+ * component grammar with the patient-facing
16
+ * `united-kingdom-national-health-service-wales-patients.css`, and
17
+ * deliberately diverges on geometry and rhythm to serve a different
18
+ * audience: healthcare practitioners (doctors, nurses, midwives,
19
+ * pharmacists, AHPs, paramedics, dentists, ward clerks, MDT
20
+ * coordinators) using electronic patient records, observation charts,
21
+ * order entry, results review, scheduling boards, and ward whiteboards.
22
+ *
23
+ * What changes vs. the patient-facing Wales theme:
24
+ *
25
+ * - Information density. Practitioners scan large datasets quickly
26
+ * (vital signs panels, medication lists, allergy summaries, results
27
+ * trends). Padding, line-height, gap, and margin tokens are roughly
28
+ * halved so more rows fit per screen without horizontal scrolling.
29
+ *
30
+ * - Smaller control targets. Patient-facing NHS digital surfaces
31
+ * target ~44px (WCAG 2.5.5 target-size guidance) because they are
32
+ * touched on phones by members of the public, often one-handed.
33
+ * Practitioner workstations are predominantly mouse + keyboard +
34
+ * barcode/RFID, so this variant shrinks min-heights to ~28px
35
+ * (1.75rem) on inputs and buttons. Touch-first practitioner devices
36
+ * (ward iPads, COWs) should stay on the patient-facing Wales theme
37
+ * or override these tokens.
38
+ *
39
+ * - Tighter borders / sharper radii. 0.125rem (2px) radius and 1px
40
+ * borders give a crisper, more "data-grid" feel that matches the
41
+ * visual language practitioners already know from PACS, LIMS, ePMA,
42
+ * and the EPR workstation shells (Cerner, Epic, System C, TPP SystmOne,
43
+ * Welsh Clinical Portal).
44
+ *
45
+ * - Smaller base type. 0.875rem (14px) body copy — the same baseline
46
+ * as most practitioner workstation shells. Still passes WCAG 1.4.4
47
+ * (resize text up to 200%) because tokens are rem-based.
48
+ *
49
+ * - Reduced shadows. Depth is for affordance, not decoration —
50
+ * practitioner environments are often glare-prone (bedside, theatre).
51
+ *
52
+ * What stays the same:
53
+ *
54
+ * - The NHS Identity colour palette (NHS Blue, NHS Wales aqua-green
55
+ * accent, NHS Black, NHS Pale Grey, Emergency Services Red).
56
+ * - WCAG 2.2 AAA targets on body text and primary CTA contrast.
57
+ * - All Lily class hooks (492 components) — practitioner applications
58
+ * drop into the same headless components as patient-facing ones.
59
+ * - The bilingual NHS / GIG identity. Welsh-language practitioner UIs
60
+ * use the same diacritic-supporting font stack.
61
+ *
62
+ * When to use this theme:
63
+ *
64
+ * - Electronic patient records (EPR), practitioner portals, results viewers
65
+ * - Observation charts, NEWS2 / PEWS / MEWS scoring screens
66
+ * - Medication administration records (eMAR), e-prescribing (ePMA)
67
+ * - Order communications (lab/radiology/pharmacy)
68
+ * - Theatre lists, ward boards, bed management dashboards
69
+ * - Discharge summaries, MDT (multi-disciplinary team) review screens
70
+ * - Any practitioner-staff-only workstation interface
71
+ *
72
+ * When NOT to use this theme:
73
+ *
74
+ * - Patient-facing NHS Wales websites (use the patients Wales theme)
75
+ * - Patient portals, appointment booking by patients (use the patients
76
+ * Wales theme — patient touch targets must meet WCAG 2.5.5)
77
+ * - Anywhere a member of the public reads or interacts with the screen
78
+ *
79
+ * --------------------------------------------------------------------------
80
+ * Provenance
81
+ * --------------------------------------------------------------------------
82
+ * NHS Wales is one of four national NHS bodies in the United Kingdom (alongside
83
+ * NHS England, NHS Scotland, and Health and Social Care Northern Ireland). It
84
+ * is administered by the Welsh Government on behalf of seven local health
85
+ * boards and three national NHS trusts (Public Health Wales, Velindre
86
+ * University NHS Trust, and the Welsh Ambulance Services NHS Trust), plus
87
+ * Health Education and Improvement Wales (HEIW) and Digital Health and Care
88
+ * Wales (DHCW).
89
+ *
90
+ * The NHS Wales visual identity is the standard NHS Identity (the same
91
+ * lozenge mark, the same Frutiger typeface, the same official colour palette)
92
+ * presented bilingually as "NHS / GIG" — Gwasanaeth Iechyd Gwladol being the
93
+ * Welsh translation of National Health Service. Every NHS Wales organisation
94
+ * (HEIW, PHW, DHCW, Welsh Ambulance, Velindre, Aneurin Bevan UHB, Betsi
95
+ * Cadwaladr UHB, Cardiff and Vale UHB, Cwm Taf Morgannwg UHB, Hywel Dda UHB,
96
+ * Powys THB, Swansea Bay UHB) operates under this shared mark.
97
+ *
98
+ * NHS Wales now *does* publish a digital design system. Digital Health and
99
+ * Care Wales maintains the DHCW / NHSW design system and its accompanying
100
+ * SCSS component library, which extends the NHS service manual with
101
+ * Wales-specific tokens and components:
102
+ * https://dhcw-digital-health-and-care-wales.github.io/nhsw-component-library/
103
+ * https://github.com/dhcw-digital-health-and-care-wales/nhsw-component-library
104
+ *
105
+ * This theme is derived from, in order of precedence:
106
+ * 1. The DHCW / NHSW component library tokens (`src/tokens/_colours.scss`,
107
+ * `_typography.scss`, `_spacing.scss`) — the authoritative Wales source
108
+ * 2. The NHS Identity Guidelines colour palette, which DHCW still sits
109
+ * inside for the lozenge, NHS Blue, and the support families
110
+ * https://www.england.nhs.uk/nhsidentity/identity-guidelines/colours/
111
+ * 3. The NHS Wales branding guidelines (HEIW, 2022)
112
+ * https://heiw.nhs.wales/files/nhs-wales-branding-guidelines-eng/
113
+ * 4. The NHS digital service manual, which DHCW explicitly builds on and
114
+ * diverges from only where it documents a reason
115
+ * https://service-manual.nhs.uk/design-system/styles/colour
116
+ * 5. The NHS Wales Website Style Guide v4.0 (October 2022)
117
+ * https://cmssupport.nhs.wales/files/cms-documents/cms-style-guide/
118
+ *
119
+ * What distinguishes the Wales theme from the England theme
120
+ * ---------------------------------------------------------
121
+ * Earlier revisions of this file leaned on the NHS *Aqua Green* support
122
+ * colour as the Wales accent. That was an inference drawn before DHCW
123
+ * published anything; the published system makes a different choice, and
124
+ * evidence beats inference. The three concrete DHCW divergences are:
125
+ *
126
+ * - **Navy, not NHS Blue, carries action.** DHCW's primary button is
127
+ * `#1B365D` (`$nhsw-colour-button-primary`), and its "text blue" is the
128
+ * same navy. NHS Blue `#005EB8` is retained for links and the site
129
+ * header band, so the page still reads as NHS at a glance, but the
130
+ * thing you click is navy. NHS England, by contrast, uses green
131
+ * `#007F3B` for its primary button.
132
+ *
133
+ * - **Gold is the accent.** `$nhsw-colour-border-gold` `#AA8630` rails
134
+ * the DHCW inset-text component and tops the mobile bottom-nav bar. It
135
+ * is the only hue DHCW introduces that is not in the NHS Identity
136
+ * palette, so it — not aqua — is the Wales signature here. Gold is a
137
+ * *rail* colour only; see the accessibility note below.
138
+ *
139
+ * - **Yellow focus, black underline.** DHCW mandates the GDS-lineage
140
+ * focus indicator (`#FFEB3B` block with a `#0B0C0C` bar) rather than a
141
+ * brand-coloured outline, and applies it to every interactive element.
142
+ * On a practitioner workstation this matters more, not less: keyboard
143
+ * is the primary input device for clinical data entry, so the focus
144
+ * indicator is load-bearing rather than an accessibility afterthought.
145
+ *
146
+ * NHS Aqua Green is not discarded — it survives as `--lily-support-aqua`
147
+ * and as the `aqua-green` tag tone, both of which DHCW itself ships. And
148
+ * Cymru's cultural palette still shows through: the error red below is
149
+ * Pantone 485, the red of Y Ddraig Goch, and the gold reads as the
150
+ * daffodil rather than as a decorative flourish.
151
+ *
152
+ * --------------------------------------------------------------------------
153
+ * Colour-space notes
154
+ * --------------------------------------------------------------------------
155
+ * Values are expressed in OKLCH (lightness % / chroma / hue°) to match the
156
+ * sibling NHS themes (England, Scotland) in this directory. OKLCH was chosen
157
+ * across the Lily theme suite because it gives perceptually uniform lightness
158
+ * steps, which helps maintain WCAG 2.2 AAA contrast ratios when the theme is
159
+ * recoloured for high-contrast or dark variants.
160
+ *
161
+ * Each colour comment below gives the canonical sRGB hex, its source
162
+ * (a DHCW SCSS variable name, or an NHS Identity Pantone reference where
163
+ * the colour comes from the identity palette rather than from DHCW), and
164
+ * the rounded OKLCH translation that this file uses. Where a value is
165
+ * DHCW's, the SCSS variable name is quoted so the derivation can be
166
+ * checked against the published library rather than taken on trust.
167
+ *
168
+ * --------------------------------------------------------------------------
169
+ * Accessibility
170
+ * --------------------------------------------------------------------------
171
+ * Target: WCAG 2.2 AAA (per Lily's accessibility design principle).
172
+ * Measured against --color-base-100 (#FFFFFF) unless stated:
173
+ * - Body text (--color-base-content, #212B32): 14.4:1 — AAA
174
+ * - --color-primary (DHCW navy #1B365D): 12.1:1 — AAA at any size
175
+ * - --color-secondary (NHS Blue #005EB8): 6.4:1 — AAA large, AA body
176
+ * - --color-error (Pantone 485 #DA291C): 4.6:1 — AA large only
177
+ * - --lily-focus-text on --lily-focus-ring (#0B0C0C on #FFEB3B): 16.0:1
178
+ * - Every DHCW tag tone below: ≥ 7.2:1 — all AAA
179
+ *
180
+ * The tag tones matter more in the practitioner variant than the patient
181
+ * one: a worklist row that encodes triage, specialty, or result status in a
182
+ * chip is exactly the "colour-only meaning" trap WCAG 1.4.1 warns about.
183
+ * Every tone below carries a distinct border as well as a distinct fill,
184
+ * and none of them is legible-by-hue alone — pair each with a text label.
185
+ *
186
+ * The gold caveat, stated plainly
187
+ * -------------------------------
188
+ * DHCW gold #AA8630 is **3.4:1 on white**. That is below AA for body text
189
+ * and below the 3:1 floor for adjacent-colour meaning only by a hair. It is
190
+ * therefore declared as --color-accent but used *exclusively* as a rail,
191
+ * border, and underline colour, never as a text colour and never as a fill
192
+ * behind text. Where a Lily component fills with the accent and puts a
193
+ * label on it (`.ai-label`), this theme substitutes --lily-accent-deep
194
+ * (#554318, 9.5:1 with white) instead. Do not "fix" that by putting text
195
+ * back on the bright gold.
196
+ *
197
+ * Components whose colour cannot reach AAA behind body copy (warning
198
+ * yellow, gold) carry inverted or substituted content colours so the paired
199
+ * text stays legible.
200
+ *
201
+ * --------------------------------------------------------------------------
202
+ * Typography (informational — Lily does not bake font choices into the theme)
203
+ * --------------------------------------------------------------------------
204
+ * DHCW departs from the NHS Identity here. The NHSW component library sets
205
+ * `$nhsw-font-family-base: Roboto, Arial, sans-serif` — Roboto, not
206
+ * Frutiger. The likely reason is licensing: Frutiger is a paid typeface and
207
+ * Roboto is not, which matters for a system meant to be adopted across
208
+ * seven health boards and three trusts without a per-seat font bill.
209
+ *
210
+ * Both typefaces carry the full Welsh diacritic set (â, ê, î, ô, û, ŵ, ŷ
211
+ * and their uppercase forms), so either is safe for bilingual pages — but
212
+ * check any *substituted* fallback: ŵ and ŷ are the two characters most
213
+ * often missing from an otherwise complete font, and they are common in
214
+ * Welsh (e.g. "gŵr", "gwŷr", "Cymraeg gyfŵn").
215
+ *
216
+ * DHCW type scale, for consumers matching it exactly (desktop / mobile):
217
+ * h1 3rem/2rem · h2 2.25rem/1.6875rem · h3 1.625rem/1.375rem
218
+ * h4 1.375rem/1.1875rem · h5 1.1875rem/1rem
219
+ * body 1.1875rem/1rem · body-small 1rem
220
+ *
221
+ * Practitioner surfaces routinely run body copy at the `body-small` step
222
+ * (1rem) to fit a worklist on one screen. That is acceptable here because
223
+ * the text-size-picker helper is expected to be present on the page shell;
224
+ * it is not acceptable as a fixed floor. Never ship below 1rem.
225
+ *
226
+ * Consumers wiring this theme should declare:
227
+ * font-family: Roboto, "Frutiger W01", "Frutiger", Arial, sans-serif;
228
+ *
229
+ * The Frutiger entries are kept in the middle of the stack deliberately: an
230
+ * NHS Wales organisation that already licenses Frutiger for print will fall
231
+ * through to it if Roboto is unavailable, rather than dropping to Arial.
232
+ *
233
+ * --------------------------------------------------------------------------
234
+ * Geometry
235
+ * --------------------------------------------------------------------------
236
+ * Practitioner geometry is crisper than the patients Wales theme:
237
+ * - 0.125rem (2px) selector/field/box radius — almost-square corners
238
+ * match the data-grid aesthetic of practitioner workstation shells,
239
+ * and halve DHCW's own 4px
240
+ * - 1px borders, no blur shadows (depth: 0), no grain (noise: 0)
241
+ * - Density tokens collapse the spacing scale to roughly half:
242
+ * xs 2px, sm 4px, md 8px, lg 12px, xl 16px
243
+ * - Depth is expressed as a *hard ledge* under buttons and cards, per
244
+ * DHCW, rather than as a blur. A hard offset is the one form of
245
+ * elevation that survives both forced-colours mode and a monochrome
246
+ * print. DHCW's ledges are 2px (button) and 4px (card); this variant
247
+ * halves both to 1px and 2px, in line with the shadow scale.
248
+ *
249
+ * Four things deliberately do NOT get the density treatment, because
250
+ * shrinking them would cost correctness rather than space:
251
+ * - the focus ring (3px, zero offset — full DHCW width)
252
+ * - the inset-text rail (0.5rem — it must out-weigh the table rules it
253
+ * sits among, so it is the one measurement here that grows)
254
+ * - the warning-callout band (full type size — its job is to interrupt)
255
+ * - Welsh-language wrapping (see the bilingual section) — a truncated
256
+ * Welsh column header is a clinician reading the wrong column
257
+ * ========================================================================== */
258
+
259
+ /* Theme tokens — the colour, radius, border, and depth variables consumed
260
+ * by every Lily component rule below. The theme applies by default (`:root`)
261
+ * and also when explicitly opted into via
262
+ * `<html data-theme="united-kingdom-national-health-service-wales-for-practitioners">`,
263
+ * so multiple themes can coexist in one document and be switched at runtime
264
+ * by updating the `data-theme` attribute. */
265
+
266
+ :where(:root, :root[data-theme="united-kingdom-national-health-service-wales-for-practitioners"]) {
267
+ color-scheme: light;
268
+
269
+ /* -----------------------------------------------------------
270
+ --------- * Base surfaces * * Taken from the DHCW greys
271
+ rather than the NHS Identity greys. DHCW * ships `$nhsw-
272
+ colour-background-grey: #F0F4F5` and * `$nhsw-colour-
273
+ border-grey: #D8DDE0`, both a shade cooler and lighter *
274
+ than NHS Pale Grey #E8EDEE. The difference is small in
275
+ isolation but * visible when a DHCW-styled panel sits next
276
+ to an England-styled one, * and matching the published
277
+ value is what makes a Wales service look * like a Wales
278
+ service. * * These greys carry more weight on a
279
+ practitioner surface than a * patient one, because they are
280
+ what separates a zebra-striped worklist * row from its
281
+ neighbour. #F0F4F5 against #FFFFFF is a deliberately *
282
+ quiet 1.05:1 — enough to track a row across a wide table,
283
+ not enough * to read as a state change. Do not darken it to
284
+ "make rows clearer"; * that is what --lily-surface-sunken
285
+ and a real selected-row treatment * are for. * ------------
286
+ ------------------------------------------------------ */
287
+ /* #FFFFFF — pure white, NHS Identity primary surface
288
+ (`$nhsw-colour-background-white`) */
289
+ --color-base-100: oklch(100% 0 0);
290
+ /* #F0F4F5 — DHCW background grey (`$nhsw-colour-background-
291
+ grey`) — card backs, panel fills, alternating table rows */
292
+ --color-base-200: oklch(96% 0.004 214);
293
+ /* #D8DDE0 — DHCW border grey (`$nhsw-colour-border-grey`) —
294
+ borders, dividers, table rules, hover surfaces */
295
+ --color-base-300: oklch(89% 0.007 234);
296
+ /* #212B32 — DHCW text primary (`$nhsw-colour-text-primary`).
297
+ Note this is the NHS service-manual near-black, not NHS
298
+ Black #231F20: it has a slight blue cast that sits better
299
+ against the cool greys above. 14.4:1 on white — AAA. */
300
+ --color-base-content: oklch(28% 0.019 239);
301
+ /* -----------------------------------------------------------
302
+ --------- * Primary — DHCW Navy (#1B365D) * * `$nhsw-
303
+ colour-button-primary` and `$nhsw-colour-text-blue` in the
304
+ * NHSW component library. This is the colour of the thing
305
+ you click on * a DHCW page, and it is *not* NHS Blue. * *
306
+ The choice is defensible on contrast grounds alone. NHS
307
+ Blue on white * is 6.4:1 — fine for large text, short of
308
+ AAA for a button label at * body size. Navy is 12.1:1,
309
+ which clears AAA at any size. NHS England * solved the same
310
+ problem by going green; DHCW went dark. * * On a
311
+ practitioner shell the navy also does real work as chrome:
312
+ a * dark header band separates the application frame from
313
+ the clinical * content beneath it, which an NHS Blue band
314
+ does less convincingly. * ---------------------------------
315
+ --------------------------------- */
316
+ --color-primary: oklch(33% 0.077 258);
317
+ --color-primary-content: oklch(100% 0 0);
318
+ /* -----------------------------------------------------------
319
+ --------- * Secondary — NHS Blue (#005EB8 / Pantone 300) *
320
+ * Demoted from primary, but not diminished. Research cited
321
+ in the NHS * Identity Guidelines reports that 87% of UK
322
+ adults spontaneously * associate "blue and white" with the
323
+ NHS, and the GIG Cymru lozenge is * the same NHS Blue as
324
+ the NHS England lozenge. DHCW keeps it for the * site
325
+ header band (`$nhsw-colour-site-header-background`) and for
326
+ * links (`$nhsw-colour-link`). * * Used for: links, brand
327
+ chrome, hovered primary surfaces. * -----------------------
328
+ ------------------------------------------- */
329
+ --color-secondary: oklch(49% 0.161 255);
330
+ --color-secondary-content: oklch(100% 0 0);
331
+ /* -----------------------------------------------------------
332
+ --------- * Accent — DHCW Gold (#AA8630) * * `$nhsw-colour-
333
+ border-gold`. The only hue DHCW introduces that is not * in
334
+ the NHS Identity palette, which makes it the genuine Wales
335
+ * signature. In the published library it rails the inset-
336
+ text component * and tops the mobile bottom-nav bar — both
337
+ times as a *border*, never * as a fill or a text colour. *
338
+ * This theme honours that constraint rather than
339
+ generalising the * colour. Gold is 3.4:1 on white: legal as
340
+ a non-text graphical * boundary, illegal as body copy.
341
+ --color-accent-content is set to NHS * Black so that any
342
+ component which does fill with the accent at least * pairs
343
+ it with the darkest available text (4.2:1 — AA large only),
344
+ and * the components that would otherwise put small text on
345
+ gold are * redirected to --lily-accent-deep below. * ------
346
+ -----------------------------------------------------------
347
+ - */
348
+ --color-accent: oklch(64% 0.111 85);
349
+ --color-accent-content: oklch(23% 0.005 30);
350
+ /* -----------------------------------------------------------
351
+ --------- * Neutral — DHCW text secondary (#4C6272) * *
352
+ `$nhsw-colour-text-secondary` and `$nhsw-colour-border-
353
+ secondary` — * DHCW uses one slate for both muted text and
354
+ strong borders, which is * why the secondary button in the
355
+ published library is a white fill * with a #4C6272 border
356
+ and a #4C6272 shadow. * * Used for: muted UI chrome,
357
+ secondary nav, breadcrumb separators, * secondary-button
358
+ borders, input borders, table rules. 6.4:1 on white * — AA
359
+ body, AAA large. Do not use it for body copy at small
360
+ sizes, * which on this variant means: do not use it for
361
+ worklist cell text. * -------------------------------------
362
+ ----------------------------- */
363
+ --color-neutral: oklch(48% 0.037 240);
364
+ --color-neutral-content: oklch(100% 0 0);
365
+ /* -----------------------------------------------------------
366
+ --------- * Info — NHS Bright Blue (#0072CE / Pantone 285)
367
+ * * Retained from the NHS Identity rather than taken from
368
+ DHCW, because * DHCW has no distinct info colour: its
369
+ notification banner reuses the * link blue #005EB8, which
370
+ this theme has already spent on * --color-secondary. Giving
371
+ info its own brighter blue keeps an info * banner
372
+ distinguishable from a link-coloured header band on the
373
+ same * page — a distinction DHCW's own preview pages
374
+ struggle to make. * ---------------------------------------
375
+ --------------------------- */
376
+ --color-info: oklch(54% 0.17 248);
377
+ --color-info-content: oklch(100% 0 0);
378
+ /* -----------------------------------------------------------
379
+ --------- * Success — DHCW notification success (#00703C) *
380
+ * `$nhsw-colour-notification-success`. Darker than NHS
381
+ Green #009639 * (which this theme previously used), and
382
+ that is the point: NHS Green * with white text is roughly
383
+ 3.3:1, which fails AA for the banner * labels it was being
384
+ asked to carry. #00703C is 6.2:1 with white. * * Used for:
385
+ confirmation banners, saved-record states, in-range *
386
+ result flags. * -------------------------------------------
387
+ ----------------------- */
388
+ --color-success: oklch(48% 0.121 154);
389
+ --color-success-content: oklch(100% 0 0);
390
+ /* -----------------------------------------------------------
391
+ --------- * Warning — DHCW warning yellow (#FACC15) * *
392
+ `$nhsw-colour-warning-callout-heading`. A cooler, more
393
+ lemon yellow * than NHS Warm Yellow #FFB81C, and noticeably
394
+ brighter. DHCW pairs it * with a very pale #FEF9C3 panel
395
+ fill rather than tinting the panel * with the heading
396
+ colour, which is why --lily-warning-surface is * declared
397
+ separately below instead of being mixed from this token. *
398
+ * Content colour is DHCW text-primary, never white: yellow
399
+ this bright * cannot carry white text at any size. * ------
400
+ -----------------------------------------------------------
401
+ - */
402
+ --color-warning: oklch(86% 0.173 92);
403
+ --color-warning-content: oklch(28% 0.019 239);
404
+ /* -----------------------------------------------------------
405
+ --------- * Error — DHCW delete red (#D5281B) * * `$nhsw-
406
+ colour-button-delete`, also the DHCW invalid-input colour.
407
+ It * sits a half-step darker than NHS Emergency Services
408
+ Red #DA291C * (Pantone 485), which this theme previously
409
+ used, and buys a real * accessibility improvement: 5.1:1
410
+ with white rather than 4.6:1. * * The Welsh resonance
411
+ survives the swap. Pantone 485 is the red of Y * Ddraig
412
+ Goch on the national flag, and #D5281B is within a couple
413
+ of * ΔE of it — close enough that no one reads the
414
+ destructive button as * a different red, far enough that
415
+ the label is legible. * * Used for: error summaries,
416
+ destructive-action buttons, validation * messages, invalid
417
+ input borders, out-of-range result flags. * ---------------
418
+ --------------------------------------------------- */
419
+ --color-error: oklch(56% 0.209 30);
420
+ --color-error-content: oklch(100% 0 0);
421
+ /* -----------------------------------------------------------
422
+ --------- * DHCW supporting colours * * Values the
423
+ published NHSW library needs that have no slot in the *
424
+ upstream `--color-*` contract. They are declared here
425
+ rather than * inline in the component rules so a consumer
426
+ can retheme a DHCW * service by overriding tokens alone. *
427
+ * Note these are colour tokens only — none of them carries
428
+ spacing. * The practitioner density scale set further down
429
+ is untouched by this * block, and the component rules that
430
+ consume these colours below use * the tight scale, not the
431
+ patient one. * --------------------------------------------
432
+ ---------------------- */ /* #FFEB3B — `$nhsw-colour-
433
+ focus`. The GDS-lineage focus block. */
434
+ --lily-focus-ring: oklch(93% 0.18 102);
435
+ /* #0B0C0C — `$nhsw-colour-focus-text`. The bar under a
436
+ focused link, and the text colour inside a focus block.
437
+ 16.0:1 on the yellow. */
438
+ --lily-focus-text: oklch(15% 0.002 197);
439
+ /* #554318 — `$nhsw-colour-shadow-exceptional-button`. The
440
+ dark gold. Stands in for --color-accent anywhere small text
441
+ must sit on the accent, because bright gold cannot carry
442
+ text. 9.5:1 with white. */
443
+ --lily-accent-deep: oklch(39% 0.064 86);
444
+ /* #F5BB66 — `$nhsw-colour-button-exceptional-light`. DHCW's
445
+ "exceptional" button: a warm gold fill with dark text, used
446
+ for the one action on a page that is neither primary nor
447
+ destructive but still must be found. 8.4:1 with text-
448
+ primary. */
449
+ --lily-accent-soft: oklch(83% 0.122 75);
450
+ /* #FEF9C3 — `$nhsw-colour-warning-callout-background`. The
451
+ pale panel behind a warning callout. Kept separate from
452
+ --color-warning because DHCW does not derive one from the
453
+ other. */
454
+ --lily-warning-surface: oklch(97% 0.069 103);
455
+ /* #007F3B — the DHCW action-link green. Not NHS Green: it is
456
+ a shade darker so the chevron reads at 5.1:1 on white. */
457
+ --lily-action-green: oklch(52% 0.141 151);
458
+ /* #7C2855 — `$nhsw-colour-link-visited`. NHS Purple. 9.2:1. */
459
+ --lily-link-visited: oklch(42% 0.126 351);
460
+ /* #003087 — `$nhsw-colour-link-hover`, NHS Dark Blue /
461
+ Pantone 287. No longer the secondary token, but still the
462
+ hovered link. 11.9:1. */
463
+ --lily-link-hover: oklch(35% 0.153 261);
464
+ /* #00A499 — NHS Aqua Green / Pantone 3272. Demoted from the
465
+ accent slot but retained: DHCW ships an `aqua-green` tag
466
+ tone, and Public Health Wales and the Welsh Ambulance
467
+ Service both use aqua in print. 3.1:1 on white — decorative
468
+ fills and chart series only. */
469
+ --lily-support-aqua: oklch(65% 0.113 186);
470
+ /* 960px — `$nhsw-page-width`. Declared for parity with the
471
+ patient variant and with DHCW, but a practitioner shell is
472
+ expected to override it: a worklist or results grid wants
473
+ the full viewport, not a 60rem reading column. Set --lily-
474
+ page-width: none on those routes. It is a token here
475
+ precisely so you can. */
476
+ --lily-page-width: 60rem;
477
+ /* -----------------------------------------------------------
478
+ --------- * Geometry — practitioner variant * *
479
+ Practitioner workstations favour sharp, "data-grid"
480
+ geometry over * the rounder, friendlier corners on patient-
481
+ facing surfaces. We * tighten every radius to 0.125rem
482
+ (2px) so cells, chips, and rows * align in dense tables
483
+ without the rounded-corner halo that softens * the grid.
484
+ Borders stay at 1px (the NHS norm), shadows remain at 0 *
485
+ (practitioner environments are often glare-prone — bedside,
486
+ * theatre, A&E). * ----------------------------------------
487
+ -------------------------- */
488
+ --radius-selector: 0.125rem;
489
+ --radius-field: 0.125rem;
490
+ --radius-box: 0.125rem;
491
+ --size-selector: 0.125rem;
492
+ --size-field: 0.125rem;
493
+ --border: 1px;
494
+ --depth: 0;
495
+ --noise: 0;
496
+ }
497
+
498
+ /* ----------------------------------------------------------------------------
499
+ * Lily component styling — applied automatically once this theme stylesheet
500
+ * is loaded. Every Lily class hook (492 components) consumes the theme tokens
501
+ * declared above, so no other stylesheet is required for the theme to render.
502
+ *
503
+ * Rules live in @layer lily with :where() selectors so the specificity stays
504
+ * at 0 — application CSS always wins. To replace a Lily component's default
505
+ * look, declare a higher-specificity rule (or a later @layer) in your own
506
+ * stylesheet.
507
+ * -------------------------------------------------------------------------- */
508
+
509
+ @layer lily {
510
+ /* ==========================================================================
511
+ * Tokens & primitives
512
+ * --------------------------------------------------------------------------
513
+ * Lily exposes a small, stable set of derived tokens (the `--lily-*`
514
+ * family) layered on top of the upstream theme primitives (the
515
+ * `--color-*`, `--radius-*`, `--border`, `--depth`, `--noise` family).
516
+ *
517
+ * Component rules consume only the `--lily-*` tokens. That indirection
518
+ * means a consumer can swap upstream themes (or invent a new one) without
519
+ * touching component CSS — the component rules below remain stable
520
+ * because their inputs do.
521
+ * ====================================================================== */
522
+
523
+ :where(:root) {
524
+ /* --- Surfaces ------------------------------------------------------ */
525
+ /* Default page surface — the canvas behind body copy. */
526
+ --lily-surface: var(--color-base-100);
527
+ /* Secondary surface — section panels, sidebars, table headers,
528
+ muted callouts. One step away from the page background. */
529
+ --lily-surface-alt: var(--color-base-200);
530
+ /* Sunken surface — used behind progress tracks, skeletons, and
531
+ inset wells. Suggests depth without requiring a shadow. */
532
+ --lily-surface-sunken: var(--color-base-300);
533
+
534
+ /* --- Text ---------------------------------------------------------- */
535
+ /* Primary body-copy colour. Maps to the theme's high-contrast
536
+ reading colour. */
537
+ --lily-text: var(--color-base-content);
538
+ /* Muted / secondary text — mixed 70% with transparent to keep the
539
+ hue but lower contrast for captions, helptext, placeholders. */
540
+ --lily-text-muted: color-mix(
541
+ in oklch,
542
+ var(--color-base-content) 70%,
543
+ transparent
544
+ );
545
+
546
+ /* --- Borders ------------------------------------------------------- */
547
+ /* Default border — 20% blend with body content colour, low-contrast,
548
+ used for subtle dividers between content blocks. */
549
+ --lily-border: color-mix(
550
+ in oklch,
551
+ var(--color-base-content) 20%,
552
+ transparent
553
+ );
554
+ /* Higher-contrast border — 40% blend, used on inputs, buttons, and
555
+ anywhere a clear edge is required (WCAG non-text contrast). */
556
+ --lily-border-strong: color-mix(
557
+ in oklch,
558
+ var(--color-base-content) 40%,
559
+ transparent
560
+ );
561
+
562
+ /* --- Focus ring ---------------------------------------------------- */
563
+ /* Deliberately NOT redeclared here. --lily-focus-ring and
564
+ --lily-focus-text are set in the unlayered token block at the top
565
+ of this file to DHCW's #FFEB3B / #0B0C0C.
566
+
567
+ The sibling Lily themes bind the focus ring to --color-primary so
568
+ focus reads as "interactive intent". DHCW rejects that, and is
569
+ right to: a brand-coloured ring is invisible the moment focus
570
+ lands on a control that is already brand-coloured — which, on a
571
+ page whose primary buttons are navy, is every primary button. A
572
+ fixed high-chroma yellow is the one colour guaranteed to contrast
573
+ with the rest of the palette, which is why GDS, the NHS service
574
+ manual, and DHCW all landed on it independently.
575
+
576
+ This variant keeps the ring at full DHCW width even though every
577
+ other measurement here is halved. Focus is the one thing that
578
+ must not get denser: a 1px ring on a 2px-radius control in a
579
+ 28-row grid is a ring nobody finds. Density is a layout budget,
580
+ not an accessibility budget.
581
+
582
+ Override --lily-focus-ring to retheme; do not reintroduce a
583
+ var(--color-primary) binding here. */
584
+
585
+ /* --- Radii --------------------------------------------------------- */
586
+ /* Three tiers of corner rounding. `sm` for chips/badges,
587
+ `md` for inputs/buttons/cards, `lg` for dialogs/heroes. */
588
+ --lily-radius-sm: var(--radius-selector);
589
+ --lily-radius-md: var(--radius-field);
590
+ --lily-radius-lg: var(--radius-box);
591
+
592
+ /* --- Spacing scale ------------------------------------------------- */
593
+ /* Five-step spacing scale used by every gap, padding, and margin.
594
+ Practitioner variant: roughly half the patients theme's values
595
+ so the same layouts fit twice as many rows of clinical data
596
+ per screen. */
597
+ --lily-space-xs: 0.125rem;
598
+ --lily-space-sm: 0.25rem;
599
+ --lily-space-md: 0.5rem;
600
+ --lily-space-lg: 0.75rem;
601
+ --lily-space-xl: 1rem;
602
+
603
+ /* --- Typography ---------------------------------------------------- */
604
+ /* Body font stack — system fonts first so consumers get a
605
+ native-feeling baseline without shipping a web font. */
606
+ --lily-font-body:
607
+ system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
608
+ Arial, sans-serif;
609
+ /* Monospaced font stack — used by code, kbd, identifiers, and
610
+ anywhere alignment matters more than reading speed. */
611
+ --lily-font-mono:
612
+ ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
613
+ "Liberation Mono", monospace;
614
+
615
+ /* --- Shadows ------------------------------------------------------- */
616
+ /* Three depth tiers. Practitioner variant reduces blur and offset
617
+ because clinical screens are often glare-prone (bedside lights,
618
+ theatre overheads, A&E task lighting); large soft shadows wash
619
+ out under those conditions and read as "smudgy" rather than as
620
+ depth. */
621
+ --lily-shadow-sm: 0 1px 1px
622
+ color-mix(in oklch, var(--color-base-content) 6%, transparent);
623
+ --lily-shadow-md: 0 2px 6px
624
+ color-mix(in oklch, var(--color-base-content) 8%, transparent);
625
+ --lily-shadow-lg: 0 6px 16px
626
+ color-mix(in oklch, var(--color-base-content) 10%, transparent);
627
+ }
628
+
629
+ /* Practitioner base type — 14px body matches the baseline of most
630
+ practitioner workstation shells (Cerner Millennium, Epic Hyperspace,
631
+ System C CareFlow, TPP SystmOne, Welsh Clinical Portal). Set on
632
+ the root so the rem cascade scales every component down in step,
633
+ while the user's browser font-size remains the source of truth for
634
+ WCAG 1.4.4 "resize text 200%" conformance. */
635
+ :where(:root) {
636
+ font-size: 0.875rem;
637
+ line-height: 1.4;
638
+ }
639
+
640
+ /* Focus ring — every interactive Lily class hook gets the same visible
641
+ focus indicator. Centralising the selector means a consumer can
642
+ restyle every focus state at once by overriding `--lily-focus-ring`
643
+ (or this rule) rather than chasing 100+ component selectors. */
644
+ :where(
645
+ [class$="-input"],
646
+ [class$="-button"],
647
+ [class$="-link"],
648
+ [class$="-list-item"],
649
+ [class$="-option"],
650
+ [class$="-picker-button"],
651
+ .button,
652
+ .input,
653
+ .select,
654
+ .option,
655
+ .menu-item,
656
+ .tab-bar-button,
657
+ .switch-button,
658
+ .toggle-button,
659
+ .checkbox-input,
660
+ .radio-input,
661
+ .file-upload,
662
+ .signature-pad,
663
+ .editable
664
+ ):focus-visible {
665
+ /* DHCW's `nhsw-focus-ring` mixin: 3px, no offset. Kept at full
666
+ width on this variant — see the note on --lily-focus-ring above.
667
+ The zero offset also earns its keep here for a density reason:
668
+ an offset ring on a 2px-gap grid overlaps the neighbouring cell,
669
+ so a flush ring is the only one that stays inside its own row. */
670
+ outline: 3px solid var(--lily-focus-ring);
671
+ outline-offset: 0;
672
+ }
673
+
674
+ /* Focused *buttons* additionally invert to the yellow block with a dark
675
+ underline bar, per `nhsw-button-base`. A ring alone is not enough on
676
+ a navy fill: the yellow-on-navy edge is high contrast, but the button
677
+ still looks unfocused from a metre away. Flipping the fill makes the
678
+ focused control the brightest thing on the page.
679
+
680
+ Scoped to :not(:active) so the button does not flash yellow during
681
+ the press — DHCW does the same, and it matters because a pointer user
682
+ who click-and-holds should see the pressed state, not the focus one. */
683
+ :where(.button, [class$="-button"]:not(.hamburger-menu-button)):focus-visible:not(
684
+ :active
685
+ ) {
686
+ background-color: var(--lily-focus-ring);
687
+ color: var(--lily-focus-text);
688
+ border-color: transparent;
689
+ box-shadow: 0 2px 0 var(--lily-focus-text);
690
+ }
691
+
692
+ /* Focused links get the same treatment in text form: yellow block,
693
+ dark text, and a 4px bar sitting on the baseline where the underline
694
+ was. The bar is drawn with box-shadow rather than a border so it
695
+ does not shift the line box and reflow the paragraph — which on a
696
+ dense grid would mean the whole table jumping a pixel per Tab press. */
697
+ :where(a, [class$="-link"]):focus-visible {
698
+ outline: none;
699
+ background-color: var(--lily-focus-ring);
700
+ color: var(--lily-focus-text);
701
+ text-decoration: none;
702
+ box-shadow:
703
+ 0 -2px 0 var(--lily-focus-ring),
704
+ 0 4px 0 var(--lily-focus-text);
705
+ }
706
+
707
+ /* Forced-colours mode discards the yellow entirely — the OS palette
708
+ wins — so restore a system-coloured ring that mode can actually
709
+ render. Without this, a high-contrast Windows user gets a focus
710
+ indicator that is whatever `Canvas` happens to be. Practitioner
711
+ workstations are disproportionately Windows and disproportionately
712
+ likely to have a high-contrast theme forced by trust IT policy, so
713
+ this branch is load-bearing here rather than defensive. */
714
+ @media (forced-colors: active) {
715
+ :where(
716
+ .button,
717
+ [class$="-button"],
718
+ a,
719
+ [class$="-link"]
720
+ ):focus-visible {
721
+ outline: 3px solid Highlight;
722
+ outline-offset: 0;
723
+ box-shadow: none;
724
+ }
725
+ }
726
+
727
+ /* ==========================================================================
728
+ * Inputs
729
+ * --------------------------------------------------------------------------
730
+ * Every Lily class ending in `-input` (textual, date, picker, range, file,
731
+ * national-identifier, etc.) shares the same baseline styling. The
732
+ * `[class$="-input"]` attribute selector catches single-class usage
733
+ * (`class="email-input"`); consumers needing multi-class composition can
734
+ * override with higher-specificity rules in their own stylesheet.
735
+ *
736
+ * The `:not([class$="-with-mask-input"])` exclusion exists because the
737
+ * masked-input variant carries its own typography (monospaced + letter
738
+ * spacing) and we don't want the generic baseline overriding it.
739
+ * Hidden inputs (`type="hidden"`) are also excluded — they must not be
740
+ * laid out as visible inputs.
741
+ * ====================================================================== */
742
+
743
+ /* Baseline visual treatment for every textual input. Practitioner density:
744
+ min-height drops from the patients theme's 2.5rem (~44px, WCAG 2.5.5
745
+ target-size guidance for touch) to 1.75rem (~28px), in line with
746
+ mouse + keyboard workstation conventions. Padding shrinks to ~4px
747
+ vertical / 6px horizontal so multiple inputs fit comfortably on
748
+ one observation chart row. */
749
+ :where(
750
+ [class$="-input"]:not([class$="-with-mask-input"]):not([type="hidden"]),
751
+ .input
752
+ ) {
753
+ appearance: none; /* drop UA native chrome (Safari especially) */
754
+ display: inline-block;
755
+ width: 100%; /* fill parent by default; consumer caps via container */
756
+ max-width: 100%;
757
+ min-height: 1.75rem; /* practitioner: mouse + keyboard, not touch */
758
+ padding: 0.25rem 0.375rem;
759
+ font: inherit;
760
+ font-family: var(--lily-font-body);
761
+ color: var(--lily-text);
762
+ background-color: var(--lily-surface);
763
+ border: var(--border) solid var(--lily-border-strong);
764
+ border-radius: var(--lily-radius-md);
765
+ box-sizing: border-box;
766
+ /* Smooth focus/hover edge transitions — short enough to feel
767
+ responsive, long enough to read as intentional. */
768
+ transition:
769
+ border-color 120ms ease,
770
+ box-shadow 120ms ease;
771
+ }
772
+
773
+ /* Hover affordance — switches the border to the primary brand colour so
774
+ users get a visual cue that the field is interactive. The `:not(:disabled)`
775
+ guard prevents the cue from appearing on locked-out controls. */
776
+ :where([class$="-input"], .input):hover:not(:disabled) {
777
+ border-color: var(--color-primary);
778
+ }
779
+
780
+ /* Disabled / aria-disabled — desaturate and shift to the muted surface so
781
+ the control reads as inactive without losing its layout footprint. */
782
+ :where([class$="-input"], .input):disabled,
783
+ :where([class$="-input"], .input)[aria-disabled="true"] {
784
+ color: var(--lily-text-muted);
785
+ background-color: var(--lily-surface-alt);
786
+ cursor: not-allowed;
787
+ }
788
+
789
+ /* Invalid input — red border. The `aria-invalid="true"` attribute is the
790
+ ARIA contract; pairing it with a colour change keeps WCAG 1.4.1
791
+ (no colour-only meaning) honest because the ARIA state is also there. */
792
+ :where([class$="-input"], .input)[aria-invalid="true"] {
793
+ border-color: var(--color-error);
794
+ }
795
+
796
+ /* Placeholder — match the muted text token. `opacity: 1` overrides Firefox's
797
+ legacy default placeholder opacity, which would otherwise compound on
798
+ our already-muted colour. */
799
+ :where([class$="-input"], .input)::placeholder {
800
+ color: var(--lily-text-muted);
801
+ opacity: 1;
802
+ }
803
+
804
+ /* --- Input type-specific tweaks ----------------------------------------- */
805
+
806
+ /* Hidden inputs must never render. */
807
+ :where(.hidden-input) {
808
+ display: none;
809
+ }
810
+
811
+ /* Submit / reset / image / generic button inputs render as buttons. */
812
+ :where(.button-input, .submit-input, .reset-input, .image-input) {
813
+ width: auto;
814
+ min-width: 1.75rem;
815
+ background-color: var(--color-primary);
816
+ color: var(--color-primary-content);
817
+ border-color: transparent;
818
+ cursor: pointer;
819
+ font-weight: 600;
820
+ }
821
+
822
+ /* Reset input is the secondary-action variant — muted surface, not the
823
+ primary brand colour, because "reset" is a destructive op. */
824
+ :where(.reset-input) {
825
+ background-color: var(--lily-surface-alt);
826
+ color: var(--lily-text);
827
+ border-color: var(--lily-border-strong);
828
+ }
829
+
830
+ /* Color input — narrow swatch with internal padding so the colour chip
831
+ reads cleanly inside the input frame. */
832
+ :where(.color-input) {
833
+ min-height: 1.75rem;
834
+ padding: 0.125rem;
835
+ width: 2rem;
836
+ cursor: pointer;
837
+ }
838
+
839
+ /* Range / angle slider — strip the input chrome so the consumer can
840
+ restyle the track. `accent-color` provides a minimal styled fallback. */
841
+ :where(.range-input, .angle-slider-range-input) {
842
+ appearance: none;
843
+ background: transparent;
844
+ border: 0;
845
+ padding: 0;
846
+ height: 1rem;
847
+ accent-color: var(--color-primary);
848
+ }
849
+
850
+ /* File inputs — slightly tighter padding because the UA-rendered
851
+ "Choose file" button consumes most of the visual space. */
852
+ :where(.file-input, .image-file-input) {
853
+ padding: 0.125rem 0.25rem;
854
+ cursor: pointer;
855
+ }
856
+
857
+ /* Checkbox / radio — small square/round controls with the brand accent.
858
+ Practitioner density: 14px tick targets — mouse-only, no touch. */
859
+ :where(.checkbox-input, .radio-input) {
860
+ width: 0.875rem;
861
+ height: 0.875rem;
862
+ min-height: 0; /* override the input baseline */
863
+ padding: 0;
864
+ accent-color: var(--color-primary);
865
+ cursor: pointer;
866
+ }
867
+ :where(.radio-input) {
868
+ border-radius: 50%;
869
+ }
870
+ :where(.checkbox-input) {
871
+ border-radius: var(--lily-radius-sm);
872
+ }
873
+
874
+ /* Textarea variants — taller minimum, line-height tuned for reading,
875
+ vertical-only resize so layout columns aren't disturbed. Practitioner
876
+ density: 4rem starting height suits a short progress note; consumers
877
+ can grow it by setting `rows` or overriding min-height. */
878
+ :where(.text-area-input, .text-area-input-with-character-counter) {
879
+ min-height: 4rem;
880
+ line-height: 1.4;
881
+ resize: vertical;
882
+ }
883
+ /* Character-counter variant stacks the textarea and the counter span. */
884
+ :where(.text-area-input-with-character-counter) {
885
+ display: flex;
886
+ flex-direction: column;
887
+ }
888
+
889
+ /* --- Composite inputs with built-in DOM structure ----------------------- */
890
+
891
+ /* PIN input — N small slots side-by-side. The wrapper is invisible;
892
+ the children carry the styling. */
893
+ :where(.pin-input-div) {
894
+ display: inline-flex;
895
+ gap: 0.25rem;
896
+ padding: 0;
897
+ background: transparent;
898
+ border: 0;
899
+ width: auto;
900
+ min-height: 0;
901
+ }
902
+ :where(.pin-input-div > input) {
903
+ width: 1.75rem;
904
+ height: 2rem;
905
+ padding: 0;
906
+ text-align: center;
907
+ font-size: 1rem;
908
+ font-weight: 600;
909
+ background-color: var(--lily-surface);
910
+ border: var(--border) solid var(--lily-border-strong);
911
+ border-radius: var(--lily-radius-md);
912
+ color: var(--lily-text);
913
+ }
914
+
915
+ /* Password-toggle composite — text input + reveal button, sharing
916
+ a single visual frame via flush radii. */
917
+ :where(.password-input-or-text-input-div) {
918
+ display: inline-flex;
919
+ align-items: stretch;
920
+ width: 100%;
921
+ background: transparent;
922
+ border: 0;
923
+ padding: 0;
924
+ min-height: 0;
925
+ }
926
+ :where(.password-input-or-text-input-div > input) {
927
+ flex: 1;
928
+ min-height: 1.75rem;
929
+ padding: 0.25rem 0.375rem;
930
+ background-color: var(--lily-surface);
931
+ color: var(--lily-text);
932
+ border: var(--border) solid var(--lily-border-strong);
933
+ border-right: 0; /* drop the seam where input meets button */
934
+ border-radius: var(--lily-radius-md) 0 0 var(--lily-radius-md);
935
+ }
936
+ :where(.password-input-or-text-input-div > button) {
937
+ padding: 0 0.375rem;
938
+ background-color: var(--lily-surface-alt);
939
+ color: var(--lily-text);
940
+ border: var(--border) solid var(--lily-border-strong);
941
+ border-radius: 0 var(--lily-radius-md) var(--lily-radius-md) 0;
942
+ cursor: pointer;
943
+ }
944
+
945
+ /* --- Input compositions ------------------------------------------------- */
946
+
947
+ /* Input group — flush-joined controls (input + button, etc.). The negative
948
+ margin collapses overlapping borders into a single shared edge. */
949
+ :where(.input-group) {
950
+ display: inline-flex;
951
+ align-items: stretch;
952
+ width: 100%;
953
+ }
954
+ :where(.input-group > *) {
955
+ border-radius: 0;
956
+ margin-left: calc(-1 * var(--border));
957
+ }
958
+ :where(.input-group > *:first-child) {
959
+ border-top-left-radius: var(--lily-radius-md);
960
+ border-bottom-left-radius: var(--lily-radius-md);
961
+ margin-left: 0;
962
+ }
963
+ :where(.input-group > *:last-child) {
964
+ border-top-right-radius: var(--lily-radius-md);
965
+ border-bottom-right-radius: var(--lily-radius-md);
966
+ }
967
+
968
+ /* Masked input — monospaced font + slight letter-spacing so the mask
969
+ template (phone, SSN, etc.) aligns over the typed characters. */
970
+ :where(.input-with-mask) {
971
+ font-family: var(--lily-font-mono);
972
+ letter-spacing: 0.05em;
973
+ }
974
+
975
+ /* Search input — reserve space on the inline-start for the magnifier
976
+ glyph. The `linear-gradient(transparent, transparent)` is a no-op
977
+ background-image; it ensures the property exists so consumers can
978
+ layer an SVG icon over it by overriding `background-image`. */
979
+ :where(.text-input-with-search, .search-input) {
980
+ background-image: linear-gradient(transparent, transparent);
981
+ padding-inline-start: 1.5rem;
982
+ }
983
+
984
+ /* ==========================================================================
985
+ * Views
986
+ * --------------------------------------------------------------------------
987
+ * Read-only display chips for identifiers, dates, ratings, and other
988
+ * pre-formatted data. Every Lily class ending in `-view` is a span/div
989
+ * that renders a value the user shouldn't edit.
990
+ *
991
+ * The exclusions (`theme-view`, `split-view`)
992
+ * are larger containers that don't fit the chip pattern — they're
993
+ * styled separately further down.
994
+ * ====================================================================== */
995
+
996
+ :where(
997
+ [class$="-view"]:not(.theme-view):not(
998
+ .split-view
999
+ )
1000
+ ) {
1001
+ display: inline-block;
1002
+ padding: 0 0.25rem;
1003
+ font-family: var(--lily-font-mono); /* fixed-width for alignment */
1004
+ color: var(--lily-text);
1005
+ background-color: var(--lily-surface-alt);
1006
+ border: var(--border) solid var(--lily-border);
1007
+ border-radius: var(--lily-radius-md);
1008
+ line-height: 1.3;
1009
+ }
1010
+
1011
+ /* Dates / ranges are human-readable, not codes — switch back to body font. */
1012
+ :where(.date-time-view, .date-range, .review-date) {
1013
+ font-family: var(--lily-font-body);
1014
+ }
1015
+
1016
+ /* Theme-view — small label + colour swatch composition. */
1017
+ :where(.theme-view) {
1018
+ display: inline-flex;
1019
+ align-items: center;
1020
+ gap: 0.25rem;
1021
+ padding: 0.125rem 0.25rem;
1022
+ color: var(--lily-text);
1023
+ }
1024
+
1025
+ /* ==========================================================================
1026
+ * Buttons
1027
+ * --------------------------------------------------------------------------
1028
+ * Every class ending in `-button` and the bare `.button` share one
1029
+ * baseline. Hamburger-menu is excluded because it's an icon-only square
1030
+ * with its own minimal frame styling (further down).
1031
+ * ====================================================================== */
1032
+
1033
+ /* Baseline button — primary-coloured rectangle. Variants override colour
1034
+ or shape below. Practitioner density: 28px (1.75rem) min-height vs. the
1035
+ patients theme's 44px, because practitioners use mouse + keyboard at
1036
+ workstations rather than touch screens. */
1037
+ :where(.button, [class$="-button"]:not(.hamburger-menu-button)) {
1038
+ appearance: none;
1039
+ display: inline-flex;
1040
+ align-items: center;
1041
+ justify-content: center;
1042
+ gap: 0.25rem; /* leave room for an icon + label */
1043
+ min-height: 1.75rem;
1044
+ padding: 0.25rem 0.5rem;
1045
+ font: inherit;
1046
+ font-family: var(--lily-font-body);
1047
+ /* DHCW `nhsw-button-base` sets 700. The weight is adopted; the
1048
+ 44px min-height and 12px/16px padding around it are NOT, because
1049
+ this is a mouse-and-keyboard workstation surface. Density stays
1050
+ at 28px / 4px / 8px. */
1051
+ font-weight: 700;
1052
+ line-height: 1.25;
1053
+ color: var(--color-primary-content);
1054
+ background-color: var(--color-primary);
1055
+ border: var(--border) solid transparent;
1056
+ border-radius: var(--lily-radius-md);
1057
+ /* The DHCW "ledge", halved to 1px to match this variant's shadow
1058
+ scale. At 28px tall a 2px ledge is 7% of the control and reads as
1059
+ a misaligned border; 1px reads as depth. */
1060
+ box-shadow: 0 1px 0
1061
+ color-mix(in oklch, var(--color-primary) 70%, black);
1062
+ cursor: pointer;
1063
+ user-select: none; /* prevent text drag-select on double-click */
1064
+ text-decoration: none;
1065
+ transition:
1066
+ background-color 120ms ease,
1067
+ color 120ms ease,
1068
+ border-color 120ms ease;
1069
+ }
1070
+
1071
+ /* Hover — DARKEN the primary rather than swapping to the secondary
1072
+ token. The sibling Lily themes swap primary→secondary on hover, which
1073
+ under the DHCW palette would take a navy button to NHS Blue: hovering
1074
+ would make the control *lighter* and lower-contrast, exactly backwards.
1075
+ DHCW's own hover is #1B2A49 against a #1B365D rest state, i.e. a
1076
+ darkening, and that is what this mix reproduces. */
1077
+ :where(.button, [class$="-button"]):hover:not(:disabled):not(
1078
+ [aria-disabled="true"]
1079
+ ) {
1080
+ background-color: color-mix(
1081
+ in oklch,
1082
+ var(--color-primary) 85%,
1083
+ black
1084
+ );
1085
+ color: var(--color-primary-content);
1086
+ }
1087
+
1088
+ /* Press — sink into the ledge. The 1px shadow means the transform is
1089
+ the whole ledge here, so the control flattens completely on press.
1090
+ That is the intended read at this size. */
1091
+ :where(.button, [class$="-button"]):active:not(:disabled):not(
1092
+ [aria-disabled="true"]
1093
+ ) {
1094
+ transform: translateY(1px);
1095
+ box-shadow: none;
1096
+ }
1097
+
1098
+ /* Honour a reduced-motion preference by dropping the press transform
1099
+ and the colour transitions. The state changes remain; only the
1100
+ movement between them goes. */
1101
+ @media (prefers-reduced-motion: reduce) {
1102
+ :where(.button, [class$="-button"]) {
1103
+ transition: none;
1104
+ }
1105
+ :where(.button, [class$="-button"]):active {
1106
+ transform: none;
1107
+ }
1108
+ }
1109
+
1110
+ /* Disabled — half opacity, not-allowed cursor. */
1111
+ :where(.button, [class$="-button"]):disabled,
1112
+ :where(.button, [class$="-button"])[aria-disabled="true"] {
1113
+ opacity: 0.5;
1114
+ cursor: not-allowed;
1115
+ }
1116
+
1117
+ /* Pressed (toggle/switch active state) — re-use the secondary colour. */
1118
+ :where(.button, [class$="-button"])[aria-pressed="true"] {
1119
+ background-color: var(--color-secondary);
1120
+ color: var(--color-secondary-content);
1121
+ }
1122
+
1123
+ /* --- DHCW button variants ------------------------------------------------
1124
+ * The published library ships five button styles as BEM modifiers
1125
+ * (`nhsw-button--primary/secondary/outline/warning/exceptional`). Lily
1126
+ * is headless and does not mint modifier classes, so they are exposed
1127
+ * here as `data-variant` on any `-button` hook — the same mechanism the
1128
+ * care-card and alert families already use in this file.
1129
+ *
1130
+ * <button class="button" data-variant="warning">Delete record</button>
1131
+ *
1132
+ * `primary` is the default and needs no attribute. All ledges are 1px
1133
+ * here rather than DHCW's 2px, matching this variant's shadow scale.
1134
+ * ---------------------------------------------------------------------- */
1135
+
1136
+ /* Secondary — white fill, slate border, slate ledge. DHCW's is a true
1137
+ outline button rather than a grey fill, which keeps two buttons side
1138
+ by side clearly ranked without a size difference — worth more on a
1139
+ toolbar of eight controls than on a page with two. */
1140
+ :where([class$="-button"], .button)[data-variant="secondary"] {
1141
+ background-color: var(--lily-surface);
1142
+ color: var(--lily-text);
1143
+ border-color: var(--color-neutral);
1144
+ box-shadow: 0 1px 0 var(--color-neutral);
1145
+ }
1146
+ :where([class$="-button"], .button)[data-variant="secondary"]:hover:not(
1147
+ :disabled
1148
+ ) {
1149
+ background-color: var(--lily-surface-alt);
1150
+ color: var(--lily-text);
1151
+ }
1152
+
1153
+ /* Warning / destructive — DHCW calls this `--warning`, but it is the
1154
+ delete button: red fill, white label. Named `warning` here to match
1155
+ the published library rather than inventing a third vocabulary, at
1156
+ the cost of it not meaning "caution". If you need caution, that is
1157
+ the warning-callout below, not a button.
1158
+
1159
+ On a clinical surface, reserve this for actions that destroy or
1160
+ retract a record. A red button in a worklist toolbar that merely
1161
+ filters is how people learn to click red buttons without reading. */
1162
+ :where([class$="-button"], .button)[data-variant="warning"] {
1163
+ background-color: var(--color-error);
1164
+ color: var(--color-error-content);
1165
+ border-color: var(--color-error);
1166
+ box-shadow: 0 1px 0 color-mix(in oklch, var(--color-error) 60%, black);
1167
+ }
1168
+ :where([class$="-button"], .button)[data-variant="warning"]:hover:not(
1169
+ :disabled
1170
+ ) {
1171
+ background-color: color-mix(in oklch, var(--color-error) 75%, black);
1172
+ color: var(--color-error-content);
1173
+ }
1174
+
1175
+ /* Exceptional — the warm gold fill. DHCW's name for "the one action on
1176
+ this page that is neither the primary path nor destructive, but must
1177
+ still be found": escalate, override, request a second opinion. Dark
1178
+ text on #F5BB66 is 8.4:1, so unlike the bright gold rail this one can
1179
+ safely carry a label. */
1180
+ :where([class$="-button"], .button)[data-variant="exceptional"] {
1181
+ background-color: var(--lily-accent-soft);
1182
+ color: var(--color-base-content);
1183
+ border-color: var(--lily-accent-soft);
1184
+ box-shadow: 0 1px 0 var(--lily-accent-deep);
1185
+ }
1186
+ :where([class$="-button"], .button)[data-variant="exceptional"]:hover:not(
1187
+ :disabled
1188
+ ) {
1189
+ background-color: color-mix(
1190
+ in oklch,
1191
+ var(--lily-accent-soft) 88%,
1192
+ black
1193
+ );
1194
+ color: var(--color-base-content);
1195
+ }
1196
+
1197
+ /* Full-width — opt-in only. DHCW makes buttons full-width below 40rem;
1198
+ this variant deliberately does NOT reproduce that automatically,
1199
+ because a practitioner form is a two-column layout on a 1080p
1200
+ workstation and a stretched submit button there is wasted space. Set
1201
+ it explicitly where you want it. */
1202
+ :where([class$="-button"], .button)[data-variant="full"] {
1203
+ width: 100%;
1204
+ }
1205
+
1206
+ /* --- Button variants ---------------------------------------------------- */
1207
+
1208
+ /* Icon button — square; aspect-ratio keeps it from squashing when the
1209
+ icon font changes size. */
1210
+ :where(.icon-button) {
1211
+ padding: 0.25rem;
1212
+ min-width: 1.75rem;
1213
+ aspect-ratio: 1;
1214
+ }
1215
+
1216
+ /* Floating action button — pinned to the bottom-right of the viewport.
1217
+ Uses logical inset properties so it flips correctly under RTL.
1218
+ Practitioner density: smaller FAB so it doesn't obstruct dense charts. */
1219
+ :where(.float-button) {
1220
+ position: fixed;
1221
+ inset-block-end: 0.75rem;
1222
+ inset-inline-end: 0.75rem;
1223
+ box-shadow: var(--lily-shadow-lg);
1224
+ border-radius: 999px;
1225
+ width: 2.5rem;
1226
+ height: 2.5rem;
1227
+ padding: 0;
1228
+ }
1229
+
1230
+ /* Split button — extra padding-end for the dropdown caret. */
1231
+ :where(.split-button) {
1232
+ padding-inline-end: 0.25rem;
1233
+ }
1234
+
1235
+ /* Download button — stacks the action label over a hint (filesize, etc.). */
1236
+ :where(.download-button) {
1237
+ flex-direction: column;
1238
+ align-items: flex-start;
1239
+ text-align: start;
1240
+ }
1241
+
1242
+ /* Clipboard-copy — muted style because it's auxiliary, not primary. */
1243
+ :where(.clipboard-copy-button) {
1244
+ background-color: var(--lily-surface-alt);
1245
+ color: var(--lily-text);
1246
+ border-color: var(--lily-border-strong);
1247
+ }
1248
+
1249
+ /* Switch button — iOS-style toggle pill. The track is the sunken
1250
+ surface; the thumb is drawn by the consumer (or via ::before in
1251
+ higher-specificity overrides). Practitioner density: ~32px × 16px. */
1252
+ :where(.switch-button) {
1253
+ position: relative;
1254
+ width: 2rem;
1255
+ min-width: 2rem;
1256
+ height: 1rem;
1257
+ min-height: 1rem;
1258
+ padding: 0;
1259
+ background-color: var(--lily-surface-sunken);
1260
+ border-radius: 999px;
1261
+ }
1262
+ :where(
1263
+ .switch-button[aria-pressed="true"],
1264
+ .switch-button[aria-checked="true"]
1265
+ ) {
1266
+ background-color: var(--color-primary);
1267
+ }
1268
+
1269
+ /* Toggle button — when un-pressed, render as a muted secondary chip. */
1270
+ :where(.toggle-button[aria-pressed="false"]) {
1271
+ background-color: var(--lily-surface-alt);
1272
+ color: var(--lily-text);
1273
+ border-color: var(--lily-border-strong);
1274
+ }
1275
+
1276
+ /* Link-style "button" — for back/skip/go-to controls that look like
1277
+ text links but use the button machinery for keyboard semantics. */
1278
+ :where(
1279
+ .back-link,
1280
+ .skip-link,
1281
+ .go-to-top,
1282
+ .go-to-next-section,
1283
+ .go-to-previous-section
1284
+ ) {
1285
+ background-color: transparent;
1286
+ color: var(--color-primary);
1287
+ border-color: transparent;
1288
+ text-decoration: underline;
1289
+ text-underline-offset: 0.2em;
1290
+ }
1291
+
1292
+ /* Skip-link — visually hidden off-screen until focused, then snaps to the
1293
+ top-left of the page. Required pattern for keyboard accessibility
1294
+ (WCAG 2.4.1 bypass blocks). */
1295
+ :where(.skip-link) {
1296
+ position: absolute;
1297
+ top: -3rem;
1298
+ left: 0.25rem;
1299
+ padding: 0.25rem 0.5rem;
1300
+ background-color: var(--color-primary);
1301
+ color: var(--color-primary-content);
1302
+ text-decoration: none;
1303
+ border-radius: var(--lily-radius-md);
1304
+ z-index: 9999;
1305
+ }
1306
+ :where(.skip-link:focus) {
1307
+ top: 0.25rem;
1308
+ }
1309
+
1310
+ /* Button group — horizontal cluster of related actions. */
1311
+ :where(.button-group) {
1312
+ display: inline-flex;
1313
+ gap: 0.125rem;
1314
+ }
1315
+
1316
+ /* --- Bars of buttons (action / menu / tab / task / tool) ---------------- */
1317
+
1318
+ :where(.action-bar, .menu-bar, .tab-bar, .task-bar, .tool-bar) {
1319
+ display: flex;
1320
+ align-items: center;
1321
+ gap: 0.125rem;
1322
+ padding: 0.25rem;
1323
+ background-color: var(--lily-surface-alt);
1324
+ border: var(--border) solid var(--lily-border);
1325
+ border-radius: var(--lily-radius-md);
1326
+ }
1327
+ /* Children of bars render flush with the bar — transparent background,
1328
+ no border, so the bar's surface acts as the visual frame. */
1329
+ :where(
1330
+ .action-bar-button,
1331
+ .menu-bar-button,
1332
+ .tab-bar-button,
1333
+ .task-bar-button,
1334
+ .tool-bar-button
1335
+ ) {
1336
+ background-color: transparent;
1337
+ color: var(--lily-text);
1338
+ border-color: transparent;
1339
+ }
1340
+ /* Selected tab — explicit primary colour. */
1341
+ :where(.tab-bar-button[aria-selected="true"]) {
1342
+ background-color: var(--color-primary);
1343
+ color: var(--color-primary-content);
1344
+ }
1345
+ /* Panel revealed by the active tab. */
1346
+ :where(.tab-panel) {
1347
+ padding: 0.5rem;
1348
+ background-color: var(--lily-surface);
1349
+ border: var(--border) solid var(--lily-border);
1350
+ border-radius: var(--lily-radius-md);
1351
+ }
1352
+
1353
+ /* ==========================================================================
1354
+ * Links, anchors, link-like
1355
+ * --------------------------------------------------------------------------
1356
+ * Generic `-link` styling — primary-coloured underlined text. The
1357
+ * exclusions (`skip-link`, `back-link`) are styled as buttons above.
1358
+ * ====================================================================== */
1359
+
1360
+ /* Links are NHS Blue (`$nhsw-colour-link`), i.e. --color-secondary
1361
+ under this palette — not the navy primary. Keeping links blue and
1362
+ buttons navy is what lets a reader tell "this navigates" from "this
1363
+ acts" without reading either one. */
1364
+ :where([class$="-link"]:not(.skip-link):not(.back-link)) {
1365
+ color: var(--color-secondary);
1366
+ text-decoration: underline;
1367
+ text-underline-offset: 0.12em;
1368
+ }
1369
+ /* Hover — DHCW drops to NHS Dark Blue #003087 and thickens the rule.
1370
+ Both, not either: colour alone is not a state change (WCAG 1.4.1). */
1371
+ :where([class$="-link"]):hover {
1372
+ color: var(--lily-link-hover);
1373
+ text-decoration-thickness: max(3px, 0.1875rem);
1374
+ }
1375
+ /* Visited — NHS Purple #7C2855 (`$nhsw-colour-link-visited`).
1376
+ Deliberately NOT applied inside a data grid: on a worklist, "visited"
1377
+ marks which patient records this user happened to open, which is
1378
+ both clinically meaningless and a small privacy leak to anyone
1379
+ shoulder-surfing a shared workstation. Rows stay link-coloured. */
1380
+ :where([class$="-link"]):visited {
1381
+ color: var(--lily-link-visited);
1382
+ }
1383
+ :where(
1384
+ .data-table,
1385
+ .table,
1386
+ .kanban-table,
1387
+ .gantt-table
1388
+ ) :where([class$="-link"]):visited {
1389
+ color: var(--color-secondary);
1390
+ }
1391
+
1392
+ /* ----------------------------------------------------------------------
1393
+ * Action link — DHCW's green-chevron call to action
1394
+ *
1395
+ * The one link on a page that is really a call to action. DHCW gives it
1396
+ * a 36px green chevron, h4-size bold text, and — unusually — text in
1397
+ * text-primary rather than link blue, with the green carried entirely
1398
+ * by the icon.
1399
+ *
1400
+ * That inversion is the interesting decision. A large bold blue link
1401
+ * competes with the page's real headings; a large bold *black* link
1402
+ * with a green arrow reads as an action without pretending to be a
1403
+ * heading. The colour is in the glyph, where it has no contrast
1404
+ * obligation, so the green can be as saturated as it likes.
1405
+ *
1406
+ * Lily's action-link is a bare <a> with a children slot — the consumer
1407
+ * supplies the SVG, per the headless rule that no icon assets ship. The
1408
+ * rule below colours and sizes whatever <svg> it finds, and degrades to
1409
+ * plain bold text if there is none.
1410
+ *
1411
+ * Density: 24px chevron and body-size text against DHCW's 36px and
1412
+ * h4. This component is deliberately still the largest interactive
1413
+ * thing in this file — it is the one that must be found — but on a
1414
+ * practitioner shell it sits in a sidebar or a task panel rather than
1415
+ * mid-article, and a 36px chevron there is a button pretending to be a
1416
+ * link.
1417
+ * -------------------------------------------------------------------- */
1418
+ :where(.action-link) {
1419
+ display: flex;
1420
+ align-items: center;
1421
+ gap: 0.375rem;
1422
+ margin-bottom: 0.5rem;
1423
+ color: var(--lily-text);
1424
+ font-size: 1rem;
1425
+ font-weight: 700;
1426
+ line-height: 1.5;
1427
+ text-decoration: none;
1428
+ }
1429
+ :where(.action-link):visited {
1430
+ color: var(--lily-text);
1431
+ }
1432
+ :where(.action-link > svg) {
1433
+ flex-shrink: 0;
1434
+ width: 1.5rem;
1435
+ height: 1.5rem;
1436
+ fill: var(--lily-action-green);
1437
+ }
1438
+ :where(.action-link):hover {
1439
+ color: var(--lily-text);
1440
+ text-decoration: underline;
1441
+ text-decoration-thickness: max(3px, 0.1875rem);
1442
+ text-underline-offset: 0.12em;
1443
+ }
1444
+ :where(.action-link):hover > svg {
1445
+ fill: color-mix(in oklch, var(--lily-action-green) 80%, black);
1446
+ }
1447
+ /* On a dark surface the chevron goes white and the text inverts —
1448
+ DHCW's `--on-dark` modifier, exposed as data-tone. Expect to need
1449
+ this one: the practitioner header band is navy. */
1450
+ :where(.action-link[data-tone="on-dark"]),
1451
+ :where(.action-link[data-tone="on-dark"]):visited {
1452
+ color: var(--color-primary-content);
1453
+ }
1454
+ :where(.action-link[data-tone="on-dark"]) > svg {
1455
+ fill: var(--color-primary-content);
1456
+ }
1457
+
1458
+ /* ==========================================================================
1459
+ * Lists & list items
1460
+ * --------------------------------------------------------------------------
1461
+ * Lily wraps `<ol>` / `<ul>` in role-specific classes (CheckList, DoList,
1462
+ * etc.). The baseline kills the default browser bullet styling and zeroes
1463
+ * margins so consumers can compose lists inside other components.
1464
+ * ====================================================================== */
1465
+
1466
+ /* Baseline — kill bullets and the indent gutter on every list class. */
1467
+ :where(
1468
+ .accordion-list,
1469
+ .breadcrumb-list,
1470
+ .chat-list,
1471
+ .check-list,
1472
+ .collection-list,
1473
+ .contents-list,
1474
+ .description-list,
1475
+ .document-list,
1476
+ .do-list,
1477
+ .dont-list,
1478
+ .icon-list,
1479
+ .pagination-list,
1480
+ .section-list,
1481
+ .step-list,
1482
+ .summary-list,
1483
+ .task-list,
1484
+ .timeline-list,
1485
+ .tour-list,
1486
+ .tree-list,
1487
+ .validation-list
1488
+ ) {
1489
+ list-style: none;
1490
+ margin: 0;
1491
+ padding: 0;
1492
+ }
1493
+
1494
+ /* Baseline — every list-item gets a thin hairline rule between rows.
1495
+ Practitioner density: 0.25rem (4px) vertical padding for tight rows. */
1496
+ :where(
1497
+ .accordion-list-item,
1498
+ .breadcrumb-list-item,
1499
+ .chat-list-item,
1500
+ .check-list-item,
1501
+ .collection-list-item,
1502
+ .contents-list-item,
1503
+ .description-list-item,
1504
+ .document-list-item,
1505
+ .do-list-item,
1506
+ .dont-list-item,
1507
+ .icon-list-item,
1508
+ .pagination-list-item,
1509
+ .section-list-item,
1510
+ .step-list-item,
1511
+ .summary-list-item,
1512
+ .task-list-item,
1513
+ .timeline-list-item,
1514
+ .tour-list-item,
1515
+ .tree-list-item,
1516
+ .validation-list-item
1517
+ ) {
1518
+ padding: 0.25rem 0;
1519
+ border-bottom: var(--border) solid var(--lily-border);
1520
+ }
1521
+
1522
+ /* --- Breadcrumb --------------------------------------------------------- */
1523
+ /* Horizontal list separated by a "/" pseudo-element between items. */
1524
+ :where(.breadcrumb-list) {
1525
+ display: flex;
1526
+ flex-wrap: wrap;
1527
+ gap: 0.25rem;
1528
+ align-items: center;
1529
+ }
1530
+ :where(.breadcrumb-list-item) {
1531
+ border: 0; /* override the generic list-item bottom-border */
1532
+ padding: 0;
1533
+ display: inline-flex;
1534
+ align-items: center;
1535
+ }
1536
+ /* Slash separator generated between adjacent items, not before the first. */
1537
+ :where(.breadcrumb-list-item + .breadcrumb-list-item)::before {
1538
+ content: "/";
1539
+ margin-inline-end: 0.25rem;
1540
+ color: var(--lily-text-muted);
1541
+ }
1542
+ /* Final crumb (the current page) — muted because it isn't a link. */
1543
+ :where(.breadcrumb-list-item[aria-current="page"]) {
1544
+ color: var(--lily-text-muted);
1545
+ }
1546
+
1547
+ /* --- Pagination --------------------------------------------------------- */
1548
+ :where(.pagination-list) {
1549
+ display: flex;
1550
+ flex-wrap: wrap;
1551
+ gap: 0.125rem;
1552
+ align-items: center;
1553
+ }
1554
+ :where(.pagination-list-item) {
1555
+ border: 0;
1556
+ padding: 0;
1557
+ }
1558
+ /* Pagination link — square-ish chip; practitioner density uses a mouse-sized
1559
+ target, not the patients theme's WCAG 2.5.5 touch target. */
1560
+ :where(.pagination-link) {
1561
+ display: inline-flex;
1562
+ align-items: center;
1563
+ justify-content: center;
1564
+ min-width: 1.75rem;
1565
+ padding: 0.25rem 0.375rem;
1566
+ border-radius: var(--lily-radius-md);
1567
+ text-decoration: none;
1568
+ }
1569
+ /* Current page — filled with the primary brand colour. */
1570
+ :where(.pagination-link[aria-current="page"]) {
1571
+ background-color: var(--color-primary);
1572
+ color: var(--color-primary-content);
1573
+ }
1574
+
1575
+ /* --- Marker glyphs ------------------------------------------------------ */
1576
+ /* Check / do — green ✓; dont — red ✗. The `::marker` pseudo-element
1577
+ only works on list-items with the default list-style display, so the
1578
+ generic baseline above intentionally kept that. */
1579
+ :where(.check-list-item)::marker {
1580
+ content: "✓ ";
1581
+ color: var(--color-success);
1582
+ }
1583
+ :where(.do-list-item)::marker {
1584
+ content: "✓ ";
1585
+ color: var(--color-success);
1586
+ }
1587
+ :where(.dont-list-item)::marker {
1588
+ content: "✗ ";
1589
+ color: var(--color-error);
1590
+ }
1591
+
1592
+ /* --- Tree --------------------------------------------------------------- */
1593
+ /* Nested tree lists indent their children. Practitioner: tighter indent. */
1594
+ :where(.tree-list .tree-list) {
1595
+ padding-inline-start: 0.75rem;
1596
+ }
1597
+ :where(.tree-list-item) {
1598
+ border: 0;
1599
+ }
1600
+
1601
+ /* --- Step list ---------------------------------------------------------- */
1602
+ /* CSS counter draws the step number in a circular badge. */
1603
+ :where(.step-list) {
1604
+ counter-reset: lily-step;
1605
+ }
1606
+ :where(.step-list-item) {
1607
+ counter-increment: lily-step;
1608
+ display: flex;
1609
+ align-items: flex-start;
1610
+ gap: 0.375rem;
1611
+ }
1612
+ :where(.step-list-item)::before {
1613
+ content: counter(lily-step);
1614
+ flex: 0 0 auto;
1615
+ display: inline-flex;
1616
+ align-items: center;
1617
+ justify-content: center;
1618
+ width: 1.25rem;
1619
+ height: 1.25rem;
1620
+ border-radius: 50%;
1621
+ background-color: var(--lily-surface-sunken);
1622
+ color: var(--lily-text);
1623
+ font-weight: 600;
1624
+ }
1625
+ /* Step status variants — colour-coded badge fill. */
1626
+ :where(.step-list-item[data-status="finished"])::before {
1627
+ background-color: var(--color-success);
1628
+ color: var(--color-success-content);
1629
+ }
1630
+ :where(.step-list-item[data-status="in-progress"])::before {
1631
+ background-color: var(--color-primary);
1632
+ color: var(--color-primary-content);
1633
+ }
1634
+ :where(.step-list-item[data-status="error"])::before {
1635
+ background-color: var(--color-error);
1636
+ color: var(--color-error-content);
1637
+ }
1638
+
1639
+ /* --- Task list ---------------------------------------------------------- */
1640
+ /* Flex row so the checkbox aligns with its label. */
1641
+ :where(.task-list-item) {
1642
+ display: flex;
1643
+ align-items: center;
1644
+ gap: 0.25rem;
1645
+ }
1646
+
1647
+ /* --- Timeline ----------------------------------------------------------- */
1648
+ /* Vertical rule down the left, with a dot per event. */
1649
+ :where(.timeline-list-item) {
1650
+ position: relative;
1651
+ padding-inline-start: 0.75rem;
1652
+ border-left: var(--border) solid var(--lily-border-strong);
1653
+ margin-inline-start: 0.25rem;
1654
+ }
1655
+ :where(.timeline-list-item)::before {
1656
+ content: "";
1657
+ position: absolute;
1658
+ inset-inline-start: -0.3rem;
1659
+ inset-block-start: 0.4rem;
1660
+ width: 0.5rem;
1661
+ height: 0.5rem;
1662
+ border-radius: 50%;
1663
+ background-color: var(--color-primary);
1664
+ }
1665
+
1666
+ /* --- Validation list — pass / fail / pending status colours ------------ */
1667
+ :where(.validation-list-item[data-status="passed"]) {
1668
+ color: var(--color-success);
1669
+ }
1670
+ :where(.validation-list-item[data-status="failed"]) {
1671
+ color: var(--color-error);
1672
+ }
1673
+ :where(.validation-list-item[data-status="pending"]) {
1674
+ color: var(--lily-text-muted);
1675
+ }
1676
+
1677
+ /* --- Description list (semantic <dl>) ----------------------------------- */
1678
+ /* Two-column grid: term in the left column, description in the right. */
1679
+ :where(.description-list) {
1680
+ display: grid;
1681
+ grid-template-columns: minmax(min-content, max-content) 1fr;
1682
+ column-gap: 0.5rem;
1683
+ row-gap: 0.125rem;
1684
+ }
1685
+ /* `display: contents` lets the dt/dd children participate directly in
1686
+ the parent grid without an intermediate row wrapper. */
1687
+ :where(.description-list-item) {
1688
+ display: contents;
1689
+ border: 0;
1690
+ }
1691
+ :where(.description-list dt) {
1692
+ font-weight: 600;
1693
+ }
1694
+
1695
+ /* --- Summary list (three-column key/value/action grid) ------------------ */
1696
+ :where(.summary-list) {
1697
+ display: grid;
1698
+ grid-template-columns: minmax(0, max-content) 1fr auto;
1699
+ column-gap: 0.5rem;
1700
+ row-gap: 0.125rem;
1701
+ }
1702
+ :where(.summary-list-item) {
1703
+ display: contents;
1704
+ border: 0;
1705
+ }
1706
+
1707
+ /* --- Icon / collection / document lists — flex rows with leading icon -- */
1708
+ :where(.icon-list-item, .collection-list-item, .document-list-item) {
1709
+ display: flex;
1710
+ align-items: flex-start;
1711
+ gap: 0.375rem;
1712
+ }
1713
+
1714
+ /* --- Navs --------------------------------------------------------------- */
1715
+ /* Nav wrappers are layout-neutral by default — the inner list handles
1716
+ layout, the nav just provides the landmark semantics. */
1717
+ :where(
1718
+ .accordion-nav,
1719
+ .breadcrumb-nav,
1720
+ .chat-nav,
1721
+ .contents-nav,
1722
+ .pagination-nav,
1723
+ .section-nav,
1724
+ .tree-nav,
1725
+ .navigation-menu
1726
+ ) {
1727
+ display: block;
1728
+ padding: 0;
1729
+ }
1730
+
1731
+ /* ==========================================================================
1732
+ * Tables (plain, data, calendar, kanban, gantt)
1733
+ * --------------------------------------------------------------------------
1734
+ * Every Lily table variant shares the same baseline: full-width, collapsed
1735
+ * borders, body font. Variants override below for specialised behaviour
1736
+ * (kanban columns, gantt bars, calendar selection).
1737
+ * ====================================================================== */
1738
+
1739
+ :where(.table, .data-table, .calendar-table, .kanban-table, .gantt-table) {
1740
+ width: 100%;
1741
+ border-collapse: collapse; /* shared edges between cells */
1742
+ background-color: var(--lily-surface);
1743
+ color: var(--lily-text);
1744
+ font-family: var(--lily-font-body);
1745
+ }
1746
+
1747
+ /* Thead gets the alt surface so it visually anchors above the body. */
1748
+ :where(
1749
+ .table-head,
1750
+ .data-table-head,
1751
+ .calendar-table-head,
1752
+ .kanban-table-head,
1753
+ .gantt-table-thead
1754
+ ) {
1755
+ background-color: var(--lily-surface-alt);
1756
+ }
1757
+
1758
+ /* Header cells — bold, tight padding, with a 2px under-rule.
1759
+ Practitioner density: 4px / 6px padding for compact column headers. */
1760
+ :where(
1761
+ .table-th,
1762
+ .data-table-th,
1763
+ .calendar-table-th,
1764
+ .kanban-table-th,
1765
+ .gantt-table-th
1766
+ ) {
1767
+ padding: 0.25rem 0.375rem;
1768
+ text-align: start;
1769
+ font-weight: 600;
1770
+ border-bottom: 2px solid var(--lily-border-strong);
1771
+ }
1772
+
1773
+ /* Body cells — top-aligned (multi-line content reads better when the
1774
+ baseline is at the top, not centred). Practitioner density allows
1775
+ ~25 table rows of patient data on a 1080p workstation. */
1776
+ :where(
1777
+ .table-td,
1778
+ .data-table-td,
1779
+ .calendar-table-td,
1780
+ .kanban-table-td,
1781
+ .gantt-table-td
1782
+ ) {
1783
+ padding: 0.25rem 0.375rem;
1784
+ border-bottom: var(--border) solid var(--lily-border);
1785
+ vertical-align: top;
1786
+ }
1787
+
1788
+ /* Row hover — subtle highlight for scanning long tables. */
1789
+ :where(
1790
+ .table-row,
1791
+ .data-table-row,
1792
+ .calendar-table-row,
1793
+ .kanban-table-row,
1794
+ .gantt-table-tr
1795
+ ):hover {
1796
+ background-color: var(--lily-surface-alt);
1797
+ }
1798
+
1799
+ /* Data-table filter form — chips that sit above the table. */
1800
+ :where(.data-filter-form) {
1801
+ display: flex;
1802
+ flex-wrap: wrap;
1803
+ gap: 0.375rem;
1804
+ padding: 0.5rem;
1805
+ background-color: var(--lily-surface-alt);
1806
+ border-radius: var(--lily-radius-md);
1807
+ }
1808
+
1809
+ /* --- Calendar specifics ------------------------------------------------- */
1810
+ /* Selected date — filled with the primary brand colour. */
1811
+ :where(.calendar-table-td[aria-selected="true"]) {
1812
+ background-color: var(--color-primary);
1813
+ color: var(--color-primary-content);
1814
+ }
1815
+ /* Today — outlined rather than filled, so a selected non-today date
1816
+ still reads as the "selected" one. */
1817
+ :where(.calendar-table-td[aria-current="date"]) {
1818
+ outline: 2px solid var(--color-primary);
1819
+ outline-offset: -2px;
1820
+ }
1821
+
1822
+ /* --- Kanban specifics --------------------------------------------------- */
1823
+ /* Each column renders as a `<td>`; widen the cells so cards have room. */
1824
+ :where(.kanban-table) {
1825
+ display: table;
1826
+ }
1827
+ :where(.kanban-table-td) {
1828
+ vertical-align: top;
1829
+ background-color: var(--lily-surface);
1830
+ min-width: 10rem;
1831
+ }
1832
+
1833
+ /* --- Gantt specifics ---------------------------------------------------- */
1834
+ /* Cells marked as bars are filled with the primary colour to render the
1835
+ schedule bar inline within the grid. */
1836
+ :where(.gantt-table-td[data-state="bar"]) {
1837
+ background-color: var(--color-primary);
1838
+ }
1839
+
1840
+ /* ==========================================================================
1841
+ * Cards, panels, surfaces
1842
+ * --------------------------------------------------------------------------
1843
+ * Family of generic content containers. Differ mostly in elevation
1844
+ * (shadow depth) and accent colour.
1845
+ * ====================================================================== */
1846
+
1847
+ :where(
1848
+ .card,
1849
+ .feature-card,
1850
+ .product-card,
1851
+ .care-card,
1852
+ .hover-card,
1853
+ .panel,
1854
+ .floating-panel,
1855
+ .success-panel
1856
+ ) {
1857
+ display: block;
1858
+ padding: 0.5rem;
1859
+ background-color: var(--lily-surface);
1860
+ color: var(--lily-text);
1861
+ border: var(--border) solid var(--lily-border);
1862
+ border-radius: var(--lily-radius-lg);
1863
+ box-shadow: var(--lily-shadow-sm);
1864
+ }
1865
+ /* DHCW's card is a bordered white box with a solid ledge under it
1866
+ rather than a blur — `box-shadow: 0 4px 0 #D8DDE0`. Same reasoning as
1867
+ the button ledge: a hard offset survives forced-colours mode and
1868
+ prints, where a soft shadow becomes either invisible or a grey smear.
1869
+ On hover the whole frame darkens to text-primary, which is a much
1870
+ stronger affordance than the usual lift-and-blur.
1871
+
1872
+ Halved to 2px here. A grid of cards at this density is a card every
1873
+ ~120px vertically; a 4px ledge on each one adds up to a page that
1874
+ looks ruled. */
1875
+ :where(.card, .feature-card, .product-card, .care-card) {
1876
+ box-shadow: 0 2px 0 var(--lily-border);
1877
+ }
1878
+ :where(.card, .feature-card, .product-card):has(a:hover) {
1879
+ border-color: var(--lily-text);
1880
+ box-shadow: 0 2px 0 var(--lily-text);
1881
+ }
1882
+ /* Whole-card focus. DHCW rings the card, not the link inside it, so a
1883
+ keyboard user sees the same target a mouse user clicks. Full 3px —
1884
+ focus does not get the density treatment. */
1885
+ :where(.card, .feature-card, .product-card):focus-within {
1886
+ outline: 3px solid var(--lily-focus-ring);
1887
+ outline-offset: 0;
1888
+ }
1889
+
1890
+ /* Feature card — slightly more padding because it leads the page. */
1891
+ :where(.feature-card) {
1892
+ padding: 0.75rem;
1893
+ }
1894
+ /* Hover card — popover-like; lift it with the medium shadow. */
1895
+ :where(.hover-card) {
1896
+ box-shadow: var(--lily-shadow-md);
1897
+ }
1898
+ /* Floating panel — pinned overlay; largest shadow. */
1899
+ :where(.floating-panel) {
1900
+ box-shadow: var(--lily-shadow-lg);
1901
+ }
1902
+ /* Success panel — green border + faint green tint. */
1903
+ :where(.success-panel) {
1904
+ border-color: var(--color-success);
1905
+ background-color: color-mix(
1906
+ in oklch,
1907
+ var(--color-success) 8%,
1908
+ var(--lily-surface)
1909
+ );
1910
+ }
1911
+ /* Care card urgency variants — coloured left rail (urgent/non-urgent/
1912
+ primary). Pattern adapted from the NHS care-card guidance. */
1913
+ :where(.care-card[data-urgency="urgent"]) {
1914
+ border-color: var(--color-error);
1915
+ border-left-width: 0.25rem;
1916
+ }
1917
+ :where(.care-card[data-urgency="non-urgent"]) {
1918
+ border-color: var(--color-info);
1919
+ border-left-width: 0.25rem;
1920
+ }
1921
+ :where(.care-card[data-urgency="primary"]) {
1922
+ border-color: var(--color-primary);
1923
+ border-left-width: 0.25rem;
1924
+ }
1925
+
1926
+ /* ==========================================================================
1927
+ * Banners, alerts, callouts, notifications
1928
+ * --------------------------------------------------------------------------
1929
+ * Family of messages with a coloured left rail. The rail colour is set
1930
+ * per severity so the message reads at a glance.
1931
+ * ====================================================================== */
1932
+
1933
+ /* Baseline — alert/banner/callout/notification all share the same frame.
1934
+ Practitioner density: 4px / 6px padding so a chart can show several alert
1935
+ messages (allergy, fall risk, infection control) without the panel
1936
+ eating half the column. */
1937
+ :where(
1938
+ .alert,
1939
+ .banner,
1940
+ .announcement-banner,
1941
+ .phase-banner,
1942
+ .government-banner,
1943
+ .medical-banner,
1944
+ .super-banner,
1945
+ .information-callout,
1946
+ .warning-callout,
1947
+ .summary-box,
1948
+ .notification,
1949
+ .toast,
1950
+ .sonner
1951
+ ) {
1952
+ display: flex;
1953
+ align-items: flex-start;
1954
+ gap: 0.375rem;
1955
+ padding: 0.375rem 0.5rem;
1956
+ background-color: var(--lily-surface-alt);
1957
+ color: var(--lily-text);
1958
+ border: var(--border) solid var(--lily-border);
1959
+ border-inline-start-width: 0.25rem; /* the coloured rail */
1960
+ border-radius: var(--lily-radius-md);
1961
+ }
1962
+
1963
+ /* Severity variants — rail + faint background tint. */
1964
+ :where(
1965
+ .alert[data-severity="info"],
1966
+ .information-callout,
1967
+ .announcement-banner
1968
+ ) {
1969
+ border-inline-start-color: var(--color-info);
1970
+ background-color: color-mix(
1971
+ in oklch,
1972
+ var(--color-info) 8%,
1973
+ var(--lily-surface)
1974
+ );
1975
+ }
1976
+ :where(.alert[data-severity="success"]) {
1977
+ border-inline-start-color: var(--color-success);
1978
+ background-color: color-mix(
1979
+ in oklch,
1980
+ var(--color-success) 8%,
1981
+ var(--lily-surface)
1982
+ );
1983
+ }
1984
+ :where(.alert[data-severity="warning"], .phase-banner) {
1985
+ border-inline-start-color: var(--color-warning);
1986
+ background-color: color-mix(
1987
+ in oklch,
1988
+ var(--color-warning) 12%,
1989
+ var(--lily-surface)
1990
+ );
1991
+ }
1992
+
1993
+ /* ----------------------------------------------------------------------
1994
+ * Warning callout — DHCW's tabbed band
1995
+ *
1996
+ * Deliberately pulled OUT of the left-rail alert family above, because
1997
+ * DHCW's warning callout is not a rail component. It is a bordered pale
1998
+ * panel (#FEF9C3) with a solid yellow heading band (#FACC15) that
1999
+ * overlaps the panel's top edge — the NHS "Important" pattern, which
2000
+ * DHCW inherits and recolours.
2001
+ *
2002
+ * The band matters, and it matters more here than on the patient
2003
+ * variant. A practitioner screen is already dense with bordered panels;
2004
+ * a left rail is one more vertical line among a dozen. The band is the
2005
+ * only treatment in this file that changes the *shape* of a container
2006
+ * rather than its colour, which is what makes an allergy or interaction
2007
+ * warning survive a screen the user has stopped really looking at.
2008
+ *
2009
+ * Lily's warning-callout is a single <div> with a children slot and no
2010
+ * `__heading` sub-class, so the band is applied to whatever heading
2011
+ * element the consumer puts first. Markup that gets the full pattern:
2012
+ *
2013
+ * <div class="warning-callout" role="alert">
2014
+ * <h3>Rhybudd / Warning</h3>
2015
+ * <p>…</p>
2016
+ * </div>
2017
+ *
2018
+ * Without a heading child it degrades to a plain bordered yellow panel,
2019
+ * which is still correct — just quieter.
2020
+ *
2021
+ * Yellow #FACC15 is 1.5:1 against the #FEF9C3 fill, so the band is
2022
+ * distinguishable by *position and text*, never by hue alone; the
2023
+ * border carries the boundary. Text on both is DHCW text-primary, at
2024
+ * 13.4:1 on the fill and 8.6:1 on the band.
2025
+ *
2026
+ * Density: 12px band padding and 12px panel padding, against DHCW's
2027
+ * 8px/24px and 24px/48px. The band keeps its full type size — this is
2028
+ * a warning, and shrinking the one component whose job is to interrupt
2029
+ * would defeat the point of having it.
2030
+ * -------------------------------------------------------------------- */
2031
+ :where(.warning-callout) {
2032
+ display: block; /* undo the alert family's flex row */
2033
+ position: relative;
2034
+ margin: 0 0 0.75rem;
2035
+ padding: 0;
2036
+ padding-top: 1rem;
2037
+ border: none;
2038
+ background-color: transparent;
2039
+ color: var(--lily-text);
2040
+ }
2041
+ /* The band — inline-block so it hugs its own text rather than spanning
2042
+ the panel, which is what makes it read as a tab rather than a
2043
+ header row. */
2044
+ :where(.warning-callout > :is(h1, h2, h3, h4, h5, h6)) {
2045
+ position: relative;
2046
+ z-index: 2;
2047
+ display: inline-block;
2048
+ margin: 0;
2049
+ padding: 0.25rem 0.75rem;
2050
+ background-color: var(--color-warning);
2051
+ color: var(--color-warning-content);
2052
+ font-size: 1rem;
2053
+ line-height: 1.5;
2054
+ }
2055
+ /* The panel — pulled up under the band by the band's own height, so
2056
+ the two overlap instead of stacking. */
2057
+ :where(.warning-callout > :is(h1, h2, h3, h4, h5, h6)) ~ * {
2058
+ position: relative;
2059
+ z-index: 1;
2060
+ }
2061
+ :where(.warning-callout)::after {
2062
+ content: "";
2063
+ position: absolute;
2064
+ inset: 1rem 0 0;
2065
+ z-index: 0;
2066
+ border: var(--border) solid var(--color-warning);
2067
+ background-color: var(--lily-warning-surface);
2068
+ }
2069
+ :where(.warning-callout > :is(h1, h2, h3, h4, h5, h6)) ~ :first-of-type,
2070
+ :where(.warning-callout > :not(:is(h1, h2, h3, h4, h5, h6)):first-child) {
2071
+ margin-top: 0;
2072
+ }
2073
+ /* Content inset. Applied to the children rather than the container so
2074
+ the band can still break out of the panel's left edge. */
2075
+ :where(.warning-callout > :not(:is(h1, h2, h3, h4, h5, h6))) {
2076
+ position: relative;
2077
+ z-index: 1;
2078
+ margin-inline: 0;
2079
+ padding-inline: 0.75rem;
2080
+ }
2081
+ :where(.warning-callout > :not(:is(h1, h2, h3, h4, h5, h6)):last-child) {
2082
+ padding-bottom: 0.75rem;
2083
+ margin-bottom: 0;
2084
+ }
2085
+ :where(
2086
+ .warning-callout > :is(h1, h2, h3, h4, h5, h6) + *
2087
+ ) {
2088
+ padding-top: 0.75rem;
2089
+ }
2090
+ :where(
2091
+ .alert[data-severity="error"],
2092
+ .error-message,
2093
+ .error-summary,
2094
+ .super-banner
2095
+ ) {
2096
+ border-inline-start-color: var(--color-error);
2097
+ background-color: color-mix(
2098
+ in oklch,
2099
+ var(--color-error) 8%,
2100
+ var(--lily-surface)
2101
+ );
2102
+ }
2103
+ /* Medical banner — primary brand colour. */
2104
+ :where(.medical-banner) {
2105
+ border-inline-start-color: var(--color-primary);
2106
+ }
2107
+ /* Medical banner inner boxes — danger / advice variants. */
2108
+ :where(.medical-banner-box-for-danger) {
2109
+ border-color: var(--color-error);
2110
+ background-color: color-mix(
2111
+ in oklch,
2112
+ var(--color-error) 8%,
2113
+ var(--lily-surface)
2114
+ );
2115
+ }
2116
+ :where(.medical-banner-box-for-advice) {
2117
+ border-color: var(--color-info);
2118
+ background-color: color-mix(
2119
+ in oklch,
2120
+ var(--color-info) 8%,
2121
+ var(--lily-surface)
2122
+ );
2123
+ }
2124
+ /* Government banner — solid neutral surface (US-government / GOV.UK
2125
+ style: a chrome bar at the top of the page identifying the site
2126
+ as an official property). No coloured rail. */
2127
+ :where(.government-banner) {
2128
+ background-color: var(--color-neutral);
2129
+ color: var(--color-neutral-content);
2130
+ border-color: transparent;
2131
+ border-inline-start-color: transparent;
2132
+ }
2133
+ /* Summary box — primary-coloured rail; used for "answer at a glance"
2134
+ callouts on long forms (e.g. cost summary). */
2135
+ :where(.summary-box) {
2136
+ border-color: var(--color-primary);
2137
+ border-inline-start-width: 0.25rem;
2138
+ }
2139
+
2140
+ /* Banner boxes — chrome strip variants used inside header areas. */
2141
+ :where(.banner-box, .medical-banner-box) {
2142
+ display: flex;
2143
+ align-items: center;
2144
+ gap: 0.375rem;
2145
+ padding: 0.25rem 0.5rem;
2146
+ }
2147
+
2148
+ /* Sticky promo — sits at the very top of the viewport on scroll. */
2149
+ :where(.sticky-promo-banner) {
2150
+ position: sticky;
2151
+ inset-block-start: 0;
2152
+ z-index: 10;
2153
+ }
2154
+
2155
+ /* Error message — inline form-validation message. */
2156
+ :where(.error-message) {
2157
+ color: var(--color-error);
2158
+ font-weight: 600;
2159
+ }
2160
+ /* Error summary — heavyweight border (GOV.UK pattern) so users scrolling
2161
+ back to the top of a form see it immediately. Practitioner density
2162
+ keeps a 2px rule and tighter padding. */
2163
+ :where(.error-summary) {
2164
+ padding: 0.5rem;
2165
+ border: 0.125rem solid var(--color-error);
2166
+ }
2167
+
2168
+ /* Toast / Sonner — floating notifications; medium elevation. */
2169
+ :where(.toast, .sonner) {
2170
+ box-shadow: var(--lily-shadow-md);
2171
+ }
2172
+ /* In-page notification — small elevation. */
2173
+ :where(.notification) {
2174
+ box-shadow: var(--lily-shadow-sm);
2175
+ }
2176
+
2177
+ /* ==========================================================================
2178
+ * Forms
2179
+ * --------------------------------------------------------------------------
2180
+ * Form / field / fieldset layout primitives. Stack vertically with a
2181
+ * consistent gap so consumer markup stays brief.
2182
+ * ====================================================================== */
2183
+
2184
+ :where(.form, .editable-form) {
2185
+ display: flex;
2186
+ flex-direction: column;
2187
+ gap: 0.5rem;
2188
+ }
2189
+ /* Field — wraps a single label + input + hint/error trio. */
2190
+ :where(.field) {
2191
+ display: flex;
2192
+ flex-direction: column;
2193
+ gap: 0.125rem;
2194
+ }
2195
+ /* Fieldset — semantic <fieldset>; border + padded inset, legend bold. */
2196
+ :where(.fieldset) {
2197
+ border: var(--border) solid var(--lily-border);
2198
+ border-radius: var(--lily-radius-md);
2199
+ padding: 0.5rem;
2200
+ }
2201
+ :where(.fieldset > legend) {
2202
+ padding-inline: 0.25rem;
2203
+ font-weight: 600;
2204
+ }
2205
+ /* Label — bold and dark; consumers can override per-form. */
2206
+ :where(.label) {
2207
+ font-weight: 600;
2208
+ color: var(--lily-text);
2209
+ }
2210
+ /* Hint — small muted helptext under the input. */
2211
+ :where(.hint) {
2212
+ color: var(--lily-text-muted);
2213
+ font-size: 0.8125rem;
2214
+ }
2215
+ /* Character counter — same colour/size as hint but right-aligned. */
2216
+ :where(.character-counter) {
2217
+ color: var(--lily-text-muted);
2218
+ font-size: 0.8125rem;
2219
+ text-align: end;
2220
+ }
2221
+
2222
+ /* ==========================================================================
2223
+ * Layout & containers
2224
+ * ====================================================================== */
2225
+
2226
+ /* Page-width containers. */
2227
+ :where(
2228
+ .container,
2229
+ .container-with-fixed-width,
2230
+ .container-with-fluid-width
2231
+ ) {
2232
+ display: block;
2233
+ width: 100%;
2234
+ margin-inline: auto;
2235
+ padding-inline: 0.5rem;
2236
+ box-sizing: border-box;
2237
+ }
2238
+ /* Fixed-width — caps at a comfortable reading width (~1152px). */
2239
+ :where(.container-with-fixed-width) {
2240
+ max-width: 72rem;
2241
+ }
2242
+ /* Fluid-width — fills the viewport edge-to-edge. */
2243
+ :where(.container-with-fluid-width) {
2244
+ max-width: none;
2245
+ }
2246
+ /* Reset the inline padding when a container should butt against the edge. */
2247
+ :where(.padding-reset) {
2248
+ padding-inline: 0;
2249
+ }
2250
+
2251
+ /* Flex / grid / masonry primitives. */
2252
+ :where(.flex-stack) {
2253
+ display: flex;
2254
+ flex-direction: column;
2255
+ gap: 0.5rem;
2256
+ }
2257
+ :where(.grid) {
2258
+ display: grid;
2259
+ gap: 0.5rem;
2260
+ }
2261
+ :where(.masonry) {
2262
+ column-count: 3;
2263
+ column-gap: 0.5rem;
2264
+ }
2265
+ /* Horizontal-rule replacement that respects theme borders. */
2266
+ :where(.separator) {
2267
+ border: 0;
2268
+ border-top: var(--border) solid var(--lily-border);
2269
+ margin: 0.5rem 0;
2270
+ }
2271
+ /* Generic content tile. */
2272
+ :where(.tile) {
2273
+ display: block;
2274
+ padding: 0.5rem;
2275
+ background-color: var(--lily-surface);
2276
+ border: var(--border) solid var(--lily-border);
2277
+ border-radius: var(--lily-radius-md);
2278
+ }
2279
+ /* Sidebar — secondary-surface panel; sits beside main content. */
2280
+ :where(.sidebar) {
2281
+ display: block;
2282
+ background-color: var(--lily-surface-alt);
2283
+ padding: 0.5rem;
2284
+ border-radius: var(--lily-radius-md);
2285
+ }
2286
+ /* Aspect-ratio box — 16:9 default; consumers override `aspect-ratio`. */
2287
+ :where(.aspect-ratio-container) {
2288
+ position: relative;
2289
+ display: block;
2290
+ aspect-ratio: 16 / 9;
2291
+ overflow: hidden;
2292
+ border-radius: var(--lily-radius-md);
2293
+ }
2294
+ /* Framer — bordered box for embeds / illustrations. */
2295
+ :where(.framer) {
2296
+ padding: 0.5rem;
2297
+ border: var(--border) solid var(--lily-border);
2298
+ border-radius: var(--lily-radius-md);
2299
+ }
2300
+ /* Article-layout — declares column-width tokens that content-block reads.
2301
+ Pattern lifted from the Reuters Graphics components system. */
2302
+ :where(.article-layout) {
2303
+ --content-width-narrower: 20.625rem; /* 330px */
2304
+ --content-width-narrow: 31.875rem; /* 510px */
2305
+ --content-width-normal: 41.25rem; /* 660px (default reading width) */
2306
+ --content-width-wide: 58.125rem; /* 930px (chart escape) */
2307
+ --content-width-wider: 75rem; /* 1200px (full-bleed escape) */
2308
+ display: block;
2309
+ }
2310
+ /* Content-block — reads the column-width tokens; switch via data-width. */
2311
+ :where(.content-block) {
2312
+ max-width: var(--content-width-normal);
2313
+ margin-inline: auto;
2314
+ padding-inline: 0.5rem;
2315
+ box-sizing: border-box;
2316
+ }
2317
+ :where(.content-block[data-width="narrower"]) {
2318
+ max-width: var(--content-width-narrower);
2319
+ }
2320
+ :where(.content-block[data-width="narrow"]) {
2321
+ max-width: var(--content-width-narrow);
2322
+ }
2323
+ :where(.content-block[data-width="wide"]) {
2324
+ max-width: var(--content-width-wide);
2325
+ }
2326
+ :where(.content-block[data-width="wider"]) {
2327
+ max-width: var(--content-width-wider);
2328
+ }
2329
+
2330
+ /* --- Grail layout (5-region responsive page shell) --------------------- */
2331
+ /* Header spans the top; left-aside / main / right-aside fill the middle;
2332
+ footer spans the bottom. At ≤768px the asides drop below the main. */
2333
+ :where(.grail-layout) {
2334
+ display: grid;
2335
+ min-height: 100vh;
2336
+ grid-template-areas:
2337
+ "header header header"
2338
+ "left main right"
2339
+ "footer footer footer";
2340
+ grid-template-rows: auto 1fr auto;
2341
+ grid-template-columns: minmax(0, 1fr) minmax(0, 3fr) minmax(0, 1fr);
2342
+ background-color: var(--lily-surface);
2343
+ color: var(--lily-text);
2344
+ }
2345
+ :where(.grail-layout-top-header) {
2346
+ grid-area: header;
2347
+ }
2348
+ :where(.grail-layout-left-aside) {
2349
+ grid-area: left;
2350
+ }
2351
+ :where(.grail-layout-center-main) {
2352
+ grid-area: main;
2353
+ }
2354
+ :where(.grail-layout-right-aside) {
2355
+ grid-area: right;
2356
+ }
2357
+ :where(.grail-layout-bottom-footer) {
2358
+ grid-area: footer;
2359
+ }
2360
+ /* Header / footer chrome — secondary surface, padded. */
2361
+ :where(.grail-layout-top-header, .grail-layout-bottom-footer) {
2362
+ background-color: var(--lily-surface-alt);
2363
+ padding: 0.375rem 0.5rem;
2364
+ }
2365
+ /* Asides — secondary surface, padded. */
2366
+ :where(.grail-layout-left-aside, .grail-layout-right-aside) {
2367
+ padding: 0.5rem;
2368
+ background-color: var(--lily-surface-alt);
2369
+ }
2370
+ /* Main content area — primary surface, slightly more generous padding. */
2371
+ :where(.grail-layout-center-main) {
2372
+ padding: 0.5rem;
2373
+ }
2374
+ /* Mobile breakpoint — collapse to a single column, stacking the regions. */
2375
+ @media (max-width: 48rem) {
2376
+ :where(.grail-layout) {
2377
+ grid-template-areas: "header" "main" "left" "right" "footer";
2378
+ grid-template-columns: 1fr;
2379
+ grid-template-rows: auto auto auto auto auto;
2380
+ }
2381
+ }
2382
+
2383
+ /* Standalone semantic header / footer landmarks. */
2384
+ :where(.header) {
2385
+ display: block;
2386
+ padding: 0.375rem 0.5rem;
2387
+ background-color: var(--lily-surface);
2388
+ border-bottom: var(--border) solid var(--lily-border);
2389
+ }
2390
+ :where(.footer) {
2391
+ display: block;
2392
+ padding: 0.375rem 0.5rem;
2393
+ background-color: var(--lily-surface-alt);
2394
+ border-top: var(--border) solid var(--lily-border);
2395
+ }
2396
+
2397
+ /* ==========================================================================
2398
+ * Dialogs, popovers, overlays
2399
+ * --------------------------------------------------------------------------
2400
+ * Modal and floating UI. Backdrop is a blur + 50% black overlay; dialog
2401
+ * frames are rounded with the largest shadow.
2402
+ * ====================================================================== */
2403
+
2404
+ /* Native <dialog> + variant types. */
2405
+ :where(
2406
+ .dialog,
2407
+ .alert-dialog,
2408
+ .timeout-dialog,
2409
+ .popconfirm-dialog,
2410
+ .file-dialog
2411
+ ) {
2412
+ background-color: var(--lily-surface);
2413
+ color: var(--lily-text);
2414
+ border: var(--border) solid var(--lily-border);
2415
+ border-radius: var(--lily-radius-lg);
2416
+ padding: 0.75rem;
2417
+ box-shadow: var(--lily-shadow-lg);
2418
+ max-width: min(36rem, 90vw); /* cap on big screens, fit on small */
2419
+ }
2420
+ /* Backdrop — dim the page and blur slightly so focus stays on the dialog. */
2421
+ :where(
2422
+ .dialog::backdrop,
2423
+ .alert-dialog::backdrop,
2424
+ .timeout-dialog::backdrop,
2425
+ .popconfirm-dialog::backdrop,
2426
+ .file-dialog::backdrop
2427
+ ) {
2428
+ background-color: color-mix(in oklch, black 50%, transparent);
2429
+ backdrop-filter: blur(2px);
2430
+ }
2431
+ /* Alert dialog — red rail so the user knows it's destructive. */
2432
+ :where(.alert-dialog) {
2433
+ border-color: var(--color-error);
2434
+ border-inline-start-width: 0.25rem;
2435
+ }
2436
+
2437
+ /* Popover / tooltip family — small floating labels. */
2438
+ :where(.popover, .popup, .tooltip, .coachmark, .contextual-help) {
2439
+ display: block;
2440
+ padding: 0.25rem 0.375rem;
2441
+ background-color: var(--color-neutral);
2442
+ color: var(--color-neutral-content);
2443
+ border-radius: var(--lily-radius-md);
2444
+ box-shadow: var(--lily-shadow-md);
2445
+ font-size: 0.8125rem;
2446
+ max-width: 18rem;
2447
+ }
2448
+ /* Popover / popup — surface-coloured (more substantial than a tooltip). */
2449
+ :where(.popover, .popup) {
2450
+ background-color: var(--lily-surface);
2451
+ color: var(--lily-text);
2452
+ border: var(--border) solid var(--lily-border);
2453
+ max-width: 24rem;
2454
+ }
2455
+ /* Coachmark — primary-coloured border so it reads as a tutorial hint. */
2456
+ :where(.coachmark) {
2457
+ border: 2px solid var(--color-primary);
2458
+ }
2459
+
2460
+ /* Full-page overlay container — dims everything beneath a high-z child. */
2461
+ :where(.overlay-container) {
2462
+ position: fixed;
2463
+ inset: 0;
2464
+ background-color: color-mix(in oklch, black 50%, transparent);
2465
+ z-index: 100;
2466
+ }
2467
+
2468
+ /* Drawers — side panels (right edge by default; RTL-aware via logical
2469
+ properties). Sit above the overlay container (z-index 101). */
2470
+ :where(.drawer, .slide-out-drawer, .sheet) {
2471
+ position: fixed;
2472
+ inset-block: 0;
2473
+ inset-inline-end: 0;
2474
+ width: min(24rem, 90vw);
2475
+ background-color: var(--lily-surface);
2476
+ color: var(--lily-text);
2477
+ border-inline-start: var(--border) solid var(--lily-border);
2478
+ box-shadow: var(--lily-shadow-lg);
2479
+ padding: 0.5rem;
2480
+ z-index: 101;
2481
+ }
2482
+
2483
+ /* ==========================================================================
2484
+ * Menus
2485
+ * ====================================================================== */
2486
+
2487
+ /* Menu containers — surface-coloured popover with thin border + sm shadow. */
2488
+ :where(.menu, .context-menu, .dropdown-menu, .tree-menu, .command) {
2489
+ display: block;
2490
+ background-color: var(--lily-surface);
2491
+ color: var(--lily-text);
2492
+ border: var(--border) solid var(--lily-border);
2493
+ border-radius: var(--lily-radius-md);
2494
+ padding: 0.125rem;
2495
+ box-shadow: var(--lily-shadow-sm);
2496
+ list-style: none;
2497
+ margin: 0;
2498
+ }
2499
+ /* Menu items — flex row with icon + label slot. */
2500
+ :where(.menu-item, .context-menu-item) {
2501
+ display: flex;
2502
+ align-items: center;
2503
+ gap: 0.25rem;
2504
+ padding: 0.125rem 0.375rem;
2505
+ border-radius: var(--lily-radius-md);
2506
+ cursor: pointer;
2507
+ }
2508
+ /* Highlight on hover / aria-selected — alt surface. */
2509
+ :where(
2510
+ .menu-item:hover,
2511
+ .context-menu-item:hover,
2512
+ .menu-item[aria-selected="true"]
2513
+ ) {
2514
+ background-color: var(--lily-surface-alt);
2515
+ }
2516
+ /* Menu group — section within a menu. The aria-label becomes a header. */
2517
+ :where(.menu-group) {
2518
+ padding: 0.125rem 0;
2519
+ }
2520
+ :where(.menu-group::before) {
2521
+ content: attr(aria-label);
2522
+ display: block;
2523
+ padding: 0.125rem 0.375rem;
2524
+ color: var(--lily-text-muted);
2525
+ font-size: 0.75rem;
2526
+ text-transform: uppercase;
2527
+ letter-spacing: 0.05em;
2528
+ }
2529
+ /* Cascader / navigation-menu — horizontal flex row of menu columns. */
2530
+ :where(.cascader, .navigation-menu) {
2531
+ display: flex;
2532
+ gap: 0.125rem;
2533
+ }
2534
+ /* Hamburger — bordered icon-button. */
2535
+ :where(.hamburger-menu) {
2536
+ display: inline-flex;
2537
+ align-items: center;
2538
+ justify-content: center;
2539
+ padding: 0.25rem;
2540
+ background: transparent;
2541
+ border: var(--border) solid var(--lily-border-strong);
2542
+ border-radius: var(--lily-radius-md);
2543
+ color: var(--lily-text);
2544
+ cursor: pointer;
2545
+ }
2546
+
2547
+ /* ==========================================================================
2548
+ * Segments, toggles, groups
2549
+ * ====================================================================== */
2550
+
2551
+ /* Generic group containers — inline-flex, wrap, small gap. */
2552
+ :where(
2553
+ .segment-group,
2554
+ .toggle-group,
2555
+ .checkbox-group,
2556
+ .radio-group,
2557
+ .dial-group,
2558
+ .tag-group,
2559
+ .avatar-group,
2560
+ .mutually-exclusive
2561
+ ) {
2562
+ display: inline-flex;
2563
+ gap: 0.125rem;
2564
+ flex-wrap: wrap;
2565
+ }
2566
+ /* Segment group / toggle group get an inset background (iOS-segmented
2567
+ control aesthetic). */
2568
+ :where(.segment-group, .toggle-group) {
2569
+ padding: 0.125rem;
2570
+ background-color: var(--lily-surface-alt);
2571
+ border-radius: var(--lily-radius-md);
2572
+ }
2573
+ /* Segment items — transparent until pressed; then primary fill. */
2574
+ :where(.segment-group-item) {
2575
+ padding: 0.375rem 0.75rem;
2576
+ background: transparent;
2577
+ border: 0;
2578
+ border-radius: var(--lily-radius-md);
2579
+ color: var(--lily-text);
2580
+ cursor: pointer;
2581
+ }
2582
+ :where(
2583
+ .segment-group-item[aria-pressed="true"],
2584
+ .segment-group-item[aria-selected="true"]
2585
+ ) {
2586
+ background-color: var(--color-primary);
2587
+ color: var(--color-primary-content);
2588
+ }
2589
+ /* Avatar group — overlap each subsequent avatar by 0.25rem, with a
2590
+ contrast ring for separation. */
2591
+ :where(.avatar-group > *) {
2592
+ margin-inline-start: -0.25rem;
2593
+ border: 2px solid var(--lily-surface);
2594
+ }
2595
+
2596
+ /* ==========================================================================
2597
+ * Progress, loading
2598
+ * ====================================================================== */
2599
+
2600
+ /* Native <progress> / <meter> / bar variant — short, pill-shaped track.
2601
+ Practitioner density: 4px high so the bar fits within a single observation
2602
+ row without dominating the cell. */
2603
+ :where(.progress, .progress-bar, .meter) {
2604
+ display: block;
2605
+ width: 100%;
2606
+ height: 0.25rem;
2607
+ appearance: none;
2608
+ background-color: var(--lily-surface-sunken);
2609
+ border: 0;
2610
+ border-radius: 999px;
2611
+ overflow: hidden;
2612
+ }
2613
+ /* Vendor pseudo-elements: track + value. */
2614
+ :where(.progress::-webkit-progress-bar, .meter::-webkit-meter-bar) {
2615
+ background-color: var(--lily-surface-sunken);
2616
+ }
2617
+ :where(
2618
+ .progress::-webkit-progress-value,
2619
+ .meter::-webkit-meter-optimum-value
2620
+ ) {
2621
+ background-color: var(--color-primary);
2622
+ }
2623
+ :where(.progress::-moz-progress-bar) {
2624
+ background-color: var(--color-primary);
2625
+ }
2626
+
2627
+ /* Progress circle — conic-gradient renders the sweep based on the
2628
+ --progress custom property (0–100). */
2629
+ :where(.progress-circle) {
2630
+ display: inline-block;
2631
+ width: 1.25rem;
2632
+ height: 1.25rem;
2633
+ border-radius: 50%;
2634
+ background: conic-gradient(
2635
+ var(--color-primary) calc(var(--progress, 0) * 1%),
2636
+ var(--lily-surface-sunken) 0
2637
+ );
2638
+ }
2639
+ /* Spinner / loading — classic CSS rotating partial-ring. */
2640
+ :where(.progress-spinner, .loading) {
2641
+ display: inline-block;
2642
+ width: 1rem;
2643
+ height: 1rem;
2644
+ border: 2px solid var(--lily-surface-sunken);
2645
+ border-top-color: var(--color-primary);
2646
+ border-radius: 50%;
2647
+ animation: lily-spin 0.8s linear infinite;
2648
+ }
2649
+ @keyframes lily-spin {
2650
+ to {
2651
+ transform: rotate(360deg);
2652
+ }
2653
+ }
2654
+ /* Skeleton — shimmering placeholder while content is loading. */
2655
+ :where(.skeleton) {
2656
+ display: block;
2657
+ background: linear-gradient(
2658
+ 90deg,
2659
+ var(--lily-surface-alt) 0%,
2660
+ var(--lily-surface-sunken) 50%,
2661
+ var(--lily-surface-alt) 100%
2662
+ );
2663
+ background-size: 200% 100%;
2664
+ animation: lily-skeleton 1.4s ease-in-out infinite;
2665
+ border-radius: var(--lily-radius-md);
2666
+ min-height: 1rem;
2667
+ }
2668
+ @keyframes lily-skeleton {
2669
+ 0% {
2670
+ background-position: 200% 0;
2671
+ }
2672
+ 100% {
2673
+ background-position: -200% 0;
2674
+ }
2675
+ }
2676
+ /* Respect prefers-reduced-motion — kill the perpetual animations.
2677
+ (WCAG 2.3.3 Animation from interactions.) */
2678
+ @media (prefers-reduced-motion: reduce) {
2679
+ :where(.progress-spinner, .loading, .skeleton, .beach-ball) {
2680
+ animation: none;
2681
+ }
2682
+ }
2683
+
2684
+ /* ==========================================================================
2685
+ * Avatars, badges, tags, status
2686
+ * ====================================================================== */
2687
+
2688
+ /* Avatar — circular tile (1.75rem) with sunken background as the
2689
+ fallback colour when no image is provided. Practitioner density: ~28px
2690
+ so avatars in patient lists align with input/button heights. */
2691
+ :where(.avatar) {
2692
+ display: inline-flex;
2693
+ align-items: center;
2694
+ justify-content: center;
2695
+ width: 1.75rem;
2696
+ height: 1.75rem;
2697
+ border-radius: 50%;
2698
+ background-color: var(--lily-surface-sunken);
2699
+ color: var(--lily-text);
2700
+ overflow: hidden;
2701
+ font-weight: 600;
2702
+ }
2703
+ :where(.avatar-image) {
2704
+ width: 100%;
2705
+ height: 100%;
2706
+ object-fit: cover;
2707
+ display: block;
2708
+ }
2709
+ /* Avatar fallback text — uppercase initials. */
2710
+ :where(.avatar-text) {
2711
+ text-transform: uppercase;
2712
+ }
2713
+
2714
+ /* Badge / flair / tag family — small pill-shaped labels. */
2715
+ :where(.badge, .flair, .status-tag, .tag, .ai-label) {
2716
+ display: inline-flex;
2717
+ align-items: center;
2718
+ gap: 0.125rem;
2719
+ padding: 0 0.25rem;
2720
+ font-size: 0.75rem;
2721
+ font-weight: 600;
2722
+ line-height: 1.4;
2723
+ background-color: var(--color-neutral);
2724
+ color: var(--color-neutral-content);
2725
+ border-radius: 999px;
2726
+ }
2727
+ /* ----------------------------------------------------------------------
2728
+ * Tag — the DHCW tag palette
2729
+ *
2730
+ * DHCW's tag is the component where it diverges furthest from NHS
2731
+ * England, and the divergence is worth adopting wholesale. England
2732
+ * ships a handful of tag colours; DHCW ships eleven, each as a
2733
+ * hand-tuned {fill, border, text} triplet rather than a tint computed
2734
+ * from one hue. Every one of the eleven clears 7:1 — the loosest is
2735
+ * aqua-green at 7.3:1 — which a computed tint would not have managed.
2736
+ *
2737
+ * This is the highest-value component on a practitioner surface,
2738
+ * because eleven mutually distinguishable, all-AAA chips is exactly
2739
+ * what a worklist needs to encode specialty, triage, and result state
2740
+ * in one column. It is also the easiest to misuse: see the note on
2741
+ * colour-only meaning below.
2742
+ *
2743
+ * Three structural choices, all DHCW's, all deliberate:
2744
+ *
2745
+ * 1. Rectangular, not pill. A pill reads as decoration; a rectangle
2746
+ * reads as a label. `border-radius: 0` overrides the badge family
2747
+ * baseline above.
2748
+ * 2. Bordered, always. The border is what keeps the pale tones
2749
+ * (yellow, orange) visible on a white surface, what separates two
2750
+ * adjacent chips in a dense cell, and what makes every tone
2751
+ * survive forced-colours mode.
2752
+ * 3. Bold. Small text needs the weight back — more so here, where
2753
+ * the chip runs at 0.75rem rather than DHCW's 0.875rem.
2754
+ *
2755
+ * Exposed as `data-tone` rather than modifier classes, per Lily's
2756
+ * headless rule that `data-*` carries state for consumer CSS:
2757
+ *
2758
+ * <span class="tag" data-tone="red">Brys / Urgent</span>
2759
+ *
2760
+ * The default (no attribute) is DHCW navy #183154 with white text.
2761
+ *
2762
+ * A tone is never the message. WCAG 1.4.1 means the tag's text has to
2763
+ * say what the tag means. On a clinical worklist this is not a
2764
+ * checklist item — a red chip that a colour-blind registrar reads as
2765
+ * grey, on a row that says nothing else about urgency, is a patient
2766
+ * safety defect and not an accessibility one.
2767
+ * -------------------------------------------------------------------- */
2768
+ :where(.tag) {
2769
+ border-radius: 0;
2770
+ /* Tight: 2px/4px against DHCW's 4px/8px. */
2771
+ padding: 0.125rem 0.25rem;
2772
+ font-size: 0.75rem;
2773
+ font-weight: 700;
2774
+ line-height: 1;
2775
+ /* #183154 — DHCW's tag navy. A touch deeper than --color-primary;
2776
+ kept literal because it is a palette entry, not a derived value. */
2777
+ background-color: oklch(31% 0.07 257);
2778
+ color: oklch(100% 0 0);
2779
+ border: var(--border) solid oklch(31% 0.07 257);
2780
+ }
2781
+
2782
+ /* The ten named tones. Each line is DHCW's exact {fill, border, text}
2783
+ triplet; border and text share a value throughout, which is what
2784
+ gives the family its consistent read. Contrast noted per tone — all
2785
+ are size-independent, so the smaller type above costs nothing. */
2786
+
2787
+ /* #FFFFFF / #212B32 — 14.4:1 */
2788
+ :where(.tag[data-tone="white"]) {
2789
+ background-color: oklch(100% 0 0);
2790
+ border-color: oklch(28% 0.019 239);
2791
+ color: oklch(28% 0.019 239);
2792
+ }
2793
+ /* #DBE0E3 / #354550 — 7.5:1 */
2794
+ :where(.tag[data-tone="grey"]) {
2795
+ background-color: oklch(90% 0.007 234);
2796
+ border-color: oklch(38% 0.028 238);
2797
+ color: oklch(38% 0.028 238);
2798
+ }
2799
+ /* #CCE5D8 / #004C23 — 7.7:1 */
2800
+ :where(.tag[data-tone="green"]) {
2801
+ background-color: oklch(90% 0.032 163);
2802
+ border-color: oklch(36% 0.096 152);
2803
+ color: oklch(36% 0.096 152);
2804
+ }
2805
+ /* #CCEDEB / #00524D — 7.3:1. The surviving trace of the NHS Aqua Green
2806
+ this theme used to build its whole accent story on. */
2807
+ :where(.tag[data-tone="aqua-green"]) {
2808
+ background-color: oklch(92% 0.035 192);
2809
+ border-color: oklch(40% 0.069 188);
2810
+ color: oklch(40% 0.069 188);
2811
+ }
2812
+ /* #D1D7DF / #132641 — 10.5:1 */
2813
+ :where(.tag[data-tone="blue"]) {
2814
+ background-color: oklch(88% 0.013 256);
2815
+ border-color: oklch(27% 0.057 257);
2816
+ color: oklch(27% 0.057 257);
2817
+ }
2818
+ /* #D6CCE3 / #240050 — 11.4:1 */
2819
+ :where(.tag[data-tone="purple"]) {
2820
+ background-color: oklch(86% 0.033 305);
2821
+ border-color: oklch(23% 0.126 294);
2822
+ color: oklch(23% 0.126 294);
2823
+ }
2824
+ /* #EFD3E3 / #57133A — 9.7:1 */
2825
+ :where(.tag[data-tone="pink"]) {
2826
+ background-color: oklch(89% 0.037 343);
2827
+ border-color: oklch(32% 0.107 350);
2828
+ color: oklch(32% 0.107 350);
2829
+ }
2830
+ /* #F7D4D1 / #6B140E — 8.8:1 */
2831
+ :where(.tag[data-tone="red"]) {
2832
+ background-color: oklch(90% 0.04 24);
2833
+ border-color: oklch(35% 0.122 29);
2834
+ color: oklch(35% 0.122 29);
2835
+ }
2836
+ /* #FFDC8E / #4D3708 — 8.5:1 */
2837
+ :where(.tag[data-tone="orange"]) {
2838
+ background-color: oklch(91% 0.104 86);
2839
+ border-color: oklch(35% 0.067 81);
2840
+ color: oklch(35% 0.067 81);
2841
+ }
2842
+ /* #FFF59D / #4D4712 — 8.5:1 */
2843
+ :where(.tag[data-tone="yellow"]) {
2844
+ background-color: oklch(96% 0.109 103);
2845
+ border-color: oklch(39% 0.071 103);
2846
+ color: oklch(39% 0.071 103);
2847
+ }
2848
+
2849
+ /* Tag group — DHCW does not ship one, but a catalogue that has .tag
2850
+ and .tag-group should not leave the second unstyled. Wrap and gap so
2851
+ a cell of specialty chips reflows instead of forcing the column
2852
+ wider. 2px gap keeps two chips distinguishable without spending a
2853
+ row's worth of height. */
2854
+ :where(.tag-group) {
2855
+ display: flex;
2856
+ flex-wrap: wrap;
2857
+ gap: 0.125rem;
2858
+ }
2859
+ /* AI label — marks AI-generated content. Uses --lily-accent-deep
2860
+ (#554318, 9.5:1 with white) rather than the bright --color-accent
2861
+ gold, which at 3.4:1 cannot legally carry a label this small. This is
2862
+ the substitution the gold caveat in the file header describes.
2863
+
2864
+ On a clinical surface this label is a provenance claim about a piece
2865
+ of decision support, which makes it one of the few things on the
2866
+ page that must never be quietly dropped for density. */
2867
+ :where(.ai-label) {
2868
+ background-color: var(--lily-accent-deep);
2869
+ color: oklch(100% 0 0);
2870
+ }
2871
+ /* Status tags — pre-baked colour for each status. */
2872
+ :where(.status-tag[data-status="success"]) {
2873
+ background-color: var(--color-success);
2874
+ color: var(--color-success-content);
2875
+ }
2876
+ :where(.status-tag[data-status="warning"]) {
2877
+ background-color: var(--color-warning);
2878
+ color: var(--color-warning-content);
2879
+ }
2880
+ :where(.status-tag[data-status="error"]) {
2881
+ background-color: var(--color-error);
2882
+ color: var(--color-error-content);
2883
+ }
2884
+ :where(.status-tag[data-status="info"]) {
2885
+ background-color: var(--color-info);
2886
+ color: var(--color-info-content);
2887
+ }
2888
+
2889
+ /* Status light — a coloured dot followed by a label. */
2890
+ :where(.status-light) {
2891
+ display: inline-flex;
2892
+ align-items: center;
2893
+ gap: 0.25rem;
2894
+ }
2895
+ :where(.status-light)::before {
2896
+ content: "";
2897
+ display: inline-block;
2898
+ width: 0.5rem;
2899
+ height: 0.5rem;
2900
+ border-radius: 50%;
2901
+ background-color: var(--color-neutral);
2902
+ }
2903
+ :where(.status-light[data-status="success"])::before {
2904
+ background-color: var(--color-success);
2905
+ }
2906
+ :where(.status-light[data-status="warning"])::before {
2907
+ background-color: var(--color-warning);
2908
+ }
2909
+ :where(.status-light[data-status="error"])::before {
2910
+ background-color: var(--color-error);
2911
+ }
2912
+ :where(.status-light[data-status="info"])::before {
2913
+ background-color: var(--color-info);
2914
+ }
2915
+
2916
+ /* ==========================================================================
2917
+ * Pickers, ratings, theme
2918
+ * ====================================================================== */
2919
+
2920
+ /* Picker baseline — inline row of selectable chips/buttons. */
2921
+ :where(
2922
+ .color-picker,
2923
+ .five-face-rating-picker,
2924
+ .five-star-rating-picker,
2925
+ .net-promoter-score-picker,
2926
+ .red-amber-green-picker,
2927
+ .red-orange-yellow-green-blue-picker,
2928
+ .emoji-character-picker,
2929
+ .calendar-range-picker
2930
+ ) {
2931
+ display: inline-flex;
2932
+ align-items: center;
2933
+ gap: 0.125rem;
2934
+ padding: 0.125rem;
2935
+ background-color: var(--lily-surface);
2936
+ border: var(--border) solid var(--lily-border);
2937
+ border-radius: var(--lily-radius-md);
2938
+ }
2939
+ /* Picker buttons — small alt-surface chips. */
2940
+ :where(
2941
+ .color-picker-button,
2942
+ .five-face-rating-picker-button,
2943
+ .five-star-rating-picker-button,
2944
+ .net-promoter-score-picker-button,
2945
+ .red-amber-green-picker-button,
2946
+ .red-orange-yellow-green-blue-picker-button
2947
+ ) {
2948
+ background-color: var(--lily-surface-alt);
2949
+ color: var(--lily-text);
2950
+ border: var(--border) solid var(--lily-border);
2951
+ min-width: 1.5rem;
2952
+ min-height: 1.5rem;
2953
+ padding: 0.125rem 0.25rem;
2954
+ }
2955
+ /* Color picker — circular swatch. */
2956
+ :where(.color-picker-button) {
2957
+ width: 1.25rem;
2958
+ height: 1.25rem;
2959
+ padding: 0;
2960
+ border-radius: 50%;
2961
+ }
2962
+ /* Filled stars — warning (gold) colour for both viewer and pressed state. */
2963
+ :where(
2964
+ .five-star-rating-view,
2965
+ .five-star-rating-picker-button[aria-pressed="true"]
2966
+ ) {
2967
+ color: var(--color-warning);
2968
+ }
2969
+ /* RAG (red/amber/green) — semantic colour per status. */
2970
+ :where(
2971
+ .red-amber-green-view[data-status="red"],
2972
+ .red-amber-green-picker-button[data-status="red"]
2973
+ ) {
2974
+ background-color: var(--color-error);
2975
+ color: var(--color-error-content);
2976
+ }
2977
+ :where(
2978
+ .red-amber-green-view[data-status="amber"],
2979
+ .red-amber-green-picker-button[data-status="amber"]
2980
+ ) {
2981
+ background-color: var(--color-warning);
2982
+ color: var(--color-warning-content);
2983
+ }
2984
+ :where(
2985
+ .red-amber-green-view[data-status="green"],
2986
+ .red-amber-green-picker-button[data-status="green"]
2987
+ ) {
2988
+ background-color: var(--color-success);
2989
+ color: var(--color-success-content);
2990
+ }
2991
+
2992
+ /* Native <select> styling — drop the UA chrome and draw our own caret
2993
+ via two stacked linear-gradients. Practitioner density: 28px tall to
2994
+ match the input baseline. */
2995
+ :where(select.theme-select, .select, .select-with-extras, .tree-select) {
2996
+ width: 100%;
2997
+ min-height: 1.75rem;
2998
+ padding: 0.25rem 1.5rem 0.25rem 0.375rem; /* extra right padding for caret */
2999
+ font: inherit;
3000
+ background-color: var(--lily-surface);
3001
+ color: var(--lily-text);
3002
+ border: var(--border) solid var(--lily-border-strong);
3003
+ border-radius: var(--lily-radius-md);
3004
+ appearance: none;
3005
+ background-image:
3006
+ linear-gradient(45deg, transparent 50%, var(--lily-text) 50%),
3007
+ linear-gradient(135deg, var(--lily-text) 50%, transparent 50%);
3008
+ background-position:
3009
+ calc(100% - 0.8rem) 0.7rem,
3010
+ calc(100% - 0.5rem) 0.7rem;
3011
+ background-size: 0.3rem 0.3rem;
3012
+ background-repeat: no-repeat;
3013
+ }
3014
+ /* Options inherit the select colours so the dropdown matches the input. */
3015
+ :where(.option, .theme-select-option) {
3016
+ background-color: var(--lily-surface);
3017
+ color: var(--lily-text);
3018
+ }
3019
+
3020
+ /* The four *-picker helpers are an icon button that opens a popup,
3021
+ so they are styled as a popup rather than a form field. No :has()
3022
+ guard is needed any more: before the July 2026 rename these shared
3023
+ the `.theme-select` hook with the catalog theme-select component
3024
+ and had to be told apart. Distinct names, no collision. */
3025
+ :where(
3026
+ .theme-picker,
3027
+ .locale-picker,
3028
+ .text-size-picker,
3029
+ .share-picker,
3030
+ .date-time-picker
3031
+ ) {
3032
+ position: relative;
3033
+ display: inline-block;
3034
+ }
3035
+
3036
+ :where(
3037
+ .theme-picker-button,
3038
+ .locale-picker-button,
3039
+ .text-size-picker-button,
3040
+ .share-picker-button,
3041
+ .date-time-picker-button
3042
+ ) {
3043
+ display: inline-flex;
3044
+ align-items: center;
3045
+ justify-content: center;
3046
+ min-width: 2.5rem;
3047
+ min-height: 2.5rem;
3048
+ padding: var(--lily-space-xs);
3049
+ font: inherit;
3050
+ line-height: 1;
3051
+ color: var(--lily-text);
3052
+ background-color: var(--lily-surface);
3053
+ border: var(--border) solid var(--lily-border-strong);
3054
+ border-radius: var(--lily-radius-md);
3055
+ cursor: pointer;
3056
+ }
3057
+
3058
+ /* The icon is decorative (aria-hidden); the button's aria-label
3059
+ carries the accessible name. Reversed 2026-09-16: five of the
3060
+ six picker icons (theme, locale, text-size, motion, share) are
3061
+ now bundled SVGs, not Unicode glyphs. Each ships its own
3062
+ explicit width="1.05rem" height="1.05rem" on the <svg> element,
3063
+ so unlike a font-dependent character glyph -- where different
3064
+ glyphs ink different fractions of their em box, needing a
3065
+ per-glyph optical correction -- every icon in this family
3066
+ already inks the same visual size by construction (the icon's
3067
+ own author controls its viewBox). This rule only supplies flex
3068
+ centering; override the icon's own width/height if you need a
3069
+ different size. */
3070
+ :where(
3071
+ .theme-picker-icon,
3072
+ .locale-picker-icon,
3073
+ .text-size-picker-icon,
3074
+ .motion-picker-icon,
3075
+ .share-picker-icon
3076
+ ) {
3077
+ display: inline-flex;
3078
+ align-items: center;
3079
+ justify-content: center;
3080
+ }
3081
+
3082
+ /* date-time-picker's calendar icon is still a Unicode glyph
3083
+ (U+1F4C5 + U+FE0E) -- out of scope for the 2026-09-16 SVG-icon
3084
+ reversal, since date-time-picker was not part of that change
3085
+ (see AGENTS/helpers.md) -- so it keeps the font-size-based
3086
+ sizing the other five used to share.
3087
+
3088
+ U+1F4C5 calendar inks 1.000 of its em box (max extent) against
3089
+ U+25D1 half-circle's reference 0.850 (the pre-reversal
3090
+ baseline, still valid here since this is the same glyph
3091
+ measurement, unchanged): 0.850/1.000 ≈ 0.85, shipped as 0.845.
3092
+ A colour-emoji font paints every one of its glyphs into one
3093
+ identical em square, so globe, calendar, rocket and heart all
3094
+ measure the same ink box -- any future emoji helper icon can
3095
+ reuse this factor without re-measuring.
3096
+
3097
+ (Measuring also confirms the U+FE0E text-presentation selector is
3098
+ ignored here: the bare and VS-15 forms are metrically identical. It
3099
+ stays in the source as a hint to the platforms that do honour it.) */
3100
+ :where(.date-time-picker-icon) {
3101
+ display: inline-flex;
3102
+ align-items: center;
3103
+ justify-content: center;
3104
+ font-size: calc(1.25em * var(--lily-picker-icon-scale, 1));
3105
+ line-height: 1;
3106
+ --lily-picker-icon-scale: 0.845;
3107
+ }
3108
+
3109
+ /* The share list holds real links and a button rather than options,
3110
+ so its items get the same padding without the option selectors. */
3111
+ :where(.share-picker-list) > :where(.share-picker-list-item) {
3112
+ list-style: none;
3113
+ }
3114
+
3115
+ :where(.share-picker-target, .share-picker-copy) {
3116
+ display: block;
3117
+ width: 100%;
3118
+ padding: var(--lily-space-xs) var(--lily-space-sm);
3119
+ font: inherit;
3120
+ text-align: start;
3121
+ color: var(--lily-text);
3122
+ background: none;
3123
+ border: 0;
3124
+ border-radius: var(--lily-radius-sm);
3125
+ cursor: pointer;
3126
+ }
3127
+
3128
+ :where(.share-picker-target:hover, .share-picker-copy:hover) {
3129
+ background-color: var(--lily-surface-alt);
3130
+ }
3131
+
3132
+ /* Copy feedback. Never display:none this — that silences the live
3133
+ region for screen-reader users. */
3134
+ :where(.share-picker-status) {
3135
+ margin: var(--lily-space-xs) 0 0;
3136
+ font-size: 0.875em;
3137
+ color: var(--lily-text-muted);
3138
+ }
3139
+
3140
+ /* The status paragraph is always rendered so the aria-live region
3141
+ exists before the first announcement (see above); empty, it still
3142
+ generates a line box and carries the margin-top set above, both of
3143
+ which push the button row out of alignment in a flex layout like
3144
+ picker-bar (found independently by two downstream consumers, each
3145
+ patching around it locally). :empty here matches a Svelte-rendered
3146
+ {status} of "" (a zero-length text node doesn't count as a child
3147
+ per the CSS spec), so this only applies while there is nothing to
3148
+ announce, and normal spacing returns the moment status has text. */
3149
+ :where(.share-picker-status:empty) {
3150
+ margin: 0;
3151
+ line-height: 0;
3152
+ }
3153
+
3154
+ :where(
3155
+ .theme-picker-list,
3156
+ .locale-picker-list,
3157
+ .text-size-picker-list,
3158
+ .share-picker-list
3159
+ ) {
3160
+ position: absolute;
3161
+ z-index: 10;
3162
+ inset-inline-start: 0;
3163
+ min-width: max-content;
3164
+ max-height: 60vh;
3165
+ margin-block-start: var(--lily-space-xs);
3166
+ padding: var(--lily-space-xs);
3167
+ overflow-y: auto;
3168
+ list-style: none;
3169
+ color: var(--lily-text);
3170
+ background-color: var(--lily-surface);
3171
+ border: var(--border) solid var(--lily-border-strong);
3172
+ border-radius: var(--lily-radius-md);
3173
+ box-shadow: var(--lily-shadow-md);
3174
+ }
3175
+
3176
+ /* Explicit, because the rules above would otherwise beat the UA
3177
+ stylesheet's [hidden] { display: none }. */
3178
+ :where(
3179
+ .theme-picker-list,
3180
+ .locale-picker-list,
3181
+ .text-size-picker-list,
3182
+ .share-picker-list
3183
+ )[hidden] {
3184
+ display: none;
3185
+ }
3186
+
3187
+ :where(.theme-picker-list, .locale-picker-list, .text-size-picker-list)
3188
+ > :where(
3189
+ .theme-picker-option,
3190
+ .locale-picker-option,
3191
+ .text-size-picker-option
3192
+ ) {
3193
+ padding: var(--lily-space-xs) var(--lily-space-sm);
3194
+ border-radius: var(--lily-radius-sm);
3195
+ cursor: pointer;
3196
+ }
3197
+
3198
+ /* data-active is the keyboard cursor (aria-activedescendant); focus
3199
+ stays on the listbox, so this is the only visible cue for it. */
3200
+ :where(.theme-picker-list, .locale-picker-list, .text-size-picker-list)
3201
+ > :where([data-active]) {
3202
+ background-color: var(--lily-surface-alt);
3203
+ outline: var(--lily-focus-ring);
3204
+ outline-offset: -2px;
3205
+ }
3206
+
3207
+ /* aria-selected marks the applied theme/locale, which is not the
3208
+ same thing as the keyboard cursor. Both can be visible at once. */
3209
+ :where(.theme-picker-list, .locale-picker-list, .text-size-picker-list)
3210
+ > :where([aria-selected="true"]) {
3211
+ font-weight: 600;
3212
+ background-color: var(--lily-surface-sunken);
3213
+ }
3214
+
3215
+ /* Locale options inherit the select colours, same as theme options. */
3216
+ :where(.locale-picker-option) {
3217
+ background-color: var(--lily-surface);
3218
+ color: var(--lily-text);
3219
+ }
3220
+
3221
+ /* ----------------------------------------------------------------------
3222
+ * date-time-picker
3223
+ *
3224
+ * The one helper that is a form control rather than a page-header
3225
+ * control, so it is the only one with a text field beside its trigger
3226
+ * and a dialog rather than a listbox. The field itself needs no rules
3227
+ * here: `.date-time-picker-input` already matches the generic
3228
+ * [class$="-input"] baseline further up.
3229
+ * -------------------------------------------------------------------- */
3230
+
3231
+ :where(.date-time-picker-field) {
3232
+ display: flex;
3233
+ align-items: center;
3234
+ gap: var(--lily-space-xs);
3235
+ }
3236
+
3237
+ :where(.date-time-picker-dialog) {
3238
+ position: absolute;
3239
+ z-index: 10;
3240
+ inset-inline-start: 0;
3241
+ margin-block-start: var(--lily-space-xs);
3242
+ padding: var(--lily-space-sm);
3243
+ color: var(--lily-text);
3244
+ background-color: var(--lily-surface);
3245
+ border: var(--border) solid var(--lily-border-strong);
3246
+ border-radius: var(--lily-radius-md);
3247
+ box-shadow: var(--lily-shadow-md);
3248
+ }
3249
+
3250
+ /* Explicit, because the rule above would otherwise beat the UA
3251
+ stylesheet's [hidden] { display: none } — the same trap the
3252
+ listbox popups have to step around. */
3253
+ :where(.date-time-picker-dialog)[hidden] {
3254
+ display: none;
3255
+ }
3256
+
3257
+ :where(.date-time-picker-header) {
3258
+ display: flex;
3259
+ align-items: center;
3260
+ justify-content: space-between;
3261
+ gap: var(--lily-space-xs);
3262
+ margin-block-end: var(--lily-space-xs);
3263
+ }
3264
+
3265
+ /* The month/year heading is a live region, so it must keep its box
3266
+ even while empty — never display:none it. */
3267
+ :where(.date-time-picker-period) {
3268
+ flex: 1;
3269
+ font-weight: 600;
3270
+ text-align: center;
3271
+ }
3272
+
3273
+ /* The four navigation buttons and the footer/shortcut buttons do not
3274
+ end in `-button`, so the generic button family never reaches them. */
3275
+ :where(
3276
+ .date-time-picker-previous-year,
3277
+ .date-time-picker-previous-month,
3278
+ .date-time-picker-previous-week,
3279
+ .date-time-picker-previous-day,
3280
+ .date-time-picker-next-day,
3281
+ .date-time-picker-next-week,
3282
+ .date-time-picker-next-month,
3283
+ .date-time-picker-next-year,
3284
+ .date-time-picker-shortcut,
3285
+ .date-time-picker-clear,
3286
+ .date-time-picker-cancel,
3287
+ .date-time-picker-confirm
3288
+ ) {
3289
+ padding: var(--lily-space-xs) var(--lily-space-sm);
3290
+ font: inherit;
3291
+ color: var(--lily-text);
3292
+ background-color: var(--lily-surface);
3293
+ border: var(--border) solid var(--lily-border-strong);
3294
+ border-radius: var(--lily-radius-sm);
3295
+ cursor: pointer;
3296
+ }
3297
+
3298
+ :where(
3299
+ .date-time-picker-previous-year:hover,
3300
+ .date-time-picker-previous-month:hover,
3301
+ .date-time-picker-previous-week:hover,
3302
+ .date-time-picker-previous-day:hover,
3303
+ .date-time-picker-next-day:hover,
3304
+ .date-time-picker-next-week:hover,
3305
+ .date-time-picker-next-month:hover,
3306
+ .date-time-picker-next-year:hover,
3307
+ .date-time-picker-shortcut:hover,
3308
+ .date-time-picker-clear:hover,
3309
+ .date-time-picker-cancel:hover,
3310
+ .date-time-picker-confirm:hover
3311
+ ) {
3312
+ background-color: var(--lily-surface-alt);
3313
+ }
3314
+
3315
+ /* The commit action is the primary one on the dialog. */
3316
+ :where(.date-time-picker-confirm) {
3317
+ color: var(--color-primary-content);
3318
+ background-color: var(--color-primary);
3319
+ border-color: var(--color-primary);
3320
+ }
3321
+
3322
+ :where(.date-time-picker-calendar) {
3323
+ border-collapse: collapse;
3324
+ table-layout: fixed;
3325
+ }
3326
+
3327
+ :where(.date-time-picker-weekday, .date-time-picker-week-heading) {
3328
+ padding: var(--lily-space-xs);
3329
+ font-size: 0.8125em;
3330
+ font-weight: 600;
3331
+ color: var(--lily-text-muted);
3332
+ }
3333
+
3334
+ /* Week numbers are reference data, not navigation: muted, and set in
3335
+ tabular figures so the column does not shimmer as months change. */
3336
+ :where(.date-time-picker-week) {
3337
+ padding-inline-end: var(--lily-space-xs);
3338
+ font-size: 0.8125em;
3339
+ font-weight: 400;
3340
+ font-variant-numeric: tabular-nums;
3341
+ color: var(--lily-text-muted);
3342
+ text-align: end;
3343
+ }
3344
+
3345
+ :where(.date-time-picker-day) {
3346
+ display: flex;
3347
+ align-items: center;
3348
+ justify-content: center;
3349
+ /* 2rem is a compromise: WCAG 2.5.8 (AA) wants 24px, 2.5.5 (AAA)
3350
+ wants 44px, and seven 44px columns do not fit a 320px viewport
3351
+ alongside dialog chrome. Consumers targeting AAA should enlarge
3352
+ these and let the dialog go full-width on small screens. */
3353
+ min-width: 2rem;
3354
+ min-height: 2rem;
3355
+ padding: 0;
3356
+ font: inherit;
3357
+ font-variant-numeric: tabular-nums;
3358
+ color: var(--lily-text);
3359
+ background: none;
3360
+ border: var(--border) solid transparent;
3361
+ border-radius: var(--lily-radius-sm);
3362
+ cursor: pointer;
3363
+ }
3364
+
3365
+ :where(.date-time-picker-day:hover:not([data-disabled])) {
3366
+ background-color: var(--lily-surface-alt);
3367
+ }
3368
+
3369
+ /* Adjacent-month days stay legible rather than being hidden: they are
3370
+ real, selectable dates, and blanking them makes the grid harder to
3371
+ scan across a month boundary. */
3372
+ :where(.date-time-picker-day[data-outside]) {
3373
+ color: var(--lily-text-muted);
3374
+ }
3375
+
3376
+ /* Today is marked with a border, the selection with a fill. Two
3377
+ different channels, so a day that is both still reads as both. */
3378
+ :where(.date-time-picker-day[data-today]) {
3379
+ border-color: var(--lily-border-strong);
3380
+ font-weight: 600;
3381
+ }
3382
+
3383
+ :where(.date-time-picker-day[data-selected]) {
3384
+ color: var(--color-primary-content);
3385
+ background-color: var(--color-primary);
3386
+ border-color: var(--color-primary);
3387
+ }
3388
+
3389
+ :where(.date-time-picker-day[data-disabled]) {
3390
+ color: var(--lily-text-muted);
3391
+ cursor: not-allowed;
3392
+ /* Struck through as well as dimmed: "unavailable" must not rest on
3393
+ contrast alone (WCAG 1.4.1). */
3394
+ text-decoration: line-through;
3395
+ }
3396
+
3397
+ :where(.date-time-picker-time) {
3398
+ display: flex;
3399
+ flex-wrap: wrap;
3400
+ align-items: center;
3401
+ gap: var(--lily-space-xs);
3402
+ margin-block-start: var(--lily-space-sm);
3403
+ }
3404
+
3405
+ :where(.date-time-picker-time-label) {
3406
+ font-size: 0.8125em;
3407
+ color: var(--lily-text-muted);
3408
+ }
3409
+
3410
+ :where(.date-time-picker-time-zone) {
3411
+ display: flex;
3412
+ flex-wrap: wrap;
3413
+ align-items: center;
3414
+ gap: var(--lily-space-xs);
3415
+ margin-block-end: var(--lily-space-sm);
3416
+ }
3417
+
3418
+ :where(.date-time-picker-time-zone-label) {
3419
+ font-size: 0.8125em;
3420
+ color: var(--lily-text-muted);
3421
+ }
3422
+
3423
+ :where(
3424
+ .date-time-picker-hour,
3425
+ .date-time-picker-minute,
3426
+ .date-time-picker-meridiem,
3427
+ .date-time-picker-time-zone-select
3428
+ ) {
3429
+ padding: var(--lily-space-xs);
3430
+ font: inherit;
3431
+ color: var(--lily-text);
3432
+ background-color: var(--lily-surface);
3433
+ border: var(--border) solid var(--lily-border-strong);
3434
+ border-radius: var(--lily-radius-sm);
3435
+ }
3436
+
3437
+ :where(.date-time-picker-shortcuts) {
3438
+ display: flex;
3439
+ flex-wrap: wrap;
3440
+ gap: var(--lily-space-xs);
3441
+ margin-block-start: var(--lily-space-sm);
3442
+ }
3443
+
3444
+ :where(.date-time-picker-footer) {
3445
+ display: flex;
3446
+ justify-content: flex-end;
3447
+ gap: var(--lily-space-xs);
3448
+ margin-block-start: var(--lily-space-sm);
3449
+ }
3450
+
3451
+ /* Combobox / listbox / autosuggest — non-native variant of <select>. */
3452
+ :where(.combobox, .listbox, .autosuggest) {
3453
+ background-color: var(--lily-surface);
3454
+ border: var(--border) solid var(--lily-border-strong);
3455
+ border-radius: var(--lily-radius-md);
3456
+ }
3457
+
3458
+ /* ==========================================================================
3459
+ * Editorial typography
3460
+ * --------------------------------------------------------------------------
3461
+ * Headlines, body text, byline, blockquote, code — the long-form reading
3462
+ * primitives. Headlines use clamp() so they shrink on narrow viewports
3463
+ * without media queries.
3464
+ * ====================================================================== */
3465
+
3466
+ /* Practitioner headlines are tighter than the patients theme —
3467
+ practitioner screens are dense and screen real-estate goes to data,
3468
+ not to display type. */
3469
+ :where(.headline, .hero-headline, .section-heading) {
3470
+ color: var(--lily-text);
3471
+ font-family: var(--lily-font-body);
3472
+ font-weight: 700;
3473
+ line-height: 1.15;
3474
+ margin: 0 0 0.25rem 0;
3475
+ }
3476
+ :where(.headline) {
3477
+ font-size: clamp(1.125rem, 2vw, 1.5rem);
3478
+ }
3479
+ :where(.hero-headline) {
3480
+ font-size: clamp(1.5rem, 3vw, 2rem);
3481
+ }
3482
+ :where(.section-heading) {
3483
+ font-size: clamp(1rem, 1.5vw, 1.25rem);
3484
+ }
3485
+ /* Body text — 1.4 line-height for compact reading on dense screens. */
3486
+ :where(.body-text) {
3487
+ line-height: 1.4;
3488
+ color: var(--lily-text);
3489
+ }
3490
+ /* Byline — author / date line under a headline. */
3491
+ :where(.byline) {
3492
+ color: var(--lily-text-muted);
3493
+ font-size: 0.8125rem;
3494
+ }
3495
+ /* Blockquote — primary-coloured rail + italic body. */
3496
+ :where(.blockquote) {
3497
+ margin: 0.5rem 0;
3498
+ padding: 0.25rem 0.5rem;
3499
+ border-inline-start: 0.125rem solid var(--color-primary);
3500
+ color: var(--lily-text);
3501
+ font-style: italic;
3502
+ }
3503
+ /* End-notes / related content — top-bordered section at the foot. */
3504
+ :where(.end-notes, .related-content) {
3505
+ margin-top: 1rem;
3506
+ padding-top: 0.5rem;
3507
+ border-top: var(--border) solid var(--lily-border);
3508
+ }
3509
+ /* Caption / footnote / citation — small muted text. */
3510
+ :where(.caption, .footnote, .citation) {
3511
+ font-size: 0.8125rem;
3512
+ color: var(--lily-text-muted);
3513
+ }
3514
+ /* KBD — keyboard key glyph; thicker bottom border simulates the keytop. */
3515
+ :where(.kbd) {
3516
+ display: inline-block;
3517
+ padding: 0.125rem 0.4rem;
3518
+ font-family: var(--lily-font-mono);
3519
+ font-size: 0.85em;
3520
+ background-color: var(--lily-surface-alt);
3521
+ border: var(--border) solid var(--lily-border-strong);
3522
+ border-bottom-width: 2px;
3523
+ border-radius: var(--lily-radius-sm);
3524
+ }
3525
+ /* Inline code — slight background tint + monospaced font. */
3526
+ :where(.code) {
3527
+ font-family: var(--lily-font-mono);
3528
+ padding: 0.1em 0.3em;
3529
+ background-color: var(--lily-surface-alt);
3530
+ border-radius: var(--lily-radius-sm);
3531
+ font-size: 0.9em;
3532
+ }
3533
+ /* Code block — full-width block with horizontal scroll. */
3534
+ :where(.code-block) {
3535
+ display: block;
3536
+ padding: 0.5rem;
3537
+ background-color: var(--color-neutral);
3538
+ color: var(--color-neutral-content);
3539
+ font-family: var(--lily-font-mono);
3540
+ border-radius: var(--lily-radius-md);
3541
+ overflow-x: auto;
3542
+ white-space: pre;
3543
+ }
3544
+ /* ----------------------------------------------------------------------
3545
+ * Inset text — the DHCW gold rail
3546
+ *
3547
+ * This is where the Wales accent actually lives. DHCW's inset-text is a
3548
+ * gold rail with NO background fill. The absent fill is the interesting
3549
+ * part: GOV.UK and NHS England both tint the panel, DHCW does not, and
3550
+ * the result is that an inset paragraph still reads as body copy that
3551
+ * has been marked rather than as a box that has been inserted. Dropping
3552
+ * the fill is also what lets several of these stack inside a clinical
3553
+ * note without the note turning into stripes.
3554
+ *
3555
+ * Gold is used here exactly as DHCW uses it: as a rail, never behind
3556
+ * text. See the gold caveat in the file header.
3557
+ *
3558
+ * Practitioner geometry: padding stays at this variant's 4px/8px, but
3559
+ * the rail widens from 2px to DHCW's 8px. That is the one measurement
3560
+ * in this file that gets *larger* than the density scale would suggest,
3561
+ * and it is deliberate — a 2px rail in the same slate family as every
3562
+ * table rule on the page is not a rail, it is a border. The rail has to
3563
+ * out-weigh the surrounding grid lines to mean anything.
3564
+ *
3565
+ * Colour variants follow DHCW's five (`--navy --blue --grey --red
3566
+ * --yellow`), exposed as `data-tone` per Lily's data-* convention.
3567
+ * -------------------------------------------------------------------- */
3568
+ :where(.inset-text) {
3569
+ margin: 0 0 0.5rem;
3570
+ padding: 0.25rem 0.5rem;
3571
+ border-inline-start: 0.5rem solid var(--color-accent);
3572
+ background-color: transparent;
3573
+ }
3574
+ :where(.inset-text > :last-child) {
3575
+ margin-bottom: 0;
3576
+ }
3577
+ :where(.inset-text[data-tone="navy"]) {
3578
+ border-inline-start-color: var(--color-primary);
3579
+ }
3580
+ :where(.inset-text[data-tone="blue"]) {
3581
+ border-inline-start-color: var(--color-secondary);
3582
+ }
3583
+ :where(.inset-text[data-tone="grey"]) {
3584
+ border-inline-start-color: var(--color-neutral);
3585
+ }
3586
+ :where(.inset-text[data-tone="red"]) {
3587
+ border-inline-start-color: var(--color-error);
3588
+ }
3589
+ :where(.inset-text[data-tone="yellow"]) {
3590
+ border-inline-start-color: var(--color-warning);
3591
+ }
3592
+ /* Generic character — monospaced single-glyph display. */
3593
+ :where(.character) {
3594
+ font-family: var(--lily-font-mono);
3595
+ }
3596
+ /* Clamp-text — truncate to N lines (controlled by --lily-clamp-lines). */
3597
+ :where(.clamp-text) {
3598
+ display: -webkit-box;
3599
+ -webkit-line-clamp: var(--lily-clamp-lines, 3);
3600
+ line-clamp: var(--lily-clamp-lines, 3);
3601
+ -webkit-box-orient: vertical;
3602
+ overflow: hidden;
3603
+ }
3604
+ /* Screen-reader-only — visually hidden but available to AT. The
3605
+ classic clip-rect pattern (works in every screen reader). */
3606
+ :where(.screen-reader-span) {
3607
+ position: absolute;
3608
+ width: 1px;
3609
+ height: 1px;
3610
+ padding: 0;
3611
+ margin: -1px;
3612
+ overflow: hidden;
3613
+ clip: rect(0, 0, 0, 0);
3614
+ white-space: nowrap;
3615
+ border: 0;
3616
+ }
3617
+
3618
+ /* ==========================================================================
3619
+ * Hero, figure, photo, media
3620
+ * ====================================================================== */
3621
+
3622
+ /* Hero block — primary-filled banner with centred content. clamp() pad
3623
+ scales vertically across viewport sizes. Practitioner: shorter hero
3624
+ (rarely needed on practitioner screens, but kept for landing
3625
+ dashboards). */
3626
+ :where(.hero) {
3627
+ display: block;
3628
+ padding: clamp(1rem, 3vw, 2rem) 0.5rem;
3629
+ background-color: var(--color-primary);
3630
+ color: var(--color-primary-content);
3631
+ border-radius: var(--lily-radius-lg);
3632
+ text-align: center;
3633
+ }
3634
+ /* Figure — centred block with vertical margin. */
3635
+ :where(.figure) {
3636
+ margin: 0.5rem 0;
3637
+ text-align: center;
3638
+ }
3639
+ /* Feature photo — responsive lead image. */
3640
+ :where(.feature-photo) {
3641
+ display: block;
3642
+ width: 100%;
3643
+ height: auto;
3644
+ border-radius: var(--lily-radius-md);
3645
+ }
3646
+ /* Photo pack — responsive grid of thumbnails. */
3647
+ :where(.photo-pack) {
3648
+ display: grid;
3649
+ grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
3650
+ gap: 0.25rem;
3651
+ }
3652
+ /* Generic image — responsive block image. */
3653
+ :where(.image) {
3654
+ max-width: 100%;
3655
+ height: auto;
3656
+ display: block;
3657
+ }
3658
+ /* QR / barcode / signature — frame them as a labelled tile. */
3659
+ :where(.qr-code-image, .barcode-image, .signature-pad) {
3660
+ display: inline-block;
3661
+ background-color: var(--lily-surface);
3662
+ border: var(--border) solid var(--lily-border);
3663
+ border-radius: var(--lily-radius-md);
3664
+ padding: 0.25rem;
3665
+ }
3666
+ /* Signature pad — drawable surface; crosshair cursor signals input. */
3667
+ :where(.signature-pad) {
3668
+ min-height: 5rem;
3669
+ cursor: crosshair;
3670
+ }
3671
+ /* Video player — responsive black-letterboxed frame. */
3672
+ :where(.video-player) {
3673
+ display: block;
3674
+ width: 100%;
3675
+ max-width: 100%;
3676
+ border-radius: var(--lily-radius-md);
3677
+ /* Video chrome is black in every theme; pair the text colour with
3678
+ it here rather than inheriting the theme's (possibly dark)
3679
+ content colour onto a black surface. */
3680
+ background-color: black;
3681
+ color: white;
3682
+ }
3683
+
3684
+ /* ==========================================================================
3685
+ * Mockups & pictograms
3686
+ * --------------------------------------------------------------------------
3687
+ * Device frames for screenshots / illustrations — browser, phone, tablet,
3688
+ * laptop, watch, shell window. All share the same rounded-outline frame
3689
+ * baseline; specific frames override the radius / border-width.
3690
+ * ====================================================================== */
3691
+
3692
+ :where(
3693
+ .mockup-browser,
3694
+ .mockup-window,
3695
+ .mockup-shell,
3696
+ .mockup-phone-portrait,
3697
+ .mockup-phone-landscape,
3698
+ .mockup-tablet-portrait,
3699
+ .mockup-tablet-landscape,
3700
+ .mockup-laptop,
3701
+ .mockup-watch
3702
+ ) {
3703
+ display: block;
3704
+ background-color: var(--lily-surface);
3705
+ border: 2px solid var(--color-neutral);
3706
+ border-radius: var(--lily-radius-lg);
3707
+ overflow: hidden;
3708
+ box-shadow: var(--lily-shadow-md);
3709
+ }
3710
+ /* Mockup shell — terminal window; dark neutral fill + mono font. */
3711
+ :where(.mockup-shell) {
3712
+ background-color: var(--color-neutral);
3713
+ color: var(--color-neutral-content);
3714
+ font-family: var(--lily-font-mono);
3715
+ padding: 0.5rem;
3716
+ }
3717
+ /* Phones — heavier bezel (6px) and more rounded corners. */
3718
+ :where(.mockup-phone-portrait, .mockup-phone-landscape) {
3719
+ border-radius: 1.5rem;
3720
+ border-width: 6px;
3721
+ }
3722
+ /* Smartwatch — chunky bezel (4px) and pill radius. */
3723
+ :where(.mockup-watch) {
3724
+ border-radius: 1rem;
3725
+ border-width: 4px;
3726
+ }
3727
+ /* Pictogram — stacked icon + caption, centred. */
3728
+ :where(.pictogram) {
3729
+ display: inline-flex;
3730
+ flex-direction: column;
3731
+ align-items: center;
3732
+ gap: 0.25rem;
3733
+ padding: 0.5rem;
3734
+ text-align: center;
3735
+ }
3736
+
3737
+ /* ==========================================================================
3738
+ * Charts (containers only — content supplied by consumer)
3739
+ * --------------------------------------------------------------------------
3740
+ * Headless: Lily provides the frame + a 6-colour palette as custom
3741
+ * properties. The consumer's chart library (D3, Chart.js, ECharts, etc.)
3742
+ * reads `var(--lily-chart-color-1..6)` for its series colours.
3743
+ * ====================================================================== */
3744
+
3745
+ :where(
3746
+ .area-chart,
3747
+ .bar-chart,
3748
+ .column-chart,
3749
+ .line-chart,
3750
+ .scatter-chart,
3751
+ .sparkline,
3752
+ .tile-map,
3753
+ .graphic-block
3754
+ ) {
3755
+ display: block;
3756
+ background-color: var(--lily-surface);
3757
+ border-radius: var(--lily-radius-md);
3758
+ padding: 0.5rem;
3759
+ color: var(--lily-text);
3760
+ /* Six-colour palette — the consumer's chart library picks these up. */
3761
+ --lily-chart-color-1: var(--color-primary);
3762
+ --lily-chart-color-2: var(--color-secondary);
3763
+ --lily-chart-color-3: var(--color-accent);
3764
+ --lily-chart-color-4: var(--color-info);
3765
+ --lily-chart-color-5: var(--color-success);
3766
+ --lily-chart-color-6: var(--color-warning);
3767
+ }
3768
+ /* Sparkline — small inline chart; no padding. */
3769
+ :where(.sparkline) {
3770
+ padding: 0;
3771
+ height: 1rem;
3772
+ width: 4rem;
3773
+ display: inline-block;
3774
+ }
3775
+ /* Graphic block — bordered chart container. */
3776
+ :where(.graphic-block) {
3777
+ border: var(--border) solid var(--lily-border);
3778
+ }
3779
+
3780
+ /* ==========================================================================
3781
+ * Scrolling, splits, resize
3782
+ * ====================================================================== */
3783
+
3784
+ /* Scroll area — capped at 16rem, auto-overflow. */
3785
+ :where(.scroll-area) {
3786
+ overflow: auto;
3787
+ max-height: 16rem;
3788
+ }
3789
+ /* Horizontal scroller — flex row that overflows on x. */
3790
+ :where(.horizontal-scroller) {
3791
+ overflow-x: auto;
3792
+ display: flex;
3793
+ gap: 0.25rem;
3794
+ }
3795
+ /* Scroller (Reuters-pattern scrollytelling) — positioning anchor. */
3796
+ :where(.scroller, .scroller-base, .scroller-video) {
3797
+ position: relative;
3798
+ display: block;
3799
+ }
3800
+ /* Custom scrollbar track. */
3801
+ :where(.scroll-bar) {
3802
+ background-color: var(--lily-surface-sunken);
3803
+ border-radius: 999px;
3804
+ height: 0.25rem;
3805
+ }
3806
+ /* Affix — stick to the top of the viewport on scroll. */
3807
+ :where(.affix) {
3808
+ position: sticky;
3809
+ inset-block-start: 0;
3810
+ }
3811
+ /* Visible — IntersectionObserver target; baseline is a visible block. */
3812
+ :where(.visible) {
3813
+ display: block;
3814
+ }
3815
+ /* Resizable — native CSS resize handle (corner). */
3816
+ :where(.resizable) {
3817
+ resize: both;
3818
+ overflow: auto;
3819
+ }
3820
+ /* Split-view + splitter — two-pane horizontal layout. */
3821
+ :where(.split-view) {
3822
+ display: flex;
3823
+ align-items: stretch;
3824
+ gap: 0;
3825
+ }
3826
+ :where(.splitter) {
3827
+ flex: 0 0 0.25rem;
3828
+ background-color: var(--lily-border);
3829
+ cursor: col-resize;
3830
+ }
3831
+
3832
+ /* ==========================================================================
3833
+ * Misc unique components
3834
+ * ====================================================================== */
3835
+
3836
+ /* Statistic — stacked label + value; value is large and primary-coloured.
3837
+ Practitioner: vital-signs panels are everywhere — keep the value strongly
3838
+ weighted but reduce the size so a 6-tile NEWS2 row fits cleanly. */
3839
+ :where(.statistic) {
3840
+ display: inline-flex;
3841
+ flex-direction: column;
3842
+ gap: 0;
3843
+ }
3844
+ :where(.statistic > [data-part="value"]) {
3845
+ font-size: 1.125rem;
3846
+ font-weight: 700;
3847
+ color: var(--color-primary);
3848
+ }
3849
+ /* Watermark — diagonal repeating-line pattern at 8% opacity behind content. */
3850
+ :where(.watermark) {
3851
+ position: absolute;
3852
+ inset: 0;
3853
+ pointer-events: none;
3854
+ opacity: 0.08;
3855
+ background: repeating-linear-gradient(
3856
+ -45deg,
3857
+ var(--lily-text) 0 1px,
3858
+ transparent 1px 8rem
3859
+ );
3860
+ }
3861
+ /* Beach ball (macOS "waiting" indicator) — six-colour conic wheel. */
3862
+ :where(.beach-ball) {
3863
+ display: inline-block;
3864
+ width: 1.25rem;
3865
+ height: 1.25rem;
3866
+ border-radius: 50%;
3867
+ background: conic-gradient(
3868
+ var(--color-error),
3869
+ var(--color-warning),
3870
+ var(--color-success),
3871
+ var(--color-info),
3872
+ var(--color-primary),
3873
+ var(--color-error)
3874
+ );
3875
+ animation: lily-spin 2s linear infinite;
3876
+ }
3877
+ /* Diff — side-by-side two-column comparison. */
3878
+ :where(.diff) {
3879
+ display: grid;
3880
+ grid-template-columns: 1fr 1fr;
3881
+ gap: 0.25rem;
3882
+ }
3883
+ /* Addressograph box — paper-form ID label (NHS / hospital pattern).
3884
+ Stays slightly more generous than the rest of the practitioner theme
3885
+ because the addressograph is the *anchor* identifier on every paper
3886
+ and screen artefact and must remain unambiguous. */
3887
+ :where(.addressograph-box) {
3888
+ display: block;
3889
+ padding: 0.375rem 0.5rem;
3890
+ background-color: var(--lily-surface-alt);
3891
+ border: 2px solid var(--lily-text);
3892
+ font-family: var(--lily-font-mono);
3893
+ line-height: 1.3;
3894
+ }
3895
+ /* Government identifier — dark chrome bar containing an ID number. */
3896
+ :where(.government-identifier) {
3897
+ display: flex;
3898
+ align-items: center;
3899
+ gap: 0.375rem;
3900
+ padding: 0.25rem 0.5rem;
3901
+ background-color: var(--color-neutral);
3902
+ color: var(--color-neutral-content);
3903
+ }
3904
+ /* Share / signup / CTA panels — alt-surface promo blocks. */
3905
+ :where(.share-page, .newsletter-signup, .call-to-action) {
3906
+ padding: 0.5rem;
3907
+ background-color: var(--lily-surface-alt);
3908
+ border-radius: var(--lily-radius-md);
3909
+ }
3910
+ /* CTA — override with primary fill so it reads as the page action. */
3911
+ :where(.call-to-action) {
3912
+ background-color: var(--color-primary);
3913
+ color: var(--color-primary-content);
3914
+ }
3915
+ /* Links inside the call-to-action inherit its content colour —
3916
+ otherwise the UA default link blue lands on the primary fill. */
3917
+ :where(.call-to-action) :where(a) {
3918
+ color: inherit;
3919
+ }
3920
+ /* Expander / collapsible — boxed disclosure controls. */
3921
+ :where(.expander, .collapsible) {
3922
+ display: block;
3923
+ border: var(--border) solid var(--lily-border);
3924
+ border-radius: var(--lily-radius-md);
3925
+ padding: 0.25rem 0.5rem;
3926
+ }
3927
+ :where(.expander > summary, .collapsible > summary) {
3928
+ cursor: pointer;
3929
+ font-weight: 600;
3930
+ padding: 0.125rem 0;
3931
+ }
3932
+
3933
+ /* ----------------------------------------------------------------------
3934
+ * Details — DHCW's unboxed disclosure
3935
+ *
3936
+ * Split out from the expander/collapsible family because DHCW's details
3937
+ * is not a box. There is no border and no padding around the closed
3938
+ * state: it is a blue underlined summary line with a triangle, and the
3939
+ * revealed content hangs off a grey left rail.
3940
+ *
3941
+ * The unboxed shape pays for itself here more than anywhere. A
3942
+ * practitioner record is largely made of collapsed sections — past
3943
+ * encounters, previous results, medication history — and a bordered box
3944
+ * per section produces a page of nested rectangles where the nesting
3945
+ * carries no meaning. An underlined line with a rail reads as an
3946
+ * outline, which is what a record actually is.
3947
+ *
3948
+ * The marker is rotated with clip-path geometry rather than swapped for
3949
+ * a different glyph, so it inherits `currentColor` and stays in sync
3950
+ * with the summary's hover and focus states for free.
3951
+ *
3952
+ * Density: 4px reveal gap and 4px/8px rail padding, against DHCW's 8px
3953
+ * and 16px/20px. The rail itself stays 4px — thinner and it merges with
3954
+ * the table rules running past it.
3955
+ * -------------------------------------------------------------------- */
3956
+ :where(.details) {
3957
+ display: block;
3958
+ margin-bottom: 0.5rem;
3959
+ border: none;
3960
+ border-radius: 0;
3961
+ padding: 0;
3962
+ color: var(--lily-text);
3963
+ }
3964
+ :where(.details > summary) {
3965
+ position: relative;
3966
+ display: inline-block;
3967
+ padding: 0;
3968
+ padding-inline-start: 1rem;
3969
+ color: var(--color-secondary);
3970
+ font-weight: 400;
3971
+ text-decoration: underline;
3972
+ text-underline-offset: 0.12em;
3973
+ cursor: pointer;
3974
+ list-style: none;
3975
+ }
3976
+ /* Hide the native disclosure triangle in every engine. */
3977
+ :where(.details > summary)::-webkit-details-marker {
3978
+ display: none;
3979
+ }
3980
+ :where(.details > summary)::marker {
3981
+ content: "";
3982
+ }
3983
+ :where(.details > summary):hover {
3984
+ color: var(--lily-link-hover);
3985
+ }
3986
+ /* The closed marker — a right-pointing triangle in currentColor.
3987
+ Scaled down with the type, unlike the focus ring and the rails. */
3988
+ :where(.details > summary)::before {
3989
+ content: "";
3990
+ position: absolute;
3991
+ inset-inline-start: 0;
3992
+ top: 0;
3993
+ bottom: 0;
3994
+ margin: auto;
3995
+ width: 0;
3996
+ height: 0;
3997
+ border-style: solid;
3998
+ border-color: transparent;
3999
+ border-width: 5px 0 5px 8px;
4000
+ border-inline-start-color: currentColor;
4001
+ }
4002
+ /* Open — the same triangle rotated to point down. */
4003
+ :where(.details[open] > summary)::before {
4004
+ border-width: 8px 5px 0 5px;
4005
+ border-inline-start-color: transparent;
4006
+ border-top-color: currentColor;
4007
+ }
4008
+ /* Revealed content — grey rail, indented. Everything after the
4009
+ summary, so it works whatever the consumer puts in the slot. */
4010
+ :where(.details > summary) ~ * {
4011
+ margin-inline-start: 0;
4012
+ }
4013
+ :where(.details[open] > summary) ~ :first-of-type {
4014
+ margin-top: 0.25rem;
4015
+ }
4016
+ :where(.details > summary) ~ *:not(summary) {
4017
+ border-inline-start: 0.25rem solid var(--lily-border);
4018
+ padding: 0.25rem 0.5rem 0.25rem 0.5rem;
4019
+ color: var(--lily-text);
4020
+ }
4021
+ /* Carousel — horizontal scroller with snap-points so each slide locks
4022
+ into view as the user swipes. */
4023
+ :where(.carousel) {
4024
+ display: flex;
4025
+ overflow-x: auto;
4026
+ scroll-snap-type: x mandatory;
4027
+ gap: 0.25rem;
4028
+ }
4029
+ :where(.carousel > *) {
4030
+ scroll-snap-align: start;
4031
+ flex: 0 0 auto;
4032
+ }
4033
+
4034
+ /* --- Semantic entities — neutral display blocks. ----------------------- */
4035
+ :where(
4036
+ .person,
4037
+ .organization,
4038
+ .place,
4039
+ .event,
4040
+ .comment,
4041
+ .question,
4042
+ .answer
4043
+ ) {
4044
+ display: block;
4045
+ padding: 0.25rem 0;
4046
+ }
4047
+ /* Draft wrapper — dashed border + optional "Draft" pill prefix. */
4048
+ :where(.draft) {
4049
+ border: 1px dashed var(--lily-border-strong);
4050
+ background-color: var(--lily-surface-alt);
4051
+ padding: 0.5rem;
4052
+ border-radius: var(--lily-radius-md);
4053
+ }
4054
+ :where(.draft[data-status="draft"])::before {
4055
+ content: "Draft";
4056
+ display: inline-block;
4057
+ margin-inline-end: 0.25rem;
4058
+ padding: 0 0.25rem;
4059
+ background-color: var(--color-warning);
4060
+ color: var(--color-warning-content);
4061
+ border-radius: 999px;
4062
+ font-size: 0.75rem;
4063
+ font-weight: 600;
4064
+ }
4065
+
4066
+ /* Dial — circular gauge driven by --dial-value (0–100).
4067
+ Practitioner density: ~40px wide so a row of dials (e.g. vital-signs
4068
+ trend gauges) fits comfortably. */
4069
+ :where(.dial) {
4070
+ display: inline-block;
4071
+ width: 2.5rem;
4072
+ height: 2.5rem;
4073
+ border-radius: 50%;
4074
+ background: conic-gradient(
4075
+ var(--color-primary) calc(var(--dial-value, 0) * 1%),
4076
+ var(--lily-surface-sunken) 0
4077
+ );
4078
+ }
4079
+
4080
+ /* File upload — drop-zone aesthetic: dashed border, alt surface. */
4081
+ :where(.file-upload) {
4082
+ display: block;
4083
+ padding: 0.75rem;
4084
+ background-color: var(--lily-surface-alt);
4085
+ border: 2px dashed var(--lily-border-strong);
4086
+ border-radius: var(--lily-radius-md);
4087
+ text-align: center;
4088
+ cursor: pointer;
4089
+ }
4090
+ /* File manager — surface-coloured frame holding a file list. */
4091
+ :where(.file-manager) {
4092
+ display: block;
4093
+ background-color: var(--lily-surface);
4094
+ border: var(--border) solid var(--lily-border);
4095
+ border-radius: var(--lily-radius-md);
4096
+ padding: 0.25rem;
4097
+ }
4098
+ /* Transfer list — two columns + central button stack. */
4099
+ :where(.transfer-list) {
4100
+ display: grid;
4101
+ grid-template-columns: 1fr auto 1fr;
4102
+ gap: 0.25rem;
4103
+ align-items: center;
4104
+ }
4105
+
4106
+ /* Tour — onboarding card with medium elevation. */
4107
+ :where(.tour) {
4108
+ display: block;
4109
+ background-color: var(--lily-surface);
4110
+ border: var(--border) solid var(--lily-border);
4111
+ border-radius: var(--lily-radius-lg);
4112
+ padding: 0.5rem;
4113
+ box-shadow: var(--lily-shadow-md);
4114
+ }
4115
+
4116
+ /* Editable text — dashed underline indicates click-to-edit. */
4117
+ :where(.editable) {
4118
+ display: inline-block;
4119
+ padding: 0 0.125rem;
4120
+ border-bottom: 1px dashed var(--lily-border-strong);
4121
+ cursor: text;
4122
+ }
4123
+ /* Editing state — alt surface + primary underline. */
4124
+ :where(.editable[aria-expanded="true"]) {
4125
+ background-color: var(--lily-surface-alt);
4126
+ border-bottom-color: var(--color-primary);
4127
+ }
4128
+
4129
+ /* Accordion-checkbox — a checkbox that toggles a panel via sibling
4130
+ combinator (no JS needed). */
4131
+ :where(.accordion-checkbox) {
4132
+ display: block;
4133
+ border: var(--border) solid var(--lily-border);
4134
+ border-radius: var(--lily-radius-md);
4135
+ padding: 0.25rem 0.375rem;
4136
+ }
4137
+ :where(.accordion-checkbox-input) {
4138
+ margin-inline-end: 0.25rem;
4139
+ }
4140
+ :where(.accordion-checkbox-panel) {
4141
+ display: none;
4142
+ padding-block-start: 0.25rem;
4143
+ }
4144
+ /* Reveal the panel when the input is checked. */
4145
+ :where(.accordion-checkbox-input:checked ~ .accordion-checkbox-panel) {
4146
+ display: block;
4147
+ }
4148
+
4149
+ /* Icon — 1em square container; sizes with surrounding text. */
4150
+ :where(.icon) {
4151
+ display: inline-flex;
4152
+ align-items: center;
4153
+ justify-content: center;
4154
+ width: 1em;
4155
+ height: 1em;
4156
+ flex: 0 0 auto;
4157
+ }
4158
+
4159
+ /* Emoji — inline glyph; not italicised. */
4160
+ :where(.emoji) {
4161
+ display: inline-block;
4162
+ font-style: normal;
4163
+ }
4164
+
4165
+ /* Slider — primary-filled track based on --slider-value (0–100). */
4166
+ :where(.slider) {
4167
+ display: block;
4168
+ width: 100%;
4169
+ height: 1rem;
4170
+ background: linear-gradient(
4171
+ to right,
4172
+ var(--color-primary) calc(var(--slider-value, 50) * 1%),
4173
+ var(--lily-surface-sunken) 0
4174
+ );
4175
+ border-radius: 999px;
4176
+ }
4177
+
4178
+ /* Timer — fixed-width digits so the display doesn't jiggle as it ticks. */
4179
+ :where(.timer) {
4180
+ display: inline-block;
4181
+ font-family: var(--lily-font-mono);
4182
+ font-variant-numeric: tabular-nums;
4183
+ font-weight: 600;
4184
+ }
4185
+
4186
+ /* Medical banner inner boxes — spacing-only override. */
4187
+ :where(.medical-banner-box-for-advice, .medical-banner-box-for-danger) {
4188
+ padding: 0.375rem 0.5rem;
4189
+ border-radius: var(--lily-radius-md);
4190
+ margin: 0.125rem 0;
4191
+ }
4192
+
4193
+ /* ==========================================================================
4194
+ * Bilingual — Cymraeg / Welsh
4195
+ * --------------------------------------------------------------------------
4196
+ * Neither the NHS service manual nor the DHCW component library says
4197
+ * anything about this, which is a real gap in a design system for a
4198
+ * country with two official languages. The Welsh Language (Wales)
4199
+ * Measure 2011 requires Welsh to be treated no less favourably than
4200
+ * English by Welsh public bodies, and the Welsh Language Standards make
4201
+ * that concrete for websites. A theme cannot deliver compliance — the
4202
+ * content does that — but it can stop the layout from quietly
4203
+ * penalising the Welsh half.
4204
+ *
4205
+ * The core fact these rules exist for: **Welsh runs roughly 20–30%
4206
+ * longer than English** for the same content. "Appointments" is
4207
+ * "Apwyntiadau"; "Discharge summary" is "Crynodeb rhyddhau". A column
4208
+ * header, tab strip, or button row tuned on English copy is the single
4209
+ * most common way a bilingual service ends up with a broken Welsh page,
4210
+ * and it is invisible to anyone testing in English.
4211
+ *
4212
+ * This variant is where that collides hardest with density. Every other
4213
+ * rule in this file is trying to fit more on screen; these rules are
4214
+ * the standing exception, and they win. A truncated Welsh column header
4215
+ * on a results grid is not a cosmetic defect — it is a clinician
4216
+ * reading the wrong column.
4217
+ *
4218
+ * These rules key off `:lang(cy)`, which matches on the `lang`
4219
+ * attribute — so they activate for a fully Welsh page (`<html lang="cy">`,
4220
+ * which the locale-picker helper sets) and for individual Welsh
4221
+ * fragments inside an English page (`<span lang="cy">`), which is the
4222
+ * normal shape of a bilingual UK public-sector site. It is also the
4223
+ * normal shape of a *clinical* one, where a Welsh-language patient
4224
+ * name or free-text note sits inside an English application shell.
4225
+ *
4226
+ * Setting `lang` correctly is not optional decoration: it is what tells
4227
+ * a screen reader to switch voice. Welsh read aloud by an English
4228
+ * synthesiser is not merely accented, it is unintelligible.
4229
+ * ====================================================================== */
4230
+
4231
+ /* Let Welsh labels wrap instead of truncating. Any component that
4232
+ clamps to one line on English copy will clip a Welsh label, and a
4233
+ clipped label in a nav or a column header is a navigation failure,
4234
+ not a cosmetic one. Wrapping is always the better failure mode than
4235
+ an ellipsis — a taller row is recoverable, a wrong word is not. */
4236
+ :where(:lang(cy)) :where(
4237
+ .tab-bar-button,
4238
+ .menu-item,
4239
+ .breadcrumb-list-item,
4240
+ .pagination-list-item,
4241
+ .section-list-item,
4242
+ .contents-list-item,
4243
+ .button,
4244
+ [class$="-button"],
4245
+ .tag,
4246
+ .status-tag,
4247
+ .badge,
4248
+ [class$="-table-th"],
4249
+ [class$="-table-td"]
4250
+ ),
4251
+ :where(
4252
+ .tab-bar-button,
4253
+ .menu-item,
4254
+ .breadcrumb-list-item,
4255
+ .pagination-list-item,
4256
+ .section-list-item,
4257
+ .contents-list-item,
4258
+ .button,
4259
+ [class$="-button"],
4260
+ .tag,
4261
+ .status-tag,
4262
+ .badge,
4263
+ [class$="-table-th"],
4264
+ [class$="-table-td"]
4265
+ ):lang(cy) {
4266
+ white-space: normal;
4267
+ overflow: visible;
4268
+ text-overflow: clip;
4269
+ /* Allow a long compound to break rather than force the column
4270
+ wider — "amlroddadwy" will not fit a 6rem chip otherwise. */
4271
+ overflow-wrap: break-word;
4272
+ }
4273
+
4274
+ /* Never auto-hyphenate Welsh. Welsh treats ch, dd, ff, ng, ll, ph, rh
4275
+ and th as single letters, so a hyphenation engine without a Welsh
4276
+ dictionary will happily split "bwyd-lyn" across a digraph and produce
4277
+ a word that reads as a different word. Browser Welsh dictionaries are
4278
+ inconsistent at best. Manual soft hyphens from the content team are
4279
+ honoured; automatic ones are not attempted. */
4280
+ :where(:lang(cy)) {
4281
+ hyphens: manual;
4282
+ }
4283
+
4284
+ /* Headings wrap more evenly. A Welsh h1 is likelier to run to two lines
4285
+ than its English counterpart, and a two-line heading with one word on
4286
+ the second line reads as a mistake. */
4287
+ :where(:lang(cy)) :where(h1, h2, h3, .headline, .hero-title) {
4288
+ text-wrap: balance;
4289
+ }
4290
+
4291
+ /* Give the language switcher room. On a bilingual service this control
4292
+ is not a preference — for a Welsh speaker landing on an English page
4293
+ it is the primary navigation, and it must not be the thing that gets
4294
+ squeezed when a dense header runs out of room. */
4295
+ :where(.locale-picker) {
4296
+ flex-shrink: 0;
4297
+ }
4298
+
4299
+ /* The convention across Welsh public-sector sites is that the switcher
4300
+ is labelled in the language it switches *to* ("Cymraeg" on an English
4301
+ page, "English" on a Welsh one), never a flag. Flags are countries,
4302
+ not languages, and the Welsh flag would be wrong for both. There is
4303
+ no CSS to enforce that — it is a content decision — but it is
4304
+ recorded here because this is the file people read when they are
4305
+ building the header. */
4306
+
4307
+ /* ==========================================================================
4308
+ * Print
4309
+ * --------------------------------------------------------------------------
4310
+ * On paper, transient/floating chrome (skip-link, FAB, toast, tooltip)
4311
+ * is meaningless — hide it. Card-like surfaces drop their drop-shadow
4312
+ * and shift to a high-contrast outline so they're readable in monochrome.
4313
+ * ====================================================================== */
4314
+ @media print {
4315
+ :where(
4316
+ .skip-link,
4317
+ .float-button,
4318
+ .toast,
4319
+ .sonner,
4320
+ .tooltip,
4321
+ .coachmark
4322
+ ) {
4323
+ display: none !important;
4324
+ }
4325
+ :where(.card, .panel, .dialog, .alert) {
4326
+ box-shadow: none;
4327
+ border-color: var(--lily-text);
4328
+ }
4329
+ }
4330
+ }
4331
+
4332
+ /* ============================================================================
4333
+ * WCAG 2.2 target-size (minimum) floor — 2.5.8
4334
+ *
4335
+ * Every interactive Lily hook offers a >=24x24 CSS-pixel target. Links
4336
+ * that render as short inline text (a pagination "1", a breadcrumb crumb)
4337
+ * fail the 24px floor under typographic sizing alone, so the link-shaped
4338
+ * hooks become inline-block with a minimum box. :where() keeps specificity
4339
+ * at zero, so any consumer rule still wins. Shared verbatim by all themes.
4340
+ * ========================================================================= */
4341
+
4342
+ :where(.action-link,
4343
+ .back-link,
4344
+ .breadcrumb-list-item > a,
4345
+ .pagination-list-item > a,
4346
+ .contents-list-item > a,
4347
+ .section-list-item > a) {
4348
+ display: inline-block;
4349
+ min-inline-size: 24px;
4350
+ min-block-size: 24px;
4351
+ }
4352
+
4353
+ :where(.pagination-list-item > a) {
4354
+ text-align: center;
4355
+ }
4356
+
4357
+ :where(input:not([type="checkbox"]):not([type="radio"]), select, textarea) {
4358
+ min-block-size: 24px;
4359
+ }
4360
+
4361
+ /* ============================================================================
4362
+ * Overflow guards — narrow-viewport safety, shared verbatim by all themes.
4363
+ *
4364
+ * Content that is intrinsically wider than the viewport scrolls or wraps
4365
+ * within its own box instead of pushing the page wider. Deliberately NOT
4366
+ * `display: block` on <table> — that strips implicit table semantics for
4367
+ * assistive technology; capping width and letting cell content wrap keeps
4368
+ * both the layout and the roles. Proven in the example apps' own
4369
+ * stylesheets before the theme layer took over component styling.
4370
+ * ========================================================================= */
4371
+
4372
+ :where(pre) {
4373
+ max-width: 100%;
4374
+ overflow-x: auto;
4375
+ }
4376
+
4377
+ :where(table) {
4378
+ max-width: 100%;
4379
+ }
4380
+
4381
+ :where(th, td) {
4382
+ overflow-wrap: anywhere;
4383
+ }
4384
+
4385
+ :where(a, code) {
4386
+ overflow-wrap: anywhere;
4387
+ }