@ptbdynamic/plinth 0.0.0-stage → 0.1.1

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -2
  3. package/package.json +99 -4
  4. package/packages/css/dist/avatar.css +122 -0
  5. package/packages/css/dist/badge.css +76 -0
  6. package/packages/css/dist/button.css +273 -0
  7. package/packages/css/dist/callout.css +143 -0
  8. package/packages/css/dist/code-block.css +283 -0
  9. package/packages/css/dist/confirm-dialog.css +183 -0
  10. package/packages/css/dist/data-table.css +470 -0
  11. package/packages/css/dist/dialog.css +113 -0
  12. package/packages/css/dist/drawer.css +156 -0
  13. package/packages/css/dist/fonts/geist/LICENSE +93 -0
  14. package/packages/css/dist/fonts/geist/geist-cyrillic-ext-wght-normal.woff2 +0 -0
  15. package/packages/css/dist/fonts/geist/geist-cyrillic-wght-normal.woff2 +0 -0
  16. package/packages/css/dist/fonts/geist/geist-latin-ext-wght-normal.woff2 +0 -0
  17. package/packages/css/dist/fonts/geist/geist-latin-wght-normal.woff2 +0 -0
  18. package/packages/css/dist/fonts/geist/geist-vietnamese-wght-normal.woff2 +0 -0
  19. package/packages/css/dist/fonts/geist-mono/LICENSE +93 -0
  20. package/packages/css/dist/fonts/geist-mono/geist-mono-cyrillic-ext-wght-normal.woff2 +0 -0
  21. package/packages/css/dist/fonts/geist-mono/geist-mono-cyrillic-wght-normal.woff2 +0 -0
  22. package/packages/css/dist/fonts/geist-mono/geist-mono-latin-ext-wght-normal.woff2 +0 -0
  23. package/packages/css/dist/fonts/geist-mono/geist-mono-latin-wght-normal.woff2 +0 -0
  24. package/packages/css/dist/fonts/geist-mono/geist-mono-symbols2-wght-normal.woff2 +0 -0
  25. package/packages/css/dist/fonts/geist-mono/geist-mono-vietnamese-wght-normal.woff2 +0 -0
  26. package/packages/css/dist/fonts/index.css +109 -0
  27. package/packages/css/dist/header.css +110 -0
  28. package/packages/css/dist/icon.css +51 -0
  29. package/packages/css/dist/list-card.css +76 -0
  30. package/packages/css/dist/list-row.css +164 -0
  31. package/packages/css/dist/menu-sheet.css +84 -0
  32. package/packages/css/dist/modal.css +367 -0
  33. package/packages/css/dist/pill-row.css +197 -0
  34. package/packages/css/dist/plinth.css +3773 -0
  35. package/packages/css/dist/section-menu.css +225 -0
  36. package/packages/css/dist/sheet.css +239 -0
  37. package/packages/css/dist/tokens.css +405 -0
  38. package/packages/react/dist/PlinthAvatar.d.ts +17 -0
  39. package/packages/react/dist/PlinthBadge.d.ts +7 -0
  40. package/packages/react/dist/PlinthButton.d.ts +29 -0
  41. package/packages/react/dist/PlinthCallout.d.ts +16 -0
  42. package/packages/react/dist/PlinthCodeBlock.d.ts +24 -0
  43. package/packages/react/dist/PlinthConfirmDialog.d.ts +38 -0
  44. package/packages/react/dist/PlinthDataTable.d.ts +11 -0
  45. package/packages/react/dist/PlinthDataTableCell.d.ts +15 -0
  46. package/packages/react/dist/PlinthDataTableColumn.d.ts +13 -0
  47. package/packages/react/dist/PlinthDataTableEmpty.d.ts +9 -0
  48. package/packages/react/dist/PlinthDataTableRow.d.ts +9 -0
  49. package/packages/react/dist/PlinthDataTableSort.d.ts +4 -0
  50. package/packages/react/dist/PlinthDialog.d.ts +26 -0
  51. package/packages/react/dist/PlinthDrawer.d.ts +29 -0
  52. package/packages/react/dist/PlinthHeader.d.ts +12 -0
  53. package/packages/react/dist/PlinthIcon.d.ts +10 -0
  54. package/packages/react/dist/PlinthIconButton.d.ts +26 -0
  55. package/packages/react/dist/PlinthListCard.d.ts +14 -0
  56. package/packages/react/dist/PlinthListRow.d.ts +29 -0
  57. package/packages/react/dist/PlinthMenuSheet.d.ts +24 -0
  58. package/packages/react/dist/PlinthPill.d.ts +17 -0
  59. package/packages/react/dist/PlinthPillRow.d.ts +13 -0
  60. package/packages/react/dist/PlinthSectionMenu.d.ts +11 -0
  61. package/packages/react/dist/PlinthSectionMenuItem.d.ts +20 -0
  62. package/packages/react/dist/PlinthSheet.d.ts +26 -0
  63. package/packages/react/dist/avatar.d.ts +12 -0
  64. package/packages/react/dist/button.d.ts +21 -0
  65. package/packages/react/dist/callout.d.ts +19 -0
  66. package/packages/react/dist/code-block.d.ts +16 -0
  67. package/packages/react/dist/confirm-dialog.d.ts +23 -0
  68. package/packages/react/dist/data-table.d.ts +15 -0
  69. package/packages/react/dist/dialog.d.ts +10 -0
  70. package/packages/react/dist/drawer.d.ts +20 -0
  71. package/packages/react/dist/heading.d.ts +5 -0
  72. package/packages/react/dist/icon.d.ts +5 -0
  73. package/packages/react/dist/index.d.ts +40 -0
  74. package/packages/react/dist/index.js +1127 -0
  75. package/packages/react/dist/list-row.d.ts +5 -0
  76. package/packages/react/dist/markup.d.ts +9 -0
  77. package/packages/react/dist/sheet.d.ts +12 -0
  78. package/packages/react/dist/size.d.ts +13 -0
  79. package/packages/react/dist/swipe.d.ts +15 -0
  80. package/packages/react/dist/tones.d.ts +5 -0
  81. package/packages/tokens/dist/index.d.ts +218 -0
  82. package/packages/tokens/dist/index.js +3966 -0
  83. package/packages/tokens/dist/tailwind-theme.css +80 -0
  84. package/packages/tokens/dist/tokens.css +405 -0
  85. package/packages/tokens/dist/tokens.json +3762 -0
  86. package/packages/vue/dist/PlinthAvatar.vue.d.ts +24 -0
  87. package/packages/vue/dist/PlinthBadge.vue.d.ts +21 -0
  88. package/packages/vue/dist/PlinthButton.vue.d.ts +42 -0
  89. package/packages/vue/dist/PlinthCallout.vue.d.ts +31 -0
  90. package/packages/vue/dist/PlinthCodeBlock.vue.d.ts +49 -0
  91. package/packages/vue/dist/PlinthConfirmDialog.vue.d.ts +51 -0
  92. package/packages/vue/dist/PlinthDataTable.vue.d.ts +21 -0
  93. package/packages/vue/dist/PlinthDataTableCell.vue.d.ts +29 -0
  94. package/packages/vue/dist/PlinthDataTableColumn.vue.d.ts +26 -0
  95. package/packages/vue/dist/PlinthDataTableEmpty.vue.d.ts +19 -0
  96. package/packages/vue/dist/PlinthDataTableRow.vue.d.ts +19 -0
  97. package/packages/vue/dist/PlinthDataTableSort.vue.d.ts +13 -0
  98. package/packages/vue/dist/PlinthDialog.vue.d.ts +45 -0
  99. package/packages/vue/dist/PlinthDrawer.vue.d.ts +48 -0
  100. package/packages/vue/dist/PlinthHeader.vue.d.ts +19 -0
  101. package/packages/vue/dist/PlinthIcon.vue.d.ts +23 -0
  102. package/packages/vue/dist/PlinthIconButton.vue.d.ts +37 -0
  103. package/packages/vue/dist/PlinthListCard.vue.d.ts +26 -0
  104. package/packages/vue/dist/PlinthListRow.vue.d.ts +38 -0
  105. package/packages/vue/dist/PlinthMenuSheet.vue.d.ts +48 -0
  106. package/packages/vue/dist/PlinthPill.vue.d.ts +28 -0
  107. package/packages/vue/dist/PlinthPillRow.vue.d.ts +22 -0
  108. package/packages/vue/dist/PlinthSectionMenu.vue.d.ts +22 -0
  109. package/packages/vue/dist/PlinthSectionMenuItem.vue.d.ts +28 -0
  110. package/packages/vue/dist/PlinthSheet.vue.d.ts +45 -0
  111. package/packages/vue/dist/avatar.d.ts +12 -0
  112. package/packages/vue/dist/button.d.ts +5 -0
  113. package/packages/vue/dist/callout.d.ts +19 -0
  114. package/packages/vue/dist/code-block.d.ts +16 -0
  115. package/packages/vue/dist/confirm-dialog.d.ts +23 -0
  116. package/packages/vue/dist/data-table.d.ts +15 -0
  117. package/packages/vue/dist/dialog.d.ts +10 -0
  118. package/packages/vue/dist/drawer.d.ts +20 -0
  119. package/packages/vue/dist/heading.d.ts +5 -0
  120. package/packages/vue/dist/icon.d.ts +5 -0
  121. package/packages/vue/dist/index.d.ts +44 -0
  122. package/packages/vue/dist/index.js +1439 -0
  123. package/packages/vue/dist/list-row.d.ts +5 -0
  124. package/packages/vue/dist/sheet.d.ts +12 -0
  125. package/packages/vue/dist/size.d.ts +13 -0
  126. package/packages/vue/dist/slots.d.ts +3 -0
  127. package/packages/vue/dist/swipe.d.ts +12 -0
  128. package/packages/vue/dist/tones.d.ts +5 -0
@@ -0,0 +1,3773 @@
1
+ /* Built from packages/tokens/tokens/*.json. Do not edit by hand. */
2
+
3
+ @layer plinth {
4
+ :root {
5
+ --plinth-ref-color-neutral-0: oklch(1 0 0);
6
+ --plinth-ref-color-neutral-50: oklch(0.985 0.002 260);
7
+ --plinth-ref-color-neutral-100: oklch(0.965 0.003 260);
8
+ --plinth-ref-color-neutral-150: oklch(0.945 0.004 260);
9
+ --plinth-ref-color-neutral-200: oklch(0.92 0.004 260);
10
+ --plinth-ref-color-neutral-300: oklch(0.82 0.005 260);
11
+ --plinth-ref-color-neutral-400: oklch(0.71 0.008 260);
12
+ --plinth-ref-color-neutral-500: oklch(0.54 0.01 260);
13
+ --plinth-ref-color-neutral-600: oklch(0.45 0.01 260);
14
+ --plinth-ref-color-neutral-700: oklch(0.37 0.01 260);
15
+ --plinth-ref-color-neutral-800: oklch(0.27 0.008 260);
16
+ --plinth-ref-color-neutral-850: oklch(0.235 0.007 260);
17
+ --plinth-ref-color-neutral-900: oklch(0.205 0.006 260);
18
+ --plinth-ref-color-neutral-950: oklch(0.18 0.005 260);
19
+ --plinth-ref-color-neutral-1000: oklch(0.145 0.004 260);
20
+ --plinth-ref-color-brand-50: #eff6ff;
21
+ --plinth-ref-color-brand-100: #dbeafe;
22
+ --plinth-ref-color-brand-200: #bfdbfe;
23
+ --plinth-ref-color-brand-300: #93c5fd;
24
+ --plinth-ref-color-brand-400: #60a5fa;
25
+ --plinth-ref-color-brand-500: #3b82f6;
26
+ --plinth-ref-color-brand-600: #2563eb;
27
+ --plinth-ref-color-brand-700: #1d4ed8;
28
+ --plinth-ref-color-brand-800: #1e40af;
29
+ --plinth-ref-color-brand-900: #1e3a8a;
30
+ --plinth-ref-color-brand-950: #172554;
31
+ --plinth-ref-color-green-50: #f0fdf4;
32
+ --plinth-ref-color-green-100: #dcfce7;
33
+ --plinth-ref-color-green-200: #bbf7d0;
34
+ --plinth-ref-color-green-400: #4ade80;
35
+ --plinth-ref-color-green-600: #16a34a;
36
+ --plinth-ref-color-green-700: #15803d;
37
+ --plinth-ref-color-green-800: #166534;
38
+ --plinth-ref-color-green-950: #052e16;
39
+ --plinth-ref-color-amber-50: #fffbeb;
40
+ --plinth-ref-color-amber-100: #fef3c7;
41
+ --plinth-ref-color-amber-200: #fde68a;
42
+ --plinth-ref-color-amber-400: #fbbf24;
43
+ --plinth-ref-color-amber-500: #f59e0b;
44
+ --plinth-ref-color-amber-700: #b45309;
45
+ --plinth-ref-color-amber-800: #92400e;
46
+ --plinth-ref-color-amber-950: #451a03;
47
+ --plinth-ref-color-red-50: #fef2f2;
48
+ --plinth-ref-color-red-100: #fee2e2;
49
+ --plinth-ref-color-red-200: #fecaca;
50
+ --plinth-ref-color-red-400: #f87171;
51
+ --plinth-ref-color-red-600: #dc2626;
52
+ --plinth-ref-color-red-700: #b91c1c;
53
+ --plinth-ref-color-red-800: #991b1b;
54
+ --plinth-ref-color-red-950: #450a0a;
55
+ --plinth-ref-color-black-alpha-10: #0000001a;
56
+ --plinth-ref-color-black-alpha-15: #00000026;
57
+ --plinth-ref-color-black-alpha-30: #0000004d;
58
+ --plinth-ref-color-black-alpha-50: #00000080;
59
+ --plinth-ref-color-black-alpha-60: #00000099;
60
+ --plinth-ref-color-black-alpha-70: #000000b3;
61
+ --plinth-ref-color-ink-alpha-6: oklch(0.18 0.005 260 / 0.06);
62
+ --plinth-ref-color-ink-alpha-10: oklch(0.18 0.005 260 / 0.1);
63
+ --plinth-ref-color-ink-alpha-20: oklch(0.18 0.005 260 / 0.2);
64
+ --plinth-ref-color-chalk-alpha-6: oklch(0.965 0.003 260 / 0.06);
65
+ --plinth-ref-color-chalk-alpha-10: oklch(0.965 0.003 260 / 0.1);
66
+ --plinth-ref-color-chalk-alpha-20: oklch(0.965 0.003 260 / 0.2);
67
+ --plinth-ref-color-white-alpha-12: #ffffff1f;
68
+ --plinth-ref-space-0: 0;
69
+ --plinth-ref-space-1: 0.25rem;
70
+ --plinth-ref-space-2: 0.5rem;
71
+ --plinth-ref-space-3: 0.75rem;
72
+ --plinth-ref-space-4: 1rem;
73
+ --plinth-ref-space-5: 1.25rem;
74
+ --plinth-ref-space-6: 1.5rem;
75
+ --plinth-ref-space-8: 2rem;
76
+ --plinth-ref-space-10: 2.5rem;
77
+ --plinth-ref-space-12: 3rem;
78
+ --plinth-ref-space-14: 3.5rem;
79
+ --plinth-ref-space-16: 4rem;
80
+ --plinth-ref-space-0-5: 0.125rem;
81
+ --plinth-ref-size-16: 1rem;
82
+ --plinth-ref-size-20: 1.25rem;
83
+ --plinth-ref-size-24: 1.5rem;
84
+ --plinth-ref-size-28: 1.75rem;
85
+ --plinth-ref-size-32: 2rem;
86
+ --plinth-ref-size-40: 2.5rem;
87
+ --plinth-ref-size-44: 2.75rem;
88
+ --plinth-ref-size-56: 3.5rem;
89
+ --plinth-ref-size-160: 10rem;
90
+ --plinth-ref-size-400: 25rem;
91
+ --plinth-ref-size-480: 30rem;
92
+ --plinth-ref-radius-0: 0;
93
+ --plinth-ref-radius-2: 0.125rem;
94
+ --plinth-ref-radius-4: 0.25rem;
95
+ --plinth-ref-radius-6: 0.375rem;
96
+ --plinth-ref-radius-8: 0.5rem;
97
+ --plinth-ref-radius-10: 0.625rem;
98
+ --plinth-ref-radius-12: 0.75rem;
99
+ --plinth-ref-radius-16: 1rem;
100
+ --plinth-ref-radius-full: 9999px;
101
+ --plinth-ref-border-width-1: 1px;
102
+ --plinth-ref-border-width-2: 2px;
103
+ --plinth-ref-border-width-3: 3px;
104
+ --plinth-ref-font-family-sans: 'Geist Variable', Geist, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
105
+ --plinth-ref-font-family-mono: 'Geist Mono Variable', 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
106
+ --plinth-ref-font-size-12: 0.75rem;
107
+ --plinth-ref-font-size-13: 0.8125rem;
108
+ --plinth-ref-font-size-14: 0.875rem;
109
+ --plinth-ref-font-size-15: 0.9375rem;
110
+ --plinth-ref-font-size-16: 1rem;
111
+ --plinth-ref-font-size-18: 1.125rem;
112
+ --plinth-ref-font-size-24: 1.5rem;
113
+ --plinth-ref-font-size-30: 1.875rem;
114
+ --plinth-ref-font-size-36: 2.25rem;
115
+ --plinth-ref-font-size-48: 3rem;
116
+ --plinth-ref-font-size-64: 4rem;
117
+ --plinth-ref-font-weight-400: 400;
118
+ --plinth-ref-font-weight-500: 500;
119
+ --plinth-ref-font-weight-600: 600;
120
+ --plinth-ref-font-weight-700: 700;
121
+ --plinth-ref-font-leading-105: 1.05;
122
+ --plinth-ref-font-leading-125: 1.25;
123
+ --plinth-ref-font-leading-150: 1.5;
124
+ --plinth-ref-font-tracking-0: 0;
125
+ --plinth-ref-font-tracking-neg-1: -0.01em;
126
+ --plinth-ref-font-tracking-neg-2: -0.02em;
127
+ --plinth-ref-font-tracking-neg-4: -0.04em;
128
+ --plinth-ref-duration-120: 120ms;
129
+ --plinth-ref-duration-200: 200ms;
130
+ --plinth-ref-duration-320: 320ms;
131
+ --plinth-ref-easing-standard: cubic-bezier(0.2, 0, 0, 1);
132
+ --plinth-ref-easing-enter: cubic-bezier(0, 0, 0, 1);
133
+ --plinth-ref-easing-exit: cubic-bezier(0.3, 0, 1, 1);
134
+ --plinth-space-0: var(--plinth-ref-space-0);
135
+ --plinth-space-1: var(--plinth-ref-space-1);
136
+ --plinth-space-2: var(--plinth-ref-space-2);
137
+ --plinth-space-3: var(--plinth-ref-space-3);
138
+ --plinth-space-4: var(--plinth-ref-space-4);
139
+ --plinth-space-5: var(--plinth-ref-space-5);
140
+ --plinth-space-6: var(--plinth-ref-space-6);
141
+ --plinth-space-8: var(--plinth-ref-space-8);
142
+ --plinth-space-10: var(--plinth-ref-space-10);
143
+ --plinth-space-12: var(--plinth-ref-space-12);
144
+ --plinth-space-14: var(--plinth-ref-space-14);
145
+ --plinth-space-16: var(--plinth-ref-space-16);
146
+ --plinth-space-0-5: var(--plinth-ref-space-0-5);
147
+ --plinth-radius-control: var(--plinth-ref-radius-6);
148
+ --plinth-radius-container: var(--plinth-ref-radius-10);
149
+ --plinth-radius-pill: var(--plinth-ref-radius-full);
150
+ --plinth-border-width-thin: var(--plinth-ref-border-width-1);
151
+ --plinth-border-width-thick: var(--plinth-ref-border-width-2);
152
+ --plinth-opacity-disabled: 0.5;
153
+ --plinth-focus-ring-width: var(--plinth-ref-border-width-2);
154
+ --plinth-focus-ring-offset: 0;
155
+ --plinth-focus-ring-offset-filled: var(--plinth-ref-border-width-2);
156
+ --plinth-focus-ring-offset-inset: -2px;
157
+ --plinth-font-family-body: var(--plinth-ref-font-family-sans);
158
+ --plinth-font-family-mono: var(--plinth-ref-font-family-mono);
159
+ --plinth-font-size-xs: var(--plinth-ref-font-size-12);
160
+ --plinth-font-size-sm: var(--plinth-ref-font-size-13);
161
+ --plinth-font-size-md: var(--plinth-ref-font-size-14);
162
+ --plinth-font-size-lg: var(--plinth-ref-font-size-15);
163
+ --plinth-font-size-xl: var(--plinth-ref-font-size-18);
164
+ --plinth-font-size-2xl: var(--plinth-ref-font-size-24);
165
+ --plinth-font-size-3xl: var(--plinth-ref-font-size-30);
166
+ --plinth-font-size-4xl: var(--plinth-ref-font-size-36);
167
+ --plinth-font-size-5xl: var(--plinth-ref-font-size-48);
168
+ --plinth-font-size-6xl: var(--plinth-ref-font-size-64);
169
+ --plinth-font-size-field: var(--plinth-ref-font-size-16);
170
+ --plinth-font-weight-regular: var(--plinth-ref-font-weight-400);
171
+ --plinth-font-weight-medium: var(--plinth-ref-font-weight-500);
172
+ --plinth-font-weight-semibold: var(--plinth-ref-font-weight-600);
173
+ --plinth-font-weight-bold: var(--plinth-ref-font-weight-700);
174
+ --plinth-font-leading-display: var(--plinth-ref-font-leading-105);
175
+ --plinth-font-leading-tight: var(--plinth-ref-font-leading-125);
176
+ --plinth-font-leading-normal: var(--plinth-ref-font-leading-150);
177
+ --plinth-font-tracking-normal: var(--plinth-ref-font-tracking-0);
178
+ --plinth-font-tracking-snug: var(--plinth-ref-font-tracking-neg-1);
179
+ --plinth-font-tracking-tight: var(--plinth-ref-font-tracking-neg-2);
180
+ --plinth-font-tracking-tighter: var(--plinth-ref-font-tracking-neg-4);
181
+ --plinth-duration-fast: var(--plinth-ref-duration-120);
182
+ --plinth-duration-normal: var(--plinth-ref-duration-200);
183
+ --plinth-duration-slow: var(--plinth-ref-duration-320);
184
+ --plinth-easing-standard: var(--plinth-ref-easing-standard);
185
+ --plinth-easing-enter: var(--plinth-ref-easing-enter);
186
+ --plinth-easing-exit: var(--plinth-ref-easing-exit);
187
+ --plinth-control-sm: var(--plinth-ref-size-28);
188
+ --plinth-control-md: var(--plinth-ref-size-32);
189
+ --plinth-control-lg: var(--plinth-ref-size-40);
190
+ --plinth-touch-target: var(--plinth-ref-size-40);
191
+ --plinth-size-icon-sm: var(--plinth-ref-size-16);
192
+ --plinth-size-icon-md: var(--plinth-ref-size-20);
193
+ --plinth-size-icon-lg: var(--plinth-ref-size-24);
194
+ --plinth-size-row: var(--plinth-ref-size-44);
195
+ --plinth-size-avatar-sm: var(--plinth-ref-size-24);
196
+ --plinth-size-avatar-md: var(--plinth-ref-size-32);
197
+ --plinth-size-avatar-lg: var(--plinth-ref-size-40);
198
+ --plinth-size-avatar-xl: var(--plinth-ref-size-56);
199
+ --plinth-size-dialog: var(--plinth-ref-size-480);
200
+ --plinth-size-panel: var(--plinth-ref-size-400);
201
+ --plinth-size-peek: var(--plinth-ref-size-160);
202
+ --plinth-layer-base: 0;
203
+ --plinth-layer-sticky: 100;
204
+ --plinth-layer-overlay: 200;
205
+ --plinth-layer-sheet: 300;
206
+ --plinth-layer-toast: 400;
207
+ --plinth-breakpoint-md: 768px;
208
+ --plinth-breakpoint-lg: 1280px;
209
+ --plinth-header-height: var(--plinth-space-14);
210
+ --plinth-data-table-row-height: var(--plinth-size-row);
211
+ --plinth-list-row-height: var(--plinth-size-row);
212
+ --plinth-pill-height: var(--plinth-control-md);
213
+ --plinth-dialog-width: var(--plinth-size-dialog);
214
+ --plinth-sheet-side-width: var(--plinth-size-panel);
215
+ --plinth-drawer-peek-height: var(--plinth-size-peek);
216
+ --plinth-section-menu-height: var(--plinth-space-14);
217
+ --plinth-section-menu-item-height: var(--plinth-control-lg);
218
+ }
219
+
220
+ :root,
221
+ [data-theme="light"] {
222
+ color-scheme: light;
223
+ --plinth-color-surface: var(--plinth-ref-color-neutral-50);
224
+ --plinth-color-surface-raised: var(--plinth-ref-color-neutral-0);
225
+ --plinth-color-surface-sunken: var(--plinth-ref-color-neutral-100);
226
+ --plinth-color-surface-hover: var(--plinth-ref-color-neutral-150);
227
+ --plinth-color-text: var(--plinth-ref-color-neutral-950);
228
+ --plinth-color-text-muted: var(--plinth-ref-color-neutral-600);
229
+ --plinth-color-text-subtle: var(--plinth-ref-color-neutral-500);
230
+ --plinth-color-border: var(--plinth-ref-color-ink-alpha-10);
231
+ --plinth-color-border-strong: var(--plinth-ref-color-ink-alpha-20);
232
+ --plinth-color-divider: var(--plinth-ref-color-ink-alpha-6);
233
+ --plinth-color-primary: var(--plinth-ref-color-neutral-950);
234
+ --plinth-color-primary-hover: var(--plinth-ref-color-neutral-800);
235
+ --plinth-color-primary-active: var(--plinth-ref-color-neutral-700);
236
+ --plinth-color-on-primary: var(--plinth-ref-color-neutral-0);
237
+ --plinth-color-primary-edge: var(--plinth-ref-color-white-alpha-12);
238
+ --plinth-color-accent: var(--plinth-ref-color-brand-600);
239
+ --plinth-color-accent-text: var(--plinth-ref-color-brand-700);
240
+ --plinth-color-on-accent: var(--plinth-ref-color-neutral-0);
241
+ --plinth-color-focus-ring: var(--plinth-ref-color-brand-600);
242
+ --plinth-color-overlay: var(--plinth-ref-color-black-alpha-50);
243
+ --plinth-color-info: var(--plinth-ref-color-brand-600);
244
+ --plinth-color-info-surface: var(--plinth-ref-color-brand-100);
245
+ --plinth-color-info-text: var(--plinth-ref-color-brand-800);
246
+ --plinth-color-success: var(--plinth-ref-color-green-600);
247
+ --plinth-color-success-surface: var(--plinth-ref-color-green-100);
248
+ --plinth-color-success-text: var(--plinth-ref-color-green-800);
249
+ --plinth-color-warning: var(--plinth-ref-color-amber-500);
250
+ --plinth-color-warning-surface: var(--plinth-ref-color-amber-100);
251
+ --plinth-color-warning-text: var(--plinth-ref-color-amber-800);
252
+ --plinth-color-danger: var(--plinth-ref-color-red-600);
253
+ --plinth-color-danger-surface: var(--plinth-ref-color-red-100);
254
+ --plinth-color-danger-text: var(--plinth-ref-color-red-800);
255
+ --plinth-color-accent-hover: var(--plinth-ref-color-brand-700);
256
+ --plinth-color-accent-active: var(--plinth-ref-color-brand-800);
257
+ --plinth-color-danger-solid: var(--plinth-ref-color-red-600);
258
+ --plinth-color-danger-solid-hover: var(--plinth-ref-color-red-700);
259
+ --plinth-color-danger-solid-active: var(--plinth-ref-color-red-800);
260
+ --plinth-color-on-danger: var(--plinth-ref-color-neutral-0);
261
+ --plinth-color-accent-subtle: var(--plinth-ref-color-brand-50);
262
+ --plinth-color-accent-subtle-hover: var(--plinth-ref-color-brand-100);
263
+ --plinth-color-accent-subtle-active: var(--plinth-ref-color-brand-200);
264
+ --plinth-elevation-1: 0 1px 2px 0 #0000001a;
265
+ --plinth-elevation-2: 0 4px 12px 0 #00000026;
266
+ --plinth-elevation-3: 0 12px 32px 0 #00000026;
267
+ --plinth-callout-neutral-surface: var(--plinth-color-surface-sunken);
268
+ --plinth-callout-neutral-border: var(--plinth-color-border);
269
+ --plinth-callout-info-surface: color-mix(in srgb, var(--plinth-color-info) 6%, transparent);
270
+ --plinth-callout-info-border: color-mix(in srgb, var(--plinth-color-info) 24%, transparent);
271
+ --plinth-callout-success-surface: color-mix(in srgb, var(--plinth-color-success) 6%, transparent);
272
+ --plinth-callout-success-border: color-mix(in srgb, var(--plinth-color-success) 24%, transparent);
273
+ --plinth-callout-warning-surface: color-mix(in srgb, var(--plinth-color-warning) 6%, transparent);
274
+ --plinth-callout-warning-border: color-mix(in srgb, var(--plinth-color-warning) 24%, transparent);
275
+ --plinth-callout-danger-surface: color-mix(in srgb, var(--plinth-color-danger) 6%, transparent);
276
+ --plinth-callout-danger-border: color-mix(in srgb, var(--plinth-color-danger) 24%, transparent);
277
+ }
278
+
279
+ [data-theme="dark"],
280
+ .dark {
281
+ color-scheme: dark;
282
+ --plinth-color-surface: var(--plinth-ref-color-neutral-950);
283
+ --plinth-color-surface-raised: var(--plinth-ref-color-neutral-900);
284
+ --plinth-color-surface-sunken: var(--plinth-ref-color-neutral-1000);
285
+ --plinth-color-surface-hover: var(--plinth-ref-color-neutral-850);
286
+ --plinth-color-text: var(--plinth-ref-color-neutral-100);
287
+ --plinth-color-text-muted: var(--plinth-ref-color-neutral-300);
288
+ --plinth-color-text-subtle: var(--plinth-ref-color-neutral-400);
289
+ --plinth-color-border: var(--plinth-ref-color-chalk-alpha-10);
290
+ --plinth-color-border-strong: var(--plinth-ref-color-chalk-alpha-20);
291
+ --plinth-color-divider: var(--plinth-ref-color-chalk-alpha-6);
292
+ --plinth-color-primary: var(--plinth-ref-color-neutral-100);
293
+ --plinth-color-primary-hover: var(--plinth-ref-color-neutral-200);
294
+ --plinth-color-primary-active: var(--plinth-ref-color-neutral-300);
295
+ --plinth-color-on-primary: var(--plinth-ref-color-neutral-950);
296
+ --plinth-color-primary-edge: var(--plinth-ref-color-black-alpha-10);
297
+ --plinth-color-accent: var(--plinth-ref-color-brand-600);
298
+ --plinth-color-accent-text: var(--plinth-ref-color-brand-300);
299
+ --plinth-color-on-accent: var(--plinth-ref-color-neutral-0);
300
+ --plinth-color-focus-ring: var(--plinth-ref-color-brand-300);
301
+ --plinth-color-overlay: var(--plinth-ref-color-black-alpha-70);
302
+ --plinth-color-info: var(--plinth-ref-color-brand-400);
303
+ --plinth-color-info-surface: var(--plinth-ref-color-brand-950);
304
+ --plinth-color-info-text: var(--plinth-ref-color-brand-200);
305
+ --plinth-color-success: var(--plinth-ref-color-green-400);
306
+ --plinth-color-success-surface: var(--plinth-ref-color-green-950);
307
+ --plinth-color-success-text: var(--plinth-ref-color-green-200);
308
+ --plinth-color-warning: var(--plinth-ref-color-amber-400);
309
+ --plinth-color-warning-surface: var(--plinth-ref-color-amber-950);
310
+ --plinth-color-warning-text: var(--plinth-ref-color-amber-200);
311
+ --plinth-color-danger: var(--plinth-ref-color-red-400);
312
+ --plinth-color-danger-surface: var(--plinth-ref-color-red-950);
313
+ --plinth-color-danger-text: var(--plinth-ref-color-red-200);
314
+ --plinth-color-accent-hover: var(--plinth-ref-color-brand-700);
315
+ --plinth-color-accent-active: var(--plinth-ref-color-brand-800);
316
+ --plinth-color-danger-solid: var(--plinth-ref-color-red-600);
317
+ --plinth-color-danger-solid-hover: var(--plinth-ref-color-red-700);
318
+ --plinth-color-danger-solid-active: var(--plinth-ref-color-red-800);
319
+ --plinth-color-on-danger: var(--plinth-ref-color-neutral-0);
320
+ --plinth-color-accent-subtle: var(--plinth-ref-color-brand-950);
321
+ --plinth-color-accent-subtle-hover: var(--plinth-ref-color-brand-900);
322
+ --plinth-color-accent-subtle-active: var(--plinth-ref-color-brand-800);
323
+ --plinth-elevation-1: 0 1px 2px 0 #0000004d;
324
+ --plinth-elevation-2: 0 4px 12px 0 #00000080;
325
+ --plinth-elevation-3: 0 12px 32px 0 #00000099;
326
+ --plinth-callout-neutral-surface: var(--plinth-color-surface-sunken);
327
+ --plinth-callout-neutral-border: var(--plinth-color-border);
328
+ --plinth-callout-info-surface: color-mix(in srgb, var(--plinth-color-info) 8%, transparent);
329
+ --plinth-callout-info-border: color-mix(in srgb, var(--plinth-color-info) 24%, transparent);
330
+ --plinth-callout-success-surface: color-mix(in srgb, var(--plinth-color-success) 8%, transparent);
331
+ --plinth-callout-success-border: color-mix(in srgb, var(--plinth-color-success) 24%, transparent);
332
+ --plinth-callout-warning-surface: color-mix(in srgb, var(--plinth-color-warning) 8%, transparent);
333
+ --plinth-callout-warning-border: color-mix(in srgb, var(--plinth-color-warning) 24%, transparent);
334
+ --plinth-callout-danger-surface: color-mix(in srgb, var(--plinth-color-danger) 8%, transparent);
335
+ --plinth-callout-danger-border: color-mix(in srgb, var(--plinth-color-danger) 24%, transparent);
336
+ }
337
+
338
+ @media (prefers-color-scheme: dark) {
339
+ :root:not([data-theme="light"], .light) {
340
+ color-scheme: dark;
341
+ --plinth-color-surface: var(--plinth-ref-color-neutral-950);
342
+ --plinth-color-surface-raised: var(--plinth-ref-color-neutral-900);
343
+ --plinth-color-surface-sunken: var(--plinth-ref-color-neutral-1000);
344
+ --plinth-color-surface-hover: var(--plinth-ref-color-neutral-850);
345
+ --plinth-color-text: var(--plinth-ref-color-neutral-100);
346
+ --plinth-color-text-muted: var(--plinth-ref-color-neutral-300);
347
+ --plinth-color-text-subtle: var(--plinth-ref-color-neutral-400);
348
+ --plinth-color-border: var(--plinth-ref-color-chalk-alpha-10);
349
+ --plinth-color-border-strong: var(--plinth-ref-color-chalk-alpha-20);
350
+ --plinth-color-divider: var(--plinth-ref-color-chalk-alpha-6);
351
+ --plinth-color-primary: var(--plinth-ref-color-neutral-100);
352
+ --plinth-color-primary-hover: var(--plinth-ref-color-neutral-200);
353
+ --plinth-color-primary-active: var(--plinth-ref-color-neutral-300);
354
+ --plinth-color-on-primary: var(--plinth-ref-color-neutral-950);
355
+ --plinth-color-primary-edge: var(--plinth-ref-color-black-alpha-10);
356
+ --plinth-color-accent: var(--plinth-ref-color-brand-600);
357
+ --plinth-color-accent-text: var(--plinth-ref-color-brand-300);
358
+ --plinth-color-on-accent: var(--plinth-ref-color-neutral-0);
359
+ --plinth-color-focus-ring: var(--plinth-ref-color-brand-300);
360
+ --plinth-color-overlay: var(--plinth-ref-color-black-alpha-70);
361
+ --plinth-color-info: var(--plinth-ref-color-brand-400);
362
+ --plinth-color-info-surface: var(--plinth-ref-color-brand-950);
363
+ --plinth-color-info-text: var(--plinth-ref-color-brand-200);
364
+ --plinth-color-success: var(--plinth-ref-color-green-400);
365
+ --plinth-color-success-surface: var(--plinth-ref-color-green-950);
366
+ --plinth-color-success-text: var(--plinth-ref-color-green-200);
367
+ --plinth-color-warning: var(--plinth-ref-color-amber-400);
368
+ --plinth-color-warning-surface: var(--plinth-ref-color-amber-950);
369
+ --plinth-color-warning-text: var(--plinth-ref-color-amber-200);
370
+ --plinth-color-danger: var(--plinth-ref-color-red-400);
371
+ --plinth-color-danger-surface: var(--plinth-ref-color-red-950);
372
+ --plinth-color-danger-text: var(--plinth-ref-color-red-200);
373
+ --plinth-color-accent-hover: var(--plinth-ref-color-brand-700);
374
+ --plinth-color-accent-active: var(--plinth-ref-color-brand-800);
375
+ --plinth-color-danger-solid: var(--plinth-ref-color-red-600);
376
+ --plinth-color-danger-solid-hover: var(--plinth-ref-color-red-700);
377
+ --plinth-color-danger-solid-active: var(--plinth-ref-color-red-800);
378
+ --plinth-color-on-danger: var(--plinth-ref-color-neutral-0);
379
+ --plinth-color-accent-subtle: var(--plinth-ref-color-brand-950);
380
+ --plinth-color-accent-subtle-hover: var(--plinth-ref-color-brand-900);
381
+ --plinth-color-accent-subtle-active: var(--plinth-ref-color-brand-800);
382
+ --plinth-elevation-1: 0 1px 2px 0 #0000004d;
383
+ --plinth-elevation-2: 0 4px 12px 0 #00000080;
384
+ --plinth-elevation-3: 0 12px 32px 0 #00000099;
385
+ --plinth-callout-neutral-surface: var(--plinth-color-surface-sunken);
386
+ --plinth-callout-neutral-border: var(--plinth-color-border);
387
+ --plinth-callout-info-surface: color-mix(in srgb, var(--plinth-color-info) 8%, transparent);
388
+ --plinth-callout-info-border: color-mix(in srgb, var(--plinth-color-info) 24%, transparent);
389
+ --plinth-callout-success-surface: color-mix(in srgb, var(--plinth-color-success) 8%, transparent);
390
+ --plinth-callout-success-border: color-mix(in srgb, var(--plinth-color-success) 24%, transparent);
391
+ --plinth-callout-warning-surface: color-mix(in srgb, var(--plinth-color-warning) 8%, transparent);
392
+ --plinth-callout-warning-border: color-mix(in srgb, var(--plinth-color-warning) 24%, transparent);
393
+ --plinth-callout-danger-surface: color-mix(in srgb, var(--plinth-color-danger) 8%, transparent);
394
+ --plinth-callout-danger-border: color-mix(in srgb, var(--plinth-color-danger) 24%, transparent);
395
+ }
396
+ }
397
+
398
+ @media (prefers-reduced-motion: reduce) {
399
+ :root {
400
+ --plinth-duration-fast: 0ms;
401
+ --plinth-duration-normal: 0ms;
402
+ --plinth-duration-slow: 0ms;
403
+ }
404
+ }
405
+ }
406
+
407
+ /* avatar.css */
408
+ /*
409
+ * Avatar: a person or an organisation, as an image, initials, or a person glyph.
410
+ *
411
+ * <span class="plinth-avatar" data-size="md" role="img" aria-label="Ana Lima, Online">
412
+ * <img class="plinth-avatar-image" src="…" alt="">
413
+ * or <span class="plinth-avatar-initials" aria-hidden="true">AL</span>
414
+ * or <svg class="plinth-avatar-glyph" aria-hidden="true">…</svg>
415
+ * <span class="plinth-avatar-status" data-tone="success" aria-hidden="true"></span>
416
+ * </span>
417
+ *
418
+ * The root carries the name (role="img", aria-label), with the status label after a comma. A
419
+ * decorative avatar, next to the name it shows, has aria-hidden="true" and no role or label.
420
+ * Image first, then initials (from the name, or given), then the glyph.
421
+ *
422
+ * Sizes (data-size): sm 24px, md 32px (default), lg 40px, xl 56px. Round by default
423
+ * (radius.pill), so a brand can square it. The status dot takes the badge's tones (data-tone:
424
+ * neutral, success, warning, danger) and a ring in the page colour; on another surface, set
425
+ * --plinth-avatar-ring on the container (a card: --plinth-color-surface-raised).
426
+ */
427
+
428
+ @layer plinth {
429
+ .plinth-avatar {
430
+ --plinth-avatar-size: var(--plinth-size-avatar-md);
431
+ --plinth-avatar-surface: var(--plinth-color-surface-sunken);
432
+ --plinth-avatar-status: var(--plinth-color-text-subtle);
433
+ --plinth-avatar-ring: var(--plinth-color-surface);
434
+
435
+ position: relative;
436
+ display: inline-flex;
437
+ flex: none;
438
+ align-items: center;
439
+ justify-content: center;
440
+ inline-size: var(--plinth-avatar-size);
441
+ block-size: var(--plinth-avatar-size);
442
+ font-family: var(--plinth-font-family-body);
443
+ font-size: var(--plinth-font-size-xs);
444
+ font-weight: var(--plinth-font-weight-medium);
445
+ line-height: 1;
446
+ vertical-align: middle;
447
+ color: var(--plinth-color-text-muted);
448
+ user-select: none;
449
+ background-color: var(--plinth-avatar-surface);
450
+ border-radius: var(--plinth-radius-pill);
451
+ }
452
+
453
+ /*
454
+ * The hairline ring: over an image (positioned boxes paint after in-flow ones) and under the
455
+ * status dot (which comes later). A border, so forced-colours mode keeps the circle.
456
+ */
457
+ .plinth-avatar::before {
458
+ position: absolute;
459
+ inset: 0;
460
+ box-sizing: border-box;
461
+ pointer-events: none;
462
+ content: '';
463
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
464
+ border-radius: inherit;
465
+ }
466
+
467
+ .plinth-avatar[data-size='sm'] {
468
+ --plinth-avatar-size: var(--plinth-size-avatar-sm);
469
+ }
470
+
471
+ .plinth-avatar[data-size='lg'] {
472
+ --plinth-avatar-size: var(--plinth-size-avatar-lg);
473
+
474
+ font-size: var(--plinth-font-size-md);
475
+ }
476
+
477
+ .plinth-avatar[data-size='xl'] {
478
+ --plinth-avatar-size: var(--plinth-size-avatar-xl);
479
+
480
+ font-size: var(--plinth-font-size-xl);
481
+ }
482
+
483
+ .plinth-avatar-image {
484
+ inline-size: 100%;
485
+ block-size: 100%;
486
+ object-fit: cover;
487
+ border-radius: inherit;
488
+ }
489
+
490
+ .plinth-avatar-initials {
491
+ letter-spacing: var(--plinth-font-tracking-snug);
492
+ }
493
+
494
+ .plinth-avatar-glyph {
495
+ inline-size: 60%;
496
+ block-size: 60%;
497
+ }
498
+
499
+ .plinth-avatar-status {
500
+ position: absolute;
501
+ inset-block-end: 0;
502
+ inset-inline-end: 0;
503
+ inline-size: calc(var(--plinth-avatar-size) * 0.3);
504
+ block-size: calc(var(--plinth-avatar-size) * 0.3);
505
+ background-color: var(--plinth-avatar-status);
506
+
507
+ /* Stays round when a brand squares the avatar. */
508
+ border-radius: 50%;
509
+ box-shadow: 0 0 0 var(--plinth-border-width-thick) var(--plinth-avatar-ring);
510
+ }
511
+
512
+ .plinth-avatar-status[data-tone='success'] {
513
+ --plinth-avatar-status: var(--plinth-color-success);
514
+ }
515
+
516
+ .plinth-avatar-status[data-tone='warning'] {
517
+ --plinth-avatar-status: var(--plinth-color-warning);
518
+ }
519
+
520
+ .plinth-avatar-status[data-tone='danger'] {
521
+ --plinth-avatar-status: var(--plinth-color-danger);
522
+ }
523
+
524
+ @media (forced-colors: active) {
525
+ .plinth-avatar-status {
526
+ background-color: CanvasText;
527
+ }
528
+ }
529
+ }
530
+
531
+ /* badge.css */
532
+ /*
533
+ * Badge: a short label with an optional dot and a tone.
534
+ *
535
+ * <span class="plinth-badge" data-tone="success">
536
+ * <span class="plinth-badge-dot" aria-hidden="true"></span>
537
+ * <span class="plinth-badge-label">Paid</span>
538
+ * </span>
539
+ *
540
+ * Tones: neutral (default, also when data-tone is missing), success, warning, danger.
541
+ * The label carries the meaning; the tone and the dot only reinforce it.
542
+ */
543
+
544
+ @layer plinth {
545
+ .plinth-badge {
546
+ --plinth-badge-surface: var(--plinth-color-surface-sunken);
547
+ --plinth-badge-text: var(--plinth-color-text-muted);
548
+ --plinth-badge-accent: var(--plinth-color-text-muted);
549
+
550
+ display: inline-flex;
551
+ gap: var(--plinth-space-1);
552
+ align-items: center;
553
+ min-inline-size: 0;
554
+ max-inline-size: 100%;
555
+ padding-block: var(--plinth-space-0-5);
556
+ padding-inline: var(--plinth-space-2);
557
+ font-family: var(--plinth-font-family-body);
558
+ font-size: var(--plinth-font-size-xs);
559
+ font-weight: var(--plinth-font-weight-medium);
560
+ line-height: var(--plinth-font-leading-normal);
561
+ vertical-align: middle;
562
+ color: var(--plinth-badge-text);
563
+ white-space: nowrap;
564
+ background-color: var(--plinth-badge-surface);
565
+
566
+ /* Invisible normally; drawn by the system in forced-colours mode so the badge keeps its edge. */
567
+ border: 1px solid transparent;
568
+ border-radius: var(--plinth-radius-pill);
569
+ }
570
+
571
+ .plinth-badge[data-tone='success'] {
572
+ --plinth-badge-surface: var(--plinth-color-success-surface);
573
+ --plinth-badge-text: var(--plinth-color-success-text);
574
+ --plinth-badge-accent: var(--plinth-color-success);
575
+ }
576
+
577
+ .plinth-badge[data-tone='warning'] {
578
+ --plinth-badge-surface: var(--plinth-color-warning-surface);
579
+ --plinth-badge-text: var(--plinth-color-warning-text);
580
+ --plinth-badge-accent: var(--plinth-color-warning);
581
+ }
582
+
583
+ .plinth-badge[data-tone='danger'] {
584
+ --plinth-badge-surface: var(--plinth-color-danger-surface);
585
+ --plinth-badge-text: var(--plinth-color-danger-text);
586
+ --plinth-badge-accent: var(--plinth-color-danger);
587
+ }
588
+
589
+ .plinth-badge-dot {
590
+ flex: none;
591
+ inline-size: var(--plinth-space-2);
592
+ block-size: var(--plinth-space-2);
593
+ background-color: var(--plinth-badge-accent);
594
+ border-radius: var(--plinth-radius-pill);
595
+ }
596
+
597
+ .plinth-badge-label {
598
+ overflow: hidden;
599
+ text-overflow: ellipsis;
600
+ }
601
+
602
+ @media (forced-colors: active) {
603
+ .plinth-badge-dot {
604
+ background-color: currentcolor;
605
+ }
606
+ }
607
+ }
608
+
609
+ /* button.css */
610
+ /*
611
+ * Button: the main action control.
612
+ *
613
+ * <button type="button" class="plinth-button" data-variant="primary" data-size="md">
614
+ * <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
615
+ * <span class="plinth-button-label">Save</span>
616
+ * <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
617
+ * </button>
618
+ *
619
+ * With an href it is <a class="plinth-button" href="…"> instead; the icons are optional.
620
+ *
621
+ * data-mirror on an icon flips it in right-to-left text, for arrows (mirror-icon and
622
+ * mirror-trailing in Blade, Vue and React; mirror on the icon button).
623
+ *
624
+ * Variants (data-variant): primary (default, also when missing; inverted neutral unless a brand
625
+ * colours it), accent (the brand's colour, for marketing and brand moments), secondary, ghost, danger, and
626
+ * danger-ghost for a low-emphasis destructive action (Remove next to an item).
627
+ * Sizes (data-size): sm 28px, md 32px (default), lg 40px. On a touch screen every size keeps a
628
+ * 40px touch target through an invisible hit area. Icons inside are 16px on sm and 20px otherwise.
629
+ *
630
+ * States:
631
+ * - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
632
+ * - busy: aria-busy="true" and aria-disabled="true", with
633
+ * <span class="plinth-button-spinner" aria-hidden="true"></span> in the leading icon's place.
634
+ * It keeps focus; the app ignores activation while busy.
635
+ * - hover only where the device can hover; the focus ring on :focus-visible.
636
+ *
637
+ * Icon button: a button with only an icon, square at the button's height.
638
+ *
639
+ * <button type="button" class="plinth-button plinth-icon-button" data-variant="ghost"
640
+ * data-size="md" aria-label="More actions">
641
+ * <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
642
+ * </button>
643
+ *
644
+ * With an href it is a link instead, with the same disabled state as the button's link:
645
+ *
646
+ * <a class="plinth-button plinth-icon-button" href="…" data-variant="secondary"
647
+ * data-size="md" aria-label="Next page">
648
+ * <span class="plinth-icon" data-size="md" data-mirror aria-hidden="true"><svg>…</svg></span>
649
+ * </a>
650
+ *
651
+ * aria-label is required: it is the only name the button has. A toggle has aria-pressed
652
+ * ("true" or "false"), and pressed shows the selection tint (accent-subtle, accent-text) inside
653
+ * a 2px accent edge (border-width.thick), so colour is not its only cue; it still answers hover
654
+ * and press with a step on the tint. In forced colours it takes the Highlight system colour. A
655
+ * link is never a toggle: aria-pressed is never printed on one.
656
+ */
657
+
658
+ @layer plinth {
659
+ .plinth-button {
660
+ --plinth-button-surface: var(--plinth-color-primary);
661
+ --plinth-button-surface-hover: var(--plinth-color-primary-hover);
662
+ --plinth-button-surface-active: var(--plinth-color-primary-active);
663
+ --plinth-button-text: var(--plinth-color-on-primary);
664
+ --plinth-button-edge: transparent;
665
+ --plinth-button-inner-edge: inset 0 0 0 1px var(--plinth-color-primary-edge);
666
+ --plinth-button-height: var(--plinth-control-md);
667
+ --plinth-button-padding: var(--plinth-space-4);
668
+ --plinth-button-icon-size: var(--plinth-size-icon-md);
669
+
670
+ position: relative;
671
+ display: inline-flex;
672
+ gap: var(--plinth-space-2);
673
+ align-items: center;
674
+ justify-content: center;
675
+ max-inline-size: 100%;
676
+ min-block-size: var(--plinth-button-height);
677
+ padding-block: 0;
678
+ padding-inline: var(--plinth-button-padding);
679
+ font-family: var(--plinth-font-family-body);
680
+ font-size: var(--plinth-font-size-md);
681
+ font-weight: var(--plinth-font-weight-medium);
682
+ line-height: var(--plinth-font-leading-tight);
683
+ vertical-align: middle;
684
+ color: var(--plinth-button-text);
685
+ white-space: nowrap;
686
+ text-decoration: none;
687
+ cursor: pointer;
688
+ user-select: none;
689
+ background-color: var(--plinth-button-surface);
690
+ border: var(--plinth-border-width-thin) solid var(--plinth-button-edge);
691
+ border-radius: var(--plinth-radius-control);
692
+ box-shadow: var(--plinth-button-inner-edge);
693
+ transition:
694
+ background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
695
+ border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
696
+ box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard);
697
+ }
698
+
699
+ /* The brand's colour, for marketing and brand moments (Upgrade plan): at most one per screen. */
700
+ .plinth-button[data-variant='accent'] {
701
+ --plinth-button-surface: var(--plinth-color-accent);
702
+ --plinth-button-surface-hover: var(--plinth-color-accent-hover);
703
+ --plinth-button-surface-active: var(--plinth-color-accent-active);
704
+ --plinth-button-text: var(--plinth-color-on-accent);
705
+ }
706
+
707
+ .plinth-button[data-variant='secondary'] {
708
+ --plinth-button-inner-edge: none;
709
+ --plinth-button-surface: var(--plinth-color-surface-raised);
710
+ --plinth-button-surface-hover: var(--plinth-color-surface-hover);
711
+ --plinth-button-surface-active: var(--plinth-color-surface-sunken);
712
+ --plinth-button-text: var(--plinth-color-text);
713
+ --plinth-button-edge: var(--plinth-color-border-strong);
714
+ }
715
+
716
+ .plinth-button[data-variant='ghost'] {
717
+ --plinth-button-inner-edge: none;
718
+ --plinth-button-surface: transparent;
719
+ --plinth-button-surface-hover: var(--plinth-color-surface-hover);
720
+ --plinth-button-surface-active: var(--plinth-color-surface-sunken);
721
+ --plinth-button-text: var(--plinth-color-text);
722
+ }
723
+
724
+ .plinth-button[data-variant='danger'] {
725
+ --plinth-button-inner-edge: none;
726
+ --plinth-button-surface: var(--plinth-color-danger-solid);
727
+ --plinth-button-surface-hover: var(--plinth-color-danger-solid-hover);
728
+ --plinth-button-surface-active: var(--plinth-color-danger-solid-active);
729
+ --plinth-button-text: var(--plinth-color-on-danger);
730
+ }
731
+
732
+ .plinth-button[data-variant='danger-ghost'] {
733
+ --plinth-button-inner-edge: none;
734
+ --plinth-button-surface: transparent;
735
+ --plinth-button-surface-hover: var(--plinth-color-danger-surface);
736
+ --plinth-button-surface-active: var(--plinth-color-danger-surface);
737
+ --plinth-button-text: var(--plinth-color-danger-text);
738
+ }
739
+
740
+ .plinth-button[data-size='sm'] {
741
+ --plinth-button-height: var(--plinth-control-sm);
742
+ --plinth-button-padding: var(--plinth-space-3);
743
+ --plinth-button-icon-size: var(--plinth-size-icon-sm);
744
+
745
+ font-size: var(--plinth-font-size-sm);
746
+ }
747
+
748
+ .plinth-button[data-size='lg'] {
749
+ --plinth-button-height: var(--plinth-control-lg);
750
+ --plinth-button-padding: var(--plinth-space-5);
751
+ }
752
+
753
+ /*
754
+ * On a touch screen, every button keeps a 40px touch target through an invisible hit area,
755
+ * centred on it, without changing its look (ADR 3: controls look small, touch stays at 40px).
756
+ */
757
+ @media (pointer: coarse) {
758
+ .plinth-button::before {
759
+ position: absolute;
760
+ inset-block-start: 50%;
761
+ inset-inline: 0;
762
+ block-size: max(100%, var(--plinth-touch-target));
763
+ content: '';
764
+ translate: 0 -50%;
765
+ }
766
+
767
+ /* An icon button is narrower than 40px, so its hit area grows sideways too. */
768
+ .plinth-icon-button::before {
769
+ inset-inline: min(0%, 50% - var(--plinth-touch-target) / 2);
770
+ }
771
+ }
772
+
773
+ @media (hover: hover) {
774
+ .plinth-button:hover {
775
+ background-color: var(--plinth-button-surface-hover);
776
+ }
777
+ }
778
+
779
+ .plinth-button:active {
780
+ background-color: var(--plinth-button-surface-active);
781
+ }
782
+
783
+ .plinth-button:disabled,
784
+ .plinth-button[aria-disabled='true'] {
785
+ cursor: not-allowed;
786
+ background-color: var(--plinth-button-surface);
787
+ }
788
+
789
+ .plinth-button:disabled,
790
+ .plinth-button[aria-disabled='true']:not([aria-busy='true']) {
791
+ opacity: var(--plinth-opacity-disabled);
792
+ }
793
+
794
+ .plinth-button[aria-busy='true'] {
795
+ cursor: progress;
796
+ }
797
+
798
+ .plinth-button:focus-visible {
799
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
800
+ outline-offset: var(--plinth-focus-ring-offset-filled);
801
+ }
802
+
803
+ .plinth-button:is(
804
+ [data-variant='secondary'],
805
+ [data-variant='ghost'],
806
+ [data-variant='danger-ghost']
807
+ ):focus-visible {
808
+ outline-offset: var(--plinth-focus-ring-offset);
809
+ }
810
+
811
+ .plinth-icon-button {
812
+ flex: none;
813
+ inline-size: var(--plinth-button-height);
814
+ padding-inline: 0;
815
+ }
816
+
817
+ /* The border and an inset ring make the 2px edge, so pressed doesn't move the icon. */
818
+ .plinth-icon-button[aria-pressed='true'] {
819
+ --plinth-button-surface: var(--plinth-color-accent-subtle);
820
+ --plinth-button-surface-hover: var(--plinth-color-accent-subtle-hover);
821
+ --plinth-button-surface-active: var(--plinth-color-accent-subtle-active);
822
+ --plinth-button-text: var(--plinth-color-accent-text);
823
+ --plinth-button-edge: var(--plinth-color-accent);
824
+ --plinth-button-inner-edge: inset 0 0 0
825
+ calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
826
+ var(--plinth-color-accent);
827
+ }
828
+
829
+ /* A gap between the ring and the pressed accent edge, so focus and pressed read apart. */
830
+ .plinth-icon-button[aria-pressed='true']:focus-visible {
831
+ outline-offset: var(--plinth-focus-ring-offset-filled);
832
+ }
833
+
834
+ .plinth-button-label {
835
+ overflow: hidden;
836
+ text-overflow: ellipsis;
837
+ }
838
+
839
+ /* The same size as the icon it replaces, so the button keeps its width when it goes busy. */
840
+ .plinth-button-spinner {
841
+ flex: none;
842
+ inline-size: var(--plinth-button-icon-size);
843
+ block-size: var(--plinth-button-icon-size);
844
+ border: var(--plinth-border-width-thick) solid currentcolor;
845
+ border-inline-end-color: transparent;
846
+ border-radius: 50%;
847
+ animation: plinth-button-spin calc(var(--plinth-duration-slow) * 3) linear infinite;
848
+ }
849
+
850
+ @keyframes plinth-button-spin {
851
+ to {
852
+ transform: rotate(1turn);
853
+ }
854
+ }
855
+
856
+ @media (forced-colors: active) {
857
+ .plinth-button {
858
+ border-color: ButtonText;
859
+ }
860
+
861
+ .plinth-button:disabled,
862
+ .plinth-button[aria-disabled='true']:not([aria-busy='true']) {
863
+ color: GrayText;
864
+ border-color: GrayText;
865
+ }
866
+
867
+ /* The pressed tint is dropped in forced colours, so pressed takes the selection colours,
868
+ unless it is disabled, which keeps GrayText. */
869
+ .plinth-icon-button[aria-pressed='true']:not(
870
+ :disabled,
871
+ [aria-disabled='true']:not([aria-busy='true'])
872
+ ) {
873
+ color: HighlightText;
874
+ background-color: Highlight;
875
+ border-color: Highlight;
876
+ }
877
+
878
+ .plinth-button-spinner {
879
+ forced-color-adjust: none;
880
+ }
881
+ }
882
+ }
883
+
884
+ /* callout.css */
885
+ /*
886
+ * Callout: a note inside a page, with a tone, an icon, an optional title and actions.
887
+ *
888
+ * <div class="plinth-callout" data-tone="warning" role="note">
889
+ * <span class="plinth-icon plinth-callout-icon" data-size="md" aria-hidden="true">
890
+ * <svg viewBox="0 0 24 24">…</svg>
891
+ * </span>
892
+ * <div class="plinth-callout-body">
893
+ * <p class="plinth-callout-title">Invoices are sent at 9:00</p>
894
+ * <div class="plinth-callout-text">Drafts saved after that go out the next morning.</div>
895
+ * <div class="plinth-callout-actions">…buttons or links…</div>
896
+ * </div>
897
+ * </div>
898
+ *
899
+ * Tones (data-tone): neutral (default, also when the attribute is missing), info, success,
900
+ * warning, danger. Each tints the surface, the hairline border and the icon, and has its own icon
901
+ * shape; the title or the text still says what kind of note it is, so the tone is never the only
902
+ * signal. The tints are the component tokens --plinth-callout-<tone>-surface and -border: faint
903
+ * and translucent, quieter than the status surfaces a badge uses.
904
+ *
905
+ * role: note (default) for a note that is part of the page; status for a message the app adds
906
+ * after something happens (announced politely); alert for an urgent one (announced at once). A
907
+ * live region inserted with its content is often not announced, so for a status the app keeps an
908
+ * empty container with role="status" in the page and puts the callout in it.
909
+ *
910
+ * The text may hold paragraphs, links (underlined, in the text's colour) and inline code.
911
+ *
912
+ * The icon is decorative (aria-hidden). The tone's icon comes with the component; a project puts
913
+ * its own SVG in its place. Title, text and actions are each printed only when they have content.
914
+ */
915
+
916
+ @layer plinth {
917
+ .plinth-callout {
918
+ --plinth-callout-surface: var(--plinth-callout-neutral-surface);
919
+ --plinth-callout-border: var(--plinth-callout-neutral-border);
920
+ --plinth-callout-icon-color: var(--plinth-color-text-muted);
921
+
922
+ display: flex;
923
+ gap: var(--plinth-space-3);
924
+ align-items: flex-start;
925
+ min-inline-size: 0;
926
+ padding-block: var(--plinth-space-3);
927
+ padding-inline: var(--plinth-space-4);
928
+ font-family: var(--plinth-font-family-body);
929
+ font-size: var(--plinth-font-size-md);
930
+ line-height: var(--plinth-font-leading-normal);
931
+ color: var(--plinth-color-text);
932
+ overflow-wrap: anywhere;
933
+ background-color: var(--plinth-callout-surface);
934
+ border: 1px solid var(--plinth-callout-border);
935
+ border-radius: var(--plinth-radius-container);
936
+ }
937
+
938
+ .plinth-callout[data-tone='info'] {
939
+ --plinth-callout-surface: var(--plinth-callout-info-surface);
940
+ --plinth-callout-border: var(--plinth-callout-info-border);
941
+ --plinth-callout-icon-color: var(--plinth-color-info-text);
942
+ }
943
+
944
+ .plinth-callout[data-tone='success'] {
945
+ --plinth-callout-surface: var(--plinth-callout-success-surface);
946
+ --plinth-callout-border: var(--plinth-callout-success-border);
947
+ --plinth-callout-icon-color: var(--plinth-color-success-text);
948
+ }
949
+
950
+ .plinth-callout[data-tone='warning'] {
951
+ --plinth-callout-surface: var(--plinth-callout-warning-surface);
952
+ --plinth-callout-border: var(--plinth-callout-warning-border);
953
+ --plinth-callout-icon-color: var(--plinth-color-warning-text);
954
+ }
955
+
956
+ .plinth-callout[data-tone='danger'] {
957
+ --plinth-callout-surface: var(--plinth-callout-danger-surface);
958
+ --plinth-callout-border: var(--plinth-callout-danger-border);
959
+ --plinth-callout-icon-color: var(--plinth-color-danger-text);
960
+ }
961
+
962
+ /* Centred on the first line of text, however tall the line is. */
963
+ .plinth-callout-icon {
964
+ margin-block: calc((1lh - var(--plinth-size-icon-md)) / 2);
965
+ color: var(--plinth-callout-icon-color);
966
+ }
967
+
968
+ .plinth-callout-body {
969
+ display: flex;
970
+ flex: 1;
971
+ flex-direction: column;
972
+ gap: var(--plinth-space-1);
973
+ min-inline-size: 0;
974
+ }
975
+
976
+ .plinth-callout-title {
977
+ margin: 0;
978
+ font-weight: var(--plinth-font-weight-semibold);
979
+ }
980
+
981
+ .plinth-callout-text > * {
982
+ margin-block: 0;
983
+ }
984
+
985
+ .plinth-callout-text > * + * {
986
+ margin-block-start: var(--plinth-space-2);
987
+ }
988
+
989
+ .plinth-callout-text a {
990
+ color: inherit;
991
+ text-decoration-line: underline;
992
+ text-decoration-color: var(--plinth-color-text-muted);
993
+ text-underline-offset: 0.2em;
994
+ }
995
+
996
+ @media (hover: hover) {
997
+ .plinth-callout-text a:hover {
998
+ text-decoration-color: currentcolor;
999
+ }
1000
+ }
1001
+
1002
+ .plinth-callout-text a:focus-visible {
1003
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
1004
+ outline-offset: var(--plinth-focus-ring-offset);
1005
+ border-radius: var(--plinth-radius-control);
1006
+ }
1007
+
1008
+ .plinth-callout-text code {
1009
+ font-family: var(--plinth-font-family-mono);
1010
+ font-size: var(--plinth-font-size-sm);
1011
+ }
1012
+
1013
+ .plinth-callout-actions {
1014
+ display: flex;
1015
+ flex-wrap: wrap;
1016
+ gap: var(--plinth-space-2);
1017
+ align-items: center;
1018
+ margin-block-start: var(--plinth-space-2);
1019
+ }
1020
+
1021
+ /* Small buttons grow a 40px hit area on a touch screen; wrapped rows keep theirs apart. */
1022
+ @media (pointer: coarse) {
1023
+ .plinth-callout-actions {
1024
+ row-gap: var(--plinth-space-3);
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ /* code-block.css */
1030
+ /*
1031
+ * Code block: a block of code with an optional file name or label, a copy button and optional
1032
+ * language tabs.
1033
+ *
1034
+ * <div class="plinth-code-block">
1035
+ * <div class="plinth-code-block-header">
1036
+ * <span class="plinth-code-block-label">resources/css/brand.css</span>
1037
+ * <button type="button" class="plinth-button plinth-code-block-copy" data-variant="ghost"
1038
+ * data-size="sm" hidden>
1039
+ * <span class="plinth-button-label">Copy</span>
1040
+ * </button>
1041
+ * <span class="plinth-code-block-status" role="status"></span>
1042
+ * </div>
1043
+ * <pre class="plinth-code-block-code" dir="ltr" tabindex="0"><code>…</code></pre>
1044
+ * </div>
1045
+ *
1046
+ * With language tabs, the header holds a radio group after the label, and there is one <pre> per
1047
+ * tab, in the same order:
1048
+ *
1049
+ * <div class="plinth-code-block-tabs" role="radiogroup" aria-label="Language">
1050
+ * <label class="plinth-code-block-tab">
1051
+ * <input type="radio" class="plinth-code-block-tab-input" name="install-language" checked>
1052
+ * <span class="plinth-code-block-tab-label">Blade</span>
1053
+ * </label>
1054
+ * <label class="plinth-code-block-tab">…Vue…</label>
1055
+ * </div>
1056
+ * …
1057
+ * <pre class="plinth-code-block-code" dir="ltr" tabindex="0"><code>…Blade…</code></pre>
1058
+ * <pre class="plinth-code-block-code" dir="ltr" tabindex="0"><code>…Vue…</code></pre>
1059
+ *
1060
+ * The root is the <div>, and takes the app's id. The label is optional, and is text or a little
1061
+ * markup (a badge). The tabs are native radio buttons (ADR 2): the browser moves between them with
1062
+ * the arrow keys and remembers the checked one, and CSS shows the <pre> in the checked tab's
1063
+ * place, with no script. Their name is the root's id with -language (one is generated when the
1064
+ * root has no id), and the group's name is tabs-label in Blade, Vue and React ("Language" by
1065
+ * default). In plain HTML, give each block on a page its own name, or their tabs become one group.
1066
+ * Up to six tabs (the layers drop the rest); the first is checked unless the app picks another
1067
+ * (selected), and after that the browser keeps the reader's choice. Inside a <form> the radios
1068
+ * are part of it: they are sent with it (name=on), and a reset checks the starting tab again.
1069
+ *
1070
+ * The code is text, or the markup a highlighter prints (Phiki, Shiki: the spans inside its
1071
+ * <code>). Plinth styles the block, not the tokens: their colours are the highlighter's. The
1072
+ * <pre> keeps the code's lines and scrolls sideways inside its own box, so it takes tabindex="0"
1073
+ * (a scrolling region must be reachable by keyboard), and dir="ltr", as code reads left to right
1074
+ * on any page.
1075
+ *
1076
+ * Copy: the button (button.css) is printed hidden, and the Blade (Alpine), Vue and React layers
1077
+ * show it when the clipboard is there. It copies the text of the <pre> that shows, says Copied
1078
+ * (copied-label) for two seconds and puts the same word in the status, a polite live region,
1079
+ * hidden from sight. The plain HTML works without it, and the header keeps its place for the
1080
+ * button either way. copy: false in Blade, Vue and React leaves
1081
+ * the button and the status out.
1082
+ *
1083
+ * States: the checked tab (input:checked) is a raised surface with a strong edge; hover only
1084
+ * where the device can hover; the focus ring on :focus-visible, on the tab's input (it covers the
1085
+ * tab) and inset on the <pre>. On a touch screen each tab keeps a 40px touch target, as the
1086
+ * button does. The header hides itself when it has nothing to show.
1087
+ */
1088
+
1089
+ @layer plinth {
1090
+ .plinth-code-block {
1091
+ box-sizing: border-box;
1092
+ display: flex;
1093
+ flex-direction: column;
1094
+ min-inline-size: 0;
1095
+ max-inline-size: 100%;
1096
+ color: var(--plinth-color-text);
1097
+ background-color: var(--plinth-color-surface-sunken);
1098
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
1099
+ border-radius: var(--plinth-radius-container);
1100
+ }
1101
+
1102
+ .plinth-code-block-header {
1103
+ box-sizing: border-box;
1104
+ display: flex;
1105
+ flex-wrap: wrap;
1106
+ gap: var(--plinth-space-2);
1107
+ align-items: center;
1108
+ min-block-size: var(--plinth-touch-target);
1109
+ padding-block: calc((var(--plinth-touch-target) - var(--plinth-control-sm)) / 2);
1110
+ padding-inline: var(--plinth-space-4)
1111
+ calc((var(--plinth-touch-target) - var(--plinth-control-sm)) / 2);
1112
+ border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
1113
+ }
1114
+
1115
+ /* Tabs first, with no label: the header's start matches its end. */
1116
+ .plinth-code-block-header:not(:has(.plinth-code-block-label)) {
1117
+ padding-inline-start: calc((var(--plinth-touch-target) - var(--plinth-control-sm)) / 2);
1118
+ }
1119
+
1120
+ /*
1121
+ * Nothing to show: no label, no tabs and no copy button (copy off). A hidden copy button keeps
1122
+ * the header, so the code doesn't move down when the layer shows it.
1123
+ */
1124
+ .plinth-code-block-header:not(
1125
+ :has(.plinth-code-block-label, .plinth-code-block-tabs, .plinth-code-block-copy)
1126
+ ) {
1127
+ display: none;
1128
+ }
1129
+
1130
+ .plinth-code-block-label {
1131
+ display: inline-flex;
1132
+ flex: 0 1 auto;
1133
+ gap: var(--plinth-space-2);
1134
+ align-items: center;
1135
+ min-inline-size: 0;
1136
+ overflow: hidden;
1137
+ text-overflow: ellipsis;
1138
+ font-family: var(--plinth-font-family-mono);
1139
+ font-size: var(--plinth-font-size-xs);
1140
+ line-height: var(--plinth-font-leading-tight);
1141
+ color: var(--plinth-color-text-muted);
1142
+ white-space: nowrap;
1143
+ }
1144
+
1145
+ .plinth-code-block-label:has(+ .plinth-code-block-tabs) {
1146
+ margin-inline-end: var(--plinth-space-2);
1147
+ }
1148
+
1149
+ /* The tabs follow the label; the copy button sits at the end. */
1150
+ .plinth-code-block-tabs {
1151
+ display: flex;
1152
+ flex: 0 1 auto;
1153
+ flex-wrap: wrap;
1154
+ gap: var(--plinth-space-1);
1155
+ align-items: center;
1156
+ min-inline-size: 0;
1157
+ }
1158
+
1159
+ .plinth-code-block-tab {
1160
+ position: relative;
1161
+ box-sizing: border-box;
1162
+ display: inline-flex;
1163
+ align-items: center;
1164
+ justify-content: center;
1165
+ min-block-size: var(--plinth-control-sm);
1166
+ padding-block: 0;
1167
+ padding-inline: var(--plinth-space-3);
1168
+ font-family: var(--plinth-font-family-body);
1169
+ font-size: var(--plinth-font-size-sm);
1170
+ font-weight: var(--plinth-font-weight-medium);
1171
+ line-height: var(--plinth-font-leading-tight);
1172
+ color: var(--plinth-color-text-muted);
1173
+ white-space: nowrap;
1174
+ cursor: pointer;
1175
+ user-select: none;
1176
+ background-color: transparent;
1177
+ border: var(--plinth-border-width-thin) solid transparent;
1178
+ border-radius: var(--plinth-radius-control);
1179
+ transition:
1180
+ background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
1181
+ border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
1182
+ color var(--plinth-duration-fast) var(--plinth-easing-standard);
1183
+ }
1184
+
1185
+ /* The radio covers its tab, so the tab is what is focused, clicked and ringed. */
1186
+ .plinth-code-block-tab-input {
1187
+ position: absolute;
1188
+ inset: calc(-1 * var(--plinth-border-width-thin));
1189
+ margin: 0;
1190
+ appearance: none;
1191
+ cursor: pointer;
1192
+ background: none;
1193
+ border: 0;
1194
+ border-radius: var(--plinth-radius-control);
1195
+ }
1196
+
1197
+ .plinth-code-block-tab-input:focus-visible {
1198
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
1199
+ outline-offset: var(--plinth-focus-ring-offset);
1200
+ }
1201
+
1202
+ .plinth-code-block-tab:has(> .plinth-code-block-tab-input:checked) {
1203
+ color: var(--plinth-color-text);
1204
+ background-color: var(--plinth-color-surface-raised);
1205
+ border-color: var(--plinth-color-border-strong);
1206
+ }
1207
+
1208
+ @media (hover: hover) {
1209
+ .plinth-code-block-tab:not(:has(> .plinth-code-block-tab-input:checked)):hover {
1210
+ color: var(--plinth-color-text);
1211
+ background-color: var(--plinth-color-surface-hover);
1212
+ }
1213
+ }
1214
+
1215
+ .plinth-code-block-tab:not(:has(> .plinth-code-block-tab-input:checked)):active {
1216
+ background-color: var(--plinth-color-surface-sunken);
1217
+ }
1218
+
1219
+ /* A 40px touch target through an invisible hit area, as on the button (ADR 3). */
1220
+ @media (pointer: coarse) {
1221
+ .plinth-code-block-tab::before {
1222
+ position: absolute;
1223
+ inset-block-start: 50%;
1224
+ inset-inline: min(0%, 50% - var(--plinth-touch-target) / 2);
1225
+ block-size: max(100%, var(--plinth-touch-target));
1226
+ content: '';
1227
+ translate: 0 -50%;
1228
+ }
1229
+ }
1230
+
1231
+ .plinth-code-block-copy {
1232
+ margin-inline-start: auto;
1233
+ }
1234
+
1235
+ /* The button's own display would beat the hidden attribute. */
1236
+ .plinth-code-block-copy[hidden] {
1237
+ display: none;
1238
+ }
1239
+
1240
+ /* Read out, not shown. */
1241
+ .plinth-code-block-status {
1242
+ position: absolute;
1243
+ inline-size: 1px;
1244
+ block-size: 1px;
1245
+ padding: 0;
1246
+ margin: calc(-1 * var(--plinth-border-width-thin));
1247
+ overflow: hidden;
1248
+ white-space: nowrap;
1249
+ border: 0;
1250
+ clip-path: inset(50%);
1251
+ }
1252
+
1253
+ .plinth-code-block-code {
1254
+ box-sizing: border-box;
1255
+ max-inline-size: 100%;
1256
+ padding: var(--plinth-space-4);
1257
+ margin: 0;
1258
+ overflow-x: auto;
1259
+ overscroll-behavior-inline: contain;
1260
+ font-family: var(--plinth-font-family-mono);
1261
+ font-size: var(--plinth-font-size-sm);
1262
+ line-height: var(--plinth-font-leading-normal);
1263
+ color: var(--plinth-color-text);
1264
+ tab-size: 2;
1265
+ white-space: pre;
1266
+ border-radius: var(--plinth-radius-container);
1267
+ }
1268
+
1269
+ .plinth-code-block-code:focus-visible {
1270
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
1271
+ outline-offset: var(--plinth-focus-ring-offset-inset);
1272
+ }
1273
+
1274
+ .plinth-code-block-code > code {
1275
+ font: inherit;
1276
+ color: inherit;
1277
+ background: none;
1278
+ }
1279
+
1280
+ /*
1281
+ * With tabs, only the checked tab's <pre> shows: the nth tab shows the nth <pre>. Nothing
1282
+ * checked (a reset form) shows the first.
1283
+ */
1284
+ .plinth-code-block:has(.plinth-code-block-tabs) > .plinth-code-block-code {
1285
+ display: none;
1286
+ }
1287
+
1288
+ .plinth-code-block:not(:has(.plinth-code-block-tab-input:checked))
1289
+ > .plinth-code-block-code:first-of-type,
1290
+ .plinth-code-block:has(.plinth-code-block-tab:nth-child(1) > :checked)
1291
+ > .plinth-code-block-code:nth-of-type(1),
1292
+ .plinth-code-block:has(.plinth-code-block-tab:nth-child(2) > :checked)
1293
+ > .plinth-code-block-code:nth-of-type(2),
1294
+ .plinth-code-block:has(.plinth-code-block-tab:nth-child(3) > :checked)
1295
+ > .plinth-code-block-code:nth-of-type(3),
1296
+ .plinth-code-block:has(.plinth-code-block-tab:nth-child(4) > :checked)
1297
+ > .plinth-code-block-code:nth-of-type(4),
1298
+ .plinth-code-block:has(.plinth-code-block-tab:nth-child(5) > :checked)
1299
+ > .plinth-code-block-code:nth-of-type(5),
1300
+ .plinth-code-block:has(.plinth-code-block-tab:nth-child(6) > :checked)
1301
+ > .plinth-code-block-code:nth-of-type(6) {
1302
+ display: block;
1303
+ }
1304
+
1305
+ @media (forced-colors: active) {
1306
+ .plinth-code-block-tab:has(> .plinth-code-block-tab-input:checked) {
1307
+ color: HighlightText;
1308
+ background-color: Highlight;
1309
+ border-color: Highlight;
1310
+ }
1311
+ }
1312
+ }
1313
+
1314
+ /* confirm-dialog.css */
1315
+ /*
1316
+ * Confirmation dialog: a dialog that asks a person to confirm an action before it happens, in
1317
+ * three kinds: simple ("Archive 3 tickets?"), type to confirm ("Type harbour to delete this
1318
+ * project") and password ("Enter your password to change the billing email").
1319
+ *
1320
+ * <dialog id="delete" class="plinth-dialog plinth-confirm-dialog" role="alertdialog"
1321
+ * closedby="closerequest" aria-labelledby="delete-title" aria-describedby="delete-message">
1322
+ * <form method="dialog" class="plinth-confirm-dialog-form">
1323
+ * <div class="plinth-dialog-header">
1324
+ * <h2 id="delete-title" class="plinth-dialog-title">Delete this project?</h2>
1325
+ * </div>
1326
+ * <div class="plinth-dialog-body">
1327
+ * <p id="delete-message" class="plinth-confirm-dialog-message">Its tickets go too.</p>
1328
+ * <div class="plinth-confirm-dialog-field">
1329
+ * <label for="delete-field" class="plinth-confirm-dialog-label">
1330
+ * Type <strong>harbour</strong> to confirm</label>
1331
+ * <input id="delete-field" class="plinth-confirm-dialog-input" type="text"
1332
+ * name="confirmation" required pattern="harbour" autocomplete="off"
1333
+ * autocapitalize="none" spellcheck="false">
1334
+ * </div>
1335
+ * <p id="delete-error" class="plinth-confirm-dialog-error" role="alert">…</p>
1336
+ * </div>
1337
+ * <div class="plinth-dialog-footer">
1338
+ * <button type="button" class="plinth-button" data-variant="secondary" data-size="md"
1339
+ * commandfor="delete" command="close" value="cancel">
1340
+ * <span class="plinth-button-label">Cancel</span></button>
1341
+ * <button type="submit" class="plinth-button plinth-confirm-dialog-confirm"
1342
+ * data-variant="danger" data-size="md" value="confirm">
1343
+ * <span class="plinth-button-label">Delete</span></button>
1344
+ * </div>
1345
+ * </form>
1346
+ * </dialog>
1347
+ *
1348
+ * It is a dialog (dialog.css, on modal.css, with button.css for its actions) that is never
1349
+ * dismissible: role="alertdialog", closedby="closerequest" and no close button, so a tap on the
1350
+ * backdrop leaves it open, and it closes through its buttons, Escape acting as Cancel. The root
1351
+ * needs an id; the title's, message's, field's and error's ids are the root's id with -title,
1352
+ * -message, -field and -error. The title and the message are required: they are the alert
1353
+ * dialog's name and description. data-side-phone="bottom" makes it a bottom sheet below 768px, as
1354
+ * on the dialog. A project may set its own role and closedby.
1355
+ *
1356
+ * Its content is a <form method="dialog">. Cancel is a button with command="close" and
1357
+ * value="cancel", which closes the dialog with returnValue "cancel" and never submits, so it works
1358
+ * whatever is in the field; Escape closes it with an empty returnValue.
1359
+ * Confirm is the form's only submit button, so Enter in the field confirms too: once the browser
1360
+ * has checked the field, the form fires submit (it bubbles to the dialog, where the app listens)
1361
+ * and the dialog closes with returnValue "confirm". The browser focuses the first control when it
1362
+ * opens: the field, or Cancel when there is none. The browser doesn't empty the field when the
1363
+ * dialog closes, so the app calls reset() on the form on close: the phrase is typed afresh each
1364
+ * time, and a password doesn't stay in the page.
1365
+ *
1366
+ * Kinds (no attribute: the markup is what differs):
1367
+ * - simple: the message only.
1368
+ * - type to confirm: a text field with required and pattern, the phrase with its pattern
1369
+ * characters escaped. The browser blocks the submit until the text matches, and while the form
1370
+ * is :invalid the confirm button looks disabled, through CSS alone (it isn't disabled: a press
1371
+ * shows the browser's message and focuses the field). The label shows the phrase in <strong>;
1372
+ * a label of the app's own must keep it (:phrase), or nobody can tell what to type.
1373
+ * - password: a field with type="password", name="password", autocomplete="current-password" and
1374
+ * required, so the confirm button looks disabled until something is typed. Plinth never checks
1375
+ * a password: the app calls preventDefault() on the submit, sets
1376
+ * the confirm button busy while it checks, then closes the dialog (close("confirm")) or shows
1377
+ * the error.
1378
+ *
1379
+ * States:
1380
+ * - error: a message under the field, in danger-text with role="alert" so it is announced when it
1381
+ * appears; the field has aria-invalid="true", aria-describedby pointing at it, and a thick
1382
+ * danger edge (thick, so it isn't only a colour).
1383
+ * - busy: the confirm button's busy state (button.css: aria-busy, aria-disabled and a spinner).
1384
+ * The app ignores another submit while it is busy.
1385
+ * - tone: neutral (default) makes the confirm button primary; danger makes it the danger
1386
+ * variant, for an action that destroys something.
1387
+ *
1388
+ * Fields are 16px text on a phone, so iOS doesn't zoom the page when one takes focus, and md from
1389
+ * 768px up; they are control.md tall, and 40px on a touch screen.
1390
+ */
1391
+
1392
+ @layer plinth {
1393
+ /* The form takes the dialog's place as the column of parts, so the body still scrolls. */
1394
+ .plinth-confirm-dialog-form {
1395
+ display: flex;
1396
+ flex: 1 1 auto;
1397
+ flex-direction: column;
1398
+ min-block-size: 0;
1399
+ margin: 0;
1400
+ }
1401
+
1402
+ .plinth-confirm-dialog-message {
1403
+ margin: 0;
1404
+ overflow-wrap: anywhere;
1405
+ }
1406
+
1407
+ .plinth-confirm-dialog-field {
1408
+ display: grid;
1409
+ gap: var(--plinth-space-2);
1410
+ margin-block-start: var(--plinth-space-4);
1411
+ }
1412
+
1413
+ .plinth-confirm-dialog-label {
1414
+ font-size: var(--plinth-font-size-sm);
1415
+ font-weight: var(--plinth-font-weight-medium);
1416
+ line-height: var(--plinth-font-leading-normal);
1417
+ color: var(--plinth-color-text-muted);
1418
+ overflow-wrap: anywhere;
1419
+ }
1420
+
1421
+ .plinth-confirm-dialog-label strong {
1422
+ font-family: var(--plinth-font-family-mono);
1423
+ font-weight: var(--plinth-font-weight-semibold);
1424
+ color: var(--plinth-color-text);
1425
+ }
1426
+
1427
+ .plinth-confirm-dialog-input {
1428
+ box-sizing: border-box;
1429
+ inline-size: 100%;
1430
+ min-block-size: var(--plinth-control-md);
1431
+ padding-block: 0;
1432
+ padding-inline: var(--plinth-space-3);
1433
+ margin: 0;
1434
+ font-family: var(--plinth-font-family-body);
1435
+ font-size: var(--plinth-font-size-field);
1436
+ line-height: var(--plinth-font-leading-tight);
1437
+ color: var(--plinth-color-text);
1438
+ background-color: var(--plinth-color-surface-sunken);
1439
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
1440
+ border-radius: var(--plinth-radius-control);
1441
+ transition: border-color var(--plinth-duration-fast) var(--plinth-easing-standard);
1442
+ }
1443
+
1444
+ /* The phrase is typed as it shows in the label. */
1445
+ .plinth-confirm-dialog-input[type='text'] {
1446
+ font-family: var(--plinth-font-family-mono);
1447
+ }
1448
+
1449
+ .plinth-confirm-dialog-input:focus-visible {
1450
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
1451
+ outline-offset: var(--plinth-focus-ring-offset);
1452
+ }
1453
+
1454
+ .plinth-confirm-dialog-input[aria-invalid='true'] {
1455
+ border-color: var(--plinth-color-danger);
1456
+ border-width: var(--plinth-border-width-thick);
1457
+ }
1458
+
1459
+ .plinth-confirm-dialog-error {
1460
+ margin-block: var(--plinth-space-2) 0;
1461
+ font-size: var(--plinth-font-size-sm);
1462
+ line-height: var(--plinth-font-leading-normal);
1463
+ color: var(--plinth-color-danger-text);
1464
+ overflow-wrap: anywhere;
1465
+ }
1466
+
1467
+ /* Until the field matches, the confirm button looks disabled; it keeps its contrast. */
1468
+ .plinth-confirm-dialog-form:invalid .plinth-confirm-dialog-confirm:not([aria-busy='true']) {
1469
+ --plinth-button-surface: var(--plinth-color-surface-sunken);
1470
+ --plinth-button-surface-hover: var(--plinth-color-surface-sunken);
1471
+ --plinth-button-surface-active: var(--plinth-color-surface-sunken);
1472
+ --plinth-button-text: var(--plinth-color-text-muted);
1473
+ --plinth-button-edge: var(--plinth-color-border);
1474
+ --plinth-button-inner-edge: none;
1475
+
1476
+ cursor: not-allowed;
1477
+ }
1478
+
1479
+ @media (pointer: coarse) {
1480
+ .plinth-confirm-dialog-input {
1481
+ min-block-size: var(--plinth-touch-target);
1482
+ }
1483
+ }
1484
+
1485
+ @media (min-width: 768px) {
1486
+ .plinth-confirm-dialog-input {
1487
+ font-size: var(--plinth-font-size-md);
1488
+ }
1489
+ }
1490
+
1491
+ @media (forced-colors: active) {
1492
+ .plinth-confirm-dialog-form:invalid .plinth-confirm-dialog-confirm:not([aria-busy='true']) {
1493
+ color: GrayText;
1494
+ border-color: GrayText;
1495
+ }
1496
+ }
1497
+ }
1498
+
1499
+ /* data-table.css */
1500
+ /*
1501
+ * Data table: a collection of records (orders, users, invoices) from one markup, a stack of cards
1502
+ * on a phone and a table from 768px up. No script: the CSS alone changes the layout.
1503
+ *
1504
+ * <table class="plinth-data-table" role="table">
1505
+ * <caption class="plinth-data-table-caption">Orders</caption>
1506
+ * <thead class="plinth-data-table-head" role="rowgroup">
1507
+ * <tr class="plinth-data-table-head-row" role="row">
1508
+ * <th class="plinth-data-table-column" role="columnheader" scope="col">Order</th>
1509
+ * <th class="plinth-data-table-column" role="columnheader" scope="col" aria-sort="descending">
1510
+ * <button type="button" class="plinth-data-table-sort">
1511
+ * <span class="plinth-data-table-sort-label">Date</span>
1512
+ * <span class="plinth-icon plinth-data-table-sort-icon" data-size="sm" aria-hidden="true">
1513
+ * <svg>…<path class="plinth-data-table-sort-up"/><path class="plinth-data-table-sort-down"/></svg>
1514
+ * </span>
1515
+ * </button>
1516
+ * </th>
1517
+ * <th class="plinth-data-table-column" role="columnheader" scope="col" data-numeric>Total</th>
1518
+ * <th class="plinth-data-table-column" role="columnheader" scope="col" data-phone="hidden">…</th>
1519
+ * </tr>
1520
+ * </thead>
1521
+ * <tbody class="plinth-data-table-body" role="rowgroup">
1522
+ * <tr class="plinth-data-table-row" role="row">
1523
+ * <td class="plinth-data-table-cell" role="cell" data-kind="title">
1524
+ * <span class="plinth-data-table-label" aria-hidden="true">Order</span>
1525
+ * <span class="plinth-data-table-value">#1042</span>
1526
+ * </td>
1527
+ * <td class="plinth-data-table-cell" role="cell" data-kind="field">…</td>
1528
+ * <td class="plinth-data-table-cell" role="cell" data-kind="field" data-numeric>…</td>
1529
+ * <td class="plinth-data-table-cell" role="cell" data-kind="actions">
1530
+ * <span class="plinth-data-table-value">buttons</span>
1531
+ * </td>
1532
+ * </tr>
1533
+ * <tr class="plinth-data-table-row" role="row" aria-selected="true">…</tr>
1534
+ * </tbody>
1535
+ * </table>
1536
+ *
1537
+ * With no rows, the body holds the empty state:
1538
+ *
1539
+ * <tbody class="plinth-data-table-body" role="rowgroup">
1540
+ * <tr class="plinth-data-table-empty" role="row">
1541
+ * <td class="plinth-data-table-empty-cell" role="cell" colspan="4">No orders yet.</td>
1542
+ * </tr>
1543
+ * </tbody>
1544
+ *
1545
+ * The explicit roles (table, rowgroup, row, columnheader, cell) are part of the contract: some
1546
+ * browsers, Safari in particular, drop a table's semantics once its rows stop displaying as table
1547
+ * rows. The caption is optional; without it, name the table with aria-label or aria-labelledby.
1548
+ *
1549
+ * Each cell repeats its column's name in plinth-data-table-label, shown only on a phone's card
1550
+ * and aria-hidden, since the column header already names the cell. The label is optional (an
1551
+ * actions cell has none). The header row stays in the accessibility tree on a phone, visually
1552
+ * hidden, so every cell keeps its header.
1553
+ *
1554
+ * Choices, on the cells and the column headers:
1555
+ * - data-kind on a cell: field (the default; the label, then the value), title (the card's title,
1556
+ * first on the card, with no label) or actions (row actions, last on the card, with no label).
1557
+ * - data-numeric: numbers, aligned to the end, with figures of one width.
1558
+ * - data-phone="hidden": a less important column, left out of a phone's card and shown from 768px
1559
+ * up. Put it on the column header and on every cell of the column, so the two always match.
1560
+ * The title cell is the row's first cell and the actions cell its last: the card moves them to
1561
+ * its top and bottom with `order`, which must not change the reading and focus order.
1562
+ *
1563
+ * The app owns sorting, paging and selection (ADR 2); the table only shows them:
1564
+ * - aria-sort on a sortable column header: none, ascending or descending (one column at a time).
1565
+ * Its label is a plinth-data-table-sort button, which the app listens to; its icon shows both
1566
+ * arrows (none) or the one of the order. On a phone the sortable headers stay visible above the
1567
+ * cards as a row of sort buttons; the other headers are visually hidden.
1568
+ * - aria-selected on a row of a table whose rows can be selected: true for a selected row, false
1569
+ * for the others. A selected row takes the selection tint (accent-subtle) and an accent edge at
1570
+ * its start. aria-selected drives the styling only: screen readers don't announce it in a
1571
+ * table, so the control that selects a row (a checkbox, a pressed button) is the app's, and it
1572
+ * carries the state.
1573
+ * - the sort button's states: hover where the device can hover, pressed (:active) a surface step
1574
+ * down, disabled (while a sort loads, say) dimmed with opacity.disabled.
1575
+ * - the empty state: one plinth-data-table-empty row in the body, whose plinth-data-table-empty-cell
1576
+ * spans every column (colspan).
1577
+ * Paging controls sit outside the table.
1578
+ *
1579
+ * From 768px up the actions sit at the start of their cell, under their column header.
1580
+ *
1581
+ * Text wraps in its cell. A table wider than its container, from 768px up, belongs in a
1582
+ * scrolling container the app adds. --plinth-data-table-row-height is a body row's height (44px).
1583
+ */
1584
+
1585
+ @layer plinth {
1586
+ /* The phone: a stack of cards. */
1587
+ .plinth-data-table {
1588
+ position: relative;
1589
+ display: block;
1590
+ inline-size: 100%;
1591
+ font-family: var(--plinth-font-family-body);
1592
+ font-size: var(--plinth-font-size-md);
1593
+ line-height: var(--plinth-font-leading-tight);
1594
+ color: var(--plinth-color-text);
1595
+ border-spacing: 0;
1596
+ border-collapse: separate;
1597
+ }
1598
+
1599
+ .plinth-data-table-caption {
1600
+ display: block;
1601
+ padding-inline: var(--plinth-space-4);
1602
+ margin-block-end: var(--plinth-space-2);
1603
+ font-size: var(--plinth-font-size-sm);
1604
+ font-weight: var(--plinth-font-weight-medium);
1605
+ line-height: var(--plinth-font-leading-normal);
1606
+ color: var(--plinth-color-text-muted);
1607
+ text-align: start;
1608
+ overflow-wrap: anywhere;
1609
+ }
1610
+
1611
+ .plinth-data-table-head {
1612
+ display: block;
1613
+ }
1614
+
1615
+ .plinth-data-table-head-row {
1616
+ display: flex;
1617
+ flex-wrap: wrap;
1618
+ gap: var(--plinth-space-2);
1619
+ }
1620
+
1621
+ .plinth-data-table-head-row:has(> [aria-sort]:not([data-phone='hidden'])) {
1622
+ margin-block-end: var(--plinth-space-3);
1623
+ }
1624
+
1625
+ .plinth-data-table-column {
1626
+ display: block;
1627
+ padding: 0;
1628
+ font-size: var(--plinth-font-size-sm);
1629
+ font-weight: var(--plinth-font-weight-medium);
1630
+ color: var(--plinth-color-text-muted);
1631
+ text-align: start;
1632
+ }
1633
+
1634
+ /* Visually hidden, still a column header to assistive technology. */
1635
+ .plinth-data-table-column:not([aria-sort]) {
1636
+ position: absolute;
1637
+ inline-size: 1px;
1638
+ block-size: 1px;
1639
+ overflow: hidden;
1640
+ white-space: nowrap;
1641
+ clip-path: inset(50%);
1642
+ }
1643
+
1644
+ .plinth-data-table-sort {
1645
+ display: inline-flex;
1646
+ gap: var(--plinth-space-1);
1647
+ align-items: center;
1648
+ min-inline-size: var(--plinth-touch-target);
1649
+ min-block-size: var(--plinth-touch-target);
1650
+ padding-inline: var(--plinth-space-3);
1651
+ margin: 0;
1652
+ font: inherit;
1653
+ color: inherit;
1654
+ text-align: inherit;
1655
+ cursor: pointer;
1656
+ background-color: var(--plinth-color-surface-raised);
1657
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
1658
+ border-radius: var(--plinth-radius-pill);
1659
+ transition:
1660
+ color var(--plinth-duration-fast) var(--plinth-easing-standard),
1661
+ background-color var(--plinth-duration-fast) var(--plinth-easing-standard);
1662
+ }
1663
+
1664
+ .plinth-data-table-sort:focus-visible {
1665
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
1666
+ outline-offset: var(--plinth-focus-ring-offset);
1667
+ }
1668
+
1669
+ :is([aria-sort='ascending'], [aria-sort='descending']) > .plinth-data-table-sort {
1670
+ color: var(--plinth-color-text);
1671
+ border-color: var(--plinth-color-border-strong);
1672
+ }
1673
+
1674
+ @media (hover: hover) {
1675
+ .plinth-data-table-sort:hover {
1676
+ color: var(--plinth-color-text);
1677
+ background-color: var(--plinth-color-surface-hover);
1678
+ }
1679
+ }
1680
+
1681
+ .plinth-data-table-sort:disabled {
1682
+ cursor: not-allowed;
1683
+ opacity: var(--plinth-opacity-disabled);
1684
+ }
1685
+
1686
+ .plinth-data-table-sort:enabled:active {
1687
+ background-color: var(--plinth-color-surface-sunken);
1688
+ }
1689
+
1690
+ .plinth-data-table-sort-label {
1691
+ overflow-wrap: anywhere;
1692
+ }
1693
+
1694
+ [aria-sort='none'] .plinth-data-table-sort-icon {
1695
+ color: var(--plinth-color-text-subtle);
1696
+ }
1697
+
1698
+ [aria-sort='ascending'] .plinth-data-table-sort-down,
1699
+ [aria-sort='descending'] .plinth-data-table-sort-up {
1700
+ display: none;
1701
+ }
1702
+
1703
+ .plinth-data-table-body {
1704
+ display: flex;
1705
+ flex-direction: column;
1706
+ gap: var(--plinth-space-3);
1707
+ }
1708
+
1709
+ .plinth-data-table-row {
1710
+ position: relative;
1711
+ display: flex;
1712
+ flex-direction: column;
1713
+ padding-block: var(--plinth-space-1);
1714
+ background-color: var(--plinth-color-surface-raised);
1715
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
1716
+ border-radius: var(--plinth-radius-container);
1717
+ }
1718
+
1719
+ .plinth-data-table-cell {
1720
+ display: flex;
1721
+ gap: var(--plinth-space-3);
1722
+ align-items: baseline;
1723
+ justify-content: space-between;
1724
+ padding-block: var(--plinth-space-2);
1725
+ padding-inline: var(--plinth-space-4);
1726
+ overflow-wrap: anywhere;
1727
+ }
1728
+
1729
+ .plinth-data-table-label {
1730
+ flex: none;
1731
+ max-inline-size: 50%;
1732
+ font-size: var(--plinth-font-size-sm);
1733
+ color: var(--plinth-color-text-muted);
1734
+ }
1735
+
1736
+ .plinth-data-table-value {
1737
+ min-inline-size: 0;
1738
+ text-align: end;
1739
+ }
1740
+
1741
+ .plinth-data-table-cell[data-kind='title'] {
1742
+ order: -1;
1743
+ font-weight: var(--plinth-font-weight-medium);
1744
+ }
1745
+
1746
+ .plinth-data-table-cell:is([data-kind='title'], [data-kind='actions']) .plinth-data-table-label {
1747
+ display: none;
1748
+ }
1749
+
1750
+ .plinth-data-table-cell[data-kind='title'] .plinth-data-table-value {
1751
+ text-align: start;
1752
+ }
1753
+
1754
+ .plinth-data-table-cell[data-kind='actions'] {
1755
+ justify-content: flex-end;
1756
+ order: 1;
1757
+ padding-block-start: var(--plinth-space-3);
1758
+ margin-block-start: var(--plinth-space-1);
1759
+ border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
1760
+ }
1761
+
1762
+ .plinth-data-table-cell[data-kind='actions'] .plinth-data-table-value {
1763
+ display: flex;
1764
+ flex-wrap: wrap;
1765
+ gap: var(--plinth-space-2);
1766
+ align-items: center;
1767
+ justify-content: flex-end;
1768
+ }
1769
+
1770
+ .plinth-data-table [data-numeric] {
1771
+ font-variant-numeric: tabular-nums;
1772
+ }
1773
+
1774
+ .plinth-data-table [data-phone='hidden'] {
1775
+ display: none;
1776
+ }
1777
+
1778
+ .plinth-data-table-row[aria-selected='true'] {
1779
+ background-color: var(--plinth-color-accent-subtle);
1780
+ border-color: var(--plinth-color-accent);
1781
+ }
1782
+
1783
+ /* The selected row's edge, so the tint is not the only sign of it. */
1784
+ .plinth-data-table-row[aria-selected='true']::before {
1785
+ position: absolute;
1786
+ inset-block: var(--plinth-space-3);
1787
+ inset-inline-start: 0;
1788
+ inline-size: var(--plinth-border-width-thick);
1789
+ content: '';
1790
+ background-color: var(--plinth-color-accent);
1791
+ border-radius: var(--plinth-radius-pill);
1792
+ }
1793
+
1794
+ .plinth-data-table-empty {
1795
+ display: block;
1796
+ }
1797
+
1798
+ .plinth-data-table-empty-cell {
1799
+ display: block;
1800
+ padding-block: var(--plinth-space-8);
1801
+ padding-inline: var(--plinth-space-4);
1802
+ color: var(--plinth-color-text-muted);
1803
+ text-align: center;
1804
+ overflow-wrap: anywhere;
1805
+ background-color: var(--plinth-color-surface-raised);
1806
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
1807
+ border-radius: var(--plinth-radius-container);
1808
+ }
1809
+
1810
+ /* From 768px: a table. */
1811
+ @media (min-width: 768px) {
1812
+ .plinth-data-table {
1813
+ display: table;
1814
+ }
1815
+
1816
+ .plinth-data-table-caption {
1817
+ display: table-caption;
1818
+ padding-inline: 0;
1819
+ caption-side: top;
1820
+ }
1821
+
1822
+ .plinth-data-table-head {
1823
+ display: table-header-group;
1824
+ }
1825
+
1826
+ .plinth-data-table-head-row,
1827
+ .plinth-data-table-head-row:has(> [aria-sort]:not([data-phone='hidden'])) {
1828
+ display: table-row;
1829
+ margin: 0;
1830
+ }
1831
+
1832
+ .plinth-data-table-column,
1833
+ .plinth-data-table-column:not([aria-sort]),
1834
+ .plinth-data-table [data-phone='hidden'].plinth-data-table-column {
1835
+ position: static;
1836
+ box-sizing: border-box;
1837
+ display: table-cell;
1838
+ inline-size: auto;
1839
+ block-size: var(--plinth-touch-target);
1840
+ padding-block: 0;
1841
+ padding-inline: var(--plinth-space-3);
1842
+ overflow: visible;
1843
+ vertical-align: middle;
1844
+ white-space: normal;
1845
+ border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-border);
1846
+ clip-path: none;
1847
+ }
1848
+
1849
+ .plinth-data-table-sort {
1850
+ padding-inline: var(--plinth-space-1);
1851
+ margin-inline: calc(var(--plinth-space-1) * -1);
1852
+ background-color: transparent;
1853
+ border: 0;
1854
+ border-radius: var(--plinth-radius-control);
1855
+ }
1856
+
1857
+ @media (hover: hover) {
1858
+ .plinth-data-table-sort:hover {
1859
+ background-color: transparent;
1860
+ }
1861
+ }
1862
+
1863
+ .plinth-data-table-sort:enabled:active {
1864
+ background-color: var(--plinth-color-surface-sunken);
1865
+ }
1866
+
1867
+ .plinth-data-table-body {
1868
+ display: table-row-group;
1869
+ }
1870
+
1871
+ .plinth-data-table-row {
1872
+ position: static;
1873
+ display: table-row;
1874
+ padding: 0;
1875
+ background-color: transparent;
1876
+ border: 0;
1877
+ border-radius: 0;
1878
+ }
1879
+
1880
+ .plinth-data-table-cell,
1881
+ .plinth-data-table-cell[data-kind='actions'],
1882
+ .plinth-data-table [data-phone='hidden'].plinth-data-table-cell {
1883
+ position: relative;
1884
+ box-sizing: border-box;
1885
+ display: table-cell;
1886
+ block-size: var(--plinth-data-table-row-height);
1887
+ padding-block: var(--plinth-space-2);
1888
+ padding-inline: var(--plinth-space-3);
1889
+ margin: 0;
1890
+ vertical-align: middle;
1891
+ text-align: start;
1892
+ overflow-wrap: break-word;
1893
+ border: 0;
1894
+ }
1895
+
1896
+ .plinth-data-table-row + .plinth-data-table-row > .plinth-data-table-cell {
1897
+ border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
1898
+ }
1899
+
1900
+ .plinth-data-table-label {
1901
+ display: none;
1902
+ }
1903
+
1904
+ .plinth-data-table-value {
1905
+ text-align: inherit;
1906
+ }
1907
+
1908
+ .plinth-data-table-cell[data-kind='actions'] .plinth-data-table-value {
1909
+ display: inline-flex;
1910
+ flex-wrap: nowrap;
1911
+ }
1912
+
1913
+ .plinth-data-table-column[data-numeric],
1914
+ .plinth-data-table-cell[data-numeric] {
1915
+ text-align: end;
1916
+ }
1917
+
1918
+ .plinth-data-table-row[aria-selected='true'] {
1919
+ border-color: transparent;
1920
+ }
1921
+
1922
+ .plinth-data-table-row[aria-selected='true'] > .plinth-data-table-cell {
1923
+ background-color: var(--plinth-color-accent-subtle);
1924
+ }
1925
+
1926
+ .plinth-data-table-row[aria-selected='true']::before {
1927
+ content: none;
1928
+ }
1929
+
1930
+ .plinth-data-table-row[aria-selected='true'] > .plinth-data-table-cell:first-child::before {
1931
+ position: absolute;
1932
+ inset-block: 0;
1933
+ inset-inline-start: 0;
1934
+ inline-size: var(--plinth-border-width-thick);
1935
+ content: '';
1936
+ background-color: var(--plinth-color-accent);
1937
+ }
1938
+
1939
+ .plinth-data-table-empty {
1940
+ display: table-row;
1941
+ }
1942
+
1943
+ .plinth-data-table-empty-cell {
1944
+ display: table-cell;
1945
+ background-color: transparent;
1946
+ border: 0;
1947
+ border-radius: 0;
1948
+ }
1949
+ }
1950
+
1951
+ @media (min-width: 1280px) {
1952
+ .plinth-data-table-column,
1953
+ .plinth-data-table-column:not([aria-sort]),
1954
+ .plinth-data-table [data-phone='hidden'].plinth-data-table-column,
1955
+ .plinth-data-table-cell,
1956
+ .plinth-data-table-cell[data-kind='actions'],
1957
+ .plinth-data-table [data-phone='hidden'].plinth-data-table-cell {
1958
+ padding-inline: var(--plinth-space-4);
1959
+ }
1960
+ }
1961
+
1962
+ @media (forced-colors: active) {
1963
+ .plinth-data-table-row[aria-selected='true'],
1964
+ .plinth-data-table-row[aria-selected='true'] > .plinth-data-table-cell {
1965
+ color: HighlightText;
1966
+ background-color: Highlight;
1967
+ }
1968
+ }
1969
+ }
1970
+
1971
+ /* dialog.css */
1972
+ /*
1973
+ * Dialog: a modal for a form, the details of something or its settings, centred on the screen at
1974
+ * every width; with data-side-phone="bottom" it is a bottom sheet below 768px.
1975
+ *
1976
+ * <dialog id="settings" class="plinth-dialog" closedby="any"
1977
+ * aria-labelledby="settings-title" aria-describedby="settings-description">
1978
+ * <div class="plinth-dialog-panel">
1979
+ * <div class="plinth-dialog-handle" aria-hidden="true"></div>
1980
+ * <div class="plinth-dialog-header">
1981
+ * <h2 id="settings-title" class="plinth-dialog-title">Settings</h2>
1982
+ * <p id="settings-description" class="plinth-dialog-description">Changes apply at once.</p>
1983
+ * <button type="button" class="plinth-button plinth-icon-button plinth-dialog-close"
1984
+ * data-variant="ghost" data-size="md" aria-label="Close" commandfor="settings"
1985
+ * command="close">
1986
+ * <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
1987
+ * </button>
1988
+ * </div>
1989
+ * <div class="plinth-dialog-body">…</div>
1990
+ * <div class="plinth-dialog-footer">…</div>
1991
+ * </div>
1992
+ * </dialog>
1993
+ *
1994
+ * The root is the <dialog>, and it needs an id: commandfor and aria-labelledby point at it, and
1995
+ * the title's and description's ids are the root's id with -title and -description. The panel is
1996
+ * the root's only child and holds the parts. The handle is there only on a dismissible dialog
1997
+ * with data-side-phone="bottom": decorative (aria-hidden), shown below 768px, where the dialog is
1998
+ * dragged down to close (modal.css). The title is
1999
+ * required; the description (and its aria-describedby) and the footer are optional. The close
2000
+ * button is an icon button (button.css and icon.css). It needs modal.css, the base it shares with
2001
+ * the sheet: the surface, the backdrop, the scroll lock, the motion, the safe areas and the parts
2002
+ * are there, and this file sets only where the dialog sits and how big it is.
2003
+ *
2004
+ * Open it with showModal() or a <button commandfor="settings" command="show-modal">. Open is the
2005
+ * native state, [open]; the browser puts the dialog in the top layer, makes the page inert, traps
2006
+ * focus, and returns focus to what opened it. The dialog fires close when it closes.
2007
+ *
2008
+ * Dismissible (the default): closedby="any" and the close button; the close button, Escape and a
2009
+ * tap on the backdrop close it (closedby="any" is an enhancement Safari doesn't have yet, so the
2010
+ * close button is always there). Not dismissible, for a choice the person must make:
2011
+ * role="alertdialog", closedby="closerequest" and no close button, so a stray tap on the backdrop
2012
+ * does nothing and it closes through its own buttons, with Escape acting as Cancel (the browser
2013
+ * fires cancel, then close, with an empty returnValue). It needs an action that closes it,
2014
+ * usually in the footer, the least destructive first: a touch screen has no Escape, and the
2015
+ * browser focuses the first control. Give it a description: an alert dialog's message is its
2016
+ * accessible description. A project may set its own closedby.
2017
+ *
2018
+ * Presentation: centred at every width, dialog.width wide (less the screen's margin on a phone),
2019
+ * its height capped by the screen's, fading and rising into place. data-side-phone="bottom" (the
2020
+ * only value; leave it out for centred) makes it a bottom sheet below 768px: the screen's width,
2021
+ * on the bottom edge, sliding up, its last part clearing the home indicator; from 768px up it is
2022
+ * centred like any dialog.
2023
+ *
2024
+ * On a phone the dialog's height is capped by 100dvh, which doesn't, on its own, clear the
2025
+ * on-screen keyboard: a page with a form in a dialog sets interactive-widget=resizes-content in
2026
+ * its viewport meta where the browser supports it, as for the sheet.
2027
+ *
2028
+ * In Livewire, a re-render would morph the open dialog back to the server's markup and drop its
2029
+ * open attribute: put wire:ignore.self on the dialog (or keep it outside the component that
2030
+ * re-renders), and always pass an id, since a generated one changes on every render.
2031
+ */
2032
+
2033
+ @layer plinth {
2034
+ .plinth-dialog {
2035
+ --plinth-modal-offset: 0 var(--plinth-space-4);
2036
+ --plinth-modal-closed-opacity: 0;
2037
+
2038
+ inset: 0;
2039
+ inline-size: min(100% - 2 * var(--plinth-space-4), var(--plinth-dialog-width));
2040
+ max-block-size: calc(100dvh - 2 * var(--plinth-space-4));
2041
+ margin: auto;
2042
+ }
2043
+
2044
+ .plinth-dialog[data-side-phone='bottom'] {
2045
+ --plinth-modal-offset: 0 100%;
2046
+ --plinth-modal-closed-opacity: 1;
2047
+ --plinth-modal-safe-area-end: env(safe-area-inset-bottom);
2048
+
2049
+ inset-block: auto 0;
2050
+ inline-size: 100%;
2051
+ max-block-size: calc(100dvh - var(--plinth-space-12));
2052
+ margin: 0;
2053
+ border-block-end-width: 0;
2054
+ border-end-start-radius: 0;
2055
+ border-end-end-radius: 0;
2056
+ }
2057
+
2058
+ /* One that can be dismissed is dragged down to close (modal.css): the root fills the screen. */
2059
+ .plinth-dialog[data-side-phone='bottom']:has(> * > .plinth-dialog-handle) {
2060
+ inset: 0;
2061
+ block-size: auto;
2062
+ max-block-size: none;
2063
+ border-width: 0;
2064
+ border-radius: 0;
2065
+ }
2066
+
2067
+ @media (min-width: 768px) {
2068
+ .plinth-dialog,
2069
+ .plinth-dialog[data-side-phone='bottom'],
2070
+ .plinth-dialog[data-side-phone='bottom']:has(> * > .plinth-dialog-handle) {
2071
+ --plinth-modal-offset: 0 var(--plinth-space-4);
2072
+ --plinth-modal-closed-opacity: 0;
2073
+ --plinth-modal-safe-area-end: initial;
2074
+
2075
+ inset: 0;
2076
+ inline-size: min(100% - 2 * var(--plinth-space-8), var(--plinth-dialog-width));
2077
+ block-size: fit-content;
2078
+ max-block-size: calc(100dvh - 2 * var(--plinth-space-8));
2079
+ margin: auto;
2080
+ border-width: var(--plinth-border-width-thin);
2081
+ border-radius: var(--plinth-radius-container);
2082
+ }
2083
+ }
2084
+ }
2085
+
2086
+ /* drawer.css */
2087
+ /*
2088
+ * Drawer: a phone-first panel on the bottom edge that the person drags, like a maps app's panel,
2089
+ * with optional snap points (a peek, half and full height). From 768px up it is a dialog or a
2090
+ * sheet.
2091
+ *
2092
+ * <dialog id="route" class="plinth-drawer" data-desktop="dialog"
2093
+ * data-snap-points="peek half full" data-snap="peek" closedby="any"
2094
+ * aria-labelledby="route-title" aria-describedby="route-description">
2095
+ * <div class="plinth-drawer-panel">
2096
+ * <div class="plinth-drawer-handle" aria-hidden="true"></div>
2097
+ * <div class="plinth-drawer-header">
2098
+ * <h2 id="route-title" class="plinth-drawer-title">Route</h2>
2099
+ * <p id="route-description" class="plinth-drawer-description">12 min by bike.</p>
2100
+ * <button type="button" class="plinth-button plinth-icon-button plinth-drawer-close"
2101
+ * data-variant="ghost" data-size="md" aria-label="Close" commandfor="route"
2102
+ * command="close">
2103
+ * <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
2104
+ * </button>
2105
+ * </div>
2106
+ * <div class="plinth-drawer-body">…</div>
2107
+ * <div class="plinth-drawer-footer">…</div>
2108
+ * </div>
2109
+ * </dialog>
2110
+ *
2111
+ * The root is the <dialog>, and it needs an id: commandfor and aria-labelledby point at it, and
2112
+ * the title's and description's ids are the root's id with -title and -description. The panel is
2113
+ * the root's only child and holds the parts; the handle is always there, first, and decorative
2114
+ * (aria-hidden): Escape and the close button, which is always there too, are the ways to close it
2115
+ * without dragging. The title is required; the description (and its aria-describedby) and the
2116
+ * footer are optional. The close button is an icon button (button.css and icon.css). It needs
2117
+ * modal.css, the base it shares with the dialog and the sheet, where the dragging is described:
2118
+ * this file sets where the drawer sits, how big it is and its snap points.
2119
+ *
2120
+ * Open it with showModal() or a <button commandfor="route" command="show-modal">. Open is the
2121
+ * native state, [open]; the browser puts the drawer in the top layer, makes the page inert, traps
2122
+ * focus, closes on Escape and returns focus. The dialog fires close when it closes, whichever way
2123
+ * it closed.
2124
+ *
2125
+ * On a phone (below 768px) it is the screen's width on the bottom edge and slides up. Dragged
2126
+ * down past its lowest snap point it closes; the layers do that part (modal.css). With no snap
2127
+ * points it is as tall as its content, up to the screen's height less a gap.
2128
+ *
2129
+ * Snap points (data-snap-points, optional): any of peek, half and full, separated by spaces.
2130
+ * With them the panel is the screen's height less a gap, and it rests where it is let go at the
2131
+ * nearest one: peek shows drawer.peek-height of it (the handle, the header and the top of the
2132
+ * body), half the screen's height, full all of it. The body scrolls inside the panel at any of
2133
+ * them, so a drawer with a long body is dragged by its handle and header. data-snap is the snap
2134
+ * point it opens at, one of those listed (the layers print the largest listed when it is missing
2135
+ * or unknown); the browser opens it at full, or the largest listed, and the layers then scroll it
2136
+ * to data-snap. The drawer remembers nothing: where it rests is the person's.
2137
+ *
2138
+ * Desktop (data-desktop): its presentation from 768px up. dialog (the default, also when missing
2139
+ * or unknown) centres it like the dialog, at dialog.width; sheet makes it a full-height panel on
2140
+ * the inline-end edge, sheet.side-width wide, like the sheet. From 768px up there is no handle
2141
+ * and no dragging, the snap points don't apply, and a tap on the backdrop closes it
2142
+ * (closedby="any"; Safari doesn't have it yet, so the close button is always there).
2143
+ *
2144
+ * The motion runs on the motion tokens, which drop to 0ms under prefers-reduced-motion; the
2145
+ * snapping is the browser's scrolling.
2146
+ *
2147
+ * In Livewire, a re-render would morph the open dialog back to the server's markup and drop its
2148
+ * open attribute: put wire:ignore.self on the drawer (or keep it outside the component that
2149
+ * re-renders), and always pass an id, since a generated one changes on every render.
2150
+ */
2151
+
2152
+ @layer plinth {
2153
+ .plinth-drawer {
2154
+ --plinth-modal-offset: 0 100%;
2155
+ --plinth-modal-safe-area-end: env(safe-area-inset-bottom);
2156
+
2157
+ /* Translate isn't logical: -1 (under :dir(rtl), below) turns the sheet's slide around. */
2158
+ --plinth-drawer-direction: 1;
2159
+
2160
+ inset: 0;
2161
+ inline-size: 100%;
2162
+ block-size: auto;
2163
+ max-block-size: none;
2164
+ margin: 0;
2165
+ border-width: 0;
2166
+ border-radius: 0;
2167
+ }
2168
+
2169
+ /* With snap points the panel is its full height, and the snap points are where it rests. */
2170
+ .plinth-drawer[data-snap-points] > .plinth-drawer-panel {
2171
+ block-size: calc(
2172
+ 100% - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
2173
+ );
2174
+ }
2175
+
2176
+ /* Full is the panel's end on the screen's bottom edge, the base's open position. */
2177
+ .plinth-drawer[data-snap-points]:not([data-snap-points~='full']) > .plinth-drawer-panel {
2178
+ scroll-snap-align: none;
2179
+ }
2180
+
2181
+ /*
2182
+ * Peek and half are the panel's top that far from the screen's bottom edge. The snap areas are
2183
+ * the handle (peek) and the header (half), the first parts of the panel, so their margins add
2184
+ * what lies above them: the handle's margin, then the handle (modal.css; change them together).
2185
+ */
2186
+ .plinth-drawer[data-snap-points~='peek'] .plinth-drawer-handle {
2187
+ scroll-snap-align: start;
2188
+ scroll-margin-block-start: calc(
2189
+ 100dvh - var(--plinth-drawer-peek-height) + var(--plinth-space-2)
2190
+ );
2191
+ }
2192
+
2193
+ .plinth-drawer[data-snap-points~='half'] .plinth-drawer-header {
2194
+ scroll-snap-align: start;
2195
+ scroll-margin-block-start: calc(50dvh + var(--plinth-space-2) + var(--plinth-space-1));
2196
+ }
2197
+
2198
+ .plinth-drawer:dir(rtl) {
2199
+ --plinth-drawer-direction: -1;
2200
+ }
2201
+
2202
+ @media (min-width: 768px) {
2203
+ .plinth-drawer {
2204
+ --plinth-modal-offset: 0 var(--plinth-space-4);
2205
+ --plinth-modal-closed-opacity: 0;
2206
+ --plinth-modal-safe-area-end: initial;
2207
+
2208
+ inset: 0;
2209
+ inline-size: min(100% - 2 * var(--plinth-space-8), var(--plinth-dialog-width));
2210
+ block-size: fit-content;
2211
+ max-block-size: calc(100dvh - 2 * var(--plinth-space-8));
2212
+ margin: auto;
2213
+ border-width: var(--plinth-border-width-thin);
2214
+ border-radius: var(--plinth-radius-container);
2215
+ }
2216
+
2217
+ .plinth-drawer[data-desktop='sheet'] {
2218
+ --plinth-modal-offset: calc(100% * var(--plinth-drawer-direction)) 0;
2219
+ --plinth-modal-closed-opacity: 1;
2220
+ --plinth-modal-safe-area-end: env(safe-area-inset-bottom);
2221
+
2222
+ inset-block: 0;
2223
+ inset-inline: auto 0;
2224
+ inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
2225
+ block-size: auto;
2226
+ max-block-size: none;
2227
+ padding-block: env(safe-area-inset-top) 0;
2228
+ margin: 0;
2229
+ border-width: 0;
2230
+ border-inline-start-width: var(--plinth-border-width-thin);
2231
+ border-radius: 0;
2232
+ }
2233
+
2234
+ .plinth-drawer[data-snap-points] > .plinth-drawer-panel {
2235
+ block-size: auto;
2236
+ }
2237
+
2238
+ .plinth-drawer[data-snap-points] :is(.plinth-drawer-handle, .plinth-drawer-header) {
2239
+ scroll-snap-align: none;
2240
+ }
2241
+ }
2242
+ }
2243
+
2244
+ /* header.css */
2245
+ /*
2246
+ * Header: the top bar of the phone shell, with a leading action, the page's title and trailing
2247
+ * actions. It sticks to the top of the page as the content scrolls under it.
2248
+ *
2249
+ * <header class="plinth-header">
2250
+ * <div class="plinth-header-leading">
2251
+ * <a class="plinth-button plinth-icon-button" href="/inbox" data-variant="ghost"
2252
+ * data-size="md" aria-label="Back">
2253
+ * <span class="plinth-icon" data-size="md" data-mirror aria-hidden="true"><svg>…</svg></span>
2254
+ * </a>
2255
+ * </div>
2256
+ * <h1 class="plinth-header-title">Inbox</h1>
2257
+ * <div class="plinth-header-actions">
2258
+ * <button type="button" class="plinth-button plinth-icon-button" data-variant="ghost"
2259
+ * data-size="md" aria-label="Search">…</button>
2260
+ * </div>
2261
+ * </header>
2262
+ *
2263
+ * The root is a <header>: at the top of the page (not inside <main>, <article> or <section>) it is
2264
+ * the page's banner landmark, so a page has one. The title is an <h1>, the page's heading, and is
2265
+ * required. It stays on one line and ends in an ellipsis when it doesn't fit.
2266
+ *
2267
+ * The leading part holds one action, usually an icon button: back (a link, with data-mirror on
2268
+ * its icon so it points the other way in right-to-left text) or a menu button that opens a sheet
2269
+ * (commandfor and command="show-modal"). The actions part holds the trailing actions, at most two
2270
+ * or three icon buttons; put the rest in a menu. Both are optional, and printed only when they
2271
+ * have content. Every icon button needs its own name (label). The header needs button.css and
2272
+ * icon.css for them.
2273
+ *
2274
+ * Its height is the header.height component token (56px), plus env(safe-area-inset-top), which
2275
+ * pads it below the status bar and the notch on a phone that draws under them (viewport-fit=cover);
2276
+ * the inline padding clears the notch on a phone held sideways. It is sticky on the
2277
+ * layer.sticky z-index, so it sits above the page and below sheets and menus (the top layer).
2278
+ * Content scrolls under it, so the app sets scroll-padding-block-start on its scrolling element
2279
+ * (calc(var(--plinth-header-height) + env(safe-area-inset-top))) to keep a focused element or an
2280
+ * anchor's target from landing under the header.
2281
+ *
2282
+ * The actions sit var(--plinth-space-2) apart, so on a touch screen the 40px hit areas of 32px
2283
+ * icon buttons don't overlap.
2284
+ *
2285
+ * The app owns the state: the header has no variants or states of its own. What the actions do is
2286
+ * the app's (a link, a commandfor, wire:click, @click on the icon button).
2287
+ */
2288
+
2289
+ @layer plinth {
2290
+ .plinth-header {
2291
+ position: sticky;
2292
+ inset-block-start: 0;
2293
+ z-index: var(--plinth-layer-sticky);
2294
+ box-sizing: border-box;
2295
+ display: flex;
2296
+ gap: var(--plinth-space-2);
2297
+ align-items: center;
2298
+ min-block-size: calc(var(--plinth-header-height) + env(safe-area-inset-top));
2299
+ padding-block-start: env(safe-area-inset-top);
2300
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
2301
+ max(var(--plinth-space-2), env(safe-area-inset-right));
2302
+ font-family: var(--plinth-font-family-body);
2303
+ color: var(--plinth-color-text);
2304
+ background-color: var(--plinth-color-surface);
2305
+ border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
2306
+ }
2307
+
2308
+ .plinth-header:dir(rtl) {
2309
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
2310
+ max(var(--plinth-space-2), env(safe-area-inset-left));
2311
+ }
2312
+
2313
+ .plinth-header-leading,
2314
+ .plinth-header-actions {
2315
+ display: flex;
2316
+ flex: none;
2317
+ gap: var(--plinth-space-2);
2318
+ align-items: center;
2319
+ }
2320
+
2321
+ .plinth-header-title {
2322
+ flex: 1 1 auto;
2323
+ min-inline-size: 0;
2324
+ margin: 0;
2325
+ overflow: hidden;
2326
+ text-overflow: ellipsis;
2327
+ font-size: var(--plinth-font-size-lg);
2328
+ font-weight: var(--plinth-font-weight-semibold);
2329
+ line-height: var(--plinth-font-leading-tight);
2330
+ letter-spacing: var(--plinth-font-tracking-tight);
2331
+ white-space: nowrap;
2332
+ }
2333
+
2334
+ /* Without a leading action or trailing actions, the title keeps the text's distance from the edge. */
2335
+ .plinth-header-title:first-child {
2336
+ margin-inline-start: var(--plinth-space-2);
2337
+ }
2338
+
2339
+ .plinth-header-title:last-child {
2340
+ margin-inline-end: var(--plinth-space-2);
2341
+ }
2342
+
2343
+ @media (min-width: 768px) {
2344
+ .plinth-header {
2345
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
2346
+ max(var(--plinth-space-4), env(safe-area-inset-right));
2347
+ }
2348
+
2349
+ .plinth-header:dir(rtl) {
2350
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
2351
+ max(var(--plinth-space-4), env(safe-area-inset-left));
2352
+ }
2353
+ }
2354
+ }
2355
+
2356
+ /* icon.css */
2357
+ /*
2358
+ * Icon: a project's own SVG, sized by Plinth.
2359
+ *
2360
+ * Decorative (the default), hidden from assistive technology:
2361
+ * <span class="plinth-icon" data-size="md" aria-hidden="true">
2362
+ * <svg viewBox="0 0 24 24">…</svg>
2363
+ * </span>
2364
+ *
2365
+ * Meaningful, with a label (an img role makes the SVG inside presentational). Blade, Vue and
2366
+ * React take it as `label`, or as an `aria-label` attribute; a blank label leaves the icon
2367
+ * decorative:
2368
+ * <span class="plinth-icon" data-size="md" role="img" aria-label="Warning">
2369
+ * <svg viewBox="0 0 24 24">…</svg>
2370
+ * </span>
2371
+ *
2372
+ * Sizes (data-size): sm 16px, md 20px (default, also when the attribute is missing or unknown),
2373
+ * lg 24px. data-mirror flips the icon in right-to-left text, for icons that point a direction.
2374
+ *
2375
+ * The SVG comes from the project, inline or as <use href="sprite.svg#name">. It should draw with
2376
+ * currentColor; Plinth sets no fill or stroke, so outline and filled icon sets both keep theirs.
2377
+ * The icon takes its colour from the text around it.
2378
+ */
2379
+
2380
+ @layer plinth {
2381
+ .plinth-icon {
2382
+ --plinth-icon-current-size: var(--plinth-size-icon-md);
2383
+
2384
+ display: inline-flex;
2385
+ flex: none;
2386
+ inline-size: var(--plinth-icon-current-size);
2387
+ block-size: var(--plinth-icon-current-size);
2388
+ vertical-align: middle;
2389
+ }
2390
+
2391
+ .plinth-icon[data-size='sm'] {
2392
+ --plinth-icon-current-size: var(--plinth-size-icon-sm);
2393
+ }
2394
+
2395
+ .plinth-icon[data-size='lg'] {
2396
+ --plinth-icon-current-size: var(--plinth-size-icon-lg);
2397
+ }
2398
+
2399
+ .plinth-icon > svg {
2400
+ inline-size: 100%;
2401
+ block-size: 100%;
2402
+ }
2403
+
2404
+ .plinth-icon[data-mirror]:dir(rtl) > svg {
2405
+ transform: scaleX(-1);
2406
+ }
2407
+ }
2408
+
2409
+ /* list-card.css */
2410
+ /*
2411
+ * List card: a group of list rows on a raised surface, with an optional heading and footer note.
2412
+ *
2413
+ * <div class="plinth-list-card">
2414
+ * <h2 class="plinth-list-card-heading">Account</h2>
2415
+ * <ul class="plinth-list-card-list" role="list">
2416
+ * <li><a class="plinth-list-row" href="…">…</a></li>
2417
+ * <li><div class="plinth-list-row">…</div></li>
2418
+ * </ul>
2419
+ * <p class="plinth-list-card-footer">Changes apply to every device.</p>
2420
+ * </div>
2421
+ *
2422
+ * The heading and the footer are optional, and sit outside the raised surface: the heading above
2423
+ * it, the footer note below. The heading is an <h2> by default; pick the level that fits the page
2424
+ * (heading-level h2 to h6 in Blade, headingLevel in Vue).
2425
+ *
2426
+ * The list is the raised surface (color.surface-raised, radius.container, a hairline in
2427
+ * color.border; no shadow, per ADR 3: the card does not float). The card resets the list's
2428
+ * margin, padding and bullets, and prints role="list" so Safari keeps announcing it. Each row
2429
+ * goes in its own <li> and keeps its own root, states and divider; the surface clips the rows'
2430
+ * hover and selection tints to its corners, and anything a row positions outside itself (a
2431
+ * popover or a dialog is in the top layer, so it is not). The first and last rows take the
2432
+ * surface's inner radius, so their focus ring, drawn inside the row, follows the corners.
2433
+ */
2434
+
2435
+ @layer plinth {
2436
+ .plinth-list-card {
2437
+ display: flex;
2438
+ flex-direction: column;
2439
+ gap: var(--plinth-space-2);
2440
+ min-inline-size: 0;
2441
+ font-family: var(--plinth-font-family-body);
2442
+ color: var(--plinth-color-text);
2443
+ }
2444
+
2445
+ .plinth-list-card-heading {
2446
+ padding-inline: var(--plinth-space-4);
2447
+ margin: 0;
2448
+ font-size: var(--plinth-font-size-sm);
2449
+ font-weight: var(--plinth-font-weight-medium);
2450
+ line-height: var(--plinth-font-leading-normal);
2451
+ color: var(--plinth-color-text-muted);
2452
+ overflow-wrap: anywhere;
2453
+ }
2454
+
2455
+ .plinth-list-card-list {
2456
+ padding: 0;
2457
+ margin: 0;
2458
+ overflow: hidden;
2459
+ list-style: none;
2460
+ background-color: var(--plinth-color-surface-raised);
2461
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
2462
+ border-radius: var(--plinth-radius-container);
2463
+ }
2464
+
2465
+ .plinth-list-card-footer {
2466
+ padding-inline: var(--plinth-space-4);
2467
+ margin: 0;
2468
+ font-size: var(--plinth-font-size-sm);
2469
+ line-height: var(--plinth-font-leading-normal);
2470
+ color: var(--plinth-color-text-muted);
2471
+ overflow-wrap: anywhere;
2472
+ }
2473
+
2474
+ .plinth-list-card-list > li:first-child > .plinth-list-row {
2475
+ border-start-start-radius: calc(
2476
+ var(--plinth-radius-container) - var(--plinth-border-width-thin)
2477
+ );
2478
+ border-start-end-radius: calc(var(--plinth-radius-container) - var(--plinth-border-width-thin));
2479
+ }
2480
+
2481
+ .plinth-list-card-list > li:last-child > .plinth-list-row {
2482
+ border-end-start-radius: calc(var(--plinth-radius-container) - var(--plinth-border-width-thin));
2483
+ border-end-end-radius: calc(var(--plinth-radius-container) - var(--plinth-border-width-thin));
2484
+ }
2485
+ }
2486
+
2487
+ /* list-row.css */
2488
+ /*
2489
+ * List row: one row of a list or a sheet, 44px tall at least.
2490
+ *
2491
+ * <div class="plinth-list-row">
2492
+ * <span class="plinth-list-row-leading">an icon or an avatar</span>
2493
+ * <span class="plinth-list-row-content">
2494
+ * <span class="plinth-list-row-title">Notifications</span>
2495
+ * <span class="plinth-list-row-description">Email and push</span>
2496
+ * </span>
2497
+ * <span class="plinth-list-row-trailing">
2498
+ * a value, a badge or a control
2499
+ * <span class="plinth-icon plinth-list-row-chevron" data-size="sm" data-mirror
2500
+ * aria-hidden="true"><svg>…</svg></span>
2501
+ * </span>
2502
+ * </div>
2503
+ *
2504
+ * The leading slot, the description and the trailing slot are optional; the chevron is the
2505
+ * row's own (chevron in Blade and Vue), and points the other way in right-to-left text.
2506
+ *
2507
+ * The root is the element the app talks to (ADR 2):
2508
+ * - a static row is a <div>;
2509
+ * - a row that navigates is <a class="plinth-list-row" href="…">;
2510
+ * - a row that acts is <button type="button" class="plinth-list-row">.
2511
+ * A link or button row holds no other control: a switch or an icon button in the trailing slot
2512
+ * needs a static row, and its own accessible name.
2513
+ *
2514
+ * States:
2515
+ * - current: aria-current="page" on a link row (or a static row) for the page being shown, set
2516
+ * by the app; the selection tint (accent-subtle, accent-text) and a heavier title. A button
2517
+ * row never prints it: a button is not a page.
2518
+ * - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
2519
+ * - hover only where the device can hover; the focus ring on :focus-visible, drawn inside the
2520
+ * row (focus-ring.offset-inset).
2521
+ *
2522
+ * A hairline divider (color.divider) sits between two rows that follow each other, as siblings
2523
+ * or each as the direct child of its own <li>; a wrapper element in between drops it. The app
2524
+ * resets the list (margin, padding, list-style) and adds role="list", which Safari needs to keep
2525
+ * announcing a list without bullets. The height is --plinth-list-row-height (44px); a description makes the
2526
+ * row taller. Long titles and descriptions wrap; the trailing slot keeps its width up to half the
2527
+ * row, and only a value longer than that wraps.
2528
+ */
2529
+
2530
+ @layer plinth {
2531
+ .plinth-list-row {
2532
+ box-sizing: border-box;
2533
+ display: flex;
2534
+ gap: var(--plinth-space-3);
2535
+ align-items: center;
2536
+ inline-size: 100%;
2537
+ min-block-size: var(--plinth-list-row-height);
2538
+ padding-block: var(--plinth-space-2);
2539
+ padding-inline: var(--plinth-space-4);
2540
+ margin: 0;
2541
+ font-family: var(--plinth-font-family-body);
2542
+ font-size: var(--plinth-font-size-md);
2543
+ font-weight: var(--plinth-font-weight-regular);
2544
+ line-height: var(--plinth-font-leading-tight);
2545
+ color: var(--plinth-color-text);
2546
+ text-align: start;
2547
+ text-decoration: none;
2548
+ background-color: transparent;
2549
+ border: 0;
2550
+ transition: background-color var(--plinth-duration-fast) var(--plinth-easing-standard);
2551
+ }
2552
+
2553
+ :is(a, button).plinth-list-row {
2554
+ cursor: pointer;
2555
+ }
2556
+
2557
+ li + li > .plinth-list-row {
2558
+ border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
2559
+ }
2560
+
2561
+ .plinth-list-row + .plinth-list-row {
2562
+ border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
2563
+ }
2564
+
2565
+ @media (hover: hover) {
2566
+ a.plinth-list-row[href]:not([aria-current='page']):hover,
2567
+ button.plinth-list-row:enabled:not([aria-current='page']):hover {
2568
+ background-color: var(--plinth-color-surface-hover);
2569
+ }
2570
+ }
2571
+
2572
+ a.plinth-list-row[href]:not([aria-current='page']):active,
2573
+ button.plinth-list-row:enabled:not([aria-current='page']):active {
2574
+ background-color: var(--plinth-color-surface-sunken);
2575
+ }
2576
+
2577
+ .plinth-list-row[aria-current='page'] {
2578
+ color: var(--plinth-color-accent-text);
2579
+ background-color: var(--plinth-color-accent-subtle);
2580
+ }
2581
+
2582
+ .plinth-list-row:disabled,
2583
+ .plinth-list-row[aria-disabled='true'] {
2584
+ cursor: not-allowed;
2585
+ opacity: var(--plinth-opacity-disabled);
2586
+ }
2587
+
2588
+ /* Inside the row, so a scrolling sheet or a card doesn't cut the ring off at the sides. */
2589
+ .plinth-list-row:focus-visible {
2590
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
2591
+ outline-offset: var(--plinth-focus-ring-offset-inset);
2592
+ }
2593
+
2594
+ .plinth-list-row-leading {
2595
+ display: inline-flex;
2596
+ flex: none;
2597
+ align-items: center;
2598
+ color: var(--plinth-color-text-muted);
2599
+ }
2600
+
2601
+ .plinth-list-row-content {
2602
+ display: flex;
2603
+ flex: 1 1 auto;
2604
+ flex-direction: column;
2605
+ gap: var(--plinth-space-0-5);
2606
+ min-inline-size: 0;
2607
+ }
2608
+
2609
+ .plinth-list-row-title {
2610
+ overflow-wrap: anywhere;
2611
+ }
2612
+
2613
+ .plinth-list-row-description {
2614
+ font-size: var(--plinth-font-size-sm);
2615
+ line-height: var(--plinth-font-leading-normal);
2616
+ color: var(--plinth-color-text-muted);
2617
+ overflow-wrap: anywhere;
2618
+ }
2619
+
2620
+ /* Keeps its width up to half the row, so a long title wraps before a short value does. */
2621
+ .plinth-list-row-trailing {
2622
+ display: inline-flex;
2623
+ flex: none;
2624
+ gap: var(--plinth-space-2);
2625
+ align-items: center;
2626
+ max-inline-size: 50%;
2627
+ color: var(--plinth-color-text-muted);
2628
+ text-align: end;
2629
+ overflow-wrap: anywhere;
2630
+ }
2631
+
2632
+ .plinth-list-row-chevron {
2633
+ color: var(--plinth-color-text-subtle);
2634
+ }
2635
+
2636
+ .plinth-list-row[aria-current='page'] .plinth-list-row-title {
2637
+ font-weight: var(--plinth-font-weight-medium);
2638
+ }
2639
+
2640
+ @media (forced-colors: active) {
2641
+ .plinth-list-row[aria-current='page'] {
2642
+ color: HighlightText;
2643
+ background-color: Highlight;
2644
+ }
2645
+
2646
+ .plinth-list-row:disabled,
2647
+ .plinth-list-row[aria-disabled='true'] {
2648
+ color: GrayText;
2649
+ }
2650
+ }
2651
+ }
2652
+
2653
+ /* menu-sheet.css */
2654
+ /*
2655
+ * Menu sheet: the app shell's menu, a sheet with the signed-in user, the navigation and the
2656
+ * account actions. A side panel from 768px up, a bottom sheet on a phone.
2657
+ *
2658
+ * <dialog id="menu" class="plinth-sheet plinth-menu-sheet" data-side="end"
2659
+ * data-side-phone="bottom" closedby="any" aria-labelledby="menu-title">
2660
+ * <div class="plinth-sheet-panel">
2661
+ * <div class="plinth-sheet-handle" aria-hidden="true"></div>
2662
+ * <div class="plinth-sheet-header">
2663
+ * <h2 id="menu-title" class="plinth-sheet-title">Menu</h2>
2664
+ * <button … class="plinth-button plinth-icon-button plinth-sheet-close" commandfor="menu"
2665
+ * command="close">…</button>
2666
+ * </div>
2667
+ * <div class="plinth-sheet-body">
2668
+ * <a class="plinth-list-row plinth-menu-sheet-user" href="/profile">
2669
+ * <span class="plinth-list-row-leading">
2670
+ * <span class="plinth-avatar" data-size="lg" aria-hidden="true">…</span>
2671
+ * </span>
2672
+ * <span class="plinth-list-row-content">
2673
+ * <span class="plinth-list-row-title">Sam Lee</span>
2674
+ * <span class="plinth-list-row-description">sam@example.com</span>
2675
+ * </span>
2676
+ * <span class="plinth-list-row-trailing">…chevron…</span>
2677
+ * </a>
2678
+ * <nav class="plinth-menu-sheet-nav" aria-labelledby="menu-title">
2679
+ * <div class="plinth-list-card">…link rows, one with aria-current="page"…</div>
2680
+ * </nav>
2681
+ * <div class="plinth-menu-sheet-account">
2682
+ * <div class="plinth-list-card">…Settings, Sign out…</div>
2683
+ * </div>
2684
+ * </div>
2685
+ * </div>
2686
+ * </dialog>
2687
+ *
2688
+ * It is a sheet (sheet.css), always data-side="end" data-side-phone="bottom", with no footer;
2689
+ * everything the sheet's contract says holds, and it needs modal.css, sheet.css, list-row.css, list-card.css, avatar.css,
2690
+ * button.css and icon.css. The root's id is required in practice: the header's menu button opens
2691
+ * it with <button commandfor="menu" command="show-modal">, the close button points at it too, and
2692
+ * the dialog and the nav are named by the title, whose id is the root's id with -title.
2693
+ *
2694
+ * The parts, in the body:
2695
+ * - the user row (optional): a list row with a decorative avatar (lg), the name and an optional
2696
+ * detail (an email or a role); a static <div>, or a link to the profile with a chevron when it
2697
+ * has an href. The avatar is decorative because the name is printed next to it.
2698
+ * - the nav (always there): a <nav> named by the sheet's title (aria-labelledby), holding list
2699
+ * cards of link rows, their headings <h3> under the sheet's <h2> title. The app marks the row
2700
+ * of the page being shown with aria-current="page" (current on the list row) and keeps it up
2701
+ * to date; the menu remembers nothing.
2702
+ * - the account (optional): a list card of account actions, such as Settings and Sign out (a
2703
+ * button row; for a form post, a button row with type="submit" and form="…").
2704
+ *
2705
+ * The parts stack with a gap between them. In the full-height side panel the account sits on the
2706
+ * bottom edge (margin-block-start: auto) and rises with the content when the nav is long; the
2707
+ * body scrolls as one.
2708
+ */
2709
+
2710
+ @layer plinth {
2711
+ .plinth-menu-sheet .plinth-sheet-body {
2712
+ display: flex;
2713
+ flex-direction: column;
2714
+ gap: var(--plinth-space-5);
2715
+ }
2716
+
2717
+ .plinth-menu-sheet-nav,
2718
+ .plinth-menu-sheet-account {
2719
+ display: flex;
2720
+ flex-direction: column;
2721
+ gap: var(--plinth-space-5);
2722
+ }
2723
+
2724
+ .plinth-menu-sheet-account {
2725
+ margin-block-start: auto;
2726
+ }
2727
+
2728
+ /* The name reads as the row's heading. */
2729
+ .plinth-menu-sheet-user .plinth-list-row-title {
2730
+ font-weight: var(--plinth-font-weight-medium);
2731
+ }
2732
+
2733
+ /* A link to the profile rounds its hover tint like the list cards' corners below it. */
2734
+ a.plinth-menu-sheet-user {
2735
+ border-radius: var(--plinth-radius-container);
2736
+ }
2737
+ }
2738
+
2739
+ /* modal.css */
2740
+ /*
2741
+ * Modal base: what the dialog (dialog.css), the sheet (sheet.css) and the drawer (drawer.css)
2742
+ * share, written once so they can't drift apart (docs/adr/0005): the root's surface and motion,
2743
+ * the backdrop, the scroll lock, the safe areas, the panel and the grab handle, and the header,
2744
+ * title, description, close button, body and footer. It has no markup of its own and no class:
2745
+ * each component keeps its own root and part classes (plinth-dialog, plinth-dialog-panel, …;
2746
+ * plinth-sheet, plinth-sheet-panel, …), listed here in :is(). A page that loads the components
2747
+ * one by one loads modal.css with them.
2748
+ *
2749
+ * The parts sit in a panel, the root's only child:
2750
+ * <dialog class="plinth-sheet" …>
2751
+ * <div class="plinth-sheet-panel">
2752
+ * <div class="plinth-sheet-handle" aria-hidden="true"></div> (a bottom panel on a phone)
2753
+ * <div class="plinth-sheet-header">…</div>
2754
+ * <div class="plinth-sheet-body">…</div>
2755
+ * <div class="plinth-sheet-footer">…</div> (optional)
2756
+ * </div>
2757
+ * </dialog>
2758
+ *
2759
+ * The rules sit in a layer inside plinth (plinth.modal), so a component's own rules win over them
2760
+ * whatever the order the files load in, and a project's utilities win over both.
2761
+ *
2762
+ * Each component sets where its root sits and how big it is, and tunes the motion and the safe
2763
+ * area through three custom properties on the root; it never sets translate or opacity itself:
2764
+ * - --plinth-modal-offset: where the root slides in from and back out to (a translate value;
2765
+ * 0 0, the default, is no slide);
2766
+ * - --plinth-modal-closed-opacity: its opacity while it opens and closes (0 fades it, 1, the
2767
+ * default, doesn't);
2768
+ * - --plinth-modal-safe-area-end: env(safe-area-inset-bottom) when the root reaches the bottom
2769
+ * edge of the screen, so its last part clears the home indicator; initial (the default) when
2770
+ * it doesn't.
2771
+ *
2772
+ * The motion runs on the motion tokens, which drop to 0ms under prefers-reduced-motion. While a
2773
+ * modal is open the page under it doesn't scroll (html:has(…:modal)), and it has no scrollbar
2774
+ * gutter (scrollbar-gutter: auto, over a project's stable in an earlier layer): the top layer is
2775
+ * never drawn in the gutter, so a gutter would leave a strip of the page beside a sheet on the end
2776
+ * edge or the full width. Where the scrollbar takes space the page shifts by its width under the
2777
+ * backdrop; overlay scrollbars take no space and nothing shifts. The body scrolls, with
2778
+ * overscroll-behavior: contain; the header and footer stay put; the margins of the body's first and
2779
+ * last child are reset, so its padding is its edge. On a phone the footer's actions share its
2780
+ * width; from 768px up they sit at its end.
2781
+ *
2782
+ * A bottom panel on a phone (a sheet whose side below 768px is the bottom, a dialog with
2783
+ * data-side-phone="bottom" that can be dismissed, and the drawer) has a grab handle, and the
2784
+ * person drags it down to close it. The layers print the handle only there; its presence is what
2785
+ * turns the dragging on, below 768px. Dragging is native scrolling: the root fills the screen and
2786
+ * is a scroll-snap container, see-through, with a spacer as tall as the screen above the panel,
2787
+ * so the browser gives the momentum and the snapping. Without script the handle shows and the
2788
+ * close button and Escape close the panel. What the platform can't do, the layers add (a few
2789
+ * lines, docs/adr/0002 section 6: x-init in Blade, useSwipeToClose in Vue and React):
2790
+ * - on scrollend (scroll where it is missing), when the panel has gone off the bottom edge, they
2791
+ * call requestClose() (close() where it is missing), so cancel fires first, as for Escape; when
2792
+ * the app prevents it, the panel goes back to where it opened;
2793
+ * - a tap on the root itself, above the panel, closes it when closedby="any", since the browser
2794
+ * reads that tap as one on the dialog, not its backdrop;
2795
+ * - on toggle to open, a drawer with data-snap scrolls to that snap point (a browser that fires no
2796
+ * toggle on a dialog opens it at its largest).
2797
+ * The snapping is the browser's own scrolling, and the layers' scroll is instant.
2798
+ */
2799
+
2800
+ @layer plinth {
2801
+ @layer modal {
2802
+ html:has(:is(.plinth-dialog, .plinth-sheet, .plinth-drawer):modal) {
2803
+ overflow: hidden;
2804
+ scrollbar-gutter: auto;
2805
+ }
2806
+
2807
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer) {
2808
+ --plinth-modal-offset: 0 0;
2809
+ --plinth-modal-closed-opacity: 1;
2810
+ --plinth-modal-safe-area-end: initial;
2811
+
2812
+ position: fixed;
2813
+ box-sizing: border-box;
2814
+ flex-direction: column;
2815
+ max-inline-size: 100%;
2816
+ padding: 0;
2817
+ overflow: hidden;
2818
+ font-family: var(--plinth-font-family-body);
2819
+ color: var(--plinth-color-text);
2820
+ background-color: var(--plinth-color-surface-raised);
2821
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border);
2822
+ border-radius: var(--plinth-radius-container);
2823
+ box-shadow: var(--plinth-elevation-3);
2824
+ opacity: var(--plinth-modal-closed-opacity);
2825
+ translate: var(--plinth-modal-offset);
2826
+ transition:
2827
+ translate var(--plinth-duration-normal) var(--plinth-easing-exit),
2828
+ opacity var(--plinth-duration-normal) var(--plinth-easing-exit),
2829
+ overlay var(--plinth-duration-normal) allow-discrete,
2830
+ display var(--plinth-duration-normal) allow-discrete;
2831
+ }
2832
+
2833
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open] {
2834
+ display: flex;
2835
+ opacity: 1;
2836
+ translate: 0 0;
2837
+ transition-timing-function: var(--plinth-easing-enter);
2838
+ }
2839
+
2840
+ @starting-style {
2841
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open] {
2842
+ opacity: var(--plinth-modal-closed-opacity);
2843
+ translate: var(--plinth-modal-offset);
2844
+ }
2845
+ }
2846
+
2847
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer)::backdrop {
2848
+ background-color: var(--plinth-color-overlay);
2849
+ opacity: 0;
2850
+ transition:
2851
+ opacity var(--plinth-duration-normal) var(--plinth-easing-standard),
2852
+ overlay var(--plinth-duration-normal) allow-discrete,
2853
+ display var(--plinth-duration-normal) allow-discrete;
2854
+ }
2855
+
2856
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open]::backdrop {
2857
+ opacity: 1;
2858
+ }
2859
+
2860
+ @starting-style {
2861
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer)[open]::backdrop {
2862
+ opacity: 0;
2863
+ }
2864
+ }
2865
+
2866
+ /*
2867
+ * The panel holds the parts, the root's only child. It is the column the body scrolls in;
2868
+ * the root is the surface, except for a bottom panel on a phone (below).
2869
+ */
2870
+ :is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel) {
2871
+ display: flex;
2872
+ flex: 1 1 auto;
2873
+ flex-direction: column;
2874
+ min-block-size: 0;
2875
+ margin: 0;
2876
+ }
2877
+
2878
+ :is(.plinth-dialog-header, .plinth-sheet-header, .plinth-drawer-header) {
2879
+ display: grid;
2880
+ grid-template-columns: minmax(0, 1fr) auto;
2881
+ gap: var(--plinth-space-1) var(--plinth-space-3);
2882
+ align-items: center;
2883
+ padding-block: var(--plinth-space-3);
2884
+ padding-inline: var(--plinth-space-4) var(--plinth-space-3);
2885
+ border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
2886
+ }
2887
+
2888
+ /* With no close button, the header's padding is even. */
2889
+ :is(.plinth-dialog-header, .plinth-sheet-header, .plinth-drawer-header):not(
2890
+ :has(> :is(.plinth-dialog-close, .plinth-sheet-close, .plinth-drawer-close))
2891
+ ) {
2892
+ padding-inline-end: var(--plinth-space-4);
2893
+ }
2894
+
2895
+ :is(.plinth-dialog-title, .plinth-sheet-title, .plinth-drawer-title) {
2896
+ grid-column: 1;
2897
+ margin: 0;
2898
+ font-size: var(--plinth-font-size-lg);
2899
+ font-weight: var(--plinth-font-weight-semibold);
2900
+ line-height: var(--plinth-font-leading-tight);
2901
+ letter-spacing: var(--plinth-font-tracking-tight);
2902
+ overflow-wrap: anywhere;
2903
+ }
2904
+
2905
+ :is(.plinth-dialog-description, .plinth-sheet-description, .plinth-drawer-description) {
2906
+ grid-column: 1;
2907
+ margin: 0;
2908
+ font-size: var(--plinth-font-size-sm);
2909
+ line-height: var(--plinth-font-leading-normal);
2910
+ color: var(--plinth-color-text-muted);
2911
+ overflow-wrap: anywhere;
2912
+ }
2913
+
2914
+ :is(.plinth-dialog-close, .plinth-sheet-close, .plinth-drawer-close) {
2915
+ grid-row: 1;
2916
+ grid-column: 2;
2917
+ }
2918
+
2919
+ :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
2920
+ flex: 1 1 auto;
2921
+ min-block-size: 0;
2922
+ padding: var(--plinth-space-4);
2923
+ overflow-y: auto;
2924
+ overscroll-behavior: contain;
2925
+ font-size: var(--plinth-font-size-md);
2926
+ line-height: var(--plinth-font-leading-normal);
2927
+ }
2928
+
2929
+ /* The body's padding is its edge: the first and last child's margins don't add to it. */
2930
+ :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) > :first-child {
2931
+ margin-block-start: 0;
2932
+ }
2933
+
2934
+ :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) > :last-child {
2935
+ margin-block-end: 0;
2936
+ }
2937
+
2938
+ /*
2939
+ * A browser can focus a scrolling area that has nothing focusable in it (Chrome and Firefox
2940
+ * do, Safari doesn't yet), so a long body of text only is better with a link or a control in
2941
+ * it.
2942
+ */
2943
+ :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body):focus-visible {
2944
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
2945
+ outline-offset: var(--plinth-focus-ring-offset-inset);
2946
+ }
2947
+
2948
+ /* The last part clears the home indicator when the root reaches the bottom edge. */
2949
+ :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body):last-child {
2950
+ padding-block-end: max(
2951
+ var(--plinth-space-4),
2952
+ var(--plinth-modal-safe-area-end, var(--plinth-space-4))
2953
+ );
2954
+ }
2955
+
2956
+ :is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) {
2957
+ display: flex;
2958
+ flex-wrap: wrap;
2959
+ gap: var(--plinth-space-2);
2960
+ justify-content: flex-end;
2961
+ padding-block: var(--plinth-space-3)
2962
+ max(var(--plinth-space-3), var(--plinth-modal-safe-area-end, var(--plinth-space-3)));
2963
+ padding-inline: var(--plinth-space-4);
2964
+ border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
2965
+ }
2966
+
2967
+ /* On a phone the footer's actions share its width. */
2968
+ :is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) > * {
2969
+ flex: 1 1 0;
2970
+ }
2971
+
2972
+ /*
2973
+ * A bottom panel on a phone, dragged down to close: a root whose panel holds a handle (the
2974
+ * layers print one only where the panel is on the bottom edge below 768px, and can be
2975
+ * dismissed). The root fills the screen and is a scroll-snap container, see-through; its
2976
+ * ::before is a spacer as tall as the screen above the panel, so scrolling it down is the
2977
+ * panel going off the bottom edge. The flex column runs bottom up (column-reverse), so the
2978
+ * root opens scrolled to its end, with the panel showing. Two snap positions: the panel's
2979
+ * end on the screen's bottom edge (open), and the spacer filling the screen (closed); the
2980
+ * layers close the dialog when the scrolling settles there. The panel is the surface, and the
2981
+ * body hands a swipe on to the root once it is scrolled to its top.
2982
+ */
2983
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
2984
+ > * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
2985
+ ) {
2986
+ flex-direction: column-reverse;
2987
+ overflow-y: auto;
2988
+ overscroll-behavior: contain;
2989
+ scroll-snap-type: y mandatory;
2990
+ scrollbar-width: none;
2991
+ background-color: transparent;
2992
+ box-shadow: none;
2993
+ }
2994
+
2995
+ /* As a scrolling area the root can take focus (Chrome and Firefox), so it shows the ring. */
2996
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
2997
+ > * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
2998
+ ):focus-visible {
2999
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
3000
+ outline-offset: var(--plinth-focus-ring-offset-inset);
3001
+ }
3002
+
3003
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
3004
+ > * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
3005
+ )::before {
3006
+ flex: none;
3007
+ order: 1;
3008
+ block-size: 100%;
3009
+ content: '';
3010
+ scroll-snap-align: start;
3011
+ }
3012
+
3013
+ :is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
3014
+ > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
3015
+ ) {
3016
+ flex: none;
3017
+ max-block-size: calc(
3018
+ 100% - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
3019
+ );
3020
+ padding-inline: max(env(safe-area-inset-left), env(safe-area-inset-right));
3021
+ scroll-snap-align: end;
3022
+ background-color: var(--plinth-color-surface-raised);
3023
+ border-block-start: var(--plinth-border-width-thin) solid var(--plinth-color-border);
3024
+ border-start-start-radius: var(--plinth-radius-container);
3025
+ border-start-end-radius: var(--plinth-radius-container);
3026
+ box-shadow: var(--plinth-elevation-3);
3027
+ }
3028
+
3029
+ /*
3030
+ * The grab handle, a short pill centred at the top; decorative, since Escape and the close
3031
+ * button close the panel too. From 768px up it is hidden.
3032
+ */
3033
+ :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
3034
+ display: block;
3035
+ flex: none;
3036
+ align-self: center;
3037
+ inline-size: var(--plinth-space-10);
3038
+ block-size: var(--plinth-space-1);
3039
+ margin-block-start: var(--plinth-space-2);
3040
+ background-color: var(--plinth-color-border-strong);
3041
+ border-radius: var(--plinth-radius-pill);
3042
+ }
3043
+
3044
+ :is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
3045
+ > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
3046
+ )
3047
+ > :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
3048
+ overscroll-behavior: auto;
3049
+ }
3050
+
3051
+ @media (forced-colors: active) {
3052
+ :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
3053
+ background-color: CanvasText;
3054
+ }
3055
+ }
3056
+
3057
+ @media (min-width: 768px) {
3058
+ :is(.plinth-dialog-footer, .plinth-sheet-footer, .plinth-drawer-footer) > * {
3059
+ flex: none;
3060
+ }
3061
+
3062
+ /* From 768px up no panel is dragged: the root is the surface again. */
3063
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
3064
+ > * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
3065
+ ) {
3066
+ flex-direction: column;
3067
+ overflow: hidden;
3068
+ overscroll-behavior: auto;
3069
+ scroll-snap-type: none;
3070
+ scrollbar-width: auto;
3071
+ background-color: var(--plinth-color-surface-raised);
3072
+ box-shadow: var(--plinth-elevation-3);
3073
+ }
3074
+
3075
+ :is(.plinth-dialog, .plinth-sheet, .plinth-drawer):has(
3076
+ > * > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
3077
+ )::before {
3078
+ content: none;
3079
+ }
3080
+
3081
+ :is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
3082
+ > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
3083
+ ) {
3084
+ flex: 1 1 auto;
3085
+ max-block-size: none;
3086
+ padding-inline: 0;
3087
+ scroll-snap-align: none;
3088
+ background-color: transparent;
3089
+ border-block-start: 0;
3090
+ border-radius: 0;
3091
+ box-shadow: none;
3092
+ }
3093
+
3094
+ :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle) {
3095
+ display: none;
3096
+ }
3097
+
3098
+ :is(.plinth-dialog-panel, .plinth-sheet-panel, .plinth-drawer-panel):has(
3099
+ > :is(.plinth-dialog-handle, .plinth-sheet-handle, .plinth-drawer-handle)
3100
+ )
3101
+ > :is(.plinth-dialog-body, .plinth-sheet-body, .plinth-drawer-body) {
3102
+ overscroll-behavior: contain;
3103
+ }
3104
+ }
3105
+ }
3106
+ }
3107
+
3108
+ /* pill-row.css */
3109
+ /*
3110
+ * Pill row: a row of pills for filters, segments or sub-pages.
3111
+ *
3112
+ * <div class="plinth-pill-row" role="group" aria-label="Status">
3113
+ * <button type="button" class="plinth-pill" aria-pressed="true">
3114
+ * <span class="plinth-pill-label">Open</span>
3115
+ * <span class="plinth-pill-count">12</span>
3116
+ * </button>
3117
+ * <button type="button" class="plinth-pill" aria-pressed="false">
3118
+ * <span class="plinth-pill-label">Closed</span>
3119
+ * </button>
3120
+ * </div>
3121
+ *
3122
+ * The root is a <div role="group">, named by aria-label (label in Blade and Vue) or by the app's
3123
+ * own aria-labelledby. A row of links that navigates is <nav class="plinth-pill-row"> instead,
3124
+ * with no role (nav in Blade and Vue), and its pills are links:
3125
+ *
3126
+ * <a class="plinth-pill" href="…" aria-current="page"><span class="plinth-pill-label">…</span></a>
3127
+ *
3128
+ * Each pill is its own component (<x-plinth::pill>, PlinthPill), so the app's attributes and
3129
+ * listeners land on the pill (ADR 2). The count is optional. plinth-pill-label has no rule of its
3130
+ * own: it is a hook for a project's styles.
3131
+ *
3132
+ * States, set by the app:
3133
+ * - selected: a button pill always prints aria-pressed, "true" or "false"; a link pill prints
3134
+ * aria-current="page" for the page being shown. Selected takes the selection tint
3135
+ * (accent-subtle, accent-text) inside a 2px accent edge (border-width.thick), where the others
3136
+ * have a 1px neutral one, so colour is not its only cue. A pressed button still answers hover
3137
+ * and press with a step on the tint (accent-subtle-hover, accent-subtle-active).
3138
+ * - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
3139
+ * - hover only where the device can hover; the focus ring on :focus-visible.
3140
+ *
3141
+ * Layout: on a phone the row scrolls sideways, with no visible scrollbar, and snaps to the pills;
3142
+ * from 768px it wraps. The row's padding keeps the focus ring and the 40px touch target of the
3143
+ * pills inside the scrolling area, whatever --plinth-pill-height is (32px by default). A pill is
3144
+ * at least 40px wide.
3145
+ */
3146
+
3147
+ @layer plinth {
3148
+ .plinth-pill-row {
3149
+ box-sizing: border-box;
3150
+ display: flex;
3151
+ gap: var(--plinth-space-2);
3152
+ align-items: center;
3153
+ max-inline-size: 100%;
3154
+ padding-block: max(
3155
+ var(--plinth-space-1),
3156
+ (var(--plinth-touch-target) - var(--plinth-pill-height)) / 2
3157
+ );
3158
+ padding-inline: var(--plinth-space-1);
3159
+ margin: 0;
3160
+ overflow-x: auto;
3161
+ overscroll-behavior-inline: contain;
3162
+ scroll-snap-type: inline proximity;
3163
+ scroll-padding-inline: var(--plinth-space-1);
3164
+ scrollbar-width: none;
3165
+ }
3166
+
3167
+ .plinth-pill {
3168
+ position: relative;
3169
+ box-sizing: border-box;
3170
+ display: inline-flex;
3171
+ flex: none;
3172
+ gap: var(--plinth-space-2);
3173
+ align-items: center;
3174
+ justify-content: center;
3175
+ min-inline-size: var(--plinth-touch-target);
3176
+ min-block-size: var(--plinth-pill-height);
3177
+ padding-block: 0;
3178
+ padding-inline: var(--plinth-space-3);
3179
+ margin: 0;
3180
+ font-family: var(--plinth-font-family-body);
3181
+ font-size: var(--plinth-font-size-sm);
3182
+ font-weight: var(--plinth-font-weight-medium);
3183
+ line-height: var(--plinth-font-leading-tight);
3184
+ color: var(--plinth-color-text);
3185
+ white-space: nowrap;
3186
+ text-decoration: none;
3187
+ cursor: pointer;
3188
+ user-select: none;
3189
+ scroll-snap-align: start;
3190
+ background-color: transparent;
3191
+ border: var(--plinth-border-width-thin) solid var(--plinth-color-border-strong);
3192
+ border-radius: var(--plinth-radius-pill);
3193
+ transition:
3194
+ background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
3195
+ border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
3196
+ box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard),
3197
+ color var(--plinth-duration-fast) var(--plinth-easing-standard);
3198
+ }
3199
+
3200
+ /* A 40px touch target through an invisible hit area, as on the button (ADR 3). */
3201
+ @media (pointer: coarse) {
3202
+ .plinth-pill::before {
3203
+ position: absolute;
3204
+ inset-block-start: 50%;
3205
+ inset-inline: 0;
3206
+ block-size: max(100%, var(--plinth-touch-target));
3207
+ content: '';
3208
+ translate: 0 -50%;
3209
+ }
3210
+ }
3211
+
3212
+ @media (hover: hover) {
3213
+ a.plinth-pill[href]:not([aria-current='page']):hover,
3214
+ button.plinth-pill:enabled:not([aria-pressed='true']):hover {
3215
+ background-color: var(--plinth-color-surface-hover);
3216
+ }
3217
+ }
3218
+
3219
+ a.plinth-pill[href]:not([aria-current='page']):active,
3220
+ button.plinth-pill:enabled:not([aria-pressed='true']):active {
3221
+ background-color: var(--plinth-color-surface-sunken);
3222
+ }
3223
+
3224
+ /* The border and an inset ring make the 2px edge, so selecting doesn't move the label. */
3225
+ .plinth-pill:is([aria-pressed='true'], [aria-current='page']) {
3226
+ color: var(--plinth-color-accent-text);
3227
+ background-color: var(--plinth-color-accent-subtle);
3228
+ border-color: var(--plinth-color-accent);
3229
+ box-shadow: inset 0 0 0 calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
3230
+ var(--plinth-color-accent);
3231
+ }
3232
+
3233
+ /*
3234
+ * A pressed button can be pressed again, so it answers hover and press with a step on the tint.
3235
+ * Not in forced colours, where the tint would replace the system highlight under the label.
3236
+ */
3237
+ @media (hover: hover) and (forced-colors: none) {
3238
+ button.plinth-pill:enabled[aria-pressed='true']:hover {
3239
+ background-color: var(--plinth-color-accent-subtle-hover);
3240
+ }
3241
+ }
3242
+
3243
+ @media (forced-colors: none) {
3244
+ button.plinth-pill:enabled[aria-pressed='true']:active {
3245
+ background-color: var(--plinth-color-accent-subtle-active);
3246
+ }
3247
+ }
3248
+
3249
+ .plinth-pill:disabled,
3250
+ .plinth-pill[aria-disabled='true'] {
3251
+ cursor: not-allowed;
3252
+ opacity: var(--plinth-opacity-disabled);
3253
+ }
3254
+
3255
+ .plinth-pill:focus-visible {
3256
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
3257
+ outline-offset: var(--plinth-focus-ring-offset);
3258
+ }
3259
+
3260
+ /* A gap between the ring and the accent edge, so focus and selection read apart. */
3261
+ .plinth-pill:is([aria-pressed='true'], [aria-current='page']):focus-visible {
3262
+ outline-offset: var(--plinth-focus-ring-offset-filled);
3263
+ }
3264
+
3265
+ .plinth-pill-count {
3266
+ font-weight: var(--plinth-font-weight-regular);
3267
+ font-variant-numeric: tabular-nums;
3268
+ color: var(--plinth-color-text-muted);
3269
+ }
3270
+
3271
+ .plinth-pill:is([aria-pressed='true'], [aria-current='page']) .plinth-pill-count {
3272
+ color: inherit;
3273
+ }
3274
+
3275
+ /* Wider screens have room: the pills wrap onto more lines instead of scrolling. */
3276
+ @media (min-width: 768px) {
3277
+ .plinth-pill-row {
3278
+ flex-wrap: wrap;
3279
+ overflow-x: visible;
3280
+ }
3281
+ }
3282
+
3283
+ @media (forced-colors: active) {
3284
+ .plinth-pill {
3285
+ border-color: ButtonText;
3286
+ }
3287
+
3288
+ .plinth-pill:is([aria-pressed='true'], [aria-current='page']) {
3289
+ color: HighlightText;
3290
+ background-color: Highlight;
3291
+ border-color: Highlight;
3292
+ }
3293
+
3294
+ .plinth-pill:is([aria-pressed='true'], [aria-current='page']) .plinth-pill-count {
3295
+ color: HighlightText;
3296
+ }
3297
+
3298
+ .plinth-pill:disabled,
3299
+ .plinth-pill[aria-disabled='true'] {
3300
+ color: GrayText;
3301
+ background-color: Canvas;
3302
+ border-color: GrayText;
3303
+ }
3304
+ }
3305
+ }
3306
+
3307
+ /* section-menu.css */
3308
+ /*
3309
+ * Section menu: the row under the header that switches between the sections of a page.
3310
+ *
3311
+ * <nav class="plinth-section-menu" aria-label="Project">
3312
+ * <a class="plinth-section-menu-item" href="/overview" aria-current="page">
3313
+ * <span class="plinth-section-menu-label">Overview</span>
3314
+ * </a>
3315
+ * <a class="plinth-section-menu-item" href="/tasks">
3316
+ * <span class="plinth-section-menu-label">Tasks</span>
3317
+ * <span class="plinth-section-menu-count">8</span>
3318
+ * </a>
3319
+ * </nav>
3320
+ *
3321
+ * The root is a <nav>, named by aria-label (label in Blade and Vue) or by the app's own
3322
+ * aria-labelledby, so it reads apart from the page's other navigation. Its items are links. A
3323
+ * menu that switches views in place, without navigating, has buttons instead, and its root is
3324
+ * <div class="plinth-section-menu" role="group"> (nav set to false in Blade and Vue):
3325
+ *
3326
+ * <button type="button" class="plinth-section-menu-item" aria-pressed="true">…</button>
3327
+ *
3328
+ * Each item is its own component (<x-plinth::section-menu-item>, PlinthSectionMenuItem), so the
3329
+ * app's attributes and listeners land on the item (ADR 2). The count is optional.
3330
+ * plinth-section-menu-label has no rule of its own: it is a hook for a project's styles.
3331
+ *
3332
+ * States, set by the app:
3333
+ * - current: a link prints aria-current="page" for the section being shown; a button always
3334
+ * prints aria-pressed, "true" or "false". The current item takes the selection tint
3335
+ * (accent-subtle, accent-text) inside a 2px accent edge (border-width.thick), so colour is not
3336
+ * its only cue; the others are muted text on no surface, like a ghost button. A pressed button
3337
+ * still answers hover and press with a step on the tint (accent-subtle-hover,
3338
+ * accent-subtle-active).
3339
+ * - disabled: `disabled` on a <button>; on a link, no href, role="link" and aria-disabled="true".
3340
+ * - hover only where the device can hover; the focus ring on :focus-visible.
3341
+ *
3342
+ * Layout: a row --plinth-section-menu-height (56px) tall, on the surface with a divider below,
3343
+ * the same inline padding as the header and clear of the notch on a phone held sideways. Its
3344
+ * items are --plinth-section-menu-item-height (40px) tall, never less than the 40px touch
3345
+ * target, and at least 40px wide. The row never wraps: when the items don't fit it scrolls
3346
+ * sideways and snaps to them, with a thin scrollbar for a mouse and none on a touch screen; the
3347
+ * row's block padding keeps the focus ring inside it. The app scrolls a current item that starts
3348
+ * out of view into view.
3349
+ * Placement is the app's: put it under the header, or make it sticky below it with
3350
+ * position: sticky, inset-block-start: calc(var(--plinth-header-height) +
3351
+ * env(safe-area-inset-top)) and z-index: var(--plinth-layer-sticky).
3352
+ */
3353
+
3354
+ @layer plinth {
3355
+ .plinth-section-menu {
3356
+ box-sizing: border-box;
3357
+ display: flex;
3358
+ gap: var(--plinth-space-1);
3359
+ align-items: center;
3360
+ min-block-size: var(--plinth-section-menu-height);
3361
+
3362
+ /* What the item and the divider leave of the row's height, so the row is exactly its token. */
3363
+ padding-block: calc(
3364
+ (
3365
+ var(--plinth-section-menu-height) - var(--plinth-section-menu-item-height) -
3366
+ var(--plinth-border-width-thin)
3367
+ ) /
3368
+ 2
3369
+ );
3370
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
3371
+ max(var(--plinth-space-2), env(safe-area-inset-right));
3372
+ margin: 0;
3373
+ overflow-x: auto;
3374
+ overscroll-behavior-inline: contain;
3375
+ font-family: var(--plinth-font-family-body);
3376
+ scroll-snap-type: inline proximity;
3377
+ scroll-padding-inline: max(var(--plinth-space-2), env(safe-area-inset-left))
3378
+ max(var(--plinth-space-2), env(safe-area-inset-right));
3379
+ scrollbar-width: none;
3380
+ background-color: var(--plinth-color-surface);
3381
+ border-block-end: var(--plinth-border-width-thin) solid var(--plinth-color-divider);
3382
+ }
3383
+
3384
+ .plinth-section-menu:dir(rtl) {
3385
+ padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
3386
+ max(var(--plinth-space-2), env(safe-area-inset-left));
3387
+ scroll-padding-inline: max(var(--plinth-space-2), env(safe-area-inset-right))
3388
+ max(var(--plinth-space-2), env(safe-area-inset-left));
3389
+ }
3390
+
3391
+ /* A mouse without a sideways wheel needs a scrollbar to reach the items that don't fit. */
3392
+ @media (hover: hover) and (pointer: fine) {
3393
+ .plinth-section-menu {
3394
+ scrollbar-width: thin;
3395
+ }
3396
+ }
3397
+
3398
+ .plinth-section-menu-item {
3399
+ box-sizing: border-box;
3400
+ display: inline-flex;
3401
+ flex: none;
3402
+ gap: var(--plinth-space-2);
3403
+ align-items: center;
3404
+ justify-content: center;
3405
+ min-inline-size: var(--plinth-touch-target);
3406
+ min-block-size: max(var(--plinth-section-menu-item-height), var(--plinth-touch-target));
3407
+ padding-block: 0;
3408
+ padding-inline: var(--plinth-space-3);
3409
+ margin: 0;
3410
+ font-family: inherit;
3411
+ font-size: var(--plinth-font-size-md);
3412
+ font-weight: var(--plinth-font-weight-medium);
3413
+ line-height: var(--plinth-font-leading-tight);
3414
+ color: var(--plinth-color-text-muted);
3415
+ white-space: nowrap;
3416
+ text-decoration: none;
3417
+ cursor: pointer;
3418
+ user-select: none;
3419
+ scroll-snap-align: start;
3420
+ background-color: transparent;
3421
+ border: var(--plinth-border-width-thin) solid transparent;
3422
+ border-radius: var(--plinth-radius-control);
3423
+ transition:
3424
+ background-color var(--plinth-duration-fast) var(--plinth-easing-standard),
3425
+ border-color var(--plinth-duration-fast) var(--plinth-easing-standard),
3426
+ box-shadow var(--plinth-duration-fast) var(--plinth-easing-standard),
3427
+ color var(--plinth-duration-fast) var(--plinth-easing-standard);
3428
+ }
3429
+
3430
+ @media (hover: hover) {
3431
+ a.plinth-section-menu-item[href]:not([aria-current='page']):hover,
3432
+ button.plinth-section-menu-item:enabled:not([aria-pressed='true']):hover {
3433
+ color: var(--plinth-color-text);
3434
+ background-color: var(--plinth-color-surface-hover);
3435
+ }
3436
+ }
3437
+
3438
+ a.plinth-section-menu-item[href]:not([aria-current='page']):active,
3439
+ button.plinth-section-menu-item:enabled:not([aria-pressed='true']):active {
3440
+ color: var(--plinth-color-text);
3441
+ background-color: var(--plinth-color-surface-sunken);
3442
+ }
3443
+
3444
+ /* The border and an inset ring make the 2px edge, so selecting doesn't move the label. */
3445
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']) {
3446
+ color: var(--plinth-color-accent-text);
3447
+ background-color: var(--plinth-color-accent-subtle);
3448
+ border-color: var(--plinth-color-accent);
3449
+ box-shadow: inset 0 0 0 calc(var(--plinth-border-width-thick) - var(--plinth-border-width-thin))
3450
+ var(--plinth-color-accent);
3451
+ }
3452
+
3453
+ /*
3454
+ * A pressed button can be pressed again, so it answers hover and press with a step on the tint.
3455
+ * Not in forced colours, where the tint would replace the system highlight under the label.
3456
+ */
3457
+ @media (hover: hover) and (forced-colors: none) {
3458
+ button.plinth-section-menu-item:enabled[aria-pressed='true']:hover {
3459
+ background-color: var(--plinth-color-accent-subtle-hover);
3460
+ }
3461
+ }
3462
+
3463
+ @media (forced-colors: none) {
3464
+ button.plinth-section-menu-item:enabled[aria-pressed='true']:active {
3465
+ background-color: var(--plinth-color-accent-subtle-active);
3466
+ }
3467
+ }
3468
+
3469
+ .plinth-section-menu-item:disabled,
3470
+ .plinth-section-menu-item[aria-disabled='true'] {
3471
+ cursor: not-allowed;
3472
+ opacity: var(--plinth-opacity-disabled);
3473
+ }
3474
+
3475
+ .plinth-section-menu-item:focus-visible {
3476
+ outline: var(--plinth-focus-ring-width) solid var(--plinth-color-focus-ring);
3477
+ outline-offset: var(--plinth-focus-ring-offset);
3478
+ }
3479
+
3480
+ /* A gap between the ring and the accent edge, so focus and selection read apart. */
3481
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']):focus-visible {
3482
+ outline-offset: var(--plinth-focus-ring-offset-filled);
3483
+ }
3484
+
3485
+ .plinth-section-menu-count {
3486
+ font-family: var(--plinth-font-family-mono);
3487
+ font-size: var(--plinth-font-size-xs);
3488
+ font-weight: var(--plinth-font-weight-regular);
3489
+ font-variant-numeric: tabular-nums;
3490
+ color: var(--plinth-color-text-muted);
3491
+ }
3492
+
3493
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page'])
3494
+ .plinth-section-menu-count {
3495
+ color: inherit;
3496
+ }
3497
+
3498
+ @media (min-width: 768px) {
3499
+ .plinth-section-menu {
3500
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
3501
+ max(var(--plinth-space-4), env(safe-area-inset-right));
3502
+ scroll-padding-inline: max(var(--plinth-space-4), env(safe-area-inset-left))
3503
+ max(var(--plinth-space-4), env(safe-area-inset-right));
3504
+ }
3505
+
3506
+ .plinth-section-menu:dir(rtl) {
3507
+ padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
3508
+ max(var(--plinth-space-4), env(safe-area-inset-left));
3509
+ scroll-padding-inline: max(var(--plinth-space-4), env(safe-area-inset-right))
3510
+ max(var(--plinth-space-4), env(safe-area-inset-left));
3511
+ }
3512
+ }
3513
+
3514
+ /* The current item is only a surface tint: in forced colours it takes the system highlight. */
3515
+ @media (forced-colors: active) {
3516
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page']) {
3517
+ color: HighlightText;
3518
+ background-color: Highlight;
3519
+ border-color: Highlight;
3520
+ }
3521
+
3522
+ .plinth-section-menu-item:is([aria-pressed='true'], [aria-current='page'])
3523
+ .plinth-section-menu-count {
3524
+ color: HighlightText;
3525
+ }
3526
+
3527
+ .plinth-section-menu-item:disabled,
3528
+ .plinth-section-menu-item[aria-disabled='true'] {
3529
+ color: GrayText;
3530
+ }
3531
+ }
3532
+ }
3533
+
3534
+ /* sheet.css */
3535
+ /*
3536
+ * Sheet: a modal panel on the edge of the screen the developer picks, for filters, side
3537
+ * information or navigation. Optionally on another edge below 768px.
3538
+ *
3539
+ * <dialog id="filters" class="plinth-sheet" data-side="end" data-side-phone="bottom"
3540
+ * closedby="any" aria-labelledby="filters-title" aria-describedby="filters-description">
3541
+ * <div class="plinth-sheet-panel">
3542
+ * <div class="plinth-sheet-handle" aria-hidden="true"></div>
3543
+ * <div class="plinth-sheet-header">
3544
+ * <h2 id="filters-title" class="plinth-sheet-title">Filters</h2>
3545
+ * <p id="filters-description" class="plinth-sheet-description">Narrow the list down.</p>
3546
+ * <button type="button" class="plinth-button plinth-icon-button plinth-sheet-close"
3547
+ * data-variant="ghost" data-size="md" aria-label="Close" commandfor="filters"
3548
+ * command="close">
3549
+ * <span class="plinth-icon" data-size="md" aria-hidden="true"><svg>…</svg></span>
3550
+ * </button>
3551
+ * </div>
3552
+ * <div class="plinth-sheet-body">…</div>
3553
+ * <div class="plinth-sheet-footer">…</div>
3554
+ * </div>
3555
+ * </dialog>
3556
+ *
3557
+ * The root is the <dialog>, and it needs an id: commandfor and aria-labelledby point at it, and
3558
+ * the title's and description's ids are the root's id with -title and -description. The panel is
3559
+ * the root's only child and holds the parts. The handle is there only where the sheet's side on a
3560
+ * phone is the bottom (data-side="bottom" with no data-side-phone, or data-side-phone="bottom"):
3561
+ * decorative (aria-hidden), shown below 768px, where the sheet is dragged down to close
3562
+ * (modal.css). The title is
3563
+ * required; the description (and its aria-describedby) and the footer are optional. The close
3564
+ * button is an icon button (button.css and icon.css) and is always there: closedby="any" (tap the
3565
+ * backdrop) is an enhancement Safari doesn't have yet. A project may set closedby="closerequest"
3566
+ * on a sheet that must not close on a stray tap.
3567
+ *
3568
+ * Open it with showModal() or a <button commandfor="filters" command="show-modal">. Open is the
3569
+ * native state, [open]; the browser puts the sheet in the top layer, makes the page inert, traps
3570
+ * focus, closes on Escape and returns focus. The dialog fires close when it closes. For a modal
3571
+ * centred on the screen, use the dialog (dialog.css).
3572
+ *
3573
+ * Side (data-side), at every width: end (default, also when missing or unknown) and start are
3574
+ * full-height panels on the inline-end and inline-start edges, sheet.side-width wide and at
3575
+ * most the screen's width less a gap, so the page shows beside them; top and bottom are the
3576
+ * screen's width on the top and bottom edges, as tall as their content up to the screen's height
3577
+ * less a gap that also clears the safe area at the other edge. The sides are logical: in
3578
+ * right-to-left text end is the left edge and start the right.
3579
+ *
3580
+ * Side on a phone (data-side-phone, optional): the side below 768px, one of the same four; from
3581
+ * 768px up data-side holds. data-side="end" data-side-phone="bottom" is a side panel on a tablet
3582
+ * or desktop and a bottom sheet on a phone. Leave it out to keep data-side at every width. It
3583
+ * takes only those four values: any other makes the phone's side end (the layers never print
3584
+ * one, and leave the attribute out instead).
3585
+ *
3586
+ * The edge the sheet sits on has no border or radius; the edge facing the page has a hairline
3587
+ * (in forced colours too), and a top or bottom sheet rounds the corners facing the page. Each
3588
+ * side clears the safe areas it touches (the notch, the status bar and the home indicator, with
3589
+ * viewport-fit=cover).
3590
+ *
3591
+ * The description is a secondary line, at the size of a list row's description.
3592
+ *
3593
+ * The surface, the backdrop, the scroll lock, the motion, the safe areas and the parts come from
3594
+ * the modal base (modal.css), which the dialog shares; load it with this file. The body scrolls,
3595
+ * the header and footer stay put, and the page under an open sheet doesn't scroll. The sheet
3596
+ * slides in from its edge and back out on the motion tokens, which drop to 0ms under
3597
+ * prefers-reduced-motion. This file sets only where the sheet sits and how big it is.
3598
+ *
3599
+ * A sheet on the bottom edge has its height capped by 100dvh, which clears the home indicator
3600
+ * (env(safe-area-inset-bottom)) but not, on its own, the on-screen keyboard: iOS lays the
3601
+ * keyboard over the page. A page with a form in a sheet sets
3602
+ * <meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
3603
+ * where the browser supports it, and keeps the sheet's fields near its top.
3604
+ *
3605
+ * In Livewire, a re-render would morph the open dialog back to the server's markup and drop its
3606
+ * open attribute: put wire:ignore.self on the sheet (or keep it outside the component that
3607
+ * re-renders), and always pass an id, since a generated one changes on every render.
3608
+ */
3609
+
3610
+ @layer plinth {
3611
+ /*
3612
+ * The end side, the default. The sides are written twice: as the phone's side (data-side-phone,
3613
+ * or data-side without it), and from 768px up as data-side again for a sheet with
3614
+ * data-side-phone. The phone's rules use :where() so the later ones win on order alone.
3615
+ */
3616
+ .plinth-sheet {
3617
+ /* Translate isn't logical: -1 (under :dir(rtl), below) turns the slide around. */
3618
+ --plinth-sheet-direction: 1;
3619
+ --plinth-sheet-safe-area-inline-start: env(safe-area-inset-left);
3620
+ --plinth-sheet-safe-area-inline-end: env(safe-area-inset-right);
3621
+ --plinth-modal-offset: calc(100% * var(--plinth-sheet-direction)) 0;
3622
+ --plinth-modal-safe-area-end: env(safe-area-inset-bottom);
3623
+
3624
+ inset-block: 0;
3625
+ inset-inline: auto 0;
3626
+ inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
3627
+ block-size: auto;
3628
+ max-block-size: none;
3629
+ padding-block: env(safe-area-inset-top) 0;
3630
+ padding-inline: 0 var(--plinth-sheet-safe-area-inline-end);
3631
+ margin: 0;
3632
+ border-width: 0;
3633
+ border-inline-start-width: var(--plinth-border-width-thin);
3634
+ border-radius: 0;
3635
+ }
3636
+
3637
+ .plinth-sheet:where([data-side='start']:not([data-side-phone]), [data-side-phone='start']) {
3638
+ --plinth-modal-offset: calc(-100% * var(--plinth-sheet-direction)) 0;
3639
+
3640
+ inset-block: 0;
3641
+ inset-inline: 0 auto;
3642
+ inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
3643
+ max-block-size: none;
3644
+ padding-block: env(safe-area-inset-top) 0;
3645
+ padding-inline: var(--plinth-sheet-safe-area-inline-start) 0;
3646
+ border-width: 0;
3647
+ border-inline-end-width: var(--plinth-border-width-thin);
3648
+ border-radius: 0;
3649
+ }
3650
+
3651
+ .plinth-sheet:where([data-side='top']:not([data-side-phone]), [data-side-phone='top']) {
3652
+ --plinth-modal-offset: 0 -100%;
3653
+ --plinth-modal-safe-area-end: initial;
3654
+
3655
+ inset-block: 0 auto;
3656
+ inset-inline: 0;
3657
+ inline-size: 100%;
3658
+ max-block-size: calc(
3659
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-bottom) + var(--plinth-space-4))
3660
+ );
3661
+ padding-block: env(safe-area-inset-top) 0;
3662
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
3663
+ var(--plinth-sheet-safe-area-inline-end);
3664
+ border-width: 0;
3665
+ border-block-end-width: var(--plinth-border-width-thin);
3666
+ border-radius: 0;
3667
+ border-end-start-radius: var(--plinth-radius-container);
3668
+ border-end-end-radius: var(--plinth-radius-container);
3669
+ }
3670
+
3671
+ .plinth-sheet:where([data-side='bottom']:not([data-side-phone]), [data-side-phone='bottom']) {
3672
+ --plinth-modal-offset: 0 100%;
3673
+
3674
+ inset-block: auto 0;
3675
+ inset-inline: 0;
3676
+ inline-size: 100%;
3677
+ max-block-size: calc(
3678
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
3679
+ );
3680
+ padding-block: 0;
3681
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
3682
+ var(--plinth-sheet-safe-area-inline-end);
3683
+ border-width: 0;
3684
+ border-block-start-width: var(--plinth-border-width-thin);
3685
+ border-radius: 0;
3686
+ border-start-start-radius: var(--plinth-radius-container);
3687
+ border-start-end-radius: var(--plinth-radius-container);
3688
+ }
3689
+
3690
+ /*
3691
+ * With its grab handle, a bottom sheet on a phone is dragged down to close (modal.css): the root
3692
+ * fills the screen, see-through, and its panel is the surface, as tall as its content up to the
3693
+ * screen's height less a gap.
3694
+ */
3695
+ .plinth-sheet:where([data-side='bottom']:not([data-side-phone]), [data-side-phone='bottom']):has(
3696
+ > * > .plinth-sheet-handle
3697
+ ) {
3698
+ inset: 0;
3699
+ max-block-size: none;
3700
+ padding: 0;
3701
+ border-width: 0;
3702
+ border-radius: 0;
3703
+ }
3704
+
3705
+ /* Right-to-left text turns the slide around and swaps the safe areas. */
3706
+ .plinth-sheet:dir(rtl) {
3707
+ --plinth-sheet-direction: -1;
3708
+ --plinth-sheet-safe-area-inline-start: env(safe-area-inset-right);
3709
+ --plinth-sheet-safe-area-inline-end: env(safe-area-inset-left);
3710
+ }
3711
+
3712
+ @media (min-width: 768px) {
3713
+ .plinth-sheet[data-side-phone] {
3714
+ --plinth-modal-offset: calc(100% * var(--plinth-sheet-direction)) 0;
3715
+ --plinth-modal-safe-area-end: env(safe-area-inset-bottom);
3716
+
3717
+ inset-block: 0;
3718
+ inset-inline: auto 0;
3719
+ inline-size: min(100% - var(--plinth-space-12), var(--plinth-sheet-side-width));
3720
+ max-block-size: none;
3721
+ padding-block: env(safe-area-inset-top) 0;
3722
+ padding-inline: 0 var(--plinth-sheet-safe-area-inline-end);
3723
+ border-width: 0;
3724
+ border-inline-start-width: var(--plinth-border-width-thin);
3725
+ border-radius: 0;
3726
+ }
3727
+
3728
+ .plinth-sheet[data-side-phone][data-side='start'] {
3729
+ --plinth-modal-offset: calc(-100% * var(--plinth-sheet-direction)) 0;
3730
+
3731
+ inset-inline: 0 auto;
3732
+ padding-inline: var(--plinth-sheet-safe-area-inline-start) 0;
3733
+ border-inline-start-width: 0;
3734
+ border-inline-end-width: var(--plinth-border-width-thin);
3735
+ }
3736
+
3737
+ .plinth-sheet[data-side-phone][data-side='top'] {
3738
+ --plinth-modal-offset: 0 -100%;
3739
+ --plinth-modal-safe-area-end: initial;
3740
+
3741
+ inset-block: 0 auto;
3742
+ inset-inline: 0;
3743
+ inline-size: 100%;
3744
+ max-block-size: calc(
3745
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-bottom) + var(--plinth-space-4))
3746
+ );
3747
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
3748
+ var(--plinth-sheet-safe-area-inline-end);
3749
+ border-block-end-width: var(--plinth-border-width-thin);
3750
+ border-inline-start-width: 0;
3751
+ border-end-start-radius: var(--plinth-radius-container);
3752
+ border-end-end-radius: var(--plinth-radius-container);
3753
+ }
3754
+
3755
+ .plinth-sheet[data-side='bottom'] {
3756
+ --plinth-modal-offset: 0 100%;
3757
+
3758
+ inset-block: auto 0;
3759
+ inset-inline: 0;
3760
+ inline-size: 100%;
3761
+ max-block-size: calc(
3762
+ 100dvh - max(var(--plinth-space-12), env(safe-area-inset-top) + var(--plinth-space-4))
3763
+ );
3764
+ padding-block: 0;
3765
+ padding-inline: var(--plinth-sheet-safe-area-inline-start)
3766
+ var(--plinth-sheet-safe-area-inline-end);
3767
+ border-block-start-width: var(--plinth-border-width-thin);
3768
+ border-inline-start-width: 0;
3769
+ border-start-start-radius: var(--plinth-radius-container);
3770
+ border-start-end-radius: var(--plinth-radius-container);
3771
+ }
3772
+ }
3773
+ }