@zenit-hosting/zenit-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +400 -0
  2. package/fesm2022/zenit-hosting-zenit-ui.mjs +11289 -0
  3. package/fesm2022/zenit-hosting-zenit-ui.mjs.map +1 -0
  4. package/llms-full.txt +5120 -0
  5. package/llms.txt +179 -0
  6. package/package.json +44 -0
  7. package/schematics/collection.json +15 -0
  8. package/schematics/migrate-material/component.js +241 -0
  9. package/schematics/migrate-material/index.js +463 -0
  10. package/schematics/migrate-material/report.js +125 -0
  11. package/schematics/migrate-material/schema.js +2 -0
  12. package/schematics/migrate-material/schema.json +32 -0
  13. package/schematics/migrate-material/tables.js +138 -0
  14. package/schematics/migrate-material/template.js +871 -0
  15. package/schematics/ng-add/html.js +151 -0
  16. package/schematics/ng-add/index.js +609 -0
  17. package/schematics/ng-add/init-script.js +108 -0
  18. package/schematics/ng-add/schema.js +2 -0
  19. package/schematics/ng-add/schema.json +34 -0
  20. package/schematics/package.json +3 -0
  21. package/styles/_daten.css +138 -0
  22. package/styles/_formulare.css +97 -0
  23. package/styles/_grundlage.css +424 -0
  24. package/styles/_konfigurator.css +216 -0
  25. package/styles/_navigation.css +179 -0
  26. package/styles/_overlays.css +86 -0
  27. package/styles/_rueckmeldung.css +100 -0
  28. package/styles/_werkzeuge.css +119 -0
  29. package/styles/themes/accents.css +91 -0
  30. package/styles/themes/base.css +26 -0
  31. package/styles/themes/contrast.css +67 -0
  32. package/styles/themes/light.css +74 -0
  33. package/styles/themes.css +21 -0
  34. package/styles/tokens.css +76 -0
  35. package/styles/zenit-ui.css +12 -0
  36. package/types/zenit-hosting-zenit-ui.d.ts +5957 -0
@@ -0,0 +1,424 @@
1
+ /* Package grundlage: head (z-root), Button, Badge, Field/Input/Select, Panel,
2
+ Icon, Spinner and the layout helpers. Verbatim from spec/components/bundle.css. */
3
+
4
+ .z-root, .z-root *:not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root * { box-sizing: border-box; }
5
+ .z-root {
6
+ background: var(--bg);
7
+ color: var(--text);
8
+ font-family: var(--font-body);
9
+ -webkit-font-smoothing: antialiased;
10
+ }
11
+ /* Addition to the reference: an island without its own surface, for migrated
12
+ blocks inside an old page (docs/legacy.md). Everything of .z-root stays,
13
+ tokens, text colour, type and the base rules; only the ground of the
14
+ surrounding page shows through. The text colour is still the scheme's, so
15
+ the island needs a scheme that fits that ground: data-theme="light" from
16
+ themes.css on a light ground. Same weight as .z-root and later, so it wins. */
17
+ .z-root--transparent { background: transparent; }
18
+ /* Deviation from the reference: bundle.css writes font-size: 14px and
19
+ line-height: 20px into the rule above, which is meant for the page, while the
20
+ documented setup puts z-root on <html> as well as on <body>. On <html> those
21
+ 14px move 1rem from 16px to 14px for the whole document and override the size
22
+ the visitor set in the browser: an application that adopts the library page by
23
+ page keeps its old styles in rem, and all of them shrink by 12.5% the moment
24
+ the class is set. So the two declarations are split off into the rule below,
25
+ which leaves <html> out. The library needs neither value there, its components
26
+ compute in px. The one rem in styles/_*.css is the 1rem of .z-legacy below,
27
+ which hands the visitor's size back to a page that is not migrated; the only
28
+ other relative lengths are em on letter-spacing and text-underline-offset,
29
+ which refer to the element's own font size.
30
+ :where() adds (0,0,0), so the rule still weighs (0,1,0), exactly what .z-root
31
+ weighed before: body.z-root and every nested page container keep the same
32
+ weight against an application class on the same element, a tie decided by
33
+ source order. And since the library now sets nothing at all on <html>, an
34
+ application's own html { font-size: … } is the only author rule there and wins
35
+ at every specificity and in either include order, the html { font-size:
36
+ var(--base-font-size) } of a user setting included.
37
+ Not html.z-root { font-size: 100% }: that is (0,1,1) and beat exactly that
38
+ application rule (measured: --base-font-size 14px and 18px both rendered
39
+ 16px). Not html:where(.z-root) either: :where() adds nothing, so it is the
40
+ tag alone at (0,0,1) and loses to .z-root (0,1,0) in any order, leaving
41
+ <html> at the 14px it is meant to shed.
42
+ body.z-root carries the class itself and keeps 14px/20px; every component
43
+ inherits from there, and the CDK overlay container hangs on <body> too, so
44
+ overlays are unaffected. The class therefore has to sit on BOTH elements, as
45
+ the README prescribes: with z-root on <html> alone the page renders at
46
+ 16px/normal, because nothing sets the page size any more. */
47
+ .z-root:where(:not(html)) { font-size: 14px; line-height: 20px; }
48
+ /* Addition to the reference: .z-legacy, the class for a subtree that is not
49
+ migrated yet (docs/legacy.md). An application that adopts the library route
50
+ by route has z-root on <html> and <body>, and body.z-root hands 14px/20px
51
+ down to every old page. The absolute line height is what breaks them: a
52
+ legacy h2 of 27.2px on a 20px line overlaps itself as soon as it wraps. A
53
+ reset on <body> would take the shell apart, so the reset sits on the content
54
+ host, and a migrated page puts z-root onto its own container again:
55
+ html.z-root > body.z-root > .z-legacy > .z-root.
56
+
57
+ 1. Inherited values. Size and line height go back to what a page has
58
+ without the library, font smoothing too. Family, colour, background and
59
+ color-scheme stay as body.z-root set them: the library cannot know what
60
+ the old <body> rule said, and text in the shell's family and colour on
61
+ the shell's ground is the readable fallback. The application restates
62
+ them on the same element (snippet in docs/legacy.md). No custom property
63
+ for that: it would be a second way to write the same two declarations.
64
+
65
+ 2. Base rules for bare elements stop at .z-legacy. Every rule of the library
66
+ that styles an element without a library class is written twice:
67
+ .z-root X:not(:where(.z-legacy, .z-legacy *)),
68
+ :where(.z-legacy) .z-root X
69
+ The first selector leaves out the host and everything in it, the second
70
+ is what lets a z-root inside .z-legacy switch the rule on again. Both
71
+ :where() add (0,0,0), so each rule weighs exactly what it weighed before
72
+ and every specificity written down in this file still holds, including
73
+ the tie with an application class at (0,1,0) and all a.z-* counter-rules.
74
+ The host is part of the old page: an old main { width: 600px; padding:
75
+ 0 24px } is 648px wide without the library and has to stay 648px.
76
+ The focus rule has a third selector, :where(.z-legacy) .z-root:focus-visible,
77
+ for the nested container itself, the usual target of a skip link; outside
78
+ .z-legacy a page container is a descendant of body.z-root and gets the
79
+ ring that way.
80
+
81
+ What it costs. The exclusion sits on the universal selector, so every
82
+ element pays for it on a full style recalculation. Production build,
83
+ RecalcStyleDuration over CDP, forced full recalc, median of 40 rounds of
84
+ 10, without and with the exclusion: /daten (647 elements) 1.21 to 1.39 ms;
85
+ /muster/server-panel with 2000 console lines (4130 elements) 4.22 to
86
+ 4.9 ms, at 4x CPU throttling 23.9 to 27.8 ms. That is +14 to +18 % on
87
+ that one step, about 0.7 ms on the large page, and all of it comes from
88
+ the universal rule. A first version that said the same in one selector,
89
+ :not(:where(.z-legacy :not(.z-legacy .z-root, .z-legacy .z-root *))),
90
+ cost +25 % (5.27 ms) and did not cover the host. Covering the host is
91
+ free: with and without it measured the same within the noise.
92
+ Resetting instead (.z-legacy :where(*) { box-sizing: content-box }) is
93
+ cheaper and wrong: it flips an old * { box-sizing: border-box } and the
94
+ border-box the browser gives select and button by default.
95
+ The rules, all in this file (the other partials style classes only):
96
+ .z-root * box-sizing exempt
97
+ .z-root :where(button, input, select, ...) font, color exempt
98
+ .z-root :focus-visible outline exempt
99
+ .z-root :where(a), :where(a):hover color, line exempt
100
+ .z-root :where(p, li, ...) a:not(...) underline exempt
101
+ Kept: .z-root itself, .z-root:where(:not(html)) and html.z-root /
102
+ body.z-root (they match the class, not a bare element; the size rule is
103
+ the one .z-legacy overrides by source order), :where(:root) color-scheme and
104
+ .z-root:has(.z-stickybar[data-stuck]) (document level), and every class
105
+ rule.
106
+ The focus ring is exempt as well, for a measured reason: focus is white
107
+ in the dark scheme, and outline from this file REPLACES the two-tone ring
108
+ the browser draws by default. On the light surface of the old page in
109
+ the demo (/muster/legacy) that is white on near white, 1.04:1, where the
110
+ browser's own ring was visible. Opting in is one line in the
111
+ application (.z-legacy :focus-visible { outline: 2px solid ...; }),
112
+ opting out would be impossible: revert rolls back to the browser's
113
+ stylesheet, not to the application's rule.
114
+
115
+ 3. Alternatives, measured. @scope (.z-root) to (.z-legacy) is the natural
116
+ fit, but Angular 22 builds for "baseline widely available on 2026-05-07",
117
+ which includes Firefox 119 to 145 and Safari 17.0 to 17.3 without @scope;
118
+ they would drop every base rule. color: revert inside .z-legacy turned a
119
+ legacy link from its own colour to the browser's blue and a legacy button
120
+ from 13px to 13.33px, because revert skips every author rule, and it
121
+ cannot be undone by a nested z-root.
122
+
123
+ Ceiling: one level of re-entry. A .z-legacy inside a z-root inside a
124
+ .z-legacy counts as migrated (the base rules apply, while the inherited
125
+ values are the 1rem/normal of that inner .z-legacy); nearest-ancestor
126
+ matching needs @scope. .z-legacy never goes on <body>: the CDK overlay
127
+ container hangs there and would be part of the old page.
128
+ A library component placed straight into .z-legacy, without a z-root around
129
+ it, loses exactly the rules listed under 2; docs/legacy.md has the list. */
130
+ .z-legacy { font-size: 1rem; line-height: normal; -webkit-font-smoothing: auto; }
131
+ /* Deviation from the reference: bundle.css lists the four elements one by one
132
+ (specificity 0,1,1) and thereby beats .z-btn--primary/--ghost/--danger
133
+ (0,1,0). :where() lowers the rule to 0,1,0, the values stay the same. */
134
+ .z-root :where(button, input, select, textarea):not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(button, input, select, textarea) { font: inherit; color: inherit; }
135
+ .z-root :focus-visible:not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root:focus-visible, :where(.z-legacy) .z-root :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
136
+ /* Deviation from the reference: bundle.css writes .z-root a (0,1,1), which beats
137
+ every class an application can put on a link (0,1,0). In the first real
138
+ integration that made the application's own skip link red on red (1.29:1) and
139
+ recoloured every link of the pages not migrated yet, the moment z-root went
140
+ onto <html>. :where() lowers the two rules to (0,1,0) and (0,2,0); the values
141
+ are unchanged. It is the same fix the rule above got for
142
+ :where(button, input, select, textarea).
143
+
144
+ What that buys, exactly. At (0,1,0) an application class on a link now TIES
145
+ with this rule, and a tie is decided by source order, not by specificity: the
146
+ application stylesheet has to be loaded AFTER zenit-ui.css. The style order
147
+ the README and ng-add prescribe does that (tokens, cdk, zenit-ui, then
148
+ src/styles.css). Measured: a rule .lg { color: … } placed before zenit-ui.css
149
+ still loses and the link stays accent-text with an underline on hover; placed
150
+ after it, it wins. The hover rule weighs (0,2,0), so an application that wants
151
+ to change the hover needs .lg:hover, not .lg.
152
+
153
+ The price. An application rule at (0,1,1), such as .app a, now also reaches
154
+ plain links INSIDE library components: panel, alert, row, table, empty state,
155
+ the sub line of the page header, header, tabs, sidebar, stepper, toast, the
156
+ body of a dialog, tooltip and menu. It does not reach the footer lists
157
+ (.z-root .z-footer__list a and .z-footer__base a, both (0,2,1)) and no link
158
+ that carries a library class, because each of those has an a. counter-rule at
159
+ (0,2,1) or more: a.z-btn with its five variants and the Minecraft primary,
160
+ a.z-skip-link, a.z-tab, a.z-side__item, a.z-header__link, a.z-row,
161
+ a.z-row__link and a.z-game.
162
+
163
+ That counter-rule is the pattern to keep. No anchor the library renders today
164
+ carries a class without one, which is why lowering the base rule changes
165
+ nothing here (measured: identical computed color, text-decoration-line and
166
+ text-decoration-color for all 299 anchors of the demo and all 26 of the
167
+ example application, in dark, light and contrast). A new directive that hosts
168
+ on an anchor needs the same: its class rule has to be written .z-root a.z-xyz,
169
+ not .z-xyz, or an application rule at (0,1,1) will beat it.
170
+
171
+ The underline for links in running text below stays at (0,2,1) and therefore
172
+ cannot be switched off from an application stylesheet. Its own escape hatch is
173
+ a:not([class*="z-"]), so any class whose name contains "z-" opts a link out,
174
+ including by accident ("quiz-link"). That is a side effect of the selector,
175
+ not a documented API; the documented way to keep the library out of a
176
+ subtree is .z-legacy above. */
177
+ .z-root :where(a):not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(a) { color: var(--accent-text); text-decoration: none; }
178
+ .z-root :where(a):hover:not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(a):hover { text-decoration: underline; }
179
+ /* Deviation from the reference: bundle.css underlines a link on hover only. A
180
+ link inside running text then differs from the words around it by colour
181
+ alone, and accent-text against text stays below 3:1 in 11 of the 12 scheme
182
+ and accent combinations (dark/rot reaches 3.27:1), which fails WCAG 1.4.1
183
+ (axe: link-in-text-block). No colour can fix that in every scheme, so links
184
+ in running text are underlined at rest.
185
+ Where: inside the text containers listed below. Not inside nav, .z-header
186
+ and .z-footer, whose links stand alone and are recognisable by position, and
187
+ not on a link that carries a library class (z-btn, z-tab, z-row, z-side__item,
188
+ z-header__link ...), because those style themselves.
189
+ 0.15em: text-underline-offset has no token, and a length relative to the
190
+ font size is the only kind that fits 12px hints and 16px body alike; it
191
+ moves the line clear of the descenders of g, j and y. No px literal is added.
192
+ Weight (0,2,1), the same as .z-root a.z-btn below. */
193
+ .z-root :where(p, li, dd, label, .z-alert__body, .z-faq__body, .z-empty__body, .z-field__hint):not(:where(nav, .z-header, .z-footer) *) a:not([class*="z-"]):not(:where(.z-legacy, .z-legacy *)),
194
+ :where(.z-legacy) .z-root :where(p, li, dd, label, .z-alert__body, .z-faq__body, .z-empty__body, .z-field__hint):not(:where(nav, .z-header, .z-footer) *) a:not([class*="z-"]) {
195
+ text-decoration: underline;
196
+ text-underline-offset: 0.15em;
197
+ }
198
+ /* Deviation from the reference: a link in the text of an alert takes text, not
199
+ accent-text. On the tints of the four statuses accent-text misses 4.5:1 in
200
+ the normative dark scheme (measured with tools/check-theme-contrast.mjs:
201
+ info and warning over surface 4.38 and 4.37, all four over surface-raised
202
+ 4.04 to 4.23); text reaches 12.36 or more in every scheme and accent. The
203
+ running-text underline above stays, so the link is told apart from the
204
+ text-muted body by colour and line, and the alert's one button is not red
205
+ either. The same weight and the same .z-legacy exemption as the link rule
206
+ it overrides, so an application class on the link still wins. */
207
+ .z-root :where(.z-alert__body a):not(:where(.z-legacy, .z-legacy *)), :where(.z-legacy) .z-root :where(.z-alert__body a) { color: var(--text); }
208
+ .z-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
209
+ .z-muted { color: var(--text-muted); }
210
+ .z-subtle { color: var(--text-subtle); }
211
+ .z-icon { font-size: var(--icon); width: var(--icon); height: var(--icon); overflow: hidden; color: var(--text-muted); flex: none; }
212
+ @media (prefers-reduced-motion: no-preference) {
213
+ .z-btn, .z-input, .z-select select, .z-row, .z-side__item, .z-tab, .z-game { transition: color 150ms, background-color 150ms, border-color 150ms; }
214
+ }
215
+
216
+ /* Button */
217
+ .z-btn {
218
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
219
+ height: var(--control-md); padding: 0 var(--space-4);
220
+ border: 1px solid transparent; border-radius: var(--radius-md);
221
+ background: transparent; color: var(--text);
222
+ font-size: 14px; line-height: 20px; font-weight: 500; white-space: nowrap; cursor: pointer;
223
+ }
224
+ .z-btn--primary { background: var(--accent); color: var(--on-accent); }
225
+ .z-btn--primary:hover { background: var(--accent-hover); }
226
+ .z-btn--primary .z-icon { color: var(--on-accent); }
227
+ .z-btn--secondary { border-color: var(--border-control); }
228
+ .z-btn--secondary:hover { background: var(--surface-hover); }
229
+ .z-btn--ghost { color: var(--text-muted); }
230
+ .z-btn--ghost:hover { color: var(--text); background: var(--surface-raised); }
231
+ .z-btn--danger { border-color: var(--danger); color: var(--danger); }
232
+ .z-btn--danger:hover { background: var(--danger-subtle); }
233
+ .z-btn--danger .z-icon { color: var(--danger); }
234
+ .z-btn--sm { height: var(--control-sm); padding: 0 var(--space-3); }
235
+ .z-btn--lg { height: var(--control-lg); padding: 0 var(--space-5); font-size: 16px; line-height: 24px; }
236
+ .z-btn--icon { width: var(--control-md); padding: 0; }
237
+ .z-btn--icon.z-btn--sm { width: var(--control-sm); }
238
+ .z-btn--block { width: 100%; }
239
+ .z-btn:disabled, .z-btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
240
+ .z-theme-mc .z-btn--primary { background: var(--mc-accent); color: var(--on-mc); }
241
+ .z-theme-mc .z-btn--primary:hover { background: var(--mc-accent-hover); }
242
+ .z-theme-mc .z-btn--primary .z-icon { color: var(--on-mc); }
243
+
244
+ /* Badge */
245
+ .z-badge {
246
+ display: inline-flex; align-items: center; gap: var(--space-1);
247
+ height: 20px; padding: 0 var(--space-2);
248
+ border-radius: var(--radius-sm);
249
+ background: var(--surface-raised); color: var(--text-muted);
250
+ font-size: 12px; line-height: 16px; font-weight: 500; white-space: nowrap;
251
+ }
252
+ .z-badge__dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; }
253
+ .z-badge--success { background: var(--success-subtle); color: var(--success); }
254
+ .z-badge--warning { background: var(--warning-subtle); color: var(--warning); }
255
+ .z-badge--danger { background: var(--danger-subtle); color: var(--danger); }
256
+ .z-badge--info { background: var(--info-subtle); color: var(--info); }
257
+
258
+ /* Field, Input, Select */
259
+ .z-field { display: grid; gap: var(--space-1); min-width: 0; align-content: start; }
260
+ .z-field__label { font-size: 14px; line-height: 20px; font-weight: 600; }
261
+ .z-field__hint { font-size: 12px; line-height: 16px; color: var(--text-subtle); }
262
+ .z-field__error { font-size: 12px; line-height: 16px; color: var(--danger); }
263
+ .z-input, .z-select select {
264
+ width: 100%; height: var(--control-md); padding: 0 var(--space-3);
265
+ border: 1px solid var(--border-control); border-radius: var(--radius-sm);
266
+ background: var(--surface); color: var(--text);
267
+ }
268
+ .z-input::placeholder { color: var(--text-subtle); }
269
+ .z-input:hover, .z-select select:hover { border-color: var(--text-muted); }
270
+ .z-input[aria-invalid="true"] { border-color: var(--danger); }
271
+ .z-input:disabled, .z-select select:disabled { opacity: 0.45; cursor: not-allowed; }
272
+ .z-input--sm, .z-select--sm select { height: var(--control-sm); }
273
+ .z-input-wrap { position: relative; display: block; }
274
+ .z-input-wrap .z-icon { position: absolute; left: var(--space-3); top: 50%; margin-top: -10px; pointer-events: none; }
275
+ .z-input-wrap .z-input { padding-left: 40px; }
276
+ .z-select { position: relative; display: block; }
277
+ .z-select select { appearance: none; -webkit-appearance: none; padding-right: 36px; cursor: pointer; }
278
+ .z-select::after {
279
+ content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px;
280
+ border-right: 1.5px solid var(--text-muted); border-bottom: 1.5px solid var(--text-muted);
281
+ transform: rotate(45deg); pointer-events: none;
282
+ }
283
+
284
+ /* Panel */
285
+ .z-panel { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); min-width: 0; }
286
+ .z-panel__header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
287
+ .z-panel__title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 600; }
288
+ .z-panel__body { padding: var(--space-4); }
289
+ .z-panel__body--flush { padding: 0; }
290
+
291
+ /* ---------- Angular hosts and additions of the library ---------- */
292
+
293
+ html.z-root, body.z-root { margin: 0; min-height: 100%; }
294
+ .z-root a.z-btn, .z-root a.z-btn:hover { text-decoration: none; }
295
+ /* Gap in the reference: .z-root a (0,1,1) beats .z-btn (0,1,0), so every button
296
+ written as a link fell back to accent-text. bundle.css closes the gap for
297
+ --primary and --secondary only; the base button and the ghost and danger
298
+ variants stayed red. The rule below restores text for the bare a.z-btn and
299
+ with it for the sizes, which carry no colour of their own (--sm, --lg,
300
+ --icon, --block).
301
+ It has to stand first: it and the four variant rules all weigh (0,2,1), so
302
+ only source order decides. What still wins after it, in this order:
303
+ --primary (on-accent), --secondary (text), --ghost (text-muted, text on
304
+ hover), --danger (danger) and, at (0,3,1), the Minecraft primary link
305
+ .z-root .z-theme-mc a.z-btn--primary (on-mc). The underline is handled one
306
+ line above, for the resting and the hover state together, so a.z-btn:hover
307
+ stays free of it in every variant. */
308
+ .z-root a.z-btn { color: var(--text); }
309
+ .z-root a.z-btn--primary { color: var(--on-accent); }
310
+ .z-root a.z-btn--secondary { color: var(--text); }
311
+ .z-root a.z-btn--ghost { color: var(--text-muted); }
312
+ .z-root a.z-btn--ghost:hover { color: var(--text); }
313
+ .z-root a.z-btn--danger { color: var(--danger); }
314
+ .z-root .z-theme-mc a.z-btn--primary { color: var(--on-mc); }
315
+
316
+ .z-panel, .z-field, .z-select, .z-input-wrap { display: block; }
317
+ .z-field { display: grid; }
318
+ .z-icon--sm { font-size: 16px; width: 16px; height: 16px; }
319
+ .z-icon { line-height: 1; user-select: none; }
320
+ textarea.z-input { height: auto; min-height: 96px; padding: var(--space-2) var(--space-3); resize: vertical; }
321
+ .z-input--mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
322
+ /* .z-skip-link shares the technique: it is out of sight until it takes focus,
323
+ and the block at the end of this file unfolds it again. */
324
+ .z-visually-hidden, .z-skip-link { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
325
+
326
+ /* Spinner */
327
+ .z-spinner { display: inline-block; flex: none; width: 16px; height: 16px; border: 2px solid currentColor; border-right-color: transparent; border-radius: var(--radius-full); opacity: 0.8; }
328
+ @media (prefers-reduced-motion: no-preference) {
329
+ .z-spinner { animation: z-spin 0.7s linear infinite; }
330
+ @keyframes z-spin { to { transform: rotate(360deg); } }
331
+ }
332
+
333
+ /* Layout helpers */
334
+ .z-container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--space-4); }
335
+ @media (min-width: 640px) { .z-container { padding-inline: var(--space-5); } }
336
+ .z-stack { display: grid; gap: var(--space-5); }
337
+ .z-cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
338
+ .z-section { padding-block: var(--space-8); border-top: 1px solid var(--border); }
339
+ @media (min-width: 900px) { .z-section { padding-block: var(--space-9); } }
340
+ .z-panel-shell { display: grid; gap: var(--space-5); grid-template-columns: minmax(0, 1fr); }
341
+ @media (min-width: 900px) { .z-panel-shell { grid-template-columns: var(--sidebar) minmax(0, 1fr); align-items: start; } }
342
+
343
+ /* Addition to the reference: the dark colour scheme as the default of the base
344
+ styles. It tells the engine which palette to use for the parts a page cannot
345
+ style itself: the popup of a native <select>, the calendar and clock popups
346
+ and their indicators, the spin buttons of a number field, scrollbars, the
347
+ caret and autofill. Without it a page that follows the documented minimum
348
+ (tokens.css plus zenit-ui.css) draws light native UI on the dark ground.
349
+ tokens.css is compiled from spec/tokens.json and stays byte identical to
350
+ spec/tokens.css, so the declaration cannot live there; themes/base.css is
351
+ opt-in and does not reach that minimum either.
352
+
353
+ :where() keeps the rule at specificity (0,0,0), which is what makes it safe
354
+ in both include orders the repository uses: themes.css before zenit-ui.css
355
+ (docs/theming.md, beispiel-app) and after it (ui-demo styles.css, where the
356
+ application stylesheet is loaded last). A bare :root would weigh (0,1,0),
357
+ the same as [data-theme="light"], and would then beat the light scheme
358
+ wherever it happened to come later. At (0,0,0) every scheme block wins on
359
+ specificity alone, in any order.
360
+
361
+ themes/base.css keeps its own declaration: it carries [data-theme="dark"],
362
+ which this rule cannot cover, for a dark subtree inside a light page. */
363
+ :where(:root) { color-scheme: dark; }
364
+
365
+ /* Addition to the reference: click targets at least 40px on mobile (CLAUDE.md, Abstand und Layout). */
366
+ @media (max-width: 640px) {
367
+ .z-btn--sm { height: var(--control-md); }
368
+ .z-btn--icon.z-btn--sm { width: var(--control-md); }
369
+ .z-input--sm, .z-select--sm select { height: var(--control-md); }
370
+ .z-panel__header a:not(.z-btn) { display: inline-flex; align-items: center; min-height: var(--control-md); }
371
+ }
372
+
373
+ /* Addition to the reference: the control border of the secondary button inside
374
+ an alert. border-control is dimensioned for surface and reaches 3:1 there,
375
+ but not on a tinted alert. With the markup the Alert README prescribes (a
376
+ secondary sm button in the alert) the dark scheme measures 2.88 (info), 2.87
377
+ (warning) and 2.98 (danger) against an alert on bg, and 2.65 to 2.77 against
378
+ one inside a panel on surface. text-muted is the next token up and clears
379
+ 3:1 on every tint in every scheme, worst case 6.29 (dark, warning on
380
+ surface); .z-input:hover already uses it as a control border. The neutral
381
+ alert is opaque surface-raised, where text-muted is guarded at 7:1 already.
382
+ The rule belongs to the Alert component and would sit in _rueckmeldung.css;
383
+ that file is being changed elsewhere at the same time, so it lives here.
384
+ tools/check-theme-contrast.mjs guards the pairs for every scheme x accent. */
385
+ .z-alert .z-btn--secondary { border-color: var(--text-muted); }
386
+
387
+ /* Addition to the reference: the skip link, the first tab stop of every page
388
+ (15-zustaende.md, "Tastatur"). The reference ships no block for it, and an
389
+ application cannot supply one either: .z-root a (0,1,1) used to beat any
390
+ application class (0,1,0), which is how the first integration ended up with a
391
+ red link on the red accent surface at 1.29:1. Both ends are fixed now, the
392
+ base rule with :where() and this rule at (0,2,1) / (0,3,1), which wins in
393
+ either direction.
394
+ Hidden state: the .z-visually-hidden declarations above, so the technique is
395
+ written once.
396
+ z-index: the link has to clear everything a page may stack over it, header
397
+ (100), overlay and scrim (1000) and toast (1100). --z-toast is the highest
398
+ token tokens.css defines, and a literal above it is not allowed, so it is the
399
+ top of the documented stacking order. A toast can only tie with it, and the
400
+ two never meet: the link is visible while it has focus, and focusing it is
401
+ the first thing that happens on a page.
402
+ :focus, not :focus, :focus-visible: :focus-visible only ever matches an
403
+ element that also matches :focus, so the one selector covers the keyboard tab
404
+ and a focus set from code alike. The global 2px focus ring stays untouched.
405
+ Colour: text on surface-raised, a pair the contrast gate already measures in
406
+ all three schemes (14.77:1 light, 16.26:1 dark, 17.85:1 contrast, gated at
407
+ 12:1, tools/check-theme-contrast.mjs). The hover state keeps the
408
+ colour and drops the underline, so there is no fight with .z-root a:hover. */
409
+ .z-root a.z-skip-link, .z-root a.z-skip-link:hover { color: var(--text); text-decoration: none; }
410
+ .z-root a.z-skip-link:focus {
411
+ position: fixed; top: var(--space-2); left: var(--space-4); z-index: var(--z-toast);
412
+ display: inline-flex; align-items: center;
413
+ width: auto; height: auto; min-height: var(--control-md); margin: 0; padding: 0 var(--space-4);
414
+ border: 1px solid var(--border-control); border-radius: var(--radius-md);
415
+ background: var(--surface-raised);
416
+ clip-path: none;
417
+ }
418
+
419
+ /* Addition to the reference: the error frame of a native select. bundle.css
420
+ styles .z-input[aria-invalid="true"] only, while 15-zustaende.md asks every
421
+ field in error for a danger border plus the sentence below it, the select
422
+ included. Same declaration as the input, so both fields read alike. The
423
+ caller sets aria-invalid on the native <select>, as with input[zInput]. */
424
+ .z-select select[aria-invalid="true"] { border-color: var(--danger); }
@@ -0,0 +1,216 @@
1
+ /* Package konfigurator: OptionCard, Combobox, Wizard, Config, StickyBar,
2
+ IncludedList, Disclosure, InputAction, CostChart and the PriceSummary states.
3
+ The section "Erweiterung 3: Konfigurator" of spec/components/bundle.css
4
+ stands verbatim, followed by the Angular host rules and the additions. */
5
+
6
+ /* OptionCard */
7
+ .z-options { display: grid; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: var(--space-3); margin: 0; padding: 0; border: 0; min-width: 0; }
8
+ .z-options--compact { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
9
+ .z-options__legend { grid-column: 1 / -1; padding: 0; margin-bottom: var(--space-1); font-size: 14px; line-height: 20px; font-weight: 600; }
10
+ .z-options__legend small { margin-left: var(--space-2); font-size: 12px; font-weight: 400; color: var(--text-subtle); }
11
+ .z-option { position: relative; display: grid; align-content: start; gap: 2px; padding: var(--space-3) var(--space-4); border: 1px solid var(--border-control); border-radius: var(--radius-md); background: var(--surface); cursor: pointer; min-width: 0; }
12
+ .z-option input { position: absolute; opacity: 0; pointer-events: none; }
13
+ .z-option:hover { border-color: var(--text-muted); }
14
+ .z-option:has(input:checked) { border-color: var(--accent-text); outline: 1px solid var(--accent-text); outline-offset: -2px; background: var(--surface-raised); }
15
+ .z-option:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
16
+ .z-option:has(input:disabled) { opacity: 0.45; cursor: not-allowed; }
17
+ .z-theme-mc .z-option:has(input:checked) { border-color: var(--mc-accent); outline-color: var(--mc-accent); }
18
+ .z-option__title { font-weight: 600; }
19
+ .z-option__desc { font-size: 12px; line-height: 16px; color: var(--text-muted); }
20
+ .z-option__price { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; line-height: 16px; color: var(--text-muted); }
21
+ .z-option__badge { position: absolute; top: calc(-1 * var(--space-3)); right: var(--space-2); background-color: var(--surface-raised); border: 1px solid var(--border); }
22
+ .z-options--compact .z-option { padding: var(--space-2) var(--space-3); text-align: center; justify-items: center; }
23
+ .z-options--compact .z-option__title { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
24
+
25
+ /* Combobox */
26
+ .z-combo { position: relative; display: block; }
27
+ .z-combo .z-input { padding-right: 36px; }
28
+ .z-combo::after { content: ""; position: absolute; right: 14px; top: 20px; width: 7px; height: 7px; margin-top: -6px; border-right: 1.5px solid var(--text-muted); border-bottom: 1.5px solid var(--text-muted); transform: rotate(45deg); pointer-events: none; }
29
+ .z-listbox { display: grid; max-height: 248px; margin-top: var(--space-1); padding: var(--space-1); overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-raised); box-shadow: var(--shadow-overlay); }
30
+ .z-listbox__group { padding: var(--space-2) var(--space-3) var(--space-1); font-size: 12px; line-height: 16px; color: var(--text-muted); }
31
+ .z-listbox__option { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); min-height: 36px; padding: 0 var(--space-3); border-radius: var(--radius-sm); cursor: pointer; }
32
+ .z-listbox__option:hover, .z-listbox__option--active { background: var(--surface-hover); }
33
+ .z-listbox__option[aria-selected="true"] { font-weight: 600; }
34
+ .z-listbox__option small { font-size: 12px; font-weight: 400; color: var(--text-muted); }
35
+ .z-listbox__empty { padding: var(--space-3); color: var(--text-muted); }
36
+ /* Addition to the reference: the waiting row carries a spinner next to its
37
+ word. A modifier and not the base rule, so the empty row keeps wrapping the
38
+ way it always did.
39
+
40
+ Documented deviation from CLAUDE.md, "Bewegung": that rule puts a spinner in
41
+ a button and skeleton rows in a list. A listbox is not that kind of list.
42
+ Its children have to be options, and a skeleton row is a decorative
43
+ placeholder with no accessible name, so it would either be an option that
44
+ says nothing or a node that breaks role="listbox". One locked option with a
45
+ spinner and the word "Lädt" is the only shape that stays announceable, and
46
+ the row count is one, not the row count of an answer nobody knows yet. */
47
+ .z-listbox__loading { display: flex; align-items: center; gap: var(--space-2); }
48
+
49
+ /* Wizard */
50
+ .z-wizard { display: grid; gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
51
+ .z-wstep { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); min-width: 0; }
52
+ .z-wstep__head { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); }
53
+ .z-wstep__title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 600; }
54
+ .z-wstep__summary { margin-left: auto; color: var(--text-muted); text-align: right; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
55
+ .z-wstep__edit { flex: none; padding: 0; border: 0; background: none; cursor: pointer; color: var(--accent-text); font-weight: 500; }
56
+ .z-wstep__body { display: grid; gap: var(--space-5); padding: var(--space-2) var(--space-4) var(--space-5); border-top: 1px solid var(--border); }
57
+ .z-wstep__actions { display: flex; justify-content: space-between; gap: var(--space-2); }
58
+ .z-wstep--locked .z-wstep__head { color: var(--text-muted); }
59
+ .z-wstep--locked .z-wstep__title { font-weight: 400; }
60
+ .z-wstep[aria-current="step"] .z-step__num { border-color: var(--text); background: var(--text); color: var(--bg); }
61
+ .z-wstep--done .z-step__num { border-color: transparent; background: var(--surface-hover); color: var(--text); }
62
+ .z-config { display: grid; gap: var(--space-5); grid-template-columns: minmax(0, 1fr); align-items: start; }
63
+ @media (min-width: 900px) { .z-config { grid-template-columns: minmax(0, 1fr) 340px; } .z-config__aside { position: sticky; top: calc(var(--header) + var(--space-5)); } }
64
+
65
+ /* PriceSummary: Zustände */
66
+ .z-summary__price--pending { color: var(--text-muted); }
67
+ .z-summary__discount dd { color: var(--success); }
68
+ .z-summary__total { display: flex; justify-content: space-between; gap: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--border-control); font-weight: 600; }
69
+ .z-summary__total dd { margin: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
70
+
71
+ /* StickyBar */
72
+ .z-stickybar { position: sticky; bottom: 0; z-index: var(--z-header); display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--border); background: var(--surface-raised); }
73
+ .z-stickybar__price { display: grid; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 20px; line-height: 28px; font-weight: 600; }
74
+ .z-stickybar__price small { font-family: var(--font-body); font-size: 12px; line-height: 16px; font-weight: 400; color: var(--text-muted); }
75
+ @media (min-width: 900px) { .z-stickybar--mobile { display: none; } }
76
+
77
+ /* IncludedList */
78
+ .z-included { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; color: var(--text-muted); }
79
+ .z-included li { display: flex; align-items: flex-start; gap: var(--space-2); }
80
+ .z-included .z-icon { font-size: 16px; width: 16px; height: 16px; margin-top: 2px; color: var(--success); }
81
+
82
+ /* Disclosure */
83
+ .z-disclosure { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); }
84
+ .z-disclosure summary { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); cursor: pointer; list-style: none; font-weight: 600; }
85
+ .z-disclosure summary::-webkit-details-marker { display: none; }
86
+ .z-disclosure summary small { font-size: 12px; font-weight: 400; color: var(--text-muted); }
87
+ .z-disclosure summary::after { content: "+"; margin-left: auto; font-family: var(--font-mono); color: var(--text-muted); }
88
+ .z-disclosure[open] summary::after { content: "\2212"; }
89
+ .z-disclosure__body { display: grid; gap: var(--space-4); padding: var(--space-4); border-top: 1px solid var(--border); }
90
+
91
+ /* InputAction */
92
+ .z-input-action { display: flex; gap: var(--space-2); }
93
+ .z-input-action .z-input { flex: 1; min-width: 0; }
94
+ .z-field__success { font-size: 12px; line-height: 16px; color: var(--success); }
95
+
96
+ /* CostChart */
97
+ .z-chart { display: grid; gap: var(--space-3); min-width: 0; }
98
+ .z-chart__figures { display: flex; flex-wrap: wrap; gap: var(--space-6); }
99
+ .z-chart__figure { display: grid; }
100
+ .z-chart__figure span { font-size: 12px; line-height: 16px; color: var(--text-muted); }
101
+ .z-chart__figure strong { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 20px; line-height: 28px; font-weight: 600; }
102
+ .z-chart__plot { position: relative; }
103
+ .z-chart svg { display: block; width: 100%; height: auto; overflow: visible; }
104
+ .z-chart__grid { stroke: var(--border); stroke-width: 1; }
105
+ .z-chart__axis { fill: var(--text-subtle); font-family: var(--font-mono); font-size: 12px; }
106
+ .z-chart__line { fill: none; stroke: var(--text); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
107
+ .z-chart__cap { stroke: var(--border-control); stroke-width: 1; stroke-dasharray: 4 4; }
108
+ .z-chart__marker { fill: var(--text); stroke: var(--surface); stroke-width: 2; }
109
+ .z-chart__label { fill: var(--text-muted); font-family: var(--font-body); font-size: 12px; }
110
+ .z-chart__cross { stroke: var(--border-control); stroke-width: 1; }
111
+ .z-chart__tip { position: absolute; top: 0; left: 0; padding: var(--space-1) var(--space-2); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-raised); box-shadow: var(--shadow-overlay); font-size: 12px; line-height: 16px; white-space: nowrap; pointer-events: none; }
112
+ .z-chart__tip strong { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
113
+ .z-chart__caption { font-size: 12px; line-height: 16px; color: var(--text-muted); }
114
+
115
+ /* Angular hosts */
116
+ /* These elements wrap the markup of the reference and would be inline
117
+ otherwise. z-wizard-step is the <li> of the reference; z-wizard, z-config,
118
+ z-sticky-bar and z-cost-chart carry the reference class on the host and take
119
+ their display from it. */
120
+ z-option-group, z-combobox, z-wizard-step, z-included-list, z-disclosure, z-input-action { display: block; }
121
+
122
+ /* Addition to the reference: the action row of a step is a slot. A step
123
+ without actions would otherwise spend the space-5 gap of the body on an
124
+ empty div, and a step with only "Weiter" would put it on the left, where
125
+ space-between leaves a single child. */
126
+ .z-wstep__actions:empty { display: none; }
127
+ .z-wstep__actions > :only-child { margin-inline-start: auto; }
128
+ .z-wstep__actions > .z-btn--ghost:only-child { margin-inline: 0 auto; }
129
+
130
+ /* Addition to the reference: bundle.css gives .z-combo no width of its own. In
131
+ a z-field the wrapper is a grid item and stretches, next to a button in
132
+ .z-input-action it has to fill the row like .z-input does. */
133
+ .z-input-action .z-combo { flex: 1; min-width: 0; }
134
+
135
+ /* Addition to the reference: the listbox hangs in a CDK overlay, where the
136
+ pane has no width. The component measures the field and writes the width
137
+ onto the pane; the panel takes it over so it never reaches past the field. */
138
+ .z-combo-pane .z-listbox { width: 100%; box-sizing: border-box; }
139
+
140
+ /* Addition to the reference: a combobox that is locked reads like every other
141
+ locked control. bundle.css dims .z-input:disabled but not the chevron of
142
+ .z-combo::after, which would stay at full strength next to a 45 % field. */
143
+ .z-combo:has(.z-input:disabled)::after { opacity: 0.45; }
144
+
145
+ /* Addition to the reference: StickyBar/README.md asks for
146
+ env(safe-area-inset-bottom) in the padding, which bundle.css does not carry.
147
+ Only the bottom grows, and only where the device reports an inset. */
148
+ .z-stickybar { padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px)); }
149
+
150
+ /* Addition to the reference: CostChart/preview.html sizes the summary of the
151
+ "Als Tabelle" disclosure with an inline style (14px/20px, 12px above and
152
+ below). A library component carries no inline styles, so the same three
153
+ declarations stand here as a rule. 12px is space-3. */
154
+ .z-chart .z-faq > summary { padding: var(--space-3) 0; font-size: 14px; line-height: 20px; }
155
+
156
+ /* Addition to the reference: the plot takes the keyboard as well as the
157
+ pointer (CostChart/README.md), so it is a tab stop and needs room for the
158
+ 2px ring with its 2px offset. The svg already draws outside its box. */
159
+ .z-chart__plot:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--radius-sm); }
160
+
161
+ /* Addition to the reference: the summary column is 340px wide at every viewport
162
+ width, so an alert in it never has room for its button beside the text. It
163
+ wraps there the way bundle.css lets an alert wrap below 640px; the 32px
164
+ indent is the icon column, as in the reference rule. */
165
+ .z-summary .z-alert { flex-wrap: wrap; }
166
+ .z-summary .z-alert > .z-btn { flex-basis: 100%; margin-left: 32px; }
167
+
168
+ /* Addition to the reference: bundle.css leaves the hidden radio at its natural
169
+ 13px in the corner of the card. The browser scrolls THAT box into view and
170
+ draws the focus ring around the card, so a focused card can still sit behind
171
+ the sticky bar (WCAG 2.4.11). Stretched over the card the focused box is the
172
+ card. It stays invisible (opacity 0 from the reference) and keeps its place
173
+ in the layer order, so title, description and badge stay readable.
174
+ bundle.css also takes it out of the pointer flow entirely; it sits inside its
175
+ own <label>, so a click reaches it either way, while "pointer-events: none"
176
+ makes an enabled control read as locked to anything that measures it
177
+ (e2e/a11y-baum.spec.ts does). A locked card keeps the cursor it inherits, so
178
+ only the enabled ones get their pointer back. */
179
+ .z-option input { inset: 0; width: 100%; height: 100%; margin: 0; }
180
+ .z-option input:enabled { pointer-events: auto; }
181
+
182
+ /* Addition to the reference: the stretched radio above is positioned and the
183
+ texts are not, so the radio paints over them and the mouse selects nothing
184
+ inside an enabled card. Position the texts and they are back on top; a click
185
+ on them still reaches the card, because they sit inside its <label>. The
186
+ badge is left out on purpose: it is already positioned (absolute, above) and
187
+ "relative" here would take its place away. */
188
+ .z-option > span { position: relative; }
189
+
190
+ /* Addition to the reference: a badge sits space-3 above its card and would
191
+ cover the last pixels of the legend hint at narrow widths, where the first
192
+ row starts right under it. Only a group that has badges pays for the room. */
193
+ .z-options:has(.z-option__badge) .z-options__legend { margin-bottom: var(--space-3); }
194
+
195
+ /* Addition to the reference: a sticky bar that really lies at the bottom edge
196
+ of the viewport covers what is behind it, so a control focused there would be
197
+ invisible (WCAG 2.4.11). z-sticky-bar measures itself, marks itself with
198
+ data-stuck while it sticks AND is displayed, and writes the height of the
199
+ tallest stuck bar into --z-stickybar; the document adds that to its
200
+ scroll-padding. It needs no extra room at the end of the page: the bar is
201
+ sticky inside the flow and already occupies its own height there. A bar that
202
+ stands in the page instead (a framed demo of the gallery) and a --mobile bar
203
+ from 900px on cover nothing and are not marked, so the page keeps its
204
+ padding at 0. */
205
+ .z-root:has(.z-stickybar[data-stuck]) {
206
+ scroll-padding-bottom: calc(var(--z-stickybar, 0px) + var(--space-4));
207
+ }
208
+
209
+ /* Addition to the reference: click targets at least 40px on mobile (CLAUDE.md,
210
+ Abstand und Layout). A listbox entry measures 36px, a compact option 36px,
211
+ and "Ändern" is a bare text button of one line height. */
212
+ @media (max-width: 640px) {
213
+ .z-listbox__option { min-height: var(--control-md); }
214
+ .z-options--compact .z-option { min-height: var(--control-md); align-content: center; }
215
+ .z-wstep__edit { min-height: var(--control-md); }
216
+ }