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