@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,3404 @@
1
+ /* ============================================================================
2
+ * NHS Scotland (United Kingdom) Design System — Practitioner / Clinical variant
3
+ * ----------------------------------------------------------------------------
4
+ * This is the clinical, high-information-density sibling of
5
+ * `united-kingdom-national-health-service-scotland-for-patients.css`. The
6
+ * patient theme targets the public-facing nhsinform.scot reading
7
+ * experience: comfortable 16px body copy, generous 44px hit targets that
8
+ * meet WCAG 2.5.5 for occasional self-service users on touch devices, and
9
+ * 4px radii that read as calm and approachable.
10
+ *
11
+ * The practitioner theme targets a very different audience: clinicians,
12
+ * pharmacists, triage nurses, and admin staff who spend their entire shift
13
+ * inside an electronic patient record (EPR), prescribing workflow, or
14
+ * triage console. They use precision pointing devices (mouse + keyboard,
15
+ * sometimes stylus), read at viewing distances of 60–80cm, and need to
16
+ * fit as many fields, rows, and identifiers on screen as possible without
17
+ * scrolling. Density wins over generosity. To deliver that, this theme:
18
+ *
19
+ * - shrinks the Lily spacing scale by roughly 50% so paddings, margins,
20
+ * and gaps compact;
21
+ * - sharpens corner radii to 2px so cards and inputs read as functional
22
+ * instruments rather than friendly consumer panels;
23
+ * - drops control min-heights from 2.5rem (40–44px touch targets) to
24
+ * 1.75rem (~28px desktop targets), still WCAG 2.5.8 minimum (24px)
25
+ * and well within Fitts'-law-acceptable for mouse + keyboard users
26
+ * in a clinical setting where the operator is trained, the cursor is
27
+ * precise, and screen real estate is at a premium;
28
+ * - reduces the default body font-size from 16px to 14px, the size
29
+ * conventional clinical systems (EMIS, SystmOne, Vision, EPIC,
30
+ * Cerner) use for their data-dense panels;
31
+ * - tightens input and button padding so identifier strings (CHI
32
+ * numbers, NHS numbers, dose, allergens) fit on a single line in
33
+ * narrow side panels.
34
+ *
35
+ * Colours, focus indicator, semantic meaning of warning / error / success,
36
+ * and the NHS Scotland brand identity are unchanged — only geometry and
37
+ * typography differ. A user who switches between patient-facing and
38
+ * practitioner-facing services will see the same NHS Scotland blue and
39
+ * the same NHS Warm Yellow focus ring; the practitioner UI just packs
40
+ * more information into the same viewport.
41
+ *
42
+ * NHS Scotland is the umbrella for 14 territorial NHS boards plus seven
43
+ * special boards (NHS 24, NHS Education for Scotland, NHS National Services
44
+ * Scotland, Public Health Scotland, Scottish Ambulance Service, State
45
+ * Hospitals Board, NHS Healthcare Improvement Scotland). Its public-facing
46
+ * digital surface is split across three closely-aligned brand expressions
47
+ * that this theme harmonises into one Lily token set:
48
+ *
49
+ * 1. NHS Scotland corporate brand (Scottish Government / CMO directorate)
50
+ * — the formal navy-blue identity used by NHS boards, statutory
51
+ * publications, and the NHS Scotland staff intranet.
52
+ * 2. NHS Inform — the citizen-facing health information service published
53
+ * by NHS 24 at nhsinform.scot. Uses the deeper navy plus a bright NHS
54
+ * cyan for links and signposting.
55
+ * 3. NHS 24 service brand (111, Scotland's telehealth line) — a darker,
56
+ * more clinical reading of the same palette with magenta/violet
57
+ * callouts reserved for safety messaging.
58
+ *
59
+ * Upstream tracked: NHS.UK frontend v9.x visual language (v10 is in
60
+ * pre-release; re-check the palette when it goes stable). Independent
61
+ * implementation against Lily class hooks; not affiliated with or
62
+ * endorsed by the NHS.
63
+ *
64
+ * Canonical references
65
+ * - NHS Scotland Identity Standards (Scottish Government CMO Directorate):
66
+ * https://www.publications.scot.nhs.uk/
67
+ * - NHS Inform (citizen-facing health information):
68
+ * https://www.nhsinform.scot/
69
+ * - NHS 24 (telehealth / 111):
70
+ * https://www.nhs24.scot/
71
+ * - Scottish Government Design System (sibling system, shares typography
72
+ * conventions and accessibility targets):
73
+ * https://designsystem.gov.scot/
74
+ * - Digital Scotland Service Standard (legal accessibility baseline):
75
+ * https://www.gov.scot/publications/digital-scotland-service-standard/
76
+ * - Typography: Frutiger W01 (primary, shared with NHS UK), Roboto for
77
+ * NHS Inform body copy, Arial / system-ui as the universal fallback.
78
+ * - Base font size: 16px; modular scale matches NHS UK (14/16/19/22/26/
79
+ * 32/48/64) so components ported from the NHS UK frontend "just work".
80
+ * - Spacing scale (px): 0, 4, 8, 12, 16, 24, 32, 40, 48, 64 — identical
81
+ * to NHS UK frontend's `$nhsuk-spacing-responsive-scale`.
82
+ * - WCAG 2.2 AA mandated by the Digital Scotland Service Standard;
83
+ * Lily targets AAA. The focus indicator below clears AAA at 3:1
84
+ * against every surface in the theme.
85
+ *
86
+ * Brand palette (sRGB swatches → OKLCH equivalents used in the theme)
87
+ * NHS Scotland Navy #003087 primary brand colour (Pantone 287)
88
+ * The flagship NHS Scotland blue.
89
+ * Darker than NHS England's #005EB8;
90
+ * this is the single most distinctive
91
+ * visual difference between the two
92
+ * nations' systems.
93
+ * NHS Scotland Mid Blue #0078D4 primary action / link colour
94
+ * (NHS Inform's link blue, also used
95
+ * on NHS24 service buttons).
96
+ * NHS Bright Blue #0072CE informational accents, shared with
97
+ * the NHS UK identity for cross-border
98
+ * services and chips/badges.
99
+ * NHS Light Blue #41B6E6 illustrative accent, infographics,
100
+ * chart palette.
101
+ * NHS Aqua / Teal #00A499 supplementary accent (NHS24 chrome).
102
+ * NHS Scotland Magenta #AE2573 safety-critical callouts used by
103
+ * NHS 24 for "phone 999" messaging.
104
+ * Not exposed as a Lily role; cited
105
+ * here for completeness only.
106
+ * NHS Black #212B32 primary text on white surfaces.
107
+ * NHS Dark Grey #425563 secondary text, captions.
108
+ * NHS Mid Grey #768692 tertiary text, dividers, default
109
+ * border colour on form fields.
110
+ * NHS Pale Grey #E8EDEE sectioned surfaces, summary panels,
111
+ * table zebra striping.
112
+ * NHS Inform Off-White #F0F4F5 page wash used by nhsinform.scot
113
+ * behind the main column on wider
114
+ * viewports (slightly cooler than
115
+ * NHS UK's pale grey).
116
+ * NHS White #FFFFFF default page background.
117
+ * NHS Warm Yellow #FFB81C warning banners, also the canonical
118
+ * focus indicator across NHS UK / NHS
119
+ * Scotland / NHS Wales — a deliberate
120
+ * cross-system commitment so keyboard
121
+ * users recognise focus everywhere.
122
+ * NHS Emergency Red #DA291C error / destructive action, also
123
+ * the colour used on the "999" badge
124
+ * inside NHS Inform.
125
+ * NHS Green #009639 success / confirmation.
126
+ * NHS Dark Green #006747 success hover / pressed state.
127
+ *
128
+ * Geometry rules (shared with NHS UK frontend)
129
+ * - Border radius: 4px on cards, inputs, and buttons. Selectors
130
+ * (checkbox/radio) are square (0px) to keep the tick mark perfectly
131
+ * centred — a NHS frontend convention preserved here.
132
+ * - Borders: 1px solid hairline, drawn in NHS Mid Grey #768692.
133
+ * - Surfaces are flat: no shadow, no gradient (depth = 0). NHS Scotland
134
+ * identity guidance discourages skeuomorphism so clinical content
135
+ * reads calmly on low-contrast and reduced-vision displays.
136
+ * - No textures or noise (noise = 0).
137
+ *
138
+ * Accessibility commitments encoded in this theme
139
+ * - All foreground/background pairs target WCAG AA at minimum; the
140
+ * primary / secondary / info / success / error swatches reach AAA
141
+ * when paired with the matching *-content colour below.
142
+ * - Warning content is dark (≈ NHS Black) so #FFB81C yellow stays
143
+ * legible — yellow-on-white would fall below AA.
144
+ * - Focus ring (rendered by the example apps' CSS, not declared here)
145
+ * uses NHS Warm Yellow #FFB81C over NHS Black #212B32 to guarantee
146
+ * visibility on every theme surface including primary and error.
147
+ * - Colour is never the sole carrier of meaning (WCAG 1.4.1) — every
148
+ * status component pairs colour with an icon, text label, or ARIA
149
+ * attribute per AGENTS/accessibility.md.
150
+ *
151
+ * Relationship to sibling themes
152
+ * - united-kingdom-national-health-service-england.css — same modular
153
+ * scale and geometry; primary is lighter (#005EB8 vs #003087).
154
+ * - united-kingdom-national-health-service-wales.css — shares yellow,
155
+ * red, and green roles; primary leans toward GIG Cymru's deeper navy.
156
+ * Components are visually portable across the three nations: only the
157
+ * primary / secondary / accent tokens differ, never the geometry or
158
+ * the safety palette (warning / error / success).
159
+ * ========================================================================= */
160
+
161
+ /* Theme tokens — the colour, radius, border, and depth variables consumed
162
+ * by every Lily component rule below. The theme applies by default (`:root`)
163
+ * and also when explicitly opted into via `<html data-theme="united-kingdom-national-health-service-scotland-for-practitioners">`,
164
+ * so multiple themes can coexist in one document and be switched at runtime
165
+ * by updating the `data-theme` attribute. */
166
+
167
+ :where(:root, :root[data-theme="united-kingdom-national-health-service-scotland-for-practitioners"]) {
168
+ color-scheme: light;
169
+
170
+ /* ---- Surfaces
171
+ -------------------------------------------------------- */
172
+ /* base-100: page background. NHS White #FFFFFF — the
173
+ canvas behind */ /* body copy, primary content cards, and
174
+ dialog modals. */
175
+ --color-base-100: oklch(100% 0 0);
176
+ /* base-200: secondary surface. NHS Inform Off-White #F0F4F5 —
177
+ used */ /* on nhsinform.scot for the page wash behind the
178
+ main column at */ /* wide breakpoints, and for summary /
179
+ "at a glance" panels. */
180
+ --color-base-200: oklch(96% 0.008 220);
181
+ /* base-300: tertiary surface. NHS Pale Grey #E8EDEE — divider
182
+ rows, */ /* table zebra striping, list-row hover, and the
183
+ "grey panel" used */ /* for sectioned navigation in NHS 24
184
+ service journeys. */
185
+ --color-base-300: oklch(92% 0.01 220);
186
+ /* base-content: primary text. NHS Black #212B32 — warm near-
187
+ black */ /* tuned to reduce on-screen glare for users
188
+ reading long-form */ /* clinical content (a shared NHS UK /
189
+ NHS Scotland choice). */
190
+ --color-base-content: oklch(26% 0.015 230);
191
+ /* ---- Primary (NHS Scotland Navy)
192
+ ------------------------------------ */ /* NHS Scotland
193
+ Navy #003087 (Pantone 287) — the flagship brand */ /*
194
+ colour. Darker than NHS England's #005EB8: this is the
195
+ single */ /* clearest visual signal that a service belongs
196
+ to NHS Scotland */ /* rather than NHS England. Used on the
197
+ NHS Scotland logo block, */ /* primary buttons, the global
198
+ header band on nhsinform.scot, and */ /* the active state
199
+ of primary nav. */
200
+ --color-primary: oklch(32% 0.18 264);
201
+ --color-primary-content: oklch(100% 0 0);
202
+ /* ---- Secondary (NHS Scotland Mid Blue)
203
+ ------------------------------ */ /* NHS Scotland Mid Blue
204
+ #0078D4 — the link colour on nhsinform.scot */ /* and the
205
+ secondary-button fill on NHS 24 journeys. Sits between */
206
+ /* the navy primary and the bright accent so layered
207
+ buttons keep a */ /* clear hierarchy without losing the NHS
208
+ Scotland identity. */
209
+ --color-secondary: oklch(52% 0.18 248);
210
+ --color-secondary-content: oklch(100% 0 0);
211
+ /* ---- Accent (NHS Aqua / Teal)
212
+ --------------------------------------- */ /* NHS Aqua /
213
+ Teal #00A499 — supplementary accent associated with */ /*
214
+ NHS 24 chrome and used for illustrative chart series. Not
215
+ for */ /* primary calls to action; reserve for decorative
216
+ emphasis, */ /* category chips, and data visualisation.
217
+ Darkened from L=0.62 on 2026-08-27: white accent-content
218
+ needs >=4.5:1 for small text (axe color-contrast on ai-
219
+ label). */
220
+ --color-accent: oklch(52% 0.11 192);
221
+ --color-accent-content: oklch(100% 0 0);
222
+ /* ---- Neutral
223
+ -------------------------------------------------------- */
224
+ /* Neutral chrome (dark footer, NHS 24 dark cards, on-call
225
+ signage). */ /* Mirrors base-content so dark blocks share
226
+ the same warm near-black */ /* identity as body text,
227
+ keeping the page visually coherent. */
228
+ --color-neutral: oklch(26% 0.015 230);
229
+ --color-neutral-content: oklch(100% 0 0);
230
+ /* ---- Info (NHS Bright Blue)
231
+ ----------------------------------------- */ /* NHS Bright
232
+ Blue #0072CE — informational banners ("did you know?", */
233
+ /* "your appointment is at…"), inline help, and link chips.
234
+ Distinct */ /* from primary so message banners read as
235
+ messages, not actions. */
236
+ --color-info: oklch(54% 0.17 247);
237
+ --color-info-content: oklch(100% 0 0);
238
+ /* ---- Success (NHS Green)
239
+ -------------------------------------------- */ /* NHS
240
+ Green #009639 — confirmation panels, completed-step
241
+ indicators */ /* ("you have been booked"), and the success
242
+ state on form summaries. */ /* Identical role and contrast
243
+ target as the NHS UK system so shared */ /* components
244
+ carry the same meaning across the border. */
245
+ --color-success: oklch(56% 0.16 145);
246
+ --color-success-content: oklch(100% 0 0);
247
+ /* ---- Warning (NHS Warm Yellow)
248
+ -------------------------------------- */ /* NHS Warm
249
+ Yellow #FFB81C — caution banners and the canonical focus */
250
+ /* indicator. Yellow swatches are light, so the content
251
+ colour MUST */ /* stay dark (≈ NHS Black) to clear WCAG AA
252
+ contrast. Reusing this */ /* hue across NHS England, NHS
253
+ Scotland, and NHS Wales is deliberate: */ /* keyboard users
254
+ build muscle memory for "yellow means focus". */
255
+ --color-warning: oklch(82% 0.16 79);
256
+ --color-warning-content: oklch(26% 0.015 230);
257
+ /* ---- Error (NHS Emergency Services Red)
258
+ ----------------------------- */ /* NHS Emergency Services
259
+ Red #DA291C — validation errors, */ /* destructive actions,
260
+ "phone 999" badges inside NHS Inform. */ /* Reserved for
261
+ genuine errors and safety messages: the colour */ /*
262
+ carries strong NHS meaning across both nations and must not
263
+ be */ /* used for decorative emphasis. */
264
+ --color-error: oklch(55% 0.22 28);
265
+ --color-error-content: oklch(100% 0 0);
266
+ /* ---- Geometry tokens (clinical / high-density)
267
+ --------------------- */ /* Selectors (checkbox / radio):
268
+ square per NHS frontend defaults so */ /* the tick / dot
269
+ stays perfectly centred at every font size. Square */ /*
270
+ selectors are doubly appropriate for clinical UI where
271
+ checklist */ /* grids dominate (allergies, observations,
272
+ problem-list flags). */
273
+ --radius-selector: 0rem;
274
+ /* Form fields: 2px corners — sharper than the 4px patient
275
+ theme. */ /* Clinical instruments traditionally use crisp
276
+ rectangles; the small */ /* radius keeps a hint of softness
277
+ so the UI doesn't read as hostile */ /* while still
278
+ aligning more closely to EPR conventions. */
279
+ --radius-field: 0.125rem;
280
+ /* Card / panel / dialog: 2px corner radius. Tight corners
281
+ keep */ /* clinical worklists, observation tables, and
282
+ side-panel cards */ /* visually instrument-like and align
283
+ edges to a 2px grid. */
284
+ --radius-box: 0.125rem;
285
+ /* Inner padding hints for selectors and fields — compressed
286
+ so */ /* checklist rows and field grids pack tightly. */
287
+ --size-selector: 0.125rem;
288
+ --size-field: 0.125rem;
289
+ /* Border width: 1px hairline, matching `$nhsuk-border-width-
290
+ form`. */ /* The hairline is the smallest legible border on
291
+ the LCDs used in */ /* clinical settings; going to 0.5px
292
+ loses the edge on non-Retina */ /* workstation displays
293
+ still common in NHS estate. */
294
+ --border: 1px;
295
+ /* Depth & noise off — NHS Scotland identity guidance, like
296
+ NHS UK, */ /* avoids shadows, gradients, and textures so
297
+ the interface looks */ /* clinical, calm, and easy to read
298
+ on low-end devices common in */ /* community health
299
+ settings. */
300
+ --depth: 0;
301
+ --noise: 0;
302
+ }
303
+
304
+ /* ----------------------------------------------------------------------------
305
+ * Lily component styling — applied automatically once this theme stylesheet
306
+ * is loaded. Every Lily class hook (492 components) consumes the theme tokens
307
+ * declared above, so no other stylesheet is required for the theme to render.
308
+ *
309
+ * Rules live in @layer lily with :where() selectors so the specificity stays
310
+ * at 0 — application CSS always wins. To replace a Lily component's default
311
+ * look, declare a higher-specificity rule (or a later @layer) in your own
312
+ * stylesheet.
313
+ * -------------------------------------------------------------------------- */
314
+
315
+ @layer lily {
316
+ /* ==========================================================================
317
+ * Tokens & primitives
318
+ * --------------------------------------------------------------------------
319
+ * Lily exposes a small, stable set of derived tokens (the `--lily-*`
320
+ * family) layered on top of the upstream theme primitives (the
321
+ * `--color-*`, `--radius-*`, `--border`, `--depth`, `--noise` family).
322
+ *
323
+ * Component rules consume only the `--lily-*` tokens. That indirection
324
+ * means a consumer can swap upstream themes (or invent a new one) without
325
+ * touching component CSS — the component rules below remain stable
326
+ * because their inputs do.
327
+ * ====================================================================== */
328
+
329
+ :where(:root) {
330
+ /* --- Surfaces ------------------------------------------------------ */
331
+ /* Default page surface — the canvas behind body copy. */
332
+ --lily-surface: var(--color-base-100);
333
+ /* Secondary surface — section panels, sidebars, table headers,
334
+ muted callouts. One step away from the page background. */
335
+ --lily-surface-alt: var(--color-base-200);
336
+ /* Sunken surface — used behind progress tracks, skeletons, and
337
+ inset wells. Suggests depth without requiring a shadow. */
338
+ --lily-surface-sunken: var(--color-base-300);
339
+
340
+ /* --- Text ---------------------------------------------------------- */
341
+ /* Primary body-copy colour. Maps to the theme's high-contrast
342
+ reading colour. */
343
+ --lily-text: var(--color-base-content);
344
+ /* Muted / secondary text — mixed 70% with transparent to keep the
345
+ hue but lower contrast for captions, helptext, placeholders. */
346
+ --lily-text-muted: color-mix(
347
+ in oklch,
348
+ var(--color-base-content) 70%,
349
+ transparent
350
+ );
351
+
352
+ /* --- Borders ------------------------------------------------------- */
353
+ /* Default border — 20% blend with body content colour, low-contrast,
354
+ used for subtle dividers between content blocks. */
355
+ --lily-border: color-mix(
356
+ in oklch,
357
+ var(--color-base-content) 20%,
358
+ transparent
359
+ );
360
+ /* Higher-contrast border — 40% blend, used on inputs, buttons, and
361
+ anywhere a clear edge is required (WCAG non-text contrast). */
362
+ --lily-border-strong: color-mix(
363
+ in oklch,
364
+ var(--color-base-content) 40%,
365
+ transparent
366
+ );
367
+
368
+ /* --- Focus ring ---------------------------------------------------- */
369
+ /* Outline colour for `:focus-visible`. Always the primary brand
370
+ colour so focus is visually tied to interactive intent. */
371
+ --lily-focus-ring: var(--color-primary);
372
+
373
+ /* --- Radii --------------------------------------------------------- */
374
+ /* Three tiers of corner rounding. `sm` for chips/badges,
375
+ `md` for inputs/buttons/cards, `lg` for dialogs/heroes. */
376
+ --lily-radius-sm: var(--radius-selector);
377
+ --lily-radius-md: var(--radius-field);
378
+ --lily-radius-lg: var(--radius-box);
379
+
380
+ /* --- Spacing scale (clinical / high-density) ----------------------- */
381
+ /* Five-step spacing scale used by every gap, padding, and margin.
382
+ Roughly half the patient theme's values so clinical worklists,
383
+ observation grids, and form-dense panels pack tightly while
384
+ still reserving enough whitespace for keyboard navigation and
385
+ focus rings to remain visually distinct. */
386
+ --lily-space-xs: 0.125rem; /* hairline gaps inside tables/badges */
387
+ --lily-space-sm: 0.25rem; /* default inline gap */
388
+ --lily-space-md: 0.5rem; /* default block padding */
389
+ --lily-space-lg: 0.75rem; /* card / dialog padding */
390
+ --lily-space-xl: 1rem; /* page-level rhythm */
391
+
392
+ /* --- Typography ---------------------------------------------------- */
393
+ /* Body font stack — system fonts first so consumers get a
394
+ native-feeling baseline without shipping a web font. */
395
+ --lily-font-body:
396
+ system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
397
+ Arial, sans-serif;
398
+ /* Monospaced font stack — used by code, kbd, identifiers, and
399
+ anywhere alignment matters more than reading speed. */
400
+ --lily-font-mono:
401
+ ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
402
+ "Liberation Mono", monospace;
403
+
404
+ /* --- Shadows ------------------------------------------------------- */
405
+ /* Three depth tiers. The shadow colour is derived from the body
406
+ content token so it adapts to light/dark themes automatically. */
407
+ --lily-shadow-sm: 0 1px 2px
408
+ color-mix(in oklch, var(--color-base-content) 8%, transparent);
409
+ --lily-shadow-md: 0 4px 12px
410
+ color-mix(in oklch, var(--color-base-content) 12%, transparent);
411
+ --lily-shadow-lg: 0 12px 32px
412
+ color-mix(in oklch, var(--color-base-content) 16%, transparent);
413
+ }
414
+
415
+ /* Body typography — clinical density. 14px base body copy + 1.35
416
+ line-height matches the data-dense panels of EMIS, SystmOne,
417
+ Vision, EPIC, and Cerner. Setting `font-size` on the `body`
418
+ element (not the root) keeps `rem` anchored to the browser's
419
+ 16px so layout dimensions tied to `rem` (paddings, min-heights,
420
+ focus offsets) stay predictable; only inherited text-sized
421
+ declarations shrink. Consumers needing larger text override
422
+ `body { font-size }` in their own stylesheet. */
423
+ :where(body) {
424
+ font-size: 0.875rem;
425
+ line-height: 1.35;
426
+ font-family: var(--lily-font-body);
427
+ color: var(--lily-text);
428
+ background-color: var(--lily-surface);
429
+ }
430
+
431
+ /* Focus ring — every interactive Lily class hook gets the same visible
432
+ focus indicator. Centralising the selector means a consumer can
433
+ restyle every focus state at once by overriding `--lily-focus-ring`
434
+ (or this rule) rather than chasing 100+ component selectors. */
435
+ :where(
436
+ [class$="-input"],
437
+ [class$="-button"],
438
+ [class$="-link"],
439
+ [class$="-list-item"],
440
+ [class$="-option"],
441
+ [class$="-picker-button"],
442
+ .button,
443
+ .input,
444
+ .select,
445
+ .option,
446
+ .menu-item,
447
+ .tab-bar-button,
448
+ .switch-button,
449
+ .toggle-button,
450
+ .checkbox-input,
451
+ .radio-input,
452
+ .file-upload,
453
+ .signature-pad,
454
+ .editable
455
+ ):focus-visible {
456
+ outline: 2px solid var(--lily-focus-ring);
457
+ outline-offset: 2px;
458
+ }
459
+
460
+ /* ==========================================================================
461
+ * Inputs
462
+ * --------------------------------------------------------------------------
463
+ * Every Lily class ending in `-input` (textual, date, picker, range, file,
464
+ * national-identifier, etc.) shares the same baseline styling. The
465
+ * `[class$="-input"]` attribute selector catches single-class usage
466
+ * (`class="email-input"`); consumers needing multi-class composition can
467
+ * override with higher-specificity rules in their own stylesheet.
468
+ *
469
+ * The `:not([class$="-with-mask-input"])` exclusion exists because the
470
+ * masked-input variant carries its own typography (monospaced + letter
471
+ * spacing) and we don't want the generic baseline overriding it.
472
+ * Hidden inputs (`type="hidden"`) are also excluded — they must not be
473
+ * laid out as visible inputs.
474
+ * ====================================================================== */
475
+
476
+ /* Baseline visual treatment for every textual input. */
477
+ :where(
478
+ [class$="-input"]:not([class$="-with-mask-input"]):not([type="hidden"]),
479
+ .input
480
+ ) {
481
+ appearance: none; /* drop UA native chrome (Safari especially) */
482
+ display: inline-block;
483
+ width: 100%; /* fill parent by default; consumer caps via container */
484
+ max-width: 100%;
485
+ /* Clinical density: 1.75rem (~28px) min-height clears WCAG 2.5.8
486
+ Minimum (24px) while letting EPR-style worklists pack ~25 rows
487
+ per 1080px viewport instead of the patient theme's ~17. The
488
+ shorter row is appropriate for mouse + keyboard precision. */
489
+ min-height: 1.75rem;
490
+ padding: 0.125rem 0.375rem;
491
+ font: inherit;
492
+ font-family: var(--lily-font-body);
493
+ color: var(--lily-text);
494
+ background-color: var(--lily-surface);
495
+ border: var(--border) solid var(--lily-border-strong);
496
+ border-radius: var(--lily-radius-md);
497
+ box-sizing: border-box;
498
+ /* Smooth focus/hover edge transitions — short enough to feel
499
+ responsive, long enough to read as intentional. */
500
+ transition:
501
+ border-color 120ms ease,
502
+ box-shadow 120ms ease;
503
+ }
504
+
505
+ /* Hover affordance — switches the border to the primary brand colour so
506
+ users get a visual cue that the field is interactive. The `:not(:disabled)`
507
+ guard prevents the cue from appearing on locked-out controls. */
508
+ :where([class$="-input"], .input):hover:not(:disabled) {
509
+ border-color: var(--color-primary);
510
+ }
511
+
512
+ /* Disabled / aria-disabled — desaturate and shift to the muted surface so
513
+ the control reads as inactive without losing its layout footprint. */
514
+ :where([class$="-input"], .input):disabled,
515
+ :where([class$="-input"], .input)[aria-disabled="true"] {
516
+ color: var(--lily-text-muted);
517
+ background-color: var(--lily-surface-alt);
518
+ cursor: not-allowed;
519
+ }
520
+
521
+ /* Invalid input — red border. The `aria-invalid="true"` attribute is the
522
+ ARIA contract; pairing it with a colour change keeps WCAG 1.4.1
523
+ (no colour-only meaning) honest because the ARIA state is also there. */
524
+ :where([class$="-input"], .input)[aria-invalid="true"] {
525
+ border-color: var(--color-error);
526
+ }
527
+
528
+ /* Placeholder — match the muted text token. `opacity: 1` overrides Firefox's
529
+ legacy default placeholder opacity, which would otherwise compound on
530
+ our already-muted colour. */
531
+ :where([class$="-input"], .input)::placeholder {
532
+ color: var(--lily-text-muted);
533
+ opacity: 1;
534
+ }
535
+
536
+ /* --- Input type-specific tweaks ----------------------------------------- */
537
+
538
+ /* Hidden inputs must never render. */
539
+ :where(.hidden-input) {
540
+ display: none;
541
+ }
542
+
543
+ /* Submit / reset / image / generic button inputs render as buttons. */
544
+ :where(.button-input, .submit-input, .reset-input, .image-input) {
545
+ width: auto;
546
+ min-width: 1.75rem;
547
+ background-color: var(--color-primary);
548
+ color: var(--color-primary-content);
549
+ border-color: transparent;
550
+ cursor: pointer;
551
+ font-weight: 600;
552
+ }
553
+
554
+ /* Reset input is the secondary-action variant — muted surface, not the
555
+ primary brand colour, because "reset" is a destructive op. */
556
+ :where(.reset-input) {
557
+ background-color: var(--lily-surface-alt);
558
+ color: var(--lily-text);
559
+ border-color: var(--lily-border-strong);
560
+ }
561
+
562
+ /* Color input — narrow swatch with internal padding so the colour chip
563
+ reads cleanly inside the input frame. */
564
+ :where(.color-input) {
565
+ min-height: 1.75rem;
566
+ padding: 0.125rem;
567
+ width: 2.25rem;
568
+ cursor: pointer;
569
+ }
570
+
571
+ /* Range / angle slider — strip the input chrome so the consumer can
572
+ restyle the track. `accent-color` provides a minimal styled fallback. */
573
+ :where(.range-input, .angle-slider-range-input) {
574
+ appearance: none;
575
+ background: transparent;
576
+ border: 0;
577
+ padding: 0;
578
+ height: 1.5rem;
579
+ accent-color: var(--color-primary);
580
+ }
581
+
582
+ /* File inputs — slightly tighter padding because the UA-rendered
583
+ "Choose file" button consumes most of the visual space. */
584
+ :where(.file-input, .image-file-input) {
585
+ padding: 0.375rem 0.5rem;
586
+ cursor: pointer;
587
+ }
588
+
589
+ /* Checkbox / radio — small square/round controls with the brand accent. */
590
+ :where(.checkbox-input, .radio-input) {
591
+ width: 1.125rem;
592
+ height: 1.125rem;
593
+ min-height: 0; /* override the 2.5rem input baseline */
594
+ padding: 0;
595
+ accent-color: var(--color-primary);
596
+ cursor: pointer;
597
+ }
598
+ :where(.radio-input) {
599
+ border-radius: 50%;
600
+ }
601
+ :where(.checkbox-input) {
602
+ border-radius: var(--lily-radius-sm);
603
+ }
604
+
605
+ /* Textarea variants — taller minimum, line-height tuned for reading,
606
+ vertical-only resize so layout columns aren't disturbed. */
607
+ :where(.text-area-input, .text-area-input-with-character-counter) {
608
+ min-height: 6rem;
609
+ line-height: 1.5;
610
+ resize: vertical;
611
+ }
612
+ /* Character-counter variant stacks the textarea and the counter span. */
613
+ :where(.text-area-input-with-character-counter) {
614
+ display: flex;
615
+ flex-direction: column;
616
+ }
617
+
618
+ /* --- Composite inputs with built-in DOM structure ----------------------- */
619
+
620
+ /* PIN input — N small slots side-by-side. The wrapper is invisible;
621
+ the children carry the styling. */
622
+ :where(.pin-input-div) {
623
+ display: inline-flex;
624
+ gap: 0.5rem;
625
+ padding: 0;
626
+ background: transparent;
627
+ border: 0;
628
+ width: auto;
629
+ min-height: 0;
630
+ }
631
+ :where(.pin-input-div > input) {
632
+ width: 2.5rem;
633
+ height: 3rem;
634
+ padding: 0;
635
+ text-align: center;
636
+ font-size: 1.125rem;
637
+ font-weight: 600;
638
+ background-color: var(--lily-surface);
639
+ border: var(--border) solid var(--lily-border-strong);
640
+ border-radius: var(--lily-radius-md);
641
+ color: var(--lily-text);
642
+ }
643
+
644
+ /* Password-toggle composite — text input + reveal button, sharing
645
+ a single visual frame via flush radii. */
646
+ :where(.password-input-or-text-input-div) {
647
+ display: inline-flex;
648
+ align-items: stretch;
649
+ width: 100%;
650
+ background: transparent;
651
+ border: 0;
652
+ padding: 0;
653
+ min-height: 0;
654
+ }
655
+ :where(.password-input-or-text-input-div > input) {
656
+ flex: 1;
657
+ min-height: 1.75rem;
658
+ padding: 0.125rem 0.375rem;
659
+ background-color: var(--lily-surface);
660
+ color: var(--lily-text);
661
+ border: var(--border) solid var(--lily-border-strong);
662
+ border-right: 0; /* drop the seam where input meets button */
663
+ border-radius: var(--lily-radius-md) 0 0 var(--lily-radius-md);
664
+ }
665
+ :where(.password-input-or-text-input-div > button) {
666
+ padding: 0 0.75rem;
667
+ background-color: var(--lily-surface-alt);
668
+ color: var(--lily-text);
669
+ border: var(--border) solid var(--lily-border-strong);
670
+ border-radius: 0 var(--lily-radius-md) var(--lily-radius-md) 0;
671
+ cursor: pointer;
672
+ }
673
+
674
+ /* --- Input compositions ------------------------------------------------- */
675
+
676
+ /* Input group — flush-joined controls (input + button, etc.). The negative
677
+ margin collapses overlapping borders into a single shared edge. */
678
+ :where(.input-group) {
679
+ display: inline-flex;
680
+ align-items: stretch;
681
+ width: 100%;
682
+ }
683
+ :where(.input-group > *) {
684
+ border-radius: 0;
685
+ margin-left: calc(-1 * var(--border));
686
+ }
687
+ :where(.input-group > *:first-child) {
688
+ border-top-left-radius: var(--lily-radius-md);
689
+ border-bottom-left-radius: var(--lily-radius-md);
690
+ margin-left: 0;
691
+ }
692
+ :where(.input-group > *:last-child) {
693
+ border-top-right-radius: var(--lily-radius-md);
694
+ border-bottom-right-radius: var(--lily-radius-md);
695
+ }
696
+
697
+ /* Masked input — monospaced font + slight letter-spacing so the mask
698
+ template (phone, SSN, etc.) aligns over the typed characters. */
699
+ :where(.input-with-mask) {
700
+ font-family: var(--lily-font-mono);
701
+ letter-spacing: 0.05em;
702
+ }
703
+
704
+ /* Search input — reserve space on the inline-start for the magnifier
705
+ glyph. The `linear-gradient(transparent, transparent)` is a no-op
706
+ background-image; it ensures the property exists so consumers can
707
+ layer an SVG icon over it by overriding `background-image`. */
708
+ :where(.text-input-with-search, .search-input) {
709
+ background-image: linear-gradient(transparent, transparent);
710
+ padding-inline-start: 2.25rem;
711
+ }
712
+
713
+ /* ==========================================================================
714
+ * Views
715
+ * --------------------------------------------------------------------------
716
+ * Read-only display chips for identifiers, dates, ratings, and other
717
+ * pre-formatted data. Every Lily class ending in `-view` is a span/div
718
+ * that renders a value the user shouldn't edit.
719
+ *
720
+ * The exclusions (`theme-view`, `split-view`)
721
+ * are larger containers that don't fit the chip pattern — they're
722
+ * styled separately further down.
723
+ * ====================================================================== */
724
+
725
+ :where(
726
+ [class$="-view"]:not(.theme-view):not(
727
+ .split-view
728
+ )
729
+ ) {
730
+ display: inline-block;
731
+ padding: 0.125rem 0.5rem;
732
+ font-family: var(--lily-font-mono); /* fixed-width for alignment */
733
+ color: var(--lily-text);
734
+ background-color: var(--lily-surface-alt);
735
+ border: var(--border) solid var(--lily-border);
736
+ border-radius: var(--lily-radius-md);
737
+ line-height: 1.4;
738
+ }
739
+
740
+ /* Dates / ranges are human-readable, not codes — switch back to body font. */
741
+ :where(.date-time-view, .date-range, .review-date) {
742
+ font-family: var(--lily-font-body);
743
+ }
744
+
745
+ /* Theme-view — small label + colour swatch composition. */
746
+ :where(.theme-view) {
747
+ display: inline-flex;
748
+ align-items: center;
749
+ gap: 0.5rem;
750
+ padding: 0.25rem 0.5rem;
751
+ color: var(--lily-text);
752
+ }
753
+
754
+ /* ==========================================================================
755
+ * Buttons
756
+ * --------------------------------------------------------------------------
757
+ * Every class ending in `-button` and the bare `.button` share one
758
+ * baseline. Hamburger-menu is excluded because it's an icon-only square
759
+ * with its own minimal frame styling (further down).
760
+ * ====================================================================== */
761
+
762
+ /* Baseline button — primary-coloured pill. Variants override colour or
763
+ shape below. Clinical density: shorter, tighter padding for toolbar
764
+ rows and side-panel actions; gap drops because labels are usually
765
+ icons or short verbs ("Save", "Sign", "Print", "Refer"). */
766
+ :where(.button, [class$="-button"]:not(.hamburger-menu-button)) {
767
+ appearance: none;
768
+ display: inline-flex;
769
+ align-items: center;
770
+ justify-content: center;
771
+ gap: 0.25rem; /* icon + short label */
772
+ min-height: 1.75rem; /* WCAG 2.5.8 (24px) cleared by 4px */
773
+ padding: 0.125rem 0.5rem;
774
+ font: inherit;
775
+ font-family: var(--lily-font-body);
776
+ font-weight: 600;
777
+ line-height: 1.2;
778
+ color: var(--color-primary-content);
779
+ background-color: var(--color-primary);
780
+ border: var(--border) solid transparent;
781
+ border-radius: var(--lily-radius-md);
782
+ cursor: pointer;
783
+ user-select: none; /* prevent text drag-select on double-click */
784
+ text-decoration: none;
785
+ transition:
786
+ background-color 120ms ease,
787
+ color 120ms ease,
788
+ border-color 120ms ease;
789
+ }
790
+
791
+ /* Hover — shift to the secondary token for a subtle change of state. */
792
+ :where(.button, [class$="-button"]):hover:not(:disabled):not(
793
+ [aria-disabled="true"]
794
+ ) {
795
+ background-color: var(--color-secondary);
796
+ color: var(--color-secondary-content);
797
+ }
798
+
799
+ /* Disabled — half opacity, not-allowed cursor. */
800
+ :where(.button, [class$="-button"]):disabled,
801
+ :where(.button, [class$="-button"])[aria-disabled="true"] {
802
+ opacity: 0.5;
803
+ cursor: not-allowed;
804
+ }
805
+
806
+ /* Pressed (toggle/switch active state) — re-use the secondary colour. */
807
+ :where(.button, [class$="-button"])[aria-pressed="true"] {
808
+ background-color: var(--color-secondary);
809
+ color: var(--color-secondary-content);
810
+ }
811
+
812
+ /* --- Button variants ---------------------------------------------------- */
813
+
814
+ /* Icon button — square; aspect-ratio keeps it from squashing when the
815
+ icon font changes size. Compact for toolbars dense with action verbs. */
816
+ :where(.icon-button) {
817
+ padding: 0.125rem;
818
+ min-width: 1.75rem;
819
+ aspect-ratio: 1;
820
+ }
821
+
822
+ /* Floating action button — pinned to the bottom-right of the viewport.
823
+ Uses logical inset properties so it flips correctly under RTL.
824
+ Smaller than the patient theme so it doesn't obscure clinical
825
+ worklist data when the FAB sits over a table. */
826
+ :where(.float-button) {
827
+ position: fixed;
828
+ inset-block-end: 0.5rem;
829
+ inset-inline-end: 0.5rem;
830
+ box-shadow: var(--lily-shadow-lg);
831
+ border-radius: 999px;
832
+ width: 2.5rem;
833
+ height: 2.5rem;
834
+ padding: 0;
835
+ }
836
+
837
+ /* Split button — extra padding-end for the dropdown caret. */
838
+ :where(.split-button) {
839
+ padding-inline-end: 0.5rem;
840
+ }
841
+
842
+ /* Download button — stacks the action label over a hint (filesize, etc.). */
843
+ :where(.download-button) {
844
+ flex-direction: column;
845
+ align-items: flex-start;
846
+ text-align: start;
847
+ }
848
+
849
+ /* Clipboard-copy — muted style because it's auxiliary, not primary. */
850
+ :where(.clipboard-copy-button) {
851
+ background-color: var(--lily-surface-alt);
852
+ color: var(--lily-text);
853
+ border-color: var(--lily-border-strong);
854
+ }
855
+
856
+ /* Switch button — iOS-style toggle pill. The track is the sunken
857
+ surface; the thumb is drawn by the consumer (or via ::before in
858
+ higher-specificity overrides). */
859
+ :where(.switch-button) {
860
+ position: relative;
861
+ width: 2.75rem;
862
+ min-width: 2.75rem;
863
+ height: 1.5rem;
864
+ min-height: 1.5rem;
865
+ padding: 0;
866
+ background-color: var(--lily-surface-sunken);
867
+ border-radius: 999px;
868
+ }
869
+ :where(
870
+ .switch-button[aria-pressed="true"],
871
+ .switch-button[aria-checked="true"]
872
+ ) {
873
+ background-color: var(--color-primary);
874
+ }
875
+
876
+ /* Toggle button — when un-pressed, render as a muted secondary chip. */
877
+ :where(.toggle-button[aria-pressed="false"]) {
878
+ background-color: var(--lily-surface-alt);
879
+ color: var(--lily-text);
880
+ border-color: var(--lily-border-strong);
881
+ }
882
+
883
+ /* Link-style "button" — for back/skip/go-to controls that look like
884
+ text links but use the button machinery for keyboard semantics. */
885
+ :where(
886
+ .back-link,
887
+ .skip-link,
888
+ .go-to-top,
889
+ .go-to-next-section,
890
+ .go-to-previous-section
891
+ ) {
892
+ background-color: transparent;
893
+ color: var(--color-primary);
894
+ border-color: transparent;
895
+ text-decoration: underline;
896
+ text-underline-offset: 0.2em;
897
+ }
898
+
899
+ /* Skip-link — visually hidden off-screen until focused, then snaps to the
900
+ top-left of the page. Required pattern for keyboard accessibility
901
+ (WCAG 2.4.1 bypass blocks). */
902
+ :where(.skip-link) {
903
+ position: absolute;
904
+ top: -3rem;
905
+ left: 0.5rem;
906
+ padding: 0.5rem 1rem;
907
+ background-color: var(--color-primary);
908
+ color: var(--color-primary-content);
909
+ text-decoration: none;
910
+ border-radius: var(--lily-radius-md);
911
+ z-index: 9999;
912
+ }
913
+ :where(.skip-link:focus) {
914
+ top: 0.5rem;
915
+ }
916
+
917
+ /* Button group — horizontal cluster of related actions. */
918
+ :where(.button-group) {
919
+ display: inline-flex;
920
+ gap: 0.125rem;
921
+ }
922
+
923
+ /* --- Bars of buttons (action / menu / tab / task / tool) ---------------- */
924
+
925
+ :where(.action-bar, .menu-bar, .tab-bar, .task-bar, .tool-bar) {
926
+ display: flex;
927
+ align-items: center;
928
+ gap: 0.125rem;
929
+ padding: 0.125rem 0.25rem;
930
+ background-color: var(--lily-surface-alt);
931
+ border: var(--border) solid var(--lily-border);
932
+ border-radius: var(--lily-radius-md);
933
+ }
934
+ /* Children of bars render flush with the bar — transparent background,
935
+ no border, so the bar's surface acts as the visual frame. */
936
+ :where(
937
+ .action-bar-button,
938
+ .menu-bar-button,
939
+ .tab-bar-button,
940
+ .task-bar-button,
941
+ .tool-bar-button
942
+ ) {
943
+ background-color: transparent;
944
+ color: var(--lily-text);
945
+ border-color: transparent;
946
+ }
947
+ /* Selected tab — explicit primary colour. */
948
+ :where(.tab-bar-button[aria-selected="true"]) {
949
+ background-color: var(--color-primary);
950
+ color: var(--color-primary-content);
951
+ }
952
+ /* Panel revealed by the active tab. */
953
+ :where(.tab-panel) {
954
+ padding: 0.5rem;
955
+ background-color: var(--lily-surface);
956
+ border: var(--border) solid var(--lily-border);
957
+ border-radius: var(--lily-radius-md);
958
+ }
959
+
960
+ /* ==========================================================================
961
+ * Links, anchors, link-like
962
+ * --------------------------------------------------------------------------
963
+ * Generic `-link` styling — primary-coloured underlined text. The
964
+ * exclusions (`skip-link`, `back-link`) are styled as buttons above.
965
+ * ====================================================================== */
966
+
967
+ :where([class$="-link"]:not(.skip-link):not(.back-link)) {
968
+ color: var(--color-primary);
969
+ text-decoration: underline;
970
+ text-underline-offset: 0.15em;
971
+ }
972
+ /* Hover — thicker underline for a subtle affordance cue. */
973
+ :where([class$="-link"]):hover {
974
+ text-decoration-thickness: 2px;
975
+ }
976
+ /* Visited — secondary token. */
977
+ :where([class$="-link"]):visited {
978
+ color: var(--color-secondary);
979
+ }
980
+
981
+ /* ==========================================================================
982
+ * Lists & list items
983
+ * --------------------------------------------------------------------------
984
+ * Lily wraps `<ol>` / `<ul>` in role-specific classes (CheckList, DoList,
985
+ * etc.). The baseline kills the default browser bullet styling and zeroes
986
+ * margins so consumers can compose lists inside other components.
987
+ * ====================================================================== */
988
+
989
+ /* Baseline — kill bullets and the indent gutter on every list class. */
990
+ :where(
991
+ .accordion-list,
992
+ .breadcrumb-list,
993
+ .chat-list,
994
+ .check-list,
995
+ .collection-list,
996
+ .contents-list,
997
+ .description-list,
998
+ .document-list,
999
+ .do-list,
1000
+ .dont-list,
1001
+ .icon-list,
1002
+ .pagination-list,
1003
+ .section-list,
1004
+ .step-list,
1005
+ .summary-list,
1006
+ .task-list,
1007
+ .timeline-list,
1008
+ .tour-list,
1009
+ .tree-list,
1010
+ .validation-list
1011
+ ) {
1012
+ list-style: none;
1013
+ margin: 0;
1014
+ padding: 0;
1015
+ }
1016
+
1017
+ /* Baseline — every list-item gets a thin hairline rule between rows.
1018
+ Clinical density: rows pack to 1.5x line-height so worklists and
1019
+ problem-list/medication-list panels show more entries at once. */
1020
+ :where(
1021
+ .accordion-list-item,
1022
+ .breadcrumb-list-item,
1023
+ .chat-list-item,
1024
+ .check-list-item,
1025
+ .collection-list-item,
1026
+ .contents-list-item,
1027
+ .description-list-item,
1028
+ .document-list-item,
1029
+ .do-list-item,
1030
+ .dont-list-item,
1031
+ .icon-list-item,
1032
+ .pagination-list-item,
1033
+ .section-list-item,
1034
+ .step-list-item,
1035
+ .summary-list-item,
1036
+ .task-list-item,
1037
+ .timeline-list-item,
1038
+ .tour-list-item,
1039
+ .tree-list-item,
1040
+ .validation-list-item
1041
+ ) {
1042
+ padding: 0.25rem 0;
1043
+ border-bottom: var(--border) solid var(--lily-border);
1044
+ }
1045
+
1046
+ /* --- Breadcrumb --------------------------------------------------------- */
1047
+ /* Horizontal list separated by a "/" pseudo-element between items. */
1048
+ :where(.breadcrumb-list) {
1049
+ display: flex;
1050
+ flex-wrap: wrap;
1051
+ gap: 0.5rem;
1052
+ align-items: center;
1053
+ }
1054
+ :where(.breadcrumb-list-item) {
1055
+ border: 0; /* override the generic list-item bottom-border */
1056
+ padding: 0;
1057
+ display: inline-flex;
1058
+ align-items: center;
1059
+ }
1060
+ /* Slash separator generated between adjacent items, not before the first. */
1061
+ :where(.breadcrumb-list-item + .breadcrumb-list-item)::before {
1062
+ content: "/";
1063
+ margin-inline-end: 0.5rem;
1064
+ color: var(--lily-text-muted);
1065
+ }
1066
+ /* Final crumb (the current page) — muted because it isn't a link. */
1067
+ :where(.breadcrumb-list-item[aria-current="page"]) {
1068
+ color: var(--lily-text-muted);
1069
+ }
1070
+
1071
+ /* --- Pagination --------------------------------------------------------- */
1072
+ :where(.pagination-list) {
1073
+ display: flex;
1074
+ flex-wrap: wrap;
1075
+ gap: 0.25rem;
1076
+ align-items: center;
1077
+ }
1078
+ :where(.pagination-list-item) {
1079
+ border: 0;
1080
+ padding: 0;
1081
+ }
1082
+ /* Pagination link — square-ish chip; smaller target for mouse-driven
1083
+ clinical workflows where rows of 50–100 results are common. */
1084
+ :where(.pagination-link) {
1085
+ display: inline-flex;
1086
+ align-items: center;
1087
+ justify-content: center;
1088
+ min-width: 1.75rem;
1089
+ padding: 0.125rem 0.375rem;
1090
+ border-radius: var(--lily-radius-md);
1091
+ text-decoration: none;
1092
+ }
1093
+ /* Current page — filled with the primary brand colour. */
1094
+ :where(.pagination-link[aria-current="page"]) {
1095
+ background-color: var(--color-primary);
1096
+ color: var(--color-primary-content);
1097
+ }
1098
+
1099
+ /* --- Marker glyphs ------------------------------------------------------ */
1100
+ /* Check / do — green ✓; dont — red ✗. The `::marker` pseudo-element
1101
+ only works on list-items with the default list-style display, so the
1102
+ generic baseline above intentionally kept that. */
1103
+ :where(.check-list-item)::marker {
1104
+ content: "✓ ";
1105
+ color: var(--color-success);
1106
+ }
1107
+ :where(.do-list-item)::marker {
1108
+ content: "✓ ";
1109
+ color: var(--color-success);
1110
+ }
1111
+ :where(.dont-list-item)::marker {
1112
+ content: "✗ ";
1113
+ color: var(--color-error);
1114
+ }
1115
+
1116
+ /* --- Tree --------------------------------------------------------------- */
1117
+ /* Nested tree lists indent their children. */
1118
+ :where(.tree-list .tree-list) {
1119
+ padding-inline-start: 1.25rem;
1120
+ }
1121
+ :where(.tree-list-item) {
1122
+ border: 0;
1123
+ }
1124
+
1125
+ /* --- Step list ---------------------------------------------------------- */
1126
+ /* CSS counter draws the step number in a circular badge. */
1127
+ :where(.step-list) {
1128
+ counter-reset: lily-step;
1129
+ }
1130
+ :where(.step-list-item) {
1131
+ counter-increment: lily-step;
1132
+ display: flex;
1133
+ align-items: flex-start;
1134
+ gap: 0.75rem;
1135
+ }
1136
+ :where(.step-list-item)::before {
1137
+ content: counter(lily-step);
1138
+ flex: 0 0 auto;
1139
+ display: inline-flex;
1140
+ align-items: center;
1141
+ justify-content: center;
1142
+ width: 1.75rem;
1143
+ height: 1.75rem;
1144
+ border-radius: 50%;
1145
+ background-color: var(--lily-surface-sunken);
1146
+ color: var(--lily-text);
1147
+ font-weight: 600;
1148
+ }
1149
+ /* Step status variants — colour-coded badge fill. */
1150
+ :where(.step-list-item[data-status="finished"])::before {
1151
+ background-color: var(--color-success);
1152
+ color: var(--color-success-content);
1153
+ }
1154
+ :where(.step-list-item[data-status="in-progress"])::before {
1155
+ background-color: var(--color-primary);
1156
+ color: var(--color-primary-content);
1157
+ }
1158
+ :where(.step-list-item[data-status="error"])::before {
1159
+ background-color: var(--color-error);
1160
+ color: var(--color-error-content);
1161
+ }
1162
+
1163
+ /* --- Task list ---------------------------------------------------------- */
1164
+ /* Flex row so the checkbox aligns with its label. */
1165
+ :where(.task-list-item) {
1166
+ display: flex;
1167
+ align-items: center;
1168
+ gap: 0.5rem;
1169
+ }
1170
+
1171
+ /* --- Timeline ----------------------------------------------------------- */
1172
+ /* Vertical rule down the left, with a dot per event. */
1173
+ :where(.timeline-list-item) {
1174
+ position: relative;
1175
+ padding-inline-start: 1.5rem;
1176
+ border-left: var(--border) solid var(--lily-border-strong);
1177
+ margin-inline-start: 0.5rem;
1178
+ }
1179
+ :where(.timeline-list-item)::before {
1180
+ content: "";
1181
+ position: absolute;
1182
+ inset-inline-start: -0.4rem;
1183
+ inset-block-start: 0.65rem;
1184
+ width: 0.7rem;
1185
+ height: 0.7rem;
1186
+ border-radius: 50%;
1187
+ background-color: var(--color-primary);
1188
+ }
1189
+
1190
+ /* --- Validation list — pass / fail / pending status colours ------------ */
1191
+ :where(.validation-list-item[data-status="passed"]) {
1192
+ color: var(--color-success);
1193
+ }
1194
+ :where(.validation-list-item[data-status="failed"]) {
1195
+ color: var(--color-error);
1196
+ }
1197
+ :where(.validation-list-item[data-status="pending"]) {
1198
+ color: var(--lily-text-muted);
1199
+ }
1200
+
1201
+ /* --- Description list (semantic <dl>) ----------------------------------- */
1202
+ /* Two-column grid: term in the left column, description in the right. */
1203
+ :where(.description-list) {
1204
+ display: grid;
1205
+ grid-template-columns: minmax(min-content, max-content) 1fr;
1206
+ column-gap: 1rem;
1207
+ row-gap: 0.5rem;
1208
+ }
1209
+ /* `display: contents` lets the dt/dd children participate directly in
1210
+ the parent grid without an intermediate row wrapper. */
1211
+ :where(.description-list-item) {
1212
+ display: contents;
1213
+ border: 0;
1214
+ }
1215
+ :where(.description-list dt) {
1216
+ font-weight: 600;
1217
+ }
1218
+
1219
+ /* --- Summary list (three-column key/value/action grid) ------------------ */
1220
+ :where(.summary-list) {
1221
+ display: grid;
1222
+ grid-template-columns: minmax(0, max-content) 1fr auto;
1223
+ column-gap: 1rem;
1224
+ row-gap: 0.5rem;
1225
+ }
1226
+ :where(.summary-list-item) {
1227
+ display: contents;
1228
+ border: 0;
1229
+ }
1230
+
1231
+ /* --- Icon / collection / document lists — flex rows with leading icon -- */
1232
+ :where(.icon-list-item, .collection-list-item, .document-list-item) {
1233
+ display: flex;
1234
+ align-items: flex-start;
1235
+ gap: 0.75rem;
1236
+ }
1237
+
1238
+ /* --- Navs --------------------------------------------------------------- */
1239
+ /* Nav wrappers are layout-neutral by default — the inner list handles
1240
+ layout, the nav just provides the landmark semantics. */
1241
+ :where(
1242
+ .accordion-nav,
1243
+ .breadcrumb-nav,
1244
+ .chat-nav,
1245
+ .contents-nav,
1246
+ .pagination-nav,
1247
+ .section-nav,
1248
+ .tree-nav,
1249
+ .navigation-menu
1250
+ ) {
1251
+ display: block;
1252
+ padding: 0;
1253
+ }
1254
+
1255
+ /* ==========================================================================
1256
+ * Tables (plain, data, calendar, kanban, gantt)
1257
+ * --------------------------------------------------------------------------
1258
+ * Every Lily table variant shares the same baseline: full-width, collapsed
1259
+ * borders, body font. Variants override below for specialised behaviour
1260
+ * (kanban columns, gantt bars, calendar selection).
1261
+ * ====================================================================== */
1262
+
1263
+ :where(.table, .data-table, .calendar-table, .kanban-table, .gantt-table) {
1264
+ width: 100%;
1265
+ border-collapse: collapse; /* shared edges between cells */
1266
+ background-color: var(--lily-surface);
1267
+ color: var(--lily-text);
1268
+ font-family: var(--lily-font-body);
1269
+ }
1270
+
1271
+ /* Thead gets the alt surface so it visually anchors above the body. */
1272
+ :where(
1273
+ .table-head,
1274
+ .data-table-head,
1275
+ .calendar-table-head,
1276
+ .kanban-table-head,
1277
+ .gantt-table-thead
1278
+ ) {
1279
+ background-color: var(--lily-surface-alt);
1280
+ }
1281
+
1282
+ /* Header cells — bold, tight clinical-grid padding. The 2px under-rule
1283
+ is retained so column boundaries remain crisp in long worklists. */
1284
+ :where(
1285
+ .table-th,
1286
+ .data-table-th,
1287
+ .calendar-table-th,
1288
+ .kanban-table-th,
1289
+ .gantt-table-th
1290
+ ) {
1291
+ padding: 0.25rem 0.5rem;
1292
+ text-align: start;
1293
+ font-weight: 600;
1294
+ border-bottom: 2px solid var(--lily-border-strong);
1295
+ }
1296
+
1297
+ /* Body cells — top-aligned (multi-line content reads better when the
1298
+ baseline is at the top, not centred). Tight padding lets EPR-style
1299
+ observation grids pack ~30 rows per 800px column. */
1300
+ :where(
1301
+ .table-td,
1302
+ .data-table-td,
1303
+ .calendar-table-td,
1304
+ .kanban-table-td,
1305
+ .gantt-table-td
1306
+ ) {
1307
+ padding: 0.125rem 0.5rem;
1308
+ border-bottom: var(--border) solid var(--lily-border);
1309
+ vertical-align: top;
1310
+ }
1311
+
1312
+ /* Row hover — subtle highlight for scanning long tables. */
1313
+ :where(
1314
+ .table-row,
1315
+ .data-table-row,
1316
+ .calendar-table-row,
1317
+ .kanban-table-row,
1318
+ .gantt-table-tr
1319
+ ):hover {
1320
+ background-color: var(--lily-surface-alt);
1321
+ }
1322
+
1323
+ /* Data-table filter form — chips that sit above the table. */
1324
+ :where(.data-filter-form) {
1325
+ display: flex;
1326
+ flex-wrap: wrap;
1327
+ gap: 0.25rem;
1328
+ padding: 0.375rem 0.5rem;
1329
+ background-color: var(--lily-surface-alt);
1330
+ border-radius: var(--lily-radius-md);
1331
+ }
1332
+
1333
+ /* --- Calendar specifics ------------------------------------------------- */
1334
+ /* Selected date — filled with the primary brand colour. */
1335
+ :where(.calendar-table-td[aria-selected="true"]) {
1336
+ background-color: var(--color-primary);
1337
+ color: var(--color-primary-content);
1338
+ }
1339
+ /* Today — outlined rather than filled, so a selected non-today date
1340
+ still reads as the "selected" one. */
1341
+ :where(.calendar-table-td[aria-current="date"]) {
1342
+ outline: 2px solid var(--color-primary);
1343
+ outline-offset: -2px;
1344
+ }
1345
+
1346
+ /* --- Kanban specifics --------------------------------------------------- */
1347
+ /* Each column renders as a `<td>`; widen the cells so cards have room. */
1348
+ :where(.kanban-table) {
1349
+ display: table;
1350
+ }
1351
+ :where(.kanban-table-td) {
1352
+ vertical-align: top;
1353
+ background-color: var(--lily-surface);
1354
+ min-width: 14rem;
1355
+ }
1356
+
1357
+ /* --- Gantt specifics ---------------------------------------------------- */
1358
+ /* Cells marked as bars are filled with the primary colour to render the
1359
+ schedule bar inline within the grid. */
1360
+ :where(.gantt-table-td[data-state="bar"]) {
1361
+ background-color: var(--color-primary);
1362
+ }
1363
+
1364
+ /* ==========================================================================
1365
+ * Cards, panels, surfaces
1366
+ * --------------------------------------------------------------------------
1367
+ * Family of generic content containers. Differ mostly in elevation
1368
+ * (shadow depth) and accent colour.
1369
+ * ====================================================================== */
1370
+
1371
+ :where(
1372
+ .card,
1373
+ .feature-card,
1374
+ .product-card,
1375
+ .care-card,
1376
+ .hover-card,
1377
+ .panel,
1378
+ .floating-panel,
1379
+ .success-panel
1380
+ ) {
1381
+ display: block;
1382
+ padding: 0.5rem;
1383
+ background-color: var(--lily-surface);
1384
+ color: var(--lily-text);
1385
+ border: var(--border) solid var(--lily-border);
1386
+ border-radius: var(--lily-radius-lg);
1387
+ box-shadow: var(--lily-shadow-sm);
1388
+ }
1389
+ /* Feature card — slightly more padding because it leads the page,
1390
+ but still clinical-tight. */
1391
+ :where(.feature-card) {
1392
+ padding: 0.75rem;
1393
+ }
1394
+ /* Hover card — popover-like; lift it with the medium shadow. */
1395
+ :where(.hover-card) {
1396
+ box-shadow: var(--lily-shadow-md);
1397
+ }
1398
+ /* Floating panel — pinned overlay; largest shadow. */
1399
+ :where(.floating-panel) {
1400
+ box-shadow: var(--lily-shadow-lg);
1401
+ }
1402
+ /* Success panel — green border + faint green tint. */
1403
+ :where(.success-panel) {
1404
+ border-color: var(--color-success);
1405
+ background-color: color-mix(
1406
+ in oklch,
1407
+ var(--color-success) 8%,
1408
+ var(--lily-surface)
1409
+ );
1410
+ }
1411
+ /* Care card urgency variants — coloured left rail (urgent/non-urgent/
1412
+ primary). Pattern adapted from the NHS care-card guidance. */
1413
+ :where(.care-card[data-urgency="urgent"]) {
1414
+ border-color: var(--color-error);
1415
+ border-left-width: 0.375rem;
1416
+ }
1417
+ :where(.care-card[data-urgency="non-urgent"]) {
1418
+ border-color: var(--color-info);
1419
+ border-left-width: 0.375rem;
1420
+ }
1421
+ :where(.care-card[data-urgency="primary"]) {
1422
+ border-color: var(--color-primary);
1423
+ border-left-width: 0.375rem;
1424
+ }
1425
+
1426
+ /* ==========================================================================
1427
+ * Banners, alerts, callouts, notifications
1428
+ * --------------------------------------------------------------------------
1429
+ * Family of messages with a coloured left rail. The rail colour is set
1430
+ * per severity so the message reads at a glance.
1431
+ * ====================================================================== */
1432
+
1433
+ /* Baseline — alert/banner/callout/notification all share the same frame.
1434
+ Clinical density keeps the coloured rail at 4px so the severity
1435
+ still reads at a glance, but compresses padding so the message
1436
+ footprint shrinks in side panels. */
1437
+ :where(
1438
+ .alert,
1439
+ .banner,
1440
+ .announcement-banner,
1441
+ .phase-banner,
1442
+ .government-banner,
1443
+ .medical-banner,
1444
+ .super-banner,
1445
+ .information-callout,
1446
+ .warning-callout,
1447
+ .summary-box,
1448
+ .notification,
1449
+ .toast,
1450
+ .sonner
1451
+ ) {
1452
+ display: flex;
1453
+ align-items: flex-start;
1454
+ gap: 0.375rem;
1455
+ padding: 0.375rem 0.5rem;
1456
+ background-color: var(--lily-surface-alt);
1457
+ color: var(--lily-text);
1458
+ border: var(--border) solid var(--lily-border);
1459
+ border-inline-start-width: 0.25rem; /* the coloured rail */
1460
+ border-radius: var(--lily-radius-md);
1461
+ }
1462
+
1463
+ /* Severity variants — rail + faint background tint. */
1464
+ :where(
1465
+ .alert[data-severity="info"],
1466
+ .information-callout,
1467
+ .announcement-banner
1468
+ ) {
1469
+ border-inline-start-color: var(--color-info);
1470
+ background-color: color-mix(
1471
+ in oklch,
1472
+ var(--color-info) 8%,
1473
+ var(--lily-surface)
1474
+ );
1475
+ }
1476
+ :where(.alert[data-severity="success"]) {
1477
+ border-inline-start-color: var(--color-success);
1478
+ background-color: color-mix(
1479
+ in oklch,
1480
+ var(--color-success) 8%,
1481
+ var(--lily-surface)
1482
+ );
1483
+ }
1484
+ :where(.alert[data-severity="warning"], .warning-callout, .phase-banner) {
1485
+ border-inline-start-color: var(--color-warning);
1486
+ background-color: color-mix(
1487
+ in oklch,
1488
+ var(--color-warning) 12%,
1489
+ var(--lily-surface)
1490
+ );
1491
+ }
1492
+ :where(
1493
+ .alert[data-severity="error"],
1494
+ .error-message,
1495
+ .error-summary,
1496
+ .super-banner
1497
+ ) {
1498
+ border-inline-start-color: var(--color-error);
1499
+ background-color: color-mix(
1500
+ in oklch,
1501
+ var(--color-error) 8%,
1502
+ var(--lily-surface)
1503
+ );
1504
+ }
1505
+ /* Medical banner — primary brand colour. */
1506
+ :where(.medical-banner) {
1507
+ border-inline-start-color: var(--color-primary);
1508
+ }
1509
+ /* Medical banner inner boxes — danger / advice variants. */
1510
+ :where(.medical-banner-box-for-danger) {
1511
+ border-color: var(--color-error);
1512
+ background-color: color-mix(
1513
+ in oklch,
1514
+ var(--color-error) 8%,
1515
+ var(--lily-surface)
1516
+ );
1517
+ }
1518
+ :where(.medical-banner-box-for-advice) {
1519
+ border-color: var(--color-info);
1520
+ background-color: color-mix(
1521
+ in oklch,
1522
+ var(--color-info) 8%,
1523
+ var(--lily-surface)
1524
+ );
1525
+ }
1526
+ /* Government banner — solid neutral surface (US-government / GOV.UK
1527
+ style: a chrome bar at the top of the page identifying the site
1528
+ as an official property). No coloured rail. */
1529
+ :where(.government-banner) {
1530
+ background-color: var(--color-neutral);
1531
+ color: var(--color-neutral-content);
1532
+ border-color: transparent;
1533
+ border-inline-start-color: transparent;
1534
+ }
1535
+ /* Summary box — primary-coloured rail; used for "answer at a glance"
1536
+ callouts on long forms (e.g. cost summary). */
1537
+ :where(.summary-box) {
1538
+ border-color: var(--color-primary);
1539
+ border-inline-start-width: 0.375rem;
1540
+ }
1541
+
1542
+ /* Banner boxes — chrome strip variants used inside header areas. */
1543
+ :where(.banner-box, .medical-banner-box) {
1544
+ display: flex;
1545
+ align-items: center;
1546
+ gap: 0.375rem;
1547
+ padding: 0.25rem 0.5rem;
1548
+ }
1549
+
1550
+ /* Sticky promo — sits at the very top of the viewport on scroll. */
1551
+ :where(.sticky-promo-banner) {
1552
+ position: sticky;
1553
+ inset-block-start: 0;
1554
+ z-index: 10;
1555
+ }
1556
+
1557
+ /* Error message — inline form-validation message. */
1558
+ :where(.error-message) {
1559
+ color: var(--color-error);
1560
+ font-weight: 600;
1561
+ }
1562
+ /* Error summary — heavyweight 4px border (GOV.UK pattern) so users
1563
+ scrolling back to the top of a form see it immediately. The thick
1564
+ red border is preserved at full weight even in clinical density;
1565
+ failed-form recovery is too important to subdue. */
1566
+ :where(.error-summary) {
1567
+ padding: 0.5rem;
1568
+ border: 0.25rem solid var(--color-error);
1569
+ }
1570
+
1571
+ /* Toast / Sonner — floating notifications; medium elevation. */
1572
+ :where(.toast, .sonner) {
1573
+ box-shadow: var(--lily-shadow-md);
1574
+ }
1575
+ /* In-page notification — small elevation. */
1576
+ :where(.notification) {
1577
+ box-shadow: var(--lily-shadow-sm);
1578
+ }
1579
+
1580
+ /* ==========================================================================
1581
+ * Forms
1582
+ * --------------------------------------------------------------------------
1583
+ * Form / field / fieldset layout primitives. Stack vertically with a
1584
+ * consistent gap so consumer markup stays brief.
1585
+ * ====================================================================== */
1586
+
1587
+ :where(.form, .editable-form) {
1588
+ display: flex;
1589
+ flex-direction: column;
1590
+ gap: 0.5rem;
1591
+ }
1592
+ /* Field — wraps a single label + input + hint/error trio. */
1593
+ :where(.field) {
1594
+ display: flex;
1595
+ flex-direction: column;
1596
+ gap: 0.125rem;
1597
+ }
1598
+ /* Fieldset — semantic <fieldset>; border + padded inset, legend bold. */
1599
+ :where(.fieldset) {
1600
+ border: var(--border) solid var(--lily-border);
1601
+ border-radius: var(--lily-radius-md);
1602
+ padding: 0.5rem;
1603
+ }
1604
+ :where(.fieldset > legend) {
1605
+ padding-inline: 0.5rem;
1606
+ font-weight: 600;
1607
+ }
1608
+ /* Label — bold and dark; consumers can override per-form. */
1609
+ :where(.label) {
1610
+ font-weight: 600;
1611
+ color: var(--lily-text);
1612
+ }
1613
+ /* Hint — small muted helptext under the input. */
1614
+ :where(.hint) {
1615
+ color: var(--lily-text-muted);
1616
+ font-size: 0.875rem;
1617
+ }
1618
+ /* Character counter — same colour/size as hint but right-aligned. */
1619
+ :where(.character-counter) {
1620
+ color: var(--lily-text-muted);
1621
+ font-size: 0.875rem;
1622
+ text-align: end;
1623
+ }
1624
+
1625
+ /* ==========================================================================
1626
+ * Layout & containers
1627
+ * ====================================================================== */
1628
+
1629
+ /* Page-width containers. */
1630
+ :where(
1631
+ .container,
1632
+ .container-with-fixed-width,
1633
+ .container-with-fluid-width
1634
+ ) {
1635
+ display: block;
1636
+ width: 100%;
1637
+ margin-inline: auto;
1638
+ padding-inline: 0.5rem;
1639
+ box-sizing: border-box;
1640
+ }
1641
+ /* Fixed-width — caps at a comfortable reading width (~1152px). */
1642
+ :where(.container-with-fixed-width) {
1643
+ max-width: 72rem;
1644
+ }
1645
+ /* Fluid-width — fills the viewport edge-to-edge. */
1646
+ :where(.container-with-fluid-width) {
1647
+ max-width: none;
1648
+ }
1649
+ /* Reset the inline padding when a container should butt against the edge. */
1650
+ :where(.padding-reset) {
1651
+ padding-inline: 0;
1652
+ }
1653
+
1654
+ /* Flex / grid / masonry primitives. */
1655
+ :where(.flex-stack) {
1656
+ display: flex;
1657
+ flex-direction: column;
1658
+ gap: 0.5rem;
1659
+ }
1660
+ :where(.grid) {
1661
+ display: grid;
1662
+ gap: 0.5rem;
1663
+ }
1664
+ :where(.masonry) {
1665
+ column-count: 3;
1666
+ column-gap: 0.5rem;
1667
+ }
1668
+ /* Horizontal-rule replacement that respects theme borders. */
1669
+ :where(.separator) {
1670
+ border: 0;
1671
+ border-top: var(--border) solid var(--lily-border);
1672
+ margin: 0.5rem 0;
1673
+ }
1674
+ /* Generic content tile. */
1675
+ :where(.tile) {
1676
+ display: block;
1677
+ padding: 0.5rem;
1678
+ background-color: var(--lily-surface);
1679
+ border: var(--border) solid var(--lily-border);
1680
+ border-radius: var(--lily-radius-md);
1681
+ }
1682
+ /* Sidebar — secondary-surface panel; sits beside main content. */
1683
+ :where(.sidebar) {
1684
+ display: block;
1685
+ background-color: var(--lily-surface-alt);
1686
+ padding: 0.5rem;
1687
+ border-radius: var(--lily-radius-md);
1688
+ }
1689
+ /* Aspect-ratio box — 16:9 default; consumers override `aspect-ratio`. */
1690
+ :where(.aspect-ratio-container) {
1691
+ position: relative;
1692
+ display: block;
1693
+ aspect-ratio: 16 / 9;
1694
+ overflow: hidden;
1695
+ border-radius: var(--lily-radius-md);
1696
+ }
1697
+ /* Framer — bordered box for embeds / illustrations. */
1698
+ :where(.framer) {
1699
+ padding: 0.5rem;
1700
+ border: var(--border) solid var(--lily-border);
1701
+ border-radius: var(--lily-radius-md);
1702
+ }
1703
+ /* Article-layout — declares column-width tokens that content-block reads.
1704
+ Pattern lifted from the Reuters Graphics components system. */
1705
+ :where(.article-layout) {
1706
+ --content-width-narrower: 20.625rem; /* 330px */
1707
+ --content-width-narrow: 31.875rem; /* 510px */
1708
+ --content-width-normal: 41.25rem; /* 660px (default reading width) */
1709
+ --content-width-wide: 58.125rem; /* 930px (chart escape) */
1710
+ --content-width-wider: 75rem; /* 1200px (full-bleed escape) */
1711
+ display: block;
1712
+ }
1713
+ /* Content-block — reads the column-width tokens; switch via data-width. */
1714
+ :where(.content-block) {
1715
+ max-width: var(--content-width-normal);
1716
+ margin-inline: auto;
1717
+ padding-inline: 0.5rem;
1718
+ box-sizing: border-box;
1719
+ }
1720
+ :where(.content-block[data-width="narrower"]) {
1721
+ max-width: var(--content-width-narrower);
1722
+ }
1723
+ :where(.content-block[data-width="narrow"]) {
1724
+ max-width: var(--content-width-narrow);
1725
+ }
1726
+ :where(.content-block[data-width="wide"]) {
1727
+ max-width: var(--content-width-wide);
1728
+ }
1729
+ :where(.content-block[data-width="wider"]) {
1730
+ max-width: var(--content-width-wider);
1731
+ }
1732
+
1733
+ /* --- Grail layout (5-region responsive page shell) --------------------- */
1734
+ /* Header spans the top; left-aside / main / right-aside fill the middle;
1735
+ footer spans the bottom. At ≤768px the asides drop below the main. */
1736
+ :where(.grail-layout) {
1737
+ display: grid;
1738
+ min-height: 100vh;
1739
+ grid-template-areas:
1740
+ "header header header"
1741
+ "left main right"
1742
+ "footer footer footer";
1743
+ grid-template-rows: auto 1fr auto;
1744
+ grid-template-columns: minmax(0, 1fr) minmax(0, 3fr) minmax(0, 1fr);
1745
+ background-color: var(--lily-surface);
1746
+ color: var(--lily-text);
1747
+ }
1748
+ :where(.grail-layout-top-header) {
1749
+ grid-area: header;
1750
+ }
1751
+ :where(.grail-layout-left-aside) {
1752
+ grid-area: left;
1753
+ }
1754
+ :where(.grail-layout-center-main) {
1755
+ grid-area: main;
1756
+ }
1757
+ :where(.grail-layout-right-aside) {
1758
+ grid-area: right;
1759
+ }
1760
+ :where(.grail-layout-bottom-footer) {
1761
+ grid-area: footer;
1762
+ }
1763
+ /* Header / footer chrome — secondary surface, padded. */
1764
+ :where(.grail-layout-top-header, .grail-layout-bottom-footer) {
1765
+ background-color: var(--lily-surface-alt);
1766
+ padding: 0.375rem 0.5rem;
1767
+ }
1768
+ /* Asides — secondary surface, padded. */
1769
+ :where(.grail-layout-left-aside, .grail-layout-right-aside) {
1770
+ padding: 0.5rem;
1771
+ background-color: var(--lily-surface-alt);
1772
+ }
1773
+ /* Main content area — primary surface, slightly more padding than the
1774
+ chrome regions but still tight by clinical convention. */
1775
+ :where(.grail-layout-center-main) {
1776
+ padding: 0.75rem;
1777
+ }
1778
+ /* Mobile breakpoint — collapse to a single column, stacking the regions. */
1779
+ @media (max-width: 48rem) {
1780
+ :where(.grail-layout) {
1781
+ grid-template-areas: "header" "main" "left" "right" "footer";
1782
+ grid-template-columns: 1fr;
1783
+ grid-template-rows: auto auto auto auto auto;
1784
+ }
1785
+ }
1786
+
1787
+ /* Standalone semantic header / footer landmarks. */
1788
+ :where(.header) {
1789
+ display: block;
1790
+ padding: 0.375rem 0.5rem;
1791
+ background-color: var(--lily-surface);
1792
+ border-bottom: var(--border) solid var(--lily-border);
1793
+ }
1794
+ :where(.footer) {
1795
+ display: block;
1796
+ padding: 0.375rem 0.5rem;
1797
+ background-color: var(--lily-surface-alt);
1798
+ border-top: var(--border) solid var(--lily-border);
1799
+ }
1800
+
1801
+ /* ==========================================================================
1802
+ * Dialogs, popovers, overlays
1803
+ * --------------------------------------------------------------------------
1804
+ * Modal and floating UI. Backdrop is a blur + 50% black overlay; dialog
1805
+ * frames are rounded with the largest shadow.
1806
+ * ====================================================================== */
1807
+
1808
+ /* Native <dialog> + variant types. */
1809
+ :where(
1810
+ .dialog,
1811
+ .alert-dialog,
1812
+ .timeout-dialog,
1813
+ .popconfirm-dialog,
1814
+ .file-dialog
1815
+ ) {
1816
+ background-color: var(--lily-surface);
1817
+ color: var(--lily-text);
1818
+ border: var(--border) solid var(--lily-border);
1819
+ border-radius: var(--lily-radius-lg);
1820
+ padding: 0.75rem;
1821
+ box-shadow: var(--lily-shadow-lg);
1822
+ max-width: min(40rem, 90vw); /* cap on big screens, fit on small */
1823
+ }
1824
+ /* Backdrop — dim the page and blur slightly so focus stays on the dialog. */
1825
+ :where(
1826
+ .dialog::backdrop,
1827
+ .alert-dialog::backdrop,
1828
+ .timeout-dialog::backdrop,
1829
+ .popconfirm-dialog::backdrop,
1830
+ .file-dialog::backdrop
1831
+ ) {
1832
+ background-color: color-mix(in oklch, black 50%, transparent);
1833
+ backdrop-filter: blur(2px);
1834
+ }
1835
+ /* Alert dialog — red rail so the user knows it's destructive. */
1836
+ :where(.alert-dialog) {
1837
+ border-color: var(--color-error);
1838
+ border-inline-start-width: 0.375rem;
1839
+ }
1840
+
1841
+ /* Popover / tooltip family — small floating labels. */
1842
+ :where(.popover, .popup, .tooltip, .coachmark, .contextual-help) {
1843
+ display: block;
1844
+ padding: 0.5rem 0.75rem;
1845
+ background-color: var(--color-neutral);
1846
+ color: var(--color-neutral-content);
1847
+ border-radius: var(--lily-radius-md);
1848
+ box-shadow: var(--lily-shadow-md);
1849
+ font-size: 0.875rem;
1850
+ max-width: 20rem;
1851
+ }
1852
+ /* Popover / popup — surface-coloured (more substantial than a tooltip). */
1853
+ :where(.popover, .popup) {
1854
+ background-color: var(--lily-surface);
1855
+ color: var(--lily-text);
1856
+ border: var(--border) solid var(--lily-border);
1857
+ max-width: 24rem;
1858
+ }
1859
+ /* Coachmark — primary-coloured border so it reads as a tutorial hint. */
1860
+ :where(.coachmark) {
1861
+ border: 2px solid var(--color-primary);
1862
+ }
1863
+
1864
+ /* Full-page overlay container — dims everything beneath a high-z child. */
1865
+ :where(.overlay-container) {
1866
+ position: fixed;
1867
+ inset: 0;
1868
+ background-color: color-mix(in oklch, black 50%, transparent);
1869
+ z-index: 100;
1870
+ }
1871
+
1872
+ /* Drawers — side panels (right edge by default; RTL-aware via logical
1873
+ properties). Sit above the overlay container (z-index 101). */
1874
+ :where(.drawer, .slide-out-drawer, .sheet) {
1875
+ position: fixed;
1876
+ inset-block: 0;
1877
+ inset-inline-end: 0;
1878
+ width: min(28rem, 90vw);
1879
+ background-color: var(--lily-surface);
1880
+ color: var(--lily-text);
1881
+ border-inline-start: var(--border) solid var(--lily-border);
1882
+ box-shadow: var(--lily-shadow-lg);
1883
+ padding: 0.5rem;
1884
+ z-index: 101;
1885
+ }
1886
+
1887
+ /* ==========================================================================
1888
+ * Menus
1889
+ * ====================================================================== */
1890
+
1891
+ /* Menu containers — surface-coloured popover with thin border + sm shadow. */
1892
+ :where(.menu, .context-menu, .dropdown-menu, .tree-menu, .command) {
1893
+ display: block;
1894
+ background-color: var(--lily-surface);
1895
+ color: var(--lily-text);
1896
+ border: var(--border) solid var(--lily-border);
1897
+ border-radius: var(--lily-radius-md);
1898
+ padding: 0.25rem;
1899
+ box-shadow: var(--lily-shadow-sm);
1900
+ list-style: none;
1901
+ margin: 0;
1902
+ }
1903
+ /* Menu items — flex row with icon + label slot. */
1904
+ :where(.menu-item, .context-menu-item) {
1905
+ display: flex;
1906
+ align-items: center;
1907
+ gap: 0.375rem;
1908
+ padding: 0.125rem 0.375rem;
1909
+ border-radius: var(--lily-radius-md);
1910
+ cursor: pointer;
1911
+ }
1912
+ /* Highlight on hover / aria-selected — alt surface. */
1913
+ :where(
1914
+ .menu-item:hover,
1915
+ .context-menu-item:hover,
1916
+ .menu-item[aria-selected="true"]
1917
+ ) {
1918
+ background-color: var(--lily-surface-alt);
1919
+ }
1920
+ /* Menu group — section within a menu. The aria-label becomes a header. */
1921
+ :where(.menu-group) {
1922
+ padding: 0.25rem 0;
1923
+ }
1924
+ :where(.menu-group::before) {
1925
+ content: attr(aria-label);
1926
+ display: block;
1927
+ padding: 0.25rem 0.75rem;
1928
+ color: var(--lily-text-muted);
1929
+ font-size: 0.75rem;
1930
+ text-transform: uppercase;
1931
+ letter-spacing: 0.05em;
1932
+ }
1933
+ /* Cascader / navigation-menu — horizontal flex row of menu columns. */
1934
+ :where(.cascader, .navigation-menu) {
1935
+ display: flex;
1936
+ gap: 0.25rem;
1937
+ }
1938
+ /* Hamburger — bordered icon-button. */
1939
+ :where(.hamburger-menu) {
1940
+ display: inline-flex;
1941
+ align-items: center;
1942
+ justify-content: center;
1943
+ padding: 0.125rem;
1944
+ background: transparent;
1945
+ border: var(--border) solid var(--lily-border-strong);
1946
+ border-radius: var(--lily-radius-md);
1947
+ color: var(--lily-text);
1948
+ cursor: pointer;
1949
+ }
1950
+
1951
+ /* ==========================================================================
1952
+ * Segments, toggles, groups
1953
+ * ====================================================================== */
1954
+
1955
+ /* Generic group containers — inline-flex, wrap, small gap. */
1956
+ :where(
1957
+ .segment-group,
1958
+ .toggle-group,
1959
+ .checkbox-group,
1960
+ .radio-group,
1961
+ .dial-group,
1962
+ .tag-group,
1963
+ .avatar-group,
1964
+ .mutually-exclusive
1965
+ ) {
1966
+ display: inline-flex;
1967
+ gap: 0.25rem;
1968
+ flex-wrap: wrap;
1969
+ }
1970
+ /* Segment group / toggle group get an inset background (iOS-segmented
1971
+ control aesthetic). */
1972
+ :where(.segment-group, .toggle-group) {
1973
+ padding: 0.25rem;
1974
+ background-color: var(--lily-surface-alt);
1975
+ border-radius: var(--lily-radius-md);
1976
+ }
1977
+ /* Segment items — transparent until pressed; then primary fill. */
1978
+ :where(.segment-group-item) {
1979
+ padding: 0.375rem 0.75rem;
1980
+ background: transparent;
1981
+ border: 0;
1982
+ border-radius: var(--lily-radius-md);
1983
+ color: var(--lily-text);
1984
+ cursor: pointer;
1985
+ }
1986
+ :where(
1987
+ .segment-group-item[aria-pressed="true"],
1988
+ .segment-group-item[aria-selected="true"]
1989
+ ) {
1990
+ background-color: var(--color-primary);
1991
+ color: var(--color-primary-content);
1992
+ }
1993
+ /* Avatar group — overlap each subsequent avatar by 0.5rem, with a
1994
+ contrast ring for separation. */
1995
+ :where(.avatar-group > *) {
1996
+ margin-inline-start: -0.5rem;
1997
+ border: 2px solid var(--lily-surface);
1998
+ }
1999
+
2000
+ /* ==========================================================================
2001
+ * Progress, loading
2002
+ * ====================================================================== */
2003
+
2004
+ /* Native <progress> / <meter> / bar variant — short, pill-shaped track. */
2005
+ :where(.progress, .progress-bar, .meter) {
2006
+ display: block;
2007
+ width: 100%;
2008
+ height: 0.5rem;
2009
+ appearance: none;
2010
+ background-color: var(--lily-surface-sunken);
2011
+ border: 0;
2012
+ border-radius: 999px;
2013
+ overflow: hidden;
2014
+ }
2015
+ /* Vendor pseudo-elements: track + value. */
2016
+ :where(.progress::-webkit-progress-bar, .meter::-webkit-meter-bar) {
2017
+ background-color: var(--lily-surface-sunken);
2018
+ }
2019
+ :where(
2020
+ .progress::-webkit-progress-value,
2021
+ .meter::-webkit-meter-optimum-value
2022
+ ) {
2023
+ background-color: var(--color-primary);
2024
+ }
2025
+ :where(.progress::-moz-progress-bar) {
2026
+ background-color: var(--color-primary);
2027
+ }
2028
+
2029
+ /* Progress circle — conic-gradient renders the sweep based on the
2030
+ --progress custom property (0–100). */
2031
+ :where(.progress-circle) {
2032
+ display: inline-block;
2033
+ width: 2rem;
2034
+ height: 2rem;
2035
+ border-radius: 50%;
2036
+ background: conic-gradient(
2037
+ var(--color-primary) calc(var(--progress, 0) * 1%),
2038
+ var(--lily-surface-sunken) 0
2039
+ );
2040
+ }
2041
+ /* Spinner / loading — classic CSS rotating partial-ring. */
2042
+ :where(.progress-spinner, .loading) {
2043
+ display: inline-block;
2044
+ width: 1.5rem;
2045
+ height: 1.5rem;
2046
+ border: 3px solid var(--lily-surface-sunken);
2047
+ border-top-color: var(--color-primary);
2048
+ border-radius: 50%;
2049
+ animation: lily-spin 0.8s linear infinite;
2050
+ }
2051
+ @keyframes lily-spin {
2052
+ to {
2053
+ transform: rotate(360deg);
2054
+ }
2055
+ }
2056
+ /* Skeleton — shimmering placeholder while content is loading. */
2057
+ :where(.skeleton) {
2058
+ display: block;
2059
+ background: linear-gradient(
2060
+ 90deg,
2061
+ var(--lily-surface-alt) 0%,
2062
+ var(--lily-surface-sunken) 50%,
2063
+ var(--lily-surface-alt) 100%
2064
+ );
2065
+ background-size: 200% 100%;
2066
+ animation: lily-skeleton 1.4s ease-in-out infinite;
2067
+ border-radius: var(--lily-radius-md);
2068
+ min-height: 1rem;
2069
+ }
2070
+ @keyframes lily-skeleton {
2071
+ 0% {
2072
+ background-position: 200% 0;
2073
+ }
2074
+ 100% {
2075
+ background-position: -200% 0;
2076
+ }
2077
+ }
2078
+ /* Respect prefers-reduced-motion — kill the perpetual animations.
2079
+ (WCAG 2.3.3 Animation from interactions.) */
2080
+ @media (prefers-reduced-motion: reduce) {
2081
+ :where(.progress-spinner, .loading, .skeleton, .beach-ball) {
2082
+ animation: none;
2083
+ }
2084
+ }
2085
+
2086
+ /* ==========================================================================
2087
+ * Avatars, badges, tags, status
2088
+ * ====================================================================== */
2089
+
2090
+ /* Avatar — circular tile (1.75rem in clinical density) with sunken
2091
+ background as the fallback colour when no image is provided.
2092
+ Smaller so author/clinician chips fit in dense activity columns. */
2093
+ :where(.avatar) {
2094
+ display: inline-flex;
2095
+ align-items: center;
2096
+ justify-content: center;
2097
+ width: 1.75rem;
2098
+ height: 1.75rem;
2099
+ border-radius: 50%;
2100
+ background-color: var(--lily-surface-sunken);
2101
+ color: var(--lily-text);
2102
+ overflow: hidden;
2103
+ font-weight: 600;
2104
+ }
2105
+ :where(.avatar-image) {
2106
+ width: 100%;
2107
+ height: 100%;
2108
+ object-fit: cover;
2109
+ display: block;
2110
+ }
2111
+ /* Avatar fallback text — uppercase initials. */
2112
+ :where(.avatar-text) {
2113
+ text-transform: uppercase;
2114
+ }
2115
+
2116
+ /* Badge / flair / tag family — small pill-shaped labels. */
2117
+ :where(.badge, .flair, .status-tag, .tag, .ai-label) {
2118
+ display: inline-flex;
2119
+ align-items: center;
2120
+ gap: 0.25rem;
2121
+ padding: 0.125rem 0.5rem;
2122
+ font-size: 0.75rem;
2123
+ font-weight: 600;
2124
+ line-height: 1.5;
2125
+ background-color: var(--color-neutral);
2126
+ color: var(--color-neutral-content);
2127
+ border-radius: 999px;
2128
+ }
2129
+ /* Tag — neutral chip with a visible border (so it's not confused with
2130
+ a status badge). */
2131
+ :where(.tag) {
2132
+ background-color: var(--lily-surface-alt);
2133
+ color: var(--lily-text);
2134
+ border: var(--border) solid var(--lily-border);
2135
+ }
2136
+ /* AI label — accent colour so AI-generated content is visually marked. */
2137
+ :where(.ai-label) {
2138
+ background-color: var(--color-accent);
2139
+ color: var(--color-accent-content);
2140
+ }
2141
+ /* Status tags — pre-baked colour for each status. */
2142
+ :where(.status-tag[data-status="success"]) {
2143
+ background-color: var(--color-success);
2144
+ color: var(--color-success-content);
2145
+ }
2146
+ :where(.status-tag[data-status="warning"]) {
2147
+ background-color: var(--color-warning);
2148
+ color: var(--color-warning-content);
2149
+ }
2150
+ :where(.status-tag[data-status="error"]) {
2151
+ background-color: var(--color-error);
2152
+ color: var(--color-error-content);
2153
+ }
2154
+ :where(.status-tag[data-status="info"]) {
2155
+ background-color: var(--color-info);
2156
+ color: var(--color-info-content);
2157
+ }
2158
+
2159
+ /* Status light — a coloured dot followed by a label. */
2160
+ :where(.status-light) {
2161
+ display: inline-flex;
2162
+ align-items: center;
2163
+ gap: 0.375rem;
2164
+ }
2165
+ :where(.status-light)::before {
2166
+ content: "";
2167
+ display: inline-block;
2168
+ width: 0.625rem;
2169
+ height: 0.625rem;
2170
+ border-radius: 50%;
2171
+ background-color: var(--color-neutral);
2172
+ }
2173
+ :where(.status-light[data-status="success"])::before {
2174
+ background-color: var(--color-success);
2175
+ }
2176
+ :where(.status-light[data-status="warning"])::before {
2177
+ background-color: var(--color-warning);
2178
+ }
2179
+ :where(.status-light[data-status="error"])::before {
2180
+ background-color: var(--color-error);
2181
+ }
2182
+ :where(.status-light[data-status="info"])::before {
2183
+ background-color: var(--color-info);
2184
+ }
2185
+
2186
+ /* ==========================================================================
2187
+ * Pickers, ratings, theme
2188
+ * ====================================================================== */
2189
+
2190
+ /* Picker baseline — inline row of selectable chips/buttons. */
2191
+ :where(
2192
+ .color-picker,
2193
+ .five-face-rating-picker,
2194
+ .five-star-rating-picker,
2195
+ .net-promoter-score-picker,
2196
+ .red-amber-green-picker,
2197
+ .red-orange-yellow-green-blue-picker,
2198
+ .emoji-character-picker,
2199
+ .calendar-range-picker
2200
+ ) {
2201
+ display: inline-flex;
2202
+ align-items: center;
2203
+ gap: 0.25rem;
2204
+ padding: 0.25rem;
2205
+ background-color: var(--lily-surface);
2206
+ border: var(--border) solid var(--lily-border);
2207
+ border-radius: var(--lily-radius-md);
2208
+ }
2209
+ /* Picker buttons — small alt-surface chips. */
2210
+ :where(
2211
+ .color-picker-button,
2212
+ .five-face-rating-picker-button,
2213
+ .five-star-rating-picker-button,
2214
+ .net-promoter-score-picker-button,
2215
+ .red-amber-green-picker-button,
2216
+ .red-orange-yellow-green-blue-picker-button
2217
+ ) {
2218
+ background-color: var(--lily-surface-alt);
2219
+ color: var(--lily-text);
2220
+ border: var(--border) solid var(--lily-border);
2221
+ min-width: 2rem;
2222
+ min-height: 2rem;
2223
+ padding: 0.25rem 0.5rem;
2224
+ }
2225
+ /* Color picker — circular swatch. */
2226
+ :where(.color-picker-button) {
2227
+ width: 1.75rem;
2228
+ height: 1.75rem;
2229
+ padding: 0;
2230
+ border-radius: 50%;
2231
+ }
2232
+ /* Filled stars — warning (gold) colour for both viewer and pressed state. */
2233
+ :where(
2234
+ .five-star-rating-view,
2235
+ .five-star-rating-picker-button[aria-pressed="true"]
2236
+ ) {
2237
+ color: var(--color-warning);
2238
+ }
2239
+ /* RAG (red/amber/green) — semantic colour per status. */
2240
+ :where(
2241
+ .red-amber-green-view[data-status="red"],
2242
+ .red-amber-green-picker-button[data-status="red"]
2243
+ ) {
2244
+ background-color: var(--color-error);
2245
+ color: var(--color-error-content);
2246
+ }
2247
+ :where(
2248
+ .red-amber-green-view[data-status="amber"],
2249
+ .red-amber-green-picker-button[data-status="amber"]
2250
+ ) {
2251
+ background-color: var(--color-warning);
2252
+ color: var(--color-warning-content);
2253
+ }
2254
+ :where(
2255
+ .red-amber-green-view[data-status="green"],
2256
+ .red-amber-green-picker-button[data-status="green"]
2257
+ ) {
2258
+ background-color: var(--color-success);
2259
+ color: var(--color-success-content);
2260
+ }
2261
+
2262
+ /* Native <select> styling — drop the UA chrome and draw our own caret
2263
+ via two stacked linear-gradients. Clinical-density select matches
2264
+ the input baseline (1.75rem min-height) so dropdowns align with
2265
+ text inputs in a side-by-side field grid. */
2266
+ :where(select.theme-select, .select, .select-with-extras, .tree-select) {
2267
+ width: 100%;
2268
+ min-height: 1.75rem;
2269
+ padding: 0.125rem 1.5rem 0.125rem 0.375rem; /* extra right padding for caret */
2270
+ font: inherit;
2271
+ background-color: var(--lily-surface);
2272
+ color: var(--lily-text);
2273
+ border: var(--border) solid var(--lily-border-strong);
2274
+ border-radius: var(--lily-radius-md);
2275
+ appearance: none;
2276
+ background-image:
2277
+ linear-gradient(45deg, transparent 50%, var(--lily-text) 50%),
2278
+ linear-gradient(135deg, var(--lily-text) 50%, transparent 50%);
2279
+ background-position:
2280
+ calc(100% - 0.9rem) 0.7rem,
2281
+ calc(100% - 0.55rem) 0.7rem;
2282
+ background-size: 0.35rem 0.35rem;
2283
+ background-repeat: no-repeat;
2284
+ }
2285
+ /* Options inherit the select colours so the dropdown matches the input. */
2286
+ :where(.option, .theme-select-option) {
2287
+ background-color: var(--lily-surface);
2288
+ color: var(--lily-text);
2289
+ }
2290
+
2291
+ /* The four *-picker helpers are an icon button that opens a popup,
2292
+ so they are styled as a popup rather than a form field. No :has()
2293
+ guard is needed any more: before the July 2026 rename these shared
2294
+ the `.theme-select` hook with the catalog theme-select component
2295
+ and had to be told apart. Distinct names, no collision. */
2296
+ :where(
2297
+ .theme-picker,
2298
+ .locale-picker,
2299
+ .text-size-picker,
2300
+ .share-picker,
2301
+ .date-time-picker
2302
+ ) {
2303
+ position: relative;
2304
+ display: inline-block;
2305
+ }
2306
+
2307
+ :where(
2308
+ .theme-picker-button,
2309
+ .locale-picker-button,
2310
+ .text-size-picker-button,
2311
+ .share-picker-button,
2312
+ .date-time-picker-button
2313
+ ) {
2314
+ display: inline-flex;
2315
+ align-items: center;
2316
+ justify-content: center;
2317
+ min-width: 2.5rem;
2318
+ min-height: 2.5rem;
2319
+ padding: var(--lily-space-xs);
2320
+ font: inherit;
2321
+ line-height: 1;
2322
+ color: var(--lily-text);
2323
+ background-color: var(--lily-surface);
2324
+ border: var(--border) solid var(--lily-border-strong);
2325
+ border-radius: var(--lily-radius-md);
2326
+ cursor: pointer;
2327
+ }
2328
+
2329
+ /* The icon is decorative (aria-hidden); the button's aria-label
2330
+ carries the accessible name. Reversed 2026-09-16: five of the
2331
+ six picker icons (theme, locale, text-size, motion, share) are
2332
+ now bundled SVGs, not Unicode glyphs. Each ships its own
2333
+ explicit width="1.05rem" height="1.05rem" on the <svg> element,
2334
+ so unlike a font-dependent character glyph -- where different
2335
+ glyphs ink different fractions of their em box, needing a
2336
+ per-glyph optical correction -- every icon in this family
2337
+ already inks the same visual size by construction (the icon's
2338
+ own author controls its viewBox). This rule only supplies flex
2339
+ centering; override the icon's own width/height if you need a
2340
+ different size. */
2341
+ :where(
2342
+ .theme-picker-icon,
2343
+ .locale-picker-icon,
2344
+ .text-size-picker-icon,
2345
+ .motion-picker-icon,
2346
+ .share-picker-icon
2347
+ ) {
2348
+ display: inline-flex;
2349
+ align-items: center;
2350
+ justify-content: center;
2351
+ }
2352
+
2353
+ /* date-time-picker's calendar icon is still a Unicode glyph
2354
+ (U+1F4C5 + U+FE0E) -- out of scope for the 2026-09-16 SVG-icon
2355
+ reversal, since date-time-picker was not part of that change
2356
+ (see AGENTS/helpers.md) -- so it keeps the font-size-based
2357
+ sizing the other five used to share.
2358
+
2359
+ U+1F4C5 calendar inks 1.000 of its em box (max extent) against
2360
+ U+25D1 half-circle's reference 0.850 (the pre-reversal
2361
+ baseline, still valid here since this is the same glyph
2362
+ measurement, unchanged): 0.850/1.000 ≈ 0.85, shipped as 0.845.
2363
+ A colour-emoji font paints every one of its glyphs into one
2364
+ identical em square, so globe, calendar, rocket and heart all
2365
+ measure the same ink box -- any future emoji helper icon can
2366
+ reuse this factor without re-measuring.
2367
+
2368
+ (Measuring also confirms the U+FE0E text-presentation selector is
2369
+ ignored here: the bare and VS-15 forms are metrically identical. It
2370
+ stays in the source as a hint to the platforms that do honour it.) */
2371
+ :where(.date-time-picker-icon) {
2372
+ display: inline-flex;
2373
+ align-items: center;
2374
+ justify-content: center;
2375
+ font-size: calc(1.25em * var(--lily-picker-icon-scale, 1));
2376
+ line-height: 1;
2377
+ --lily-picker-icon-scale: 0.845;
2378
+ }
2379
+
2380
+ /* The share list holds real links and a button rather than options,
2381
+ so its items get the same padding without the option selectors. */
2382
+ :where(.share-picker-list) > :where(.share-picker-list-item) {
2383
+ list-style: none;
2384
+ }
2385
+
2386
+ :where(.share-picker-target, .share-picker-copy) {
2387
+ display: block;
2388
+ width: 100%;
2389
+ padding: var(--lily-space-xs) var(--lily-space-sm);
2390
+ font: inherit;
2391
+ text-align: start;
2392
+ color: var(--lily-text);
2393
+ background: none;
2394
+ border: 0;
2395
+ border-radius: var(--lily-radius-sm);
2396
+ cursor: pointer;
2397
+ }
2398
+
2399
+ :where(.share-picker-target:hover, .share-picker-copy:hover) {
2400
+ background-color: var(--lily-surface-alt);
2401
+ }
2402
+
2403
+ /* Copy feedback. Never display:none this — that silences the live
2404
+ region for screen-reader users. */
2405
+ :where(.share-picker-status) {
2406
+ margin: var(--lily-space-xs) 0 0;
2407
+ font-size: 0.875em;
2408
+ color: var(--lily-text-muted);
2409
+ }
2410
+
2411
+ /* The status paragraph is always rendered so the aria-live region
2412
+ exists before the first announcement (see above); empty, it still
2413
+ generates a line box and carries the margin-top set above, both of
2414
+ which push the button row out of alignment in a flex layout like
2415
+ picker-bar (found independently by two downstream consumers, each
2416
+ patching around it locally). :empty here matches a Svelte-rendered
2417
+ {status} of "" (a zero-length text node doesn't count as a child
2418
+ per the CSS spec), so this only applies while there is nothing to
2419
+ announce, and normal spacing returns the moment status has text. */
2420
+ :where(.share-picker-status:empty) {
2421
+ margin: 0;
2422
+ line-height: 0;
2423
+ }
2424
+
2425
+ :where(
2426
+ .theme-picker-list,
2427
+ .locale-picker-list,
2428
+ .text-size-picker-list,
2429
+ .share-picker-list
2430
+ ) {
2431
+ position: absolute;
2432
+ z-index: 10;
2433
+ inset-inline-start: 0;
2434
+ min-width: max-content;
2435
+ max-height: 60vh;
2436
+ margin-block-start: var(--lily-space-xs);
2437
+ padding: var(--lily-space-xs);
2438
+ overflow-y: auto;
2439
+ list-style: none;
2440
+ color: var(--lily-text);
2441
+ background-color: var(--lily-surface);
2442
+ border: var(--border) solid var(--lily-border-strong);
2443
+ border-radius: var(--lily-radius-md);
2444
+ box-shadow: var(--lily-shadow-md);
2445
+ }
2446
+
2447
+ /* Explicit, because the rules above would otherwise beat the UA
2448
+ stylesheet's [hidden] { display: none }. */
2449
+ :where(
2450
+ .theme-picker-list,
2451
+ .locale-picker-list,
2452
+ .text-size-picker-list,
2453
+ .share-picker-list
2454
+ )[hidden] {
2455
+ display: none;
2456
+ }
2457
+
2458
+ :where(.theme-picker-list, .locale-picker-list, .text-size-picker-list)
2459
+ > :where(
2460
+ .theme-picker-option,
2461
+ .locale-picker-option,
2462
+ .text-size-picker-option
2463
+ ) {
2464
+ padding: var(--lily-space-xs) var(--lily-space-sm);
2465
+ border-radius: var(--lily-radius-sm);
2466
+ cursor: pointer;
2467
+ }
2468
+
2469
+ /* data-active is the keyboard cursor (aria-activedescendant); focus
2470
+ stays on the listbox, so this is the only visible cue for it. */
2471
+ :where(.theme-picker-list, .locale-picker-list, .text-size-picker-list)
2472
+ > :where([data-active]) {
2473
+ background-color: var(--lily-surface-alt);
2474
+ outline: var(--lily-focus-ring);
2475
+ outline-offset: -2px;
2476
+ }
2477
+
2478
+ /* aria-selected marks the applied theme/locale, which is not the
2479
+ same thing as the keyboard cursor. Both can be visible at once. */
2480
+ :where(.theme-picker-list, .locale-picker-list, .text-size-picker-list)
2481
+ > :where([aria-selected="true"]) {
2482
+ font-weight: 600;
2483
+ background-color: var(--lily-surface-sunken);
2484
+ }
2485
+
2486
+ /* Locale options inherit the select colours, same as theme options. */
2487
+ :where(.locale-picker-option) {
2488
+ background-color: var(--lily-surface);
2489
+ color: var(--lily-text);
2490
+ }
2491
+
2492
+ /* ----------------------------------------------------------------------
2493
+ * date-time-picker
2494
+ *
2495
+ * The one helper that is a form control rather than a page-header
2496
+ * control, so it is the only one with a text field beside its trigger
2497
+ * and a dialog rather than a listbox. The field itself needs no rules
2498
+ * here: `.date-time-picker-input` already matches the generic
2499
+ * [class$="-input"] baseline further up.
2500
+ * -------------------------------------------------------------------- */
2501
+
2502
+ :where(.date-time-picker-field) {
2503
+ display: flex;
2504
+ align-items: center;
2505
+ gap: var(--lily-space-xs);
2506
+ }
2507
+
2508
+ :where(.date-time-picker-dialog) {
2509
+ position: absolute;
2510
+ z-index: 10;
2511
+ inset-inline-start: 0;
2512
+ margin-block-start: var(--lily-space-xs);
2513
+ padding: var(--lily-space-sm);
2514
+ color: var(--lily-text);
2515
+ background-color: var(--lily-surface);
2516
+ border: var(--border) solid var(--lily-border-strong);
2517
+ border-radius: var(--lily-radius-md);
2518
+ box-shadow: var(--lily-shadow-md);
2519
+ }
2520
+
2521
+ /* Explicit, because the rule above would otherwise beat the UA
2522
+ stylesheet's [hidden] { display: none } — the same trap the
2523
+ listbox popups have to step around. */
2524
+ :where(.date-time-picker-dialog)[hidden] {
2525
+ display: none;
2526
+ }
2527
+
2528
+ :where(.date-time-picker-header) {
2529
+ display: flex;
2530
+ align-items: center;
2531
+ justify-content: space-between;
2532
+ gap: var(--lily-space-xs);
2533
+ margin-block-end: var(--lily-space-xs);
2534
+ }
2535
+
2536
+ /* The month/year heading is a live region, so it must keep its box
2537
+ even while empty — never display:none it. */
2538
+ :where(.date-time-picker-period) {
2539
+ flex: 1;
2540
+ font-weight: 600;
2541
+ text-align: center;
2542
+ }
2543
+
2544
+ /* The four navigation buttons and the footer/shortcut buttons do not
2545
+ end in `-button`, so the generic button family never reaches them. */
2546
+ :where(
2547
+ .date-time-picker-previous-year,
2548
+ .date-time-picker-previous-month,
2549
+ .date-time-picker-previous-week,
2550
+ .date-time-picker-previous-day,
2551
+ .date-time-picker-next-day,
2552
+ .date-time-picker-next-week,
2553
+ .date-time-picker-next-month,
2554
+ .date-time-picker-next-year,
2555
+ .date-time-picker-shortcut,
2556
+ .date-time-picker-clear,
2557
+ .date-time-picker-cancel,
2558
+ .date-time-picker-confirm
2559
+ ) {
2560
+ padding: var(--lily-space-xs) var(--lily-space-sm);
2561
+ font: inherit;
2562
+ color: var(--lily-text);
2563
+ background-color: var(--lily-surface);
2564
+ border: var(--border) solid var(--lily-border-strong);
2565
+ border-radius: var(--lily-radius-sm);
2566
+ cursor: pointer;
2567
+ }
2568
+
2569
+ :where(
2570
+ .date-time-picker-previous-year:hover,
2571
+ .date-time-picker-previous-month:hover,
2572
+ .date-time-picker-previous-week:hover,
2573
+ .date-time-picker-previous-day:hover,
2574
+ .date-time-picker-next-day:hover,
2575
+ .date-time-picker-next-week:hover,
2576
+ .date-time-picker-next-month:hover,
2577
+ .date-time-picker-next-year:hover,
2578
+ .date-time-picker-shortcut:hover,
2579
+ .date-time-picker-clear:hover,
2580
+ .date-time-picker-cancel:hover,
2581
+ .date-time-picker-confirm:hover
2582
+ ) {
2583
+ background-color: var(--lily-surface-alt);
2584
+ }
2585
+
2586
+ /* The commit action is the primary one on the dialog. */
2587
+ :where(.date-time-picker-confirm) {
2588
+ color: var(--color-primary-content);
2589
+ background-color: var(--color-primary);
2590
+ border-color: var(--color-primary);
2591
+ }
2592
+
2593
+ :where(.date-time-picker-calendar) {
2594
+ border-collapse: collapse;
2595
+ table-layout: fixed;
2596
+ }
2597
+
2598
+ :where(.date-time-picker-weekday, .date-time-picker-week-heading) {
2599
+ padding: var(--lily-space-xs);
2600
+ font-size: 0.8125em;
2601
+ font-weight: 600;
2602
+ color: var(--lily-text-muted);
2603
+ }
2604
+
2605
+ /* Week numbers are reference data, not navigation: muted, and set in
2606
+ tabular figures so the column does not shimmer as months change. */
2607
+ :where(.date-time-picker-week) {
2608
+ padding-inline-end: var(--lily-space-xs);
2609
+ font-size: 0.8125em;
2610
+ font-weight: 400;
2611
+ font-variant-numeric: tabular-nums;
2612
+ color: var(--lily-text-muted);
2613
+ text-align: end;
2614
+ }
2615
+
2616
+ :where(.date-time-picker-day) {
2617
+ display: flex;
2618
+ align-items: center;
2619
+ justify-content: center;
2620
+ /* 2rem is a compromise: WCAG 2.5.8 (AA) wants 24px, 2.5.5 (AAA)
2621
+ wants 44px, and seven 44px columns do not fit a 320px viewport
2622
+ alongside dialog chrome. Consumers targeting AAA should enlarge
2623
+ these and let the dialog go full-width on small screens. */
2624
+ min-width: 2rem;
2625
+ min-height: 2rem;
2626
+ padding: 0;
2627
+ font: inherit;
2628
+ font-variant-numeric: tabular-nums;
2629
+ color: var(--lily-text);
2630
+ background: none;
2631
+ border: var(--border) solid transparent;
2632
+ border-radius: var(--lily-radius-sm);
2633
+ cursor: pointer;
2634
+ }
2635
+
2636
+ :where(.date-time-picker-day:hover:not([data-disabled])) {
2637
+ background-color: var(--lily-surface-alt);
2638
+ }
2639
+
2640
+ /* Adjacent-month days stay legible rather than being hidden: they are
2641
+ real, selectable dates, and blanking them makes the grid harder to
2642
+ scan across a month boundary. */
2643
+ :where(.date-time-picker-day[data-outside]) {
2644
+ color: var(--lily-text-muted);
2645
+ }
2646
+
2647
+ /* Today is marked with a border, the selection with a fill. Two
2648
+ different channels, so a day that is both still reads as both. */
2649
+ :where(.date-time-picker-day[data-today]) {
2650
+ border-color: var(--lily-border-strong);
2651
+ font-weight: 600;
2652
+ }
2653
+
2654
+ :where(.date-time-picker-day[data-selected]) {
2655
+ color: var(--color-primary-content);
2656
+ background-color: var(--color-primary);
2657
+ border-color: var(--color-primary);
2658
+ }
2659
+
2660
+ :where(.date-time-picker-day[data-disabled]) {
2661
+ color: var(--lily-text-muted);
2662
+ cursor: not-allowed;
2663
+ /* Struck through as well as dimmed: "unavailable" must not rest on
2664
+ contrast alone (WCAG 1.4.1). */
2665
+ text-decoration: line-through;
2666
+ }
2667
+
2668
+ :where(.date-time-picker-time) {
2669
+ display: flex;
2670
+ flex-wrap: wrap;
2671
+ align-items: center;
2672
+ gap: var(--lily-space-xs);
2673
+ margin-block-start: var(--lily-space-sm);
2674
+ }
2675
+
2676
+ :where(.date-time-picker-time-label) {
2677
+ font-size: 0.8125em;
2678
+ color: var(--lily-text-muted);
2679
+ }
2680
+
2681
+ :where(.date-time-picker-time-zone) {
2682
+ display: flex;
2683
+ flex-wrap: wrap;
2684
+ align-items: center;
2685
+ gap: var(--lily-space-xs);
2686
+ margin-block-end: var(--lily-space-sm);
2687
+ }
2688
+
2689
+ :where(.date-time-picker-time-zone-label) {
2690
+ font-size: 0.8125em;
2691
+ color: var(--lily-text-muted);
2692
+ }
2693
+
2694
+ :where(
2695
+ .date-time-picker-hour,
2696
+ .date-time-picker-minute,
2697
+ .date-time-picker-meridiem,
2698
+ .date-time-picker-time-zone-select
2699
+ ) {
2700
+ padding: var(--lily-space-xs);
2701
+ font: inherit;
2702
+ color: var(--lily-text);
2703
+ background-color: var(--lily-surface);
2704
+ border: var(--border) solid var(--lily-border-strong);
2705
+ border-radius: var(--lily-radius-sm);
2706
+ }
2707
+
2708
+ :where(.date-time-picker-shortcuts) {
2709
+ display: flex;
2710
+ flex-wrap: wrap;
2711
+ gap: var(--lily-space-xs);
2712
+ margin-block-start: var(--lily-space-sm);
2713
+ }
2714
+
2715
+ :where(.date-time-picker-footer) {
2716
+ display: flex;
2717
+ justify-content: flex-end;
2718
+ gap: var(--lily-space-xs);
2719
+ margin-block-start: var(--lily-space-sm);
2720
+ }
2721
+
2722
+ /* Combobox / listbox / autosuggest — non-native variant of <select>. */
2723
+ :where(.combobox, .listbox, .autosuggest) {
2724
+ background-color: var(--lily-surface);
2725
+ border: var(--border) solid var(--lily-border-strong);
2726
+ border-radius: var(--lily-radius-md);
2727
+ }
2728
+
2729
+ /* ==========================================================================
2730
+ * Editorial typography
2731
+ * --------------------------------------------------------------------------
2732
+ * Headlines, body text, byline, blockquote, code — the long-form reading
2733
+ * primitives. Headlines use clamp() so they shrink on narrow viewports
2734
+ * without media queries.
2735
+ * ====================================================================== */
2736
+
2737
+ :where(.headline, .hero-headline, .section-heading) {
2738
+ color: var(--lily-text);
2739
+ font-family: var(--lily-font-body);
2740
+ font-weight: 700;
2741
+ line-height: 1.15;
2742
+ margin: 0 0 0.5rem 0;
2743
+ }
2744
+ :where(.headline) {
2745
+ font-size: clamp(1.5rem, 3vw, 2.25rem);
2746
+ }
2747
+ :where(.hero-headline) {
2748
+ font-size: clamp(2rem, 5vw, 3.5rem);
2749
+ }
2750
+ :where(.section-heading) {
2751
+ font-size: clamp(1.25rem, 2vw, 1.75rem);
2752
+ }
2753
+ /* Body text — 1.6 line-height (sweet spot for reading). */
2754
+ :where(.body-text) {
2755
+ line-height: 1.6;
2756
+ color: var(--lily-text);
2757
+ }
2758
+ /* Byline — author / date line under a headline. */
2759
+ :where(.byline) {
2760
+ color: var(--lily-text-muted);
2761
+ font-size: 0.875rem;
2762
+ }
2763
+ /* Blockquote — primary-coloured rail + italic body. */
2764
+ :where(.blockquote) {
2765
+ margin: 0.5rem 0;
2766
+ padding: 0.25rem 0.5rem;
2767
+ border-inline-start: 0.25rem solid var(--color-primary);
2768
+ color: var(--lily-text);
2769
+ font-style: italic;
2770
+ }
2771
+ /* End-notes / related content — top-bordered section at the foot. */
2772
+ :where(.end-notes, .related-content) {
2773
+ margin-top: 1rem;
2774
+ padding-top: 0.5rem;
2775
+ border-top: var(--border) solid var(--lily-border);
2776
+ }
2777
+ /* Caption / footnote / citation — small muted text. */
2778
+ :where(.caption, .footnote, .citation) {
2779
+ font-size: 0.875rem;
2780
+ color: var(--lily-text-muted);
2781
+ }
2782
+ /* KBD — keyboard key glyph; thicker bottom border simulates the keytop. */
2783
+ :where(.kbd) {
2784
+ display: inline-block;
2785
+ padding: 0.125rem 0.4rem;
2786
+ font-family: var(--lily-font-mono);
2787
+ font-size: 0.85em;
2788
+ background-color: var(--lily-surface-alt);
2789
+ border: var(--border) solid var(--lily-border-strong);
2790
+ border-bottom-width: 2px;
2791
+ border-radius: var(--lily-radius-sm);
2792
+ }
2793
+ /* Inline code — slight background tint + monospaced font. */
2794
+ :where(.code) {
2795
+ font-family: var(--lily-font-mono);
2796
+ padding: 0.1em 0.3em;
2797
+ background-color: var(--lily-surface-alt);
2798
+ border-radius: var(--lily-radius-sm);
2799
+ font-size: 0.9em;
2800
+ }
2801
+ /* Code block — full-width block with horizontal scroll. */
2802
+ :where(.code-block) {
2803
+ display: block;
2804
+ padding: 0.5rem;
2805
+ background-color: var(--color-neutral);
2806
+ color: var(--color-neutral-content);
2807
+ font-family: var(--lily-font-mono);
2808
+ border-radius: var(--lily-radius-md);
2809
+ overflow-x: auto;
2810
+ white-space: pre;
2811
+ }
2812
+ /* Inset text — left-railed callout for "Important" content
2813
+ (GOV.UK / NHS pattern). */
2814
+ :where(.inset-text) {
2815
+ padding: 0.375rem 0.5rem;
2816
+ border-inline-start: 0.25rem solid var(--color-primary);
2817
+ background-color: var(--lily-surface-alt);
2818
+ }
2819
+ /* Generic character — monospaced single-glyph display. */
2820
+ :where(.character) {
2821
+ font-family: var(--lily-font-mono);
2822
+ }
2823
+ /* Clamp-text — truncate to N lines (controlled by --lily-clamp-lines). */
2824
+ :where(.clamp-text) {
2825
+ display: -webkit-box;
2826
+ -webkit-line-clamp: var(--lily-clamp-lines, 3);
2827
+ line-clamp: var(--lily-clamp-lines, 3);
2828
+ -webkit-box-orient: vertical;
2829
+ overflow: hidden;
2830
+ }
2831
+ /* Screen-reader-only — visually hidden but available to AT. The
2832
+ classic clip-rect pattern (works in every screen reader). */
2833
+ :where(.screen-reader-span) {
2834
+ position: absolute;
2835
+ width: 1px;
2836
+ height: 1px;
2837
+ padding: 0;
2838
+ margin: -1px;
2839
+ overflow: hidden;
2840
+ clip: rect(0, 0, 0, 0);
2841
+ white-space: nowrap;
2842
+ border: 0;
2843
+ }
2844
+
2845
+ /* ==========================================================================
2846
+ * Hero, figure, photo, media
2847
+ * ====================================================================== */
2848
+
2849
+ /* Hero block — primary-filled banner with centred content. clamp() pad
2850
+ scales vertically across viewport sizes. */
2851
+ :where(.hero) {
2852
+ display: block;
2853
+ padding: clamp(2rem, 5vw, 4rem) 1rem;
2854
+ background-color: var(--color-primary);
2855
+ color: var(--color-primary-content);
2856
+ border-radius: var(--lily-radius-lg);
2857
+ text-align: center;
2858
+ }
2859
+ /* Figure — centred block with vertical margin. */
2860
+ :where(.figure) {
2861
+ margin: 1rem 0;
2862
+ text-align: center;
2863
+ }
2864
+ /* Feature photo — responsive lead image. */
2865
+ :where(.feature-photo) {
2866
+ display: block;
2867
+ width: 100%;
2868
+ height: auto;
2869
+ border-radius: var(--lily-radius-md);
2870
+ }
2871
+ /* Photo pack — responsive grid of thumbnails. */
2872
+ :where(.photo-pack) {
2873
+ display: grid;
2874
+ grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
2875
+ gap: 0.5rem;
2876
+ }
2877
+ /* Generic image — responsive block image. */
2878
+ :where(.image) {
2879
+ max-width: 100%;
2880
+ height: auto;
2881
+ display: block;
2882
+ }
2883
+ /* QR / barcode / signature — frame them as a labelled tile. */
2884
+ :where(.qr-code-image, .barcode-image, .signature-pad) {
2885
+ display: inline-block;
2886
+ background-color: var(--lily-surface);
2887
+ border: var(--border) solid var(--lily-border);
2888
+ border-radius: var(--lily-radius-md);
2889
+ padding: 0.5rem;
2890
+ }
2891
+ /* Signature pad — drawable surface; crosshair cursor signals input. */
2892
+ :where(.signature-pad) {
2893
+ min-height: 8rem;
2894
+ cursor: crosshair;
2895
+ }
2896
+ /* Video player — responsive black-letterboxed frame. */
2897
+ :where(.video-player) {
2898
+ display: block;
2899
+ width: 100%;
2900
+ max-width: 100%;
2901
+ border-radius: var(--lily-radius-md);
2902
+ /* Video chrome is black in every theme; pair the text colour with
2903
+ it here rather than inheriting the theme's (possibly dark)
2904
+ content colour onto a black surface. */
2905
+ background-color: black;
2906
+ color: white;
2907
+ }
2908
+
2909
+ /* ==========================================================================
2910
+ * Mockups & pictograms
2911
+ * --------------------------------------------------------------------------
2912
+ * Device frames for screenshots / illustrations — browser, phone, tablet,
2913
+ * laptop, watch, shell window. All share the same rounded-outline frame
2914
+ * baseline; specific frames override the radius / border-width.
2915
+ * ====================================================================== */
2916
+
2917
+ :where(
2918
+ .mockup-browser,
2919
+ .mockup-window,
2920
+ .mockup-shell,
2921
+ .mockup-phone-portrait,
2922
+ .mockup-phone-landscape,
2923
+ .mockup-tablet-portrait,
2924
+ .mockup-tablet-landscape,
2925
+ .mockup-laptop,
2926
+ .mockup-watch
2927
+ ) {
2928
+ display: block;
2929
+ background-color: var(--lily-surface);
2930
+ border: 2px solid var(--color-neutral);
2931
+ border-radius: var(--lily-radius-lg);
2932
+ overflow: hidden;
2933
+ box-shadow: var(--lily-shadow-md);
2934
+ }
2935
+ /* Mockup shell — terminal window; dark neutral fill + mono font. */
2936
+ :where(.mockup-shell) {
2937
+ background-color: var(--color-neutral);
2938
+ color: var(--color-neutral-content);
2939
+ font-family: var(--lily-font-mono);
2940
+ padding: 1rem;
2941
+ }
2942
+ /* Phones — heavier bezel (6px) and more rounded corners. */
2943
+ :where(.mockup-phone-portrait, .mockup-phone-landscape) {
2944
+ border-radius: 1.5rem;
2945
+ border-width: 6px;
2946
+ }
2947
+ /* Smartwatch — chunky bezel (4px) and pill radius. */
2948
+ :where(.mockup-watch) {
2949
+ border-radius: 1rem;
2950
+ border-width: 4px;
2951
+ }
2952
+ /* Pictogram — stacked icon + caption, centred. */
2953
+ :where(.pictogram) {
2954
+ display: inline-flex;
2955
+ flex-direction: column;
2956
+ align-items: center;
2957
+ gap: 0.5rem;
2958
+ padding: 1rem;
2959
+ text-align: center;
2960
+ }
2961
+
2962
+ /* ==========================================================================
2963
+ * Charts (containers only — content supplied by consumer)
2964
+ * --------------------------------------------------------------------------
2965
+ * Headless: Lily provides the frame + a 6-colour palette as custom
2966
+ * properties. The consumer's chart library (D3, Chart.js, ECharts, etc.)
2967
+ * reads `var(--lily-chart-color-1..6)` for its series colours.
2968
+ * ====================================================================== */
2969
+
2970
+ :where(
2971
+ .area-chart,
2972
+ .bar-chart,
2973
+ .column-chart,
2974
+ .line-chart,
2975
+ .scatter-chart,
2976
+ .sparkline,
2977
+ .tile-map,
2978
+ .graphic-block
2979
+ ) {
2980
+ display: block;
2981
+ background-color: var(--lily-surface);
2982
+ border-radius: var(--lily-radius-md);
2983
+ padding: 0.5rem;
2984
+ color: var(--lily-text);
2985
+ /* Six-colour palette — the consumer's chart library picks these up. */
2986
+ --lily-chart-color-1: var(--color-primary);
2987
+ --lily-chart-color-2: var(--color-secondary);
2988
+ --lily-chart-color-3: var(--color-accent);
2989
+ --lily-chart-color-4: var(--color-info);
2990
+ --lily-chart-color-5: var(--color-success);
2991
+ --lily-chart-color-6: var(--color-warning);
2992
+ }
2993
+ /* Sparkline — small inline chart; no padding. */
2994
+ :where(.sparkline) {
2995
+ padding: 0;
2996
+ height: 1.5rem;
2997
+ width: 6rem;
2998
+ display: inline-block;
2999
+ }
3000
+ /* Graphic block — bordered chart container. */
3001
+ :where(.graphic-block) {
3002
+ border: var(--border) solid var(--lily-border);
3003
+ }
3004
+
3005
+ /* ==========================================================================
3006
+ * Scrolling, splits, resize
3007
+ * ====================================================================== */
3008
+
3009
+ /* Scroll area — capped at 20rem, auto-overflow. */
3010
+ :where(.scroll-area) {
3011
+ overflow: auto;
3012
+ max-height: 20rem;
3013
+ }
3014
+ /* Horizontal scroller — flex row that overflows on x. */
3015
+ :where(.horizontal-scroller) {
3016
+ overflow-x: auto;
3017
+ display: flex;
3018
+ gap: 0.5rem;
3019
+ }
3020
+ /* Scroller (Reuters-pattern scrollytelling) — positioning anchor. */
3021
+ :where(.scroller, .scroller-base, .scroller-video) {
3022
+ position: relative;
3023
+ display: block;
3024
+ }
3025
+ /* Custom scrollbar track. */
3026
+ :where(.scroll-bar) {
3027
+ background-color: var(--lily-surface-sunken);
3028
+ border-radius: 999px;
3029
+ height: 0.5rem;
3030
+ }
3031
+ /* Affix — stick to the top of the viewport on scroll. */
3032
+ :where(.affix) {
3033
+ position: sticky;
3034
+ inset-block-start: 0;
3035
+ }
3036
+ /* Visible — IntersectionObserver target; baseline is a visible block. */
3037
+ :where(.visible) {
3038
+ display: block;
3039
+ }
3040
+ /* Resizable — native CSS resize handle (corner). */
3041
+ :where(.resizable) {
3042
+ resize: both;
3043
+ overflow: auto;
3044
+ }
3045
+ /* Split-view + splitter — two-pane horizontal layout. */
3046
+ :where(.split-view) {
3047
+ display: flex;
3048
+ align-items: stretch;
3049
+ gap: 0;
3050
+ }
3051
+ :where(.splitter) {
3052
+ flex: 0 0 0.375rem;
3053
+ background-color: var(--lily-border);
3054
+ cursor: col-resize;
3055
+ }
3056
+
3057
+ /* ==========================================================================
3058
+ * Misc unique components
3059
+ * ====================================================================== */
3060
+
3061
+ /* Statistic — stacked label + value; value is large and primary-coloured. */
3062
+ :where(.statistic) {
3063
+ display: inline-flex;
3064
+ flex-direction: column;
3065
+ gap: 0.25rem;
3066
+ }
3067
+ :where(.statistic > [data-part="value"]) {
3068
+ font-size: 1.5rem;
3069
+ font-weight: 700;
3070
+ color: var(--color-primary);
3071
+ }
3072
+ /* Watermark — diagonal repeating-line pattern at 8% opacity behind content. */
3073
+ :where(.watermark) {
3074
+ position: absolute;
3075
+ inset: 0;
3076
+ pointer-events: none;
3077
+ opacity: 0.08;
3078
+ background: repeating-linear-gradient(
3079
+ -45deg,
3080
+ var(--lily-text) 0 1px,
3081
+ transparent 1px 8rem
3082
+ );
3083
+ }
3084
+ /* Beach ball (macOS "waiting" indicator) — six-colour conic wheel. */
3085
+ :where(.beach-ball) {
3086
+ display: inline-block;
3087
+ width: 2rem;
3088
+ height: 2rem;
3089
+ border-radius: 50%;
3090
+ background: conic-gradient(
3091
+ var(--color-error),
3092
+ var(--color-warning),
3093
+ var(--color-success),
3094
+ var(--color-info),
3095
+ var(--color-primary),
3096
+ var(--color-error)
3097
+ );
3098
+ animation: lily-spin 2s linear infinite;
3099
+ }
3100
+ /* Diff — side-by-side two-column comparison. */
3101
+ :where(.diff) {
3102
+ display: grid;
3103
+ grid-template-columns: 1fr 1fr;
3104
+ gap: 0.5rem;
3105
+ }
3106
+ /* Addressograph box — paper-form ID label (NHS / hospital pattern).
3107
+ Reads as a paper-form sticker; tight padding preserves the
3108
+ look of a clinical patient-ID label. */
3109
+ :where(.addressograph-box) {
3110
+ display: block;
3111
+ padding: 0.25rem 0.5rem;
3112
+ background-color: var(--lily-surface-alt);
3113
+ border: 2px solid var(--lily-text);
3114
+ font-family: var(--lily-font-mono);
3115
+ line-height: 1.3;
3116
+ }
3117
+ /* Government identifier — dark chrome bar containing an ID number. */
3118
+ :where(.government-identifier) {
3119
+ display: flex;
3120
+ align-items: center;
3121
+ gap: 0.375rem;
3122
+ padding: 0.25rem 0.5rem;
3123
+ background-color: var(--color-neutral);
3124
+ color: var(--color-neutral-content);
3125
+ }
3126
+ /* Share / signup / CTA panels — alt-surface promo blocks. */
3127
+ :where(.share-page, .newsletter-signup, .call-to-action) {
3128
+ padding: 0.5rem;
3129
+ background-color: var(--lily-surface-alt);
3130
+ border-radius: var(--lily-radius-md);
3131
+ }
3132
+ /* CTA — override with primary fill so it reads as the page action. */
3133
+ :where(.call-to-action) {
3134
+ background-color: var(--color-primary);
3135
+ color: var(--color-primary-content);
3136
+ }
3137
+ /* Links inside the call-to-action inherit its content colour —
3138
+ otherwise the UA default link blue lands on the primary fill. */
3139
+ :where(.call-to-action) :where(a) {
3140
+ color: inherit;
3141
+ }
3142
+ /* Expander / collapsible / details — boxed disclosure controls. */
3143
+ :where(.expander, .collapsible, .details) {
3144
+ display: block;
3145
+ border: var(--border) solid var(--lily-border);
3146
+ border-radius: var(--lily-radius-md);
3147
+ padding: 0.25rem 0.5rem;
3148
+ }
3149
+ :where(.details > summary, .expander > summary, .collapsible > summary) {
3150
+ cursor: pointer;
3151
+ font-weight: 600;
3152
+ padding: 0.25rem 0;
3153
+ }
3154
+ /* Carousel — horizontal scroller with snap-points so each slide locks
3155
+ into view as the user swipes. */
3156
+ :where(.carousel) {
3157
+ display: flex;
3158
+ overflow-x: auto;
3159
+ scroll-snap-type: x mandatory;
3160
+ gap: 0.5rem;
3161
+ }
3162
+ :where(.carousel > *) {
3163
+ scroll-snap-align: start;
3164
+ flex: 0 0 auto;
3165
+ }
3166
+
3167
+ /* --- Semantic entities — neutral display blocks. ----------------------- */
3168
+ :where(
3169
+ .person,
3170
+ .organization,
3171
+ .place,
3172
+ .event,
3173
+ .comment,
3174
+ .question,
3175
+ .answer
3176
+ ) {
3177
+ display: block;
3178
+ padding: 0.25rem 0;
3179
+ }
3180
+ /* Draft wrapper — dashed border + optional "Draft" pill prefix. */
3181
+ :where(.draft) {
3182
+ border: 1px dashed var(--lily-border-strong);
3183
+ background-color: var(--lily-surface-alt);
3184
+ padding: 0.5rem;
3185
+ border-radius: var(--lily-radius-md);
3186
+ }
3187
+ :where(.draft[data-status="draft"])::before {
3188
+ content: "Draft";
3189
+ display: inline-block;
3190
+ margin-inline-end: 0.5rem;
3191
+ padding: 0.125rem 0.5rem;
3192
+ background-color: var(--color-warning);
3193
+ color: var(--color-warning-content);
3194
+ border-radius: 999px;
3195
+ font-size: 0.75rem;
3196
+ font-weight: 600;
3197
+ }
3198
+
3199
+ /* Dial — circular gauge driven by --dial-value (0–100). */
3200
+ :where(.dial) {
3201
+ display: inline-block;
3202
+ width: 4rem;
3203
+ height: 4rem;
3204
+ border-radius: 50%;
3205
+ background: conic-gradient(
3206
+ var(--color-primary) calc(var(--dial-value, 0) * 1%),
3207
+ var(--lily-surface-sunken) 0
3208
+ );
3209
+ }
3210
+
3211
+ /* File upload — drop-zone aesthetic: dashed border, alt surface. */
3212
+ :where(.file-upload) {
3213
+ display: block;
3214
+ padding: 0.75rem;
3215
+ background-color: var(--lily-surface-alt);
3216
+ border: 2px dashed var(--lily-border-strong);
3217
+ border-radius: var(--lily-radius-md);
3218
+ text-align: center;
3219
+ cursor: pointer;
3220
+ }
3221
+ /* File manager — surface-coloured frame holding a file list. */
3222
+ :where(.file-manager) {
3223
+ display: block;
3224
+ background-color: var(--lily-surface);
3225
+ border: var(--border) solid var(--lily-border);
3226
+ border-radius: var(--lily-radius-md);
3227
+ padding: 0.5rem;
3228
+ }
3229
+ /* Transfer list — two columns + central button stack. */
3230
+ :where(.transfer-list) {
3231
+ display: grid;
3232
+ grid-template-columns: 1fr auto 1fr;
3233
+ gap: 0.5rem;
3234
+ align-items: center;
3235
+ }
3236
+
3237
+ /* Tour — onboarding card with medium elevation. */
3238
+ :where(.tour) {
3239
+ display: block;
3240
+ background-color: var(--lily-surface);
3241
+ border: var(--border) solid var(--lily-border);
3242
+ border-radius: var(--lily-radius-lg);
3243
+ padding: 0.5rem;
3244
+ box-shadow: var(--lily-shadow-md);
3245
+ }
3246
+
3247
+ /* Editable text — dashed underline indicates click-to-edit. */
3248
+ :where(.editable) {
3249
+ display: inline-block;
3250
+ padding: 0.125rem 0.25rem;
3251
+ border-bottom: 1px dashed var(--lily-border-strong);
3252
+ cursor: text;
3253
+ }
3254
+ /* Editing state — alt surface + primary underline. */
3255
+ :where(.editable[aria-expanded="true"]) {
3256
+ background-color: var(--lily-surface-alt);
3257
+ border-bottom-color: var(--color-primary);
3258
+ }
3259
+
3260
+ /* Accordion-checkbox — a checkbox that toggles a panel via sibling
3261
+ combinator (no JS needed). */
3262
+ :where(.accordion-checkbox) {
3263
+ display: block;
3264
+ border: var(--border) solid var(--lily-border);
3265
+ border-radius: var(--lily-radius-md);
3266
+ padding: 0.25rem 0.5rem;
3267
+ }
3268
+ :where(.accordion-checkbox-input) {
3269
+ margin-inline-end: 0.5rem;
3270
+ }
3271
+ :where(.accordion-checkbox-panel) {
3272
+ display: none;
3273
+ padding-block-start: 0.5rem;
3274
+ }
3275
+ /* Reveal the panel when the input is checked. */
3276
+ :where(.accordion-checkbox-input:checked ~ .accordion-checkbox-panel) {
3277
+ display: block;
3278
+ }
3279
+
3280
+ /* Icon — 1em square container; sizes with surrounding text. */
3281
+ :where(.icon) {
3282
+ display: inline-flex;
3283
+ align-items: center;
3284
+ justify-content: center;
3285
+ width: 1em;
3286
+ height: 1em;
3287
+ flex: 0 0 auto;
3288
+ }
3289
+
3290
+ /* Emoji — inline glyph; not italicised. */
3291
+ :where(.emoji) {
3292
+ display: inline-block;
3293
+ font-style: normal;
3294
+ }
3295
+
3296
+ /* Slider — primary-filled track based on --slider-value (0–100). */
3297
+ :where(.slider) {
3298
+ display: block;
3299
+ width: 100%;
3300
+ height: 1.5rem;
3301
+ background: linear-gradient(
3302
+ to right,
3303
+ var(--color-primary) calc(var(--slider-value, 50) * 1%),
3304
+ var(--lily-surface-sunken) 0
3305
+ );
3306
+ border-radius: 999px;
3307
+ }
3308
+
3309
+ /* Timer — fixed-width digits so the display doesn't jiggle as it ticks. */
3310
+ :where(.timer) {
3311
+ display: inline-block;
3312
+ font-family: var(--lily-font-mono);
3313
+ font-variant-numeric: tabular-nums;
3314
+ font-weight: 600;
3315
+ }
3316
+
3317
+ /* Medical banner inner boxes — spacing-only override. */
3318
+ :where(.medical-banner-box-for-advice, .medical-banner-box-for-danger) {
3319
+ padding: 0.75rem 1rem;
3320
+ border-radius: var(--lily-radius-md);
3321
+ margin: 0.25rem 0;
3322
+ }
3323
+
3324
+ /* ==========================================================================
3325
+ * Print
3326
+ * --------------------------------------------------------------------------
3327
+ * On paper, transient/floating chrome (skip-link, FAB, toast, tooltip)
3328
+ * is meaningless — hide it. Card-like surfaces drop their drop-shadow
3329
+ * and shift to a high-contrast outline so they're readable in monochrome.
3330
+ * ====================================================================== */
3331
+ @media print {
3332
+ :where(
3333
+ .skip-link,
3334
+ .float-button,
3335
+ .toast,
3336
+ .sonner,
3337
+ .tooltip,
3338
+ .coachmark
3339
+ ) {
3340
+ display: none !important;
3341
+ }
3342
+ :where(.card, .panel, .dialog, .alert) {
3343
+ box-shadow: none;
3344
+ border-color: var(--lily-text);
3345
+ }
3346
+ }
3347
+ }
3348
+
3349
+ /* ============================================================================
3350
+ * WCAG 2.2 target-size (minimum) floor — 2.5.8
3351
+ *
3352
+ * Every interactive Lily hook offers a >=24x24 CSS-pixel target. Links
3353
+ * that render as short inline text (a pagination "1", a breadcrumb crumb)
3354
+ * fail the 24px floor under typographic sizing alone, so the link-shaped
3355
+ * hooks become inline-block with a minimum box. :where() keeps specificity
3356
+ * at zero, so any consumer rule still wins. Shared verbatim by all themes.
3357
+ * ========================================================================= */
3358
+
3359
+ :where(.action-link,
3360
+ .back-link,
3361
+ .breadcrumb-list-item > a,
3362
+ .pagination-list-item > a,
3363
+ .contents-list-item > a,
3364
+ .section-list-item > a) {
3365
+ display: inline-block;
3366
+ min-inline-size: 24px;
3367
+ min-block-size: 24px;
3368
+ }
3369
+
3370
+ :where(.pagination-list-item > a) {
3371
+ text-align: center;
3372
+ }
3373
+
3374
+ :where(input:not([type="checkbox"]):not([type="radio"]), select, textarea) {
3375
+ min-block-size: 24px;
3376
+ }
3377
+
3378
+ /* ============================================================================
3379
+ * Overflow guards — narrow-viewport safety, shared verbatim by all themes.
3380
+ *
3381
+ * Content that is intrinsically wider than the viewport scrolls or wraps
3382
+ * within its own box instead of pushing the page wider. Deliberately NOT
3383
+ * `display: block` on <table> — that strips implicit table semantics for
3384
+ * assistive technology; capping width and letting cell content wrap keeps
3385
+ * both the layout and the roles. Proven in the example apps' own
3386
+ * stylesheets before the theme layer took over component styling.
3387
+ * ========================================================================= */
3388
+
3389
+ :where(pre) {
3390
+ max-width: 100%;
3391
+ overflow-x: auto;
3392
+ }
3393
+
3394
+ :where(table) {
3395
+ max-width: 100%;
3396
+ }
3397
+
3398
+ :where(th, td) {
3399
+ overflow-wrap: anywhere;
3400
+ }
3401
+
3402
+ :where(a, code) {
3403
+ overflow-wrap: anywhere;
3404
+ }