fertig 4.0.3 → 4.0.7

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.
package/fertig.css CHANGED
@@ -1,16 +1,16 @@
1
- /*! fertig v4.0.3 — a classless CSS stylesheet. MIT */
1
+ /*! fertig v4.0.7 — a classless CSS stylesheet. MIT */
2
2
 
3
3
  /* Opinions, so you don't have to have them:
4
4
  system type · one accent · responsive gutters · a clear content canvas.
5
- The finish is platform-neutral: opaque surfaces, hairlines, restrained
6
- corners and a visible accent focus ring.
5
+ The finish is platform-neutral: soft surfaces, rounded controls, restrained
6
+ depth and a visible accent focus ring.
7
7
  Override the tokens marked "yours"; the rest is the point of view. */
8
8
 
9
9
  /* typed so they interpolate; untyped custom properties cannot animate */
10
10
  /* Registrations are global: they cannot be layered, scoped or overridden.
11
11
  Hence the prefix — a bare --a1 would silently retype a property of yours. */
12
- @property --fertig-a1 { syntax: "<number>"; inherits: true; initial-value: .06 }
13
- @property --fertig-a2 { syntax: "<number>"; inherits: true; initial-value: .05 }
12
+ @property --fertig-a1 { syntax: "<number>"; inherits: true; initial-value: .08 }
13
+ @property --fertig-a2 { syntax: "<number>"; inherits: true; initial-value: .10 }
14
14
 
15
15
  /* Two layers, in this order. Your own unlayered CSS wins over both regardless
16
16
  of specificity, so overriding never needs !important — and neither does this
@@ -33,7 +33,7 @@
33
33
  accent-color: var(--fertig-ac);
34
34
  caret-color: var(--fertig-ac);
35
35
 
36
- --fertig-f: ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue",
36
+ --fertig-f: Inter, ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue",
37
37
  Arial, sans-serif; /* yours */
38
38
  --fertig-fm: ui-monospace, "Cascadia Code", "Roboto Mono", Consolas,
39
39
  "Liberation Mono", monospace; /* code */
@@ -42,9 +42,9 @@
42
42
  /* One radius, and the rest derived from it: set --fertig-r and the whole
43
43
  sheet rescales together. Four independent numbers meant --fertig-r: 4px
44
44
  left tight fields sitting inside cards that were still round. */
45
- --fertig-r: 8px; /* control radius: fields, buttons, menu rows */
46
- --fertig-rs: calc(var(--fertig-r) * .5); /* small: inline chips and code */
47
- --fertig-rw: calc(var(--fertig-r) * 1.33); /* large: cards, dialogs, menus */
45
+ --fertig-r: 10px; /* control radius: fields, buttons, menu rows */
46
+ --fertig-rs: calc(var(--fertig-r) * .6); /* small: inline chips and code */
47
+ --fertig-rw: calc(var(--fertig-r) * 1.6); /* large: cards, dialogs, menus */
48
48
  --fertig-rp: calc(var(--fertig-r) * 1000); /* pill: badges, switches, meters, joined ends */
49
49
  /* --fertig-rp multiplies rather than naming infinity: any non-zero radius
50
50
  still clamps to a true pill, while --fertig-r: 0 squares the whole shape
@@ -83,9 +83,9 @@
83
83
  --fertig-violet-900: oklch(28% .09 290);
84
84
 
85
85
  /* ---- semantic ----------------------------------------------- */
86
- --fertig-bg: light-dark(oklch(93.5% .008 260), oklch(15.5% .014 260)); /* page ground */
87
- --fertig-el: light-dark(oklch(99% .004 260), oklch(21% .016 260)); /* content surface */
88
- --fertig-face:light-dark(oklch(97.2% .006 260), oklch(25% .018 260)); /* controls */
86
+ --fertig-bg: light-dark(oklch(97% .004 260), oklch(13.5% .014 260)); /* page ground */
87
+ --fertig-el: light-dark(oklch(99.7% .002 260), oklch(19% .016 260)); /* raised surface */
88
+ --fertig-face:light-dark(oklch(94.5% .006 260), oklch(24% .018 260)); /* quiet controls */
89
89
  --fertig-fg: light-dark(oklch(22% .02 260), oklch(95% .012 260));
90
90
  /* lifted from 50%/70%: the weakest pairing (muted on the page ground in
91
91
  light, muted on controls in dark) was 4.96:1 and 5.99:1, a hair over AA.
@@ -93,27 +93,27 @@
93
93
  from --fertig-fg — AAA would need ~41%, which is close enough to
94
94
  --fertig-fg that "muted" stops reading as secondary at all. */
95
95
  --fertig-mut: light-dark(oklch(46% .018 260), oklch(72% .02 260));
96
- --fertig-bd: light-dark(oklch(22% .02 260 / .13), oklch(95% .012 260 / .13));
97
- --fertig-tb: light-dark(oklch(97.2% .006 260), oklch(25% .018 260));
96
+ --fertig-bd: light-dark(oklch(22% .02 260 / .10), oklch(95% .012 260 / .12));
97
+ --fertig-tb: light-dark(oklch(99% .003 260), oklch(18% .016 260));
98
98
  /* Blue is the neutral default: distinctive enough for interaction, familiar
99
99
  enough not to impose a brand. The 700/300 pair clears AA as link text and
100
100
  as a filled control in its respective theme. */
101
101
  --fertig-ac: light-dark(var(--fertig-blue-700), var(--fertig-blue-300)); /* yours */
102
102
  --fertig-on-ac: light-dark(#f7f8fb, #07071c); /* off-white / ink text on the accent */
103
103
 
104
- /* depth: hairline + compact shadow.
104
+ /* depth: a soft surface shadow and a broader overlay shadow.
105
105
  --fertig-sh is an ordinary colour — override it with any notation you like, and
106
106
  every shadow in the sheet retints. Lighter than a pure black so the shadows
107
107
  read as diffuse atmosphere rather than dark bars. The alphas are taken with
108
108
  color-mix against transparent rather than relative colour: same result, and
109
109
  it is three years older, which keeps Safari 17.5 inside the floor. */
110
- --fertig-sh: oklch(30% .012 270);
111
- --fertig-a1: .06; --fertig-a2: .05;
110
+ --fertig-sh: oklch(24% .014 270);
111
+ --fertig-a1: .08; --fertig-a2: .10;
112
112
  --fertig-sh1: color-mix(in srgb, var(--fertig-sh) calc(var(--fertig-a1) * 100%), transparent);
113
113
  --fertig-sh2: color-mix(in srgb, var(--fertig-sh) calc(var(--fertig-a2) * 100%), transparent);
114
- --fertig-up: 0 1px 1px var(--fertig-sh1);
115
- --fertig-up2: 0 1px 1px var(--fertig-sh1), 0 8px 22px -8px var(--fertig-sh2);
116
- --fertig-dn: inset 0 1px 2px color-mix(in srgb, var(--fertig-sh) 7%, transparent);
114
+ --fertig-up: 0 1px 2px var(--fertig-sh1), 0 0 0 1px var(--fertig-bd);
115
+ --fertig-up2: 0 2px 4px var(--fertig-sh1), 0 12px 32px -12px var(--fertig-sh2);
116
+ --fertig-dn: var(--fertig-up);
117
117
  --fertig-caps: .74em/1.4 var(--fertig-f);
118
118
 
119
119
  /* motion. One decelerating curve for things arriving, one accelerating for
@@ -125,7 +125,7 @@
125
125
  --fertig-dur-2: .2s; /* something appearing or leaving */
126
126
  }
127
127
  @media (prefers-color-scheme: dark) {
128
- :root:not([data-theme=light]) { --fertig-a1: .42; --fertig-a2: .38 }
128
+ :root:not([data-theme=light]) { --fertig-a1: .34; --fertig-a2: .30 }
129
129
  }
130
130
  /* --fertig-on-ac has to survive any --fertig-ac override, or a custom accent ships an
131
131
  unreadable label. contrast-color() does that properly but has almost no
@@ -139,8 +139,8 @@
139
139
  @supports (color: contrast-color(red)) {
140
140
  :root { --fertig-on-ac: color-mix(in oklab, contrast-color(var(--fertig-ac)) 96%, var(--fertig-ac)) }
141
141
  }
142
- [data-theme=light] { color-scheme: light; --fertig-a1: .06; --fertig-a2: .05 }
143
- [data-theme=dark] { color-scheme: dark; --fertig-a1: .42; --fertig-a2: .38 }
142
+ [data-theme=light] { color-scheme: light; --fertig-a1: .08; --fertig-a2: .10 }
143
+ [data-theme=dark] { color-scheme: dark; --fertig-a1: .34; --fertig-a2: .30 }
144
144
 
145
145
  /* Named accents. The ramps already exist, so this is a mapping table: put
146
146
  data-accent on any element and everything inside it — links, focus rings,
@@ -173,14 +173,14 @@ body {
173
173
  element's own corners, and setting one would reshape whatever takes focus —
174
174
  a focused dialog would snap from --fertig-rw to --fertig-r for as long as it is focused. */
175
175
  :focus-visible {
176
- outline: 3px solid color-mix(in srgb, var(--fertig-ac) 45%, transparent);
177
- outline-offset: 1px;
176
+ outline: 2px solid color-mix(in srgb, var(--fertig-ac) 52%, transparent);
177
+ outline-offset: 2px;
178
178
  }
179
179
  img, video, iframe { display: block; max-width: 100%; height: auto }
180
180
  /* svg is left inline: it is as often an icon inside a sentence or a button as
181
181
  it is a standalone figure, and display:block drops the icon onto its own row */
182
182
  svg { max-width: 100%; vertical-align: middle }
183
- img, video { border-radius: var(--fertig-r); box-shadow: var(--fertig-up2) }
183
+ img, video { border-radius: var(--fertig-rw); box-shadow: var(--fertig-up2) }
184
184
 
185
185
  /* ---- layout: full-width page containers ---------------------- */
186
186
  /* The shell is body's own children — or the children of the single wrapper a
@@ -195,7 +195,7 @@ body > :where(#root, #app, #__next, [data-fertig]) > * {
195
195
  /* the wrapper is not itself a shell element; it just gets out of the way */
196
196
  body > :where(#root, #app, #__next, [data-fertig]) { padding-inline: 0 }
197
197
  :is(body, body > :where(#root, #app, #__next, [data-fertig])) > :is(header, main, footer) {
198
- background: var(--fertig-el); border-inline: 1px solid var(--fertig-bd);
198
+ background: var(--fertig-bg);
199
199
  }
200
200
  :is(body, body > :where(#root, #app, #__next, [data-fertig])) > header { padding-block: 2.4rem .8rem }
201
201
  :is(body, body > :where(#root, #app, #__next, [data-fertig])) > main { padding-bottom: 2.8rem }
@@ -216,6 +216,7 @@ body > :where(#root, #app, #__next, [data-fertig]) { padding-inline: 0 }
216
216
  padding-block: calc(.6rem + env(safe-area-inset-top, 0px)) .6rem;
217
217
  background: var(--fertig-tb); color: var(--fertig-fg);
218
218
  border-bottom: 1px solid var(--fertig-bd); font-size: .92em;
219
+ box-shadow: 0 1px 10px color-mix(in srgb, var(--fertig-sh) 4%, transparent);
219
220
  }
220
221
  :is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav a { color: var(--fertig-fg); text-decoration: none; opacity: .75 }
221
222
  :is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav a:hover { opacity: 1 }
@@ -328,16 +329,21 @@ pre code { background: none; border: 0; padding: 0; font-size: 1em }
328
329
  :is(.hljs-addition, .token.inserted) { color: light-dark(#15803d, #7ee787) }
329
330
 
330
331
  /* ---- table -------------------------------------------------- */
331
- table { width: 100%; border-collapse: collapse; font-size: .92em }
332
+ table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: .92em }
332
333
  th, td {
333
334
  border-bottom: 1px solid var(--fertig-bd); padding-block: .55rem;
334
335
  padding-inline: 0 .7rem; text-align: start;
335
336
  }
336
337
  th:last-child, td:last-child { padding-inline-end: 0 }
337
338
  thead th {
338
- border-bottom: 1px solid color-mix(in srgb, var(--fertig-fg) 45%, transparent);
339
+ border-bottom: 0; background: var(--fertig-face);
339
340
  font: 700 var(--fertig-caps); text-transform: uppercase; letter-spacing: .09em; color: var(--fertig-mut);
340
- padding-bottom: .4rem;
341
+ padding: .5rem .7rem;
342
+ }
343
+ thead th:first-child { border-start-start-radius: var(--fertig-r); border-end-start-radius: var(--fertig-r) }
344
+ thead th:last-child {
345
+ padding-inline-end: .7rem;
346
+ border-start-end-radius: var(--fertig-r); border-end-end-radius: var(--fertig-r);
341
347
  }
342
348
  tbody tr:last-child td { border-bottom: 0 }
343
349
  /* a totals row is not just another row */
@@ -366,7 +372,7 @@ textarea { width: 100%; min-height: 5rem; resize: vertical }
366
372
  input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=image],[type=submit],[type=button],[type=reset]),
367
373
  select, textarea {
368
374
  display: block; width: 100%; padding: .52em .7em; background: var(--fertig-el);
369
- border: 1px solid var(--fertig-bd); border-radius: var(--fertig-r); box-shadow: var(--fertig-dn);
375
+ border: 1px solid transparent; border-radius: var(--fertig-r); box-shadow: var(--fertig-dn);
370
376
  }
371
377
  select {
372
378
  appearance: none; padding-inline-end: 2.2rem;
@@ -423,8 +429,54 @@ select[multiple] { background-image: none; padding-inline-end: .6em }
423
429
  @starting-style { select:open::picker(select) { opacity: 0; translate: 0 -4px } }
424
430
  }
425
431
  }
426
- :is(input,select,textarea):hover:not(:disabled) { border-color: color-mix(in srgb, var(--fertig-fg) 25%, transparent) }
427
- :is(input,select,textarea):focus-visible { border-color: var(--fertig-ac) }
432
+ :is(input,select,textarea):hover:not(:disabled) { background-color: color-mix(in srgb, var(--fertig-face) 55%, var(--fertig-el)) }
433
+ :is(input,select,textarea):focus-visible { border-color: color-mix(in srgb, var(--fertig-ac) 55%, transparent) }
434
+ :is(input,select,textarea)[data-variant=secondary] {
435
+ background: var(--fertig-face); border-color: transparent; box-shadow: none;
436
+ }
437
+
438
+ /* A compound field keeps its label, control, help and error text together. */
439
+ [data-field] { display: grid; gap: .35rem; align-content: start }
440
+ [data-field] + [data-field] { margin-top: .9rem }
441
+ [data-field] > * { margin-block: 0 }
442
+ [data-field] > :is(small, [role=alert]) { font-size: .82em }
443
+ [data-field] > [role=alert] { color: light-dark(var(--fertig-red-700), var(--fertig-red-300)) }
444
+ [data-field]:has([aria-invalid=true]) > label { color: light-dark(var(--fertig-red-700), var(--fertig-red-300)) }
445
+
446
+ /* Input group: one focus surface around an input, addons and compact actions. */
447
+ [data-input-group] {
448
+ display: flex; align-items: center; inline-size: 100%; overflow: clip;
449
+ background: var(--fertig-el); border: 1px solid transparent;
450
+ border-radius: var(--fertig-r); box-shadow: var(--fertig-dn);
451
+ }
452
+ [data-input-group] > :is(input, select, textarea) {
453
+ flex: 1 1 auto; min-inline-size: 0; border: 0; border-radius: 0;
454
+ background-color: transparent; box-shadow: none; outline: 0;
455
+ }
456
+ [data-input-group] > :is(input, select, textarea):hover:not(:disabled) {
457
+ background-color: transparent;
458
+ }
459
+ [data-input-group] > :where(span, small, output) {
460
+ flex: none; padding-inline: .75em; color: var(--fertig-mut); white-space: nowrap;
461
+ }
462
+ [data-input-group] > button {
463
+ align-self: stretch; min-width: 0; border: 0; border-radius: 0;
464
+ background: transparent; box-shadow: none;
465
+ }
466
+ [data-input-group]:focus-within {
467
+ border-color: color-mix(in srgb, var(--fertig-ac) 55%, transparent);
468
+ box-shadow: var(--fertig-up), 0 0 0 3px color-mix(in srgb, var(--fertig-ac) 16%, transparent);
469
+ }
470
+ [data-input-group]:has([aria-invalid=true]) {
471
+ border-color: light-dark(var(--fertig-red-700), var(--fertig-red-300));
472
+ }
473
+ [data-input-group]:has(textarea) { align-items: end }
474
+
475
+ /* One native field keeps OTP autofill, paste and screen-reader behavior. */
476
+ input[data-otp] {
477
+ inline-size: min(100%, 16ch); font: 650 1.25rem/1.4 var(--fertig-fm);
478
+ letter-spacing: .55em; text-indent: .55em; text-align: center;
479
+ }
428
480
  input[type=checkbox], input[type=radio] {
429
481
  width: 1.15em; height: 1.15em; margin: 0; margin-inline-end: .45em; vertical-align: -.18em;
430
482
  }
@@ -435,7 +487,7 @@ input[type=checkbox], input[type=radio] {
435
487
  }
436
488
  :is(input, textarea, [contenteditable]) { caret-color: var(--fertig-ac) }
437
489
  label:has(input[type=checkbox]), label:has(input[type=radio]) { line-height: 1.9 }
438
- input[type=range] { width: 100%; height: 1.15em }
490
+ input[type=range] { width: 100%; height: 1.15em; margin-inline: 0 }
439
491
  ::placeholder { color: var(--fertig-mut) }
440
492
  @supports selector(::spelling-error) {
441
493
  ::spelling-error { text-decoration: wavy underline light-dark(#c0392b, #ff6b6b) }
@@ -456,8 +508,8 @@ input:autofill, input:autofill:hover, input:autofill:focus {
456
508
  nostalgia — a legend keeps its magic placement under block, grid and flex
457
509
  fieldsets alike, and floating it is still the only way out. */
458
510
  fieldset {
459
- border: 1px solid var(--fertig-bd); border-radius: var(--fertig-rw); padding: .9rem 1rem 1rem;
460
- background: var(--fertig-face);
511
+ border: 1px solid transparent; border-radius: var(--fertig-rw); padding: 1rem;
512
+ background: var(--fertig-el); box-shadow: var(--fertig-up);
461
513
  }
462
514
  fieldset:focus-within { border-color: color-mix(in srgb, var(--fertig-ac) 45%, var(--fertig-bd)) }
463
515
  legend {
@@ -470,17 +522,19 @@ legend + * { clear: left }
470
522
  button, [type=submit], [type=button], [type=reset],
471
523
  :where(a.primary, a[role=button]), ::file-selector-button {
472
524
  display: inline-block; text-decoration: none; text-align: center;
473
- padding: .48em 1.1em; background: var(--fertig-el); color: var(--fertig-fg); cursor: pointer;
474
- border: 1px solid var(--fertig-bd); border-radius: var(--fertig-r);
475
- font: inherit; min-width: 5rem;
525
+ padding: .5em 1.1em; background: var(--fertig-face); color: var(--fertig-fg); cursor: pointer;
526
+ border: 1px solid transparent; border-radius: var(--fertig-r); box-shadow: none;
527
+ font: inherit; font-weight: 550; min-width: 5rem;
528
+ }
529
+ :is(button,[type=submit],[type=button],[type=reset],a.primary,a[role=button]):hover:not(:disabled) {
530
+ background: color-mix(in srgb, var(--fertig-fg) 7%, var(--fertig-face));
476
531
  }
477
- :is(button,[type=submit],[type=button],[type=reset],a.primary,a[role=button]):hover:not(:disabled) { background: var(--fertig-face) }
478
532
  [type=submit], .primary { background: var(--fertig-ac); border-color: transparent; color: var(--fertig-on-ac) }
479
533
  /* Filled controls must not keep their enabled accent after :disabled. This
480
534
  follows the filled rule deliberately: equal-specificity declarations later
481
535
  in the cascade restore the quiet control surface and muted text. */
482
536
  :is(button,[type=submit],[type=button],[type=reset]):disabled {
483
- background: var(--fertig-face); border-color: var(--fertig-bd); color: var(--fertig-mut);
537
+ background: var(--fertig-face); border-color: transparent; color: var(--fertig-mut);
484
538
  }
485
539
  /* The filled button lightens on hover. color-mix does that in oklab, which is
486
540
  close enough and is Baseline; relative colour does it by adding to L
@@ -505,6 +559,16 @@ button[data-icon] { min-width: 0; padding: .38em .62em; line-height: 1 }
505
559
  :is(button, a, input, select, label)[data-block] { display: block; width: 100%; min-width: 0 }
506
560
  :is(button, a, input)[data-variant=ghost] { background: none; border-color: transparent; box-shadow: none }
507
561
  :is(button, a, input)[data-variant=ghost]:hover:not(:disabled) { background: var(--fertig-face); border-color: var(--fertig-bd) }
562
+ :is(button, a, input)[data-variant=soft] {
563
+ background: color-mix(in srgb, var(--fertig-ac) 13%, var(--fertig-el));
564
+ border-color: transparent; box-shadow: none; color: var(--fertig-ac);
565
+ }
566
+ :is(button, a, input)[data-variant=soft]:hover:not(:disabled) {
567
+ background: color-mix(in srgb, var(--fertig-ac) 20%, var(--fertig-el));
568
+ }
569
+ :is(button, a, input)[data-variant=outline] {
570
+ background: transparent; border-color: var(--fertig-bd); box-shadow: none;
571
+ }
508
572
  :is(button, a, input)[data-variant=link] {
509
573
  background: none; border: 0; box-shadow: none; min-width: 0; padding-inline: .2em;
510
574
  color: var(--fertig-ac); text-decoration: underline; text-underline-offset: 3px;
@@ -517,22 +581,22 @@ button[aria-pressed=true]:hover:not(:disabled) {
517
581
 
518
582
  /* ---- disclosure / misc -------------------------------------- */
519
583
  details {
520
- border: 1px solid var(--fertig-bd); border-radius: var(--fertig-rw);
521
- padding: .7rem 1.1rem; background: var(--fertig-el);
584
+ border: 1px solid transparent; border-radius: var(--fertig-rw);
585
+ padding: .8rem 1.1rem; background: var(--fertig-el); box-shadow: var(--fertig-up);
522
586
  }
523
587
  summary { cursor: pointer; font-weight: 700; list-style: none }
524
588
  summary::before { content: "▸ "; color: var(--fertig-mut) }
525
589
  details[open] > summary::before { content: "▾ " }
526
590
  details[open] > summary { margin-bottom: .6rem }
527
591
  dialog {
528
- background: var(--fertig-el); color: var(--fertig-fg); border: 1px solid var(--fertig-bd);
529
- border-radius: var(--fertig-rw); padding: 1.6rem; max-width: min(30rem, 92dvw);
530
- box-shadow: 0 1px 1px color-mix(in srgb, var(--fertig-sh) 8%, transparent),
531
- 0 28px 60px -22px color-mix(in srgb, var(--fertig-sh) 28%, transparent);
592
+ --fertig-dialog-inset: 1.25rem;
593
+ background: var(--fertig-el); color: var(--fertig-fg); border: 1px solid transparent;
594
+ border-radius: var(--fertig-rw); padding: var(--fertig-dialog-inset); max-width: min(30rem, 92dvw);
595
+ box-shadow: var(--fertig-up2), 0 28px 70px -24px color-mix(in srgb, var(--fertig-sh) 34%, transparent);
532
596
  }
533
597
  dialog > :last-child:not(footer) { margin-bottom: 0 }
534
598
  dialog, [popover] { overscroll-behavior: contain }
535
- dialog::backdrop { background: color-mix(in srgb, var(--fertig-sh) 32%, transparent) }
599
+ dialog::backdrop { background: color-mix(in srgb, var(--fertig-sh) 42%, transparent) }
536
600
 
537
601
  /* dialog anatomy: an optional header and footer, pinned while the body scrolls.
538
602
  Sticky rather than a grid, because a classless sheet has no wrapper around
@@ -543,12 +607,13 @@ dialog::backdrop { background: color-mix(in srgb, var(--fertig-sh) 32%, transpar
543
607
  dialog:has(> :is(header, footer)) { max-block-size: min(44rem, 85dvh); overflow: auto }
544
608
  dialog > :is(header, footer) {
545
609
  position: sticky; z-index: 1;
546
- margin-inline: -1.6rem;
610
+ margin-inline: calc(0rem - var(--fertig-dialog-inset));
547
611
  background: var(--fertig-el); /* the dialog's own surface, so it reads as one sheet */
548
612
  }
549
613
  dialog > header {
550
- inset-block-start: -1.6rem; margin-block: -1.6rem .35rem;
551
- padding: 1.6rem 1.6rem .5rem; font-weight: 700;
614
+ inset-block-start: calc(0rem - var(--fertig-dialog-inset));
615
+ margin-block: calc(0rem - var(--fertig-dialog-inset)) .35rem;
616
+ padding: var(--fertig-dialog-inset) var(--fertig-dialog-inset) .5rem; font-weight: 700;
552
617
  border-radius: var(--fertig-rw) var(--fertig-rw) 0 0;
553
618
  }
554
619
  /* The title carries the header's weight, so it needs no margins of its own,
@@ -562,8 +627,10 @@ dialog:is([open], :modal) > header > :is(h1, h2, h3, h4, p) {
562
627
  line-height: 1.3; letter-spacing: -.01em;
563
628
  }
564
629
  dialog > footer {
565
- inset-block-end: -1.6rem; margin-block: 1.35rem -1.6rem;
566
- padding: .5rem 1.6rem 1.6rem; border-radius: 0 0 var(--fertig-rw) var(--fertig-rw);
630
+ inset-block-end: calc(0rem - var(--fertig-dialog-inset));
631
+ margin-block: 1.35rem calc(0rem - var(--fertig-dialog-inset));
632
+ padding: .5rem var(--fertig-dialog-inset) var(--fertig-dialog-inset);
633
+ border-radius: 0 0 var(--fertig-rw) var(--fertig-rw);
567
634
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
568
635
  justify-content: flex-end; /* actions sit at the end edge */
569
636
  }
@@ -608,9 +675,9 @@ dialog > footer > :is(p, span):first-child { margin: 0; margin-inline-end: auto;
608
675
  search { display: block; margin-block: 1.1rem } /* a landmark needs its own rhythm */
609
676
  ::target-text { background: color-mix(in srgb, var(--fertig-ac) 28%, transparent) }
610
677
  aside {
611
- border: 1px solid var(--fertig-bd); border-inline-start: 3px solid var(--fertig-ac);
612
- border-radius: var(--fertig-rw); border-start-start-radius: 0; border-end-start-radius: 0;
613
- background: var(--fertig-el); padding: .7rem 1.1rem;
678
+ border: 1px solid color-mix(in srgb, var(--fertig-ac) 18%, transparent);
679
+ border-radius: var(--fertig-rw);
680
+ background: color-mix(in srgb, var(--fertig-ac) 7%, var(--fertig-el)); padding: .8rem 1.1rem;
614
681
  margin-block: 1.1rem; /* or the next block sits flush against it */
615
682
  }
616
683
  progress { width: 100%; height: .5rem; border: 0; border-radius: var(--fertig-rp); background: var(--fertig-face) }
@@ -642,53 +709,56 @@ progress::-moz-progress-bar { background: var(--fertig-ac); border-radius: var(-
642
709
  The block axis is clipped rather than left to compute to auto alongside it:
643
710
  Safari paints a vertical scrollbar for a sub-pixel of overflow, and the row
644
711
  has no vertical overflow worth scrolling to in the first place. */
645
- [role=group] { display: inline-flex; vertical-align: middle; max-inline-size: 100%;
646
- overflow: auto clip; overscroll-behavior-x: contain }
647
- [role=group] > * { border-radius: 0; margin-inline-start: -1px; box-shadow: none; min-width: 0 }
712
+ [role=group] {
713
+ display: inline-flex; vertical-align: middle; max-inline-size: 100%; padding: .2rem;
714
+ overflow: auto clip; overscroll-behavior-x: contain;
715
+ border-radius: var(--fertig-rp); background: var(--fertig-face);
716
+ }
717
+ [role=group] > * { border: 0; border-radius: var(--fertig-rp); margin: 0; box-shadow: none; min-width: 0 }
648
718
  /* The buttons fill the group exactly and the group clips on the block axis,
649
719
  so an outset ring would be sliced off. Inside the edge, it is all there. */
650
720
  [role=group] > :focus-visible { outline-offset: -3px }
651
- [role=group] > :first-child {
652
- margin-inline-start: 0;
653
- border-start-start-radius: var(--fertig-rp); border-end-start-radius: var(--fertig-rp);
654
- }
655
- [role=group] > :last-child {
656
- border-start-end-radius: var(--fertig-rp); border-end-end-radius: var(--fertig-rp);
721
+ [role=group] > [aria-pressed=true] {
722
+ background: var(--fertig-el); color: var(--fertig-fg); font-weight: 650; box-shadow: var(--fertig-up);
657
723
  }
658
- [role=group] > [aria-pressed=true] { background: var(--fertig-face); color: var(--fertig-ac); font-weight: 700 }
659
724
  /* The press nudge would push a button half a pixel past the scroll port and
660
725
  flash a scrollbar, so joined buttons stay put and read the press by shadow. */
661
726
  [role=group] > :active:not(:disabled) { translate: none }
662
727
 
663
728
  /* tabs: <div role=tablist> of <button role=tab aria-selected> */
664
- [role=tablist] { display: flex; gap: .1rem; border-bottom: 1px solid var(--fertig-bd); margin: 1.1rem 0 }
729
+ [role=tablist] {
730
+ display: flex; gap: .15rem; max-inline-size: 100%; overflow-x: auto;
731
+ margin: 1.1rem 0; padding: .25rem; border: 0; border-radius: var(--fertig-rp);
732
+ background: var(--fertig-face); scrollbar-width: none;
733
+ }
734
+ [role=tablist]::-webkit-scrollbar { display: none }
665
735
  [role=tab] {
666
- background: none; border: 0; box-shadow: none; min-width: 0; color: var(--fertig-mut);
667
- padding: .45em .9em; border-radius: var(--fertig-r) var(--fertig-r) 0 0; margin-bottom: -1px;
736
+ flex: none; background: none; border: 0; box-shadow: none; min-width: 0; color: var(--fertig-mut);
737
+ padding: .4em .9em; border-radius: var(--fertig-rp); margin: 0;
668
738
  }
669
739
  [role=tab]:hover:not(:disabled) { background: var(--fertig-face); color: var(--fertig-fg) }
670
- [role=tab][aria-selected=true] { color: var(--fertig-fg); box-shadow: inset 0 -2px 0 var(--fertig-ac) }
740
+ [role=tab][aria-selected=true] { background: var(--fertig-el); color: var(--fertig-fg); box-shadow: var(--fertig-up) }
671
741
 
672
742
  /* switch: <input type=checkbox role=switch> */
673
743
  input[type=checkbox][switch],
674
744
  input[type=checkbox][role=switch] {
675
- appearance: none; width: 2.2em; height: 1.3em; border-radius: var(--fertig-rp); border: 0;
745
+ appearance: none; width: 2.5em; height: 1.45em; border-radius: var(--fertig-rp); border: 0;
676
746
  background: color-mix(in srgb, var(--fertig-fg) 20%, transparent);
677
747
  position: relative; cursor: pointer; vertical-align: -.3em; margin-inline-end: .5em;
678
748
  }
679
749
  input[switch]::after, input[role=switch]::after {
680
750
  content: ""; position: absolute; inset-block-start: .14em; inset-inline-start: .14em;
681
- width: 1.02em; height: 1.02em; border-radius: 50%; background: #fff; box-shadow: var(--fertig-up);
751
+ width: 1.17em; height: 1.17em; border-radius: 50%; background: #fff; box-shadow: var(--fertig-up);
682
752
  transition: translate .16s var(--fertig-ease-out);
683
753
  }
684
754
  input[switch]:checked, input[role=switch]:checked { background: var(--fertig-ac) }
685
755
  /* translate is physical, so the knob is told which way to travel */
686
- input[switch]:checked::after, input[role=switch]:checked::after { translate: .9em 0 }
687
- :is(input[switch], input[role=switch]):checked:dir(rtl)::after { translate: -.9em 0 }
756
+ input[switch]:checked::after, input[role=switch]:checked::after { translate: 1.05em 0 }
757
+ :is(input[switch], input[role=switch]):checked:dir(rtl)::after { translate: -1.05em 0 }
688
758
 
689
759
  /* menu: any [popover] */
690
760
  [popover] {
691
- background: var(--fertig-el); color: var(--fertig-fg); border: 1px solid var(--fertig-bd);
761
+ background: var(--fertig-el); color: var(--fertig-fg); border: 1px solid transparent;
692
762
  border-radius: var(--fertig-rw); padding: .35rem; min-width: 12rem;
693
763
  max-block-size: 60dvh; overflow: auto; font-size: .95em;
694
764
  box-shadow: var(--fertig-up2), 0 24px 44px -18px color-mix(in srgb, var(--fertig-sh) 28%, transparent);
@@ -723,6 +793,22 @@ input[switch]:checked::after, input[role=switch]:checked::after { translate: .9e
723
793
  [popover] :is(a, button):hover:not(:disabled) { background: var(--fertig-ac); color: var(--fertig-on-ac) }
724
794
  [popover] hr { margin: .3rem 0 }
725
795
 
796
+ /* listbox: a selectable collection, whether standalone or inside a popover */
797
+ [role=listbox] {
798
+ list-style: none; margin: 1rem 0; padding: .35rem;
799
+ background: var(--fertig-el); border: 1px solid transparent;
800
+ border-radius: var(--fertig-rw); box-shadow: var(--fertig-up);
801
+ }
802
+ [role=option] {
803
+ display: flex; align-items: center; gap: .7rem; padding: .5rem .7rem;
804
+ border-radius: var(--fertig-r); cursor: default;
805
+ }
806
+ [role=option]:is(:hover, :focus-visible) { background: var(--fertig-face) }
807
+ [role=option][aria-selected=true] {
808
+ background: color-mix(in srgb, var(--fertig-ac) 13%, var(--fertig-el)); color: var(--fertig-ac);
809
+ }
810
+ [popover] [role=listbox] { margin: 0; padding: 0; border: 0; box-shadow: none }
811
+
726
812
  /* breadcrumbs: <nav aria-label="Breadcrumb"><ol> */
727
813
  nav[aria-label] ol { display: flex; flex-wrap: wrap; gap: .5em; list-style: none; padding: 0; font-size: .9em }
728
814
  nav[aria-label] ol li + li::before { content: "/"; margin-inline-end: .5em; color: var(--fertig-mut) }
@@ -787,6 +873,37 @@ nav[aria-label] ol li + li::before { content: "/"; margin-inline-end: .5em; colo
787
873
  @keyframes fertig-shimmer { to { background-position: -200% 0 } }
788
874
  }
789
875
 
876
+ /* surface: the same elevation language as a card, without card anatomy */
877
+ [data-surface] {
878
+ background: var(--fertig-el); border: 1px solid transparent;
879
+ border-radius: var(--fertig-rw); padding: 1rem; box-shadow: var(--fertig-up);
880
+ }
881
+ [data-surface=flat] { background: var(--fertig-face); box-shadow: none }
882
+ [data-surface=outline] { background: transparent; border-color: var(--fertig-bd); box-shadow: none }
883
+ [data-surface] > :first-child { margin-top: 0 }
884
+ [data-surface] > :last-child { margin-bottom: 0 }
885
+
886
+ /* item: avatar or icon, flexible content, and an optional trailing action */
887
+ [data-item] {
888
+ display: flex; align-items: center; gap: .8rem; min-inline-size: 0;
889
+ padding: .7rem .8rem; border-radius: var(--fertig-r); background: var(--fertig-el);
890
+ }
891
+ [data-item] + [data-item] { margin-top: .35rem }
892
+ [data-item] > :where(div, p) { flex: 1; min-inline-size: 0; margin: 0 }
893
+ [data-item] > :where(div, p) > * { display: block; margin-block: 0 }
894
+ :is(a, button)[data-item] {
895
+ inline-size: 100%; min-width: 0; text-align: start; color: inherit; text-decoration: none;
896
+ }
897
+ :is(a, button)[data-item]:hover:not(:disabled) { background: var(--fertig-face) }
898
+
899
+ /* empty: a deliberate endpoint rather than a lonely paragraph in a panel */
900
+ [data-empty] {
901
+ display: grid; justify-items: center; gap: .6rem; text-align: center;
902
+ padding: clamp(2rem, 7vw, 4rem); border: 1px dashed var(--fertig-bd);
903
+ border-radius: var(--fertig-rw); background: color-mix(in srgb, var(--fertig-face) 45%, transparent);
904
+ }
905
+ [data-empty] > * { max-inline-size: 32rem; margin-block: 0 }
906
+
790
907
  /* sheet / drawer: the same <dialog>, docked to an edge.
791
908
  Docked to a physical edge on purpose — "right" means the right edge of the
792
909
  screen, not the end of the writing direction. inset is physical, so a drawer
@@ -794,7 +911,7 @@ nav[aria-label] ol li + li::before { content: "/"; margin-inline-end: .5em; colo
794
911
  translate is physical for the same reason: 100% of the drawer's own inline
795
912
  width moves it off the edge it came from. RTL readers still get the same
796
913
  right/left drawer as anyone else; direction would make "right" ambiguous. */
797
- dialog[data-side] { max-width: none; margin: 0; border-radius: 0; padding: 1.4rem }
914
+ dialog[data-side] { max-width: none; margin: 0; border-radius: 0 }
798
915
  dialog[data-side=right] {
799
916
  inset: 0 0 0 auto; block-size: 100dvh; inline-size: min(28rem, 92dvw);
800
917
  border-block: 0; border-inline-end: 0;
@@ -856,11 +973,12 @@ dialog[data-side=bottom] {
856
973
  /* list group: a plain <menu> outside a popover is a bordered list */
857
974
  menu {
858
975
  list-style: none; padding: 0; margin: 1.1rem 0;
859
- border: 1px solid var(--fertig-bd); border-radius: var(--fertig-rw); background: var(--fertig-el);
976
+ border: 1px solid transparent; border-radius: var(--fertig-rw);
977
+ background: var(--fertig-el); box-shadow: var(--fertig-up);
860
978
  }
861
979
  menu > li { padding: .55rem .85rem }
862
980
  menu > li + li { border-top: 1px solid var(--fertig-bd) }
863
- [popover] menu { border: 0; padding: 0; margin: 0; background: none }
981
+ [popover] menu { border: 0; padding: 0; margin: 0; background: none; box-shadow: none }
864
982
  [popover] menu > li { padding: 0 }
865
983
  [popover] menu > li + li { border-top: 0 }
866
984
 
@@ -906,7 +1024,9 @@ nav menu :is(a, button)[aria-current] {
906
1024
  /* on the toolbar .wide cannot mean "be 80rem wide" — the bar stays full-bleed
907
1025
  and its contents move out to the wide column, so an app screen's wordmark
908
1026
  lines up with the app's own content rather than floating mid-screen. */
909
- :is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav.wide { --fertig-nw: 80rem }
1027
+ :is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav.wide {
1028
+ max-width: none; --fertig-nw: 80rem;
1029
+ }
910
1030
  .row { display: flex; flex-wrap: wrap; gap: .7rem; align-items: center }
911
1031
  /* Form controls are full-width blocks by default, which is right in a form and
912
1032
  wrong in a toolbar. Inside a .row they size to the row instead. */
@@ -924,7 +1044,7 @@ nav menu :is(a, button)[aria-current] {
924
1044
  .grid + * { margin-top: 1.1rem }
925
1045
  .card {
926
1046
  container-type: inline-size; /* query it: @container (min-width: …) */
927
- border: 1px solid var(--fertig-bd); border-radius: var(--fertig-rw); padding: 1.1rem;
1047
+ border: 1px solid transparent; border-radius: var(--fertig-rw); padding: 1.1rem;
928
1048
  background: var(--fertig-el); box-shadow: var(--fertig-up2);
929
1049
  display: flex; flex-direction: column; /* so footers line up across a row */
930
1050
  gap: .85rem; /* spacing is the container's job */
@@ -983,24 +1103,18 @@ nav menu :is(a, button)[aria-current] {
983
1103
  .hidden { display: none }
984
1104
 
985
1105
  .badge {
986
- border: 1px solid var(--fertig-bd); border-radius: var(--fertig-rp); background: var(--fertig-face);
987
- padding: .1em .6em; font: 700 var(--fertig-caps); color: var(--fertig-mut);
988
- text-transform: uppercase; letter-spacing: .07em; /* a label, like h5/h6 and th */
1106
+ border: 0; border-radius: var(--fertig-rp); background: var(--fertig-face);
1107
+ padding: .18em .65em; font: 650 .78em/1.45 var(--fertig-f); color: var(--fertig-mut);
989
1108
  }
990
1109
 
991
1110
  /* card anatomy: an optional header, footer, and full-bleed media */
992
- /* No fill on the header and footer. A tinted strip across the top of a card is
993
- the old panel-heading pattern and it is what dates a sheet on sight; a
994
- hairline does the same separating job and lets the card read as one surface. */
995
- .card > :is(header, footer) { margin-inline: -1.1rem; padding-inline: 1.1rem }
1111
+ /* Header and footer are spacing hooks, not banded sub-surfaces. */
1112
+ .card > :is(header, footer) { padding-inline: 0 }
996
1113
  .card > header {
997
- margin-top: -.25rem; padding-bottom: .7rem; font-weight: 600;
998
- border-bottom: 1px solid var(--fertig-bd);
1114
+ margin-top: -.1rem; font-weight: 650;
999
1115
  }
1000
1116
  .card > footer {
1001
- margin-top: auto; margin-bottom: -.25rem; padding-top: .7rem;
1002
- color: var(--fertig-mut); font-size: .85em;
1003
- border-top: 1px solid var(--fertig-bd);
1117
+ margin-top: auto; margin-bottom: -.1rem; color: var(--fertig-mut); font-size: .85em;
1004
1118
  }
1005
1119
  .card > :is(img, video):first-child {
1006
1120
  align-self: stretch; width: auto; max-width: none; margin: -1.1rem -1.1rem 0;
@@ -1023,9 +1137,9 @@ nav menu :is(a, button)[aria-current] {
1023
1137
  text-decoration: none; font: inherit;
1024
1138
  }
1025
1139
  :is(a, button).card:hover:not(:disabled) {
1026
- border-color: color-mix(in srgb, var(--fertig-ac) 45%, var(--fertig-bd));
1140
+ border-color: color-mix(in srgb, var(--fertig-ac) 22%, var(--fertig-bd));
1027
1141
  box-shadow: var(--fertig-up2), 0 16px 30px -16px color-mix(in srgb, var(--fertig-sh) 28%, transparent);
1028
- translate: 0 -2px;
1142
+ translate: 0 -1px;
1029
1143
  }
1030
1144
  :is(a, button).card:active:not(:disabled) { translate: 0 0 }
1031
1145
 
@@ -1051,8 +1165,10 @@ meter { width: 100%; height: .5rem }
1051
1165
  optgroup { font-weight: 700; color: var(--fertig-mut) }
1052
1166
  ::file-selector-button { margin-inline-end: .8em }
1053
1167
  ::file-selector-button:hover { background: var(--fertig-face) }
1054
- input:user-invalid, textarea:user-invalid { border-color: light-dark(#c0392b, #ff6b6b) }
1055
- input:user-invalid + small { color: light-dark(#c0392b, #ff6b6b) }
1168
+ :is(input,select,textarea)[aria-invalid=true],
1169
+ :is(input,textarea):user-invalid { border-color: light-dark(#c0392b, #ff6b6b) }
1170
+ :is(input,select,textarea)[aria-invalid=true] + small,
1171
+ :is(input,textarea):user-invalid + small { color: light-dark(#c0392b, #ff6b6b) }
1056
1172
  summary:hover { color: var(--fertig-ac) }
1057
1173
  /* thin, unobtrusive scrollbars */
1058
1174
  /* scrollbar-width is Safari 18.2, scrollbar-color Safari 26.2 — both younger
@@ -1120,7 +1236,7 @@ summary:hover { color: var(--fertig-ac) }
1120
1236
  @media (prefers-reduced-motion: no-preference) and (update: fast) {
1121
1237
  a, button, input, select, textarea, summary, tr,
1122
1238
  [type=submit], [type=button], [type=reset], .card, .badge,
1123
- [role=tab], [role=group] > *, [popover] :is(a, button) {
1239
+ [role=tab], [role=option], [role=group] > *, [data-item], [popover] :is(a, button) {
1124
1240
  transition: box-shadow var(--fertig-dur-1) var(--fertig-ease),
1125
1241
  background-color var(--fertig-dur-1) var(--fertig-ease),
1126
1242
  border-color var(--fertig-dur-1) var(--fertig-ease),
@@ -1143,20 +1259,20 @@ summary:hover { color: var(--fertig-ac) }
1143
1259
  [data-tone] or [data-accent] element just inherits the one from :root. */
1144
1260
  @supports not (color: light-dark(#000, #fff)) {
1145
1261
  :root {
1146
- --fertig-bg: #e6eaef; --fertig-el: #fafcfe; --fertig-face: #f3f6fa; --fertig-fg: #151b24;
1147
- --fertig-mut: #525962; --fertig-bd: #151b2421; --fertig-tb: #f3f6fa;
1262
+ --fertig-bg: #f4f6f9; --fertig-el: #fff; --fertig-face: #eceff4; --fertig-fg: #151b24;
1263
+ --fertig-mut: #525962; --fertig-bd: #151b241a; --fertig-tb: #fbfcfe;
1148
1264
  --fertig-ac: #2850de; --fertig-on-ac: #f7f8fb;
1149
1265
  }
1150
1266
  @media (prefers-color-scheme: dark) {
1151
1267
  :root:not([data-theme=light]) {
1152
- --fertig-bg: #090c12; --fertig-el: #141920; --fertig-face: #1c222a; --fertig-fg: #eaeff7;
1153
- --fertig-mut: #9da5b1; --fertig-bd: #eaeff721; --fertig-tb: #1c222a;
1268
+ --fertig-bg: #0b0e14; --fertig-el: #151a22; --fertig-face: #202630; --fertig-fg: #eaeff7;
1269
+ --fertig-mut: #9da5b1; --fertig-bd: #eaeff71f; --fertig-tb: #131820;
1154
1270
  --fertig-ac: #90b5ff; --fertig-on-ac: #07071c;
1155
1271
  }
1156
1272
  }
1157
1273
  [data-theme=dark] {
1158
- --fertig-bg: #090c12; --fertig-el: #141920; --fertig-face: #1c222a; --fertig-fg: #eaeff7;
1159
- --fertig-mut: #9da5b1; --fertig-bd: #eaeff721; --fertig-tb: #1c222a;
1274
+ --fertig-bg: #0b0e14; --fertig-el: #151a22; --fertig-face: #202630; --fertig-fg: #eaeff7;
1275
+ --fertig-mut: #9da5b1; --fertig-bd: #eaeff71f; --fertig-tb: #131820;
1160
1276
  --fertig-ac: #90b5ff; --fertig-on-ac: #07071c;
1161
1277
  }
1162
1278
  }
@@ -1169,12 +1285,15 @@ summary:hover { color: var(--fertig-ac) }
1169
1285
  @media (forced-colors: active) {
1170
1286
  * { box-shadow: none }
1171
1287
  button, [type=submit], [type=button], [type=reset], input, select, textarea,
1172
- .card, .badge, details, dialog, [popover], fieldset, table {
1288
+ .card, .badge, details, dialog, [popover], fieldset, table,
1289
+ [data-surface], [data-input-group], [data-empty], [data-item], [role=listbox] {
1173
1290
  border: 1px solid CanvasText;
1174
1291
  }
1175
- /* [role=tab] sets border:0, so there is nothing for border-color to land
1176
- on, and the rule above just removed the box-shadow that marked it */
1177
- [role=tab][aria-selected=true] { border-bottom: 2px solid Highlight }
1292
+ /* Selected pills rely on fill and shadow in ordinary colours. Give them a
1293
+ real system-colour edge when both disappear in High Contrast. */
1294
+ [role=tab][aria-selected=true], [role=option][aria-selected=true] {
1295
+ border: 2px solid Highlight;
1296
+ }
1178
1297
  button[aria-pressed=true] { border-color: Highlight }
1179
1298
  }
1180
1299
  }