fertig 1.0.3 → 2.0.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.
- package/CHANGELOG.md +134 -4
- package/README.md +133 -32
- package/fertig.core.min.css +1 -1
- package/fertig.css +248 -79
- package/fertig.min.css +1 -1
- package/package.json +4 -3
package/fertig.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! fertig
|
|
1
|
+
/*! fertig v2.0.0 — a classless CSS stylesheet. MIT */
|
|
2
2
|
|
|
3
3
|
/* Opinions, so you don't have to have them:
|
|
4
4
|
system type · one accent · one measure · a window on a desktop.
|
|
@@ -7,8 +7,10 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
/* Registrations are global: they cannot be layered, scoped or overridden.
|
|
11
|
+
Hence the prefix — a bare --a1 would silently retype a property of yours. */
|
|
12
|
+
@property --fertig-a1 { syntax: "<number>"; inherits: true; initial-value: .08 }
|
|
13
|
+
@property --fertig-a2 { syntax: "<number>"; inherits: true; initial-value: .06 }
|
|
12
14
|
|
|
13
15
|
/* Two layers, in this order. Your own unlayered CSS wins over both regardless
|
|
14
16
|
of specificity, so overriding never needs !important — and neither does this
|
|
@@ -30,8 +32,9 @@
|
|
|
30
32
|
--fm: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
|
31
33
|
"Liberation Mono", monospace; /* code */
|
|
32
34
|
--w: 38rem; /* ~70 characters — yours */
|
|
33
|
-
--r:
|
|
34
|
-
--
|
|
35
|
+
--r: 9px; /* control radius */
|
|
36
|
+
--rs: 5px; /* small radius: inline chips and code */
|
|
37
|
+
--rw: 14px; /* window radius */
|
|
35
38
|
|
|
36
39
|
/* ---- palette ------------------------------------------------
|
|
37
40
|
Low chroma on purpose. Vivid palettes shout; these are meant to sit
|
|
@@ -73,29 +76,64 @@
|
|
|
73
76
|
--mut: light-dark(oklch(47% .009 75), oklch(73% .008 75));
|
|
74
77
|
--bd: light-dark(oklch(22% .008 75 / .13), oklch(95% .004 75 / .13));
|
|
75
78
|
--tb: light-dark(oklch(97.2% .005 75 / .85), oklch(25% .007 75 / .85));
|
|
76
|
-
|
|
79
|
+
/* indigo, not the azure every other classless sheet lands on. Measured:
|
|
80
|
+
6.28:1 as link text on paper and 6.39:1 for a white label on the fill in
|
|
81
|
+
light; 8.72:1 and 10.26:1 in dark. Both sit inside sRGB. */
|
|
82
|
+
--ac: light-dark(oklch(50% .19 275), oklch(78% .11 275)); /* yours */
|
|
77
83
|
--on-ac: light-dark(#fff, #06121f); /* text on the accent, both themes */
|
|
78
84
|
|
|
79
85
|
/* depth: hairline + soft shadow, macOS weight.
|
|
80
|
-
--sh is an ordinary colour — override it with any notation you like
|
|
81
|
-
the
|
|
86
|
+
--sh is an ordinary colour — override it with any notation you like, and
|
|
87
|
+
every shadow in the sheet retints. The alphas are taken with color-mix
|
|
88
|
+
against transparent rather than relative colour: same result, and it is
|
|
89
|
+
three years older, which keeps Safari 17.5 inside the floor. */
|
|
82
90
|
--sh: oklch(23.1% .007 286);
|
|
83
|
-
--a1: .08; --a2: .06;
|
|
84
|
-
--sh1:
|
|
85
|
-
--sh2:
|
|
91
|
+
--fertig-a1: .08; --fertig-a2: .06;
|
|
92
|
+
--sh1: color-mix(in srgb, var(--sh) calc(var(--fertig-a1) * 100%), transparent);
|
|
93
|
+
--sh2: color-mix(in srgb, var(--sh) calc(var(--fertig-a2) * 100%), transparent);
|
|
86
94
|
--up: 0 1px 1px var(--sh1);
|
|
87
95
|
--up2: 0 1px 2px var(--sh1), 0 6px 16px -6px var(--sh2);
|
|
88
|
-
--dn: inset 0 1px 2px
|
|
96
|
+
--dn: inset 0 1px 2px color-mix(in srgb, var(--sh) 8%, transparent);
|
|
89
97
|
--caps: .74em/1.4 var(--f);
|
|
98
|
+
|
|
99
|
+
/* motion. One decelerating curve for things arriving, one accelerating for
|
|
100
|
+
things leaving, and a symmetric one for state that toggles in place. */
|
|
101
|
+
--ease-out: cubic-bezier(.22, 1, .36, 1);
|
|
102
|
+
--ease-in: cubic-bezier(.55, 0, 1, .45);
|
|
103
|
+
--ease: cubic-bezier(.4, 0, .2, 1);
|
|
104
|
+
--dur-1: .12s; /* a control reacting under the pointer */
|
|
105
|
+
--dur-2: .2s; /* something appearing or leaving */
|
|
90
106
|
}
|
|
91
107
|
@media (prefers-color-scheme: dark) {
|
|
92
|
-
:root:not([data-theme=light]) { --a1: .5; --a2: .45 }
|
|
108
|
+
:root:not([data-theme=light]) { --fertig-a1: .5; --fertig-a2: .45 }
|
|
109
|
+
}
|
|
110
|
+
/* --on-ac has to survive any --ac override, or a custom accent ships an
|
|
111
|
+
unreadable label. contrast-color() does that properly but has almost no
|
|
112
|
+
support; relative colour reaches the same black-or-white decision from the
|
|
113
|
+
accent's own lightness, and has been cross-engine since 2024. Both are
|
|
114
|
+
additive — the measured light-dark() pair above stands if neither exists. */
|
|
115
|
+
@supports (color: oklch(from red l c h)) {
|
|
116
|
+
:root { --on-ac: oklch(from var(--ac) clamp(0, (l - .62) * -100, 1) 0 h) }
|
|
93
117
|
}
|
|
94
118
|
@supports (color: contrast-color(red)) {
|
|
95
|
-
:root { --on-ac: contrast-color(var(--ac)) }
|
|
119
|
+
:root { --on-ac: contrast-color(var(--ac)) }
|
|
96
120
|
}
|
|
97
|
-
[data-theme=light] { color-scheme: light; --a1: .08; --a2: .06 }
|
|
98
|
-
[data-theme=dark] { color-scheme: dark; --a1: .5; --a2: .45 }
|
|
121
|
+
[data-theme=light] { color-scheme: light; --fertig-a1: .08; --fertig-a2: .06 }
|
|
122
|
+
[data-theme=dark] { color-scheme: dark; --fertig-a1: .5; --fertig-a2: .45 }
|
|
123
|
+
|
|
124
|
+
/* Named accents. The ramps already exist, so this is a mapping table: put
|
|
125
|
+
data-accent on any element and everything inside it — links, focus rings,
|
|
126
|
+
filled buttons, the caret, the selection — follows. Scoped rather than
|
|
127
|
+
global, so one section of a page can differ from the rest.
|
|
128
|
+
Each pair is measured: the light tone clears AA as link text on paper, the
|
|
129
|
+
dark tone clears it on the dark ground. */
|
|
130
|
+
[data-accent=indigo] { --ac: light-dark(oklch(50% .19 275), oklch(78% .11 275)) }
|
|
131
|
+
[data-accent=sky] { --ac: light-dark(var(--sky-700), oklch(78% .085 245)) }
|
|
132
|
+
[data-accent=sage] { --ac: light-dark(var(--sage-700), oklch(80% .075 155)) }
|
|
133
|
+
[data-accent=clay] { --ac: light-dark(var(--clay-700), oklch(80% .085 45)) }
|
|
134
|
+
[data-accent=plum] { --ac: light-dark(var(--plum-700), oklch(80% .065 320)) }
|
|
135
|
+
[data-accent=gold] { --ac: light-dark(oklch(55% .085 80), oklch(84% .09 85)) }
|
|
136
|
+
[data-accent=slate] { --ac: light-dark(oklch(35% .01 250), oklch(86% .006 250)) }
|
|
99
137
|
|
|
100
138
|
/* ---- base --------------------------------------------------- */
|
|
101
139
|
*, ::before, ::after { box-sizing: border-box }
|
|
@@ -110,28 +148,44 @@ body {
|
|
|
110
148
|
font-variant-numeric: slashed-zero tabular-nums;
|
|
111
149
|
}
|
|
112
150
|
::selection { background: color-mix(in srgb, var(--ac) 28%, transparent) }
|
|
113
|
-
|
|
151
|
+
/* The macOS ring. No border-radius here: an outline already follows the
|
|
152
|
+
element's own corners, and setting one would reshape whatever takes focus —
|
|
153
|
+
a focused dialog would snap from --rw to --r for as long as it is focused. */
|
|
154
|
+
:focus-visible {
|
|
114
155
|
outline: 3px solid color-mix(in srgb, var(--ac) 45%, transparent);
|
|
115
|
-
outline-offset: 1px;
|
|
156
|
+
outline-offset: 1px;
|
|
116
157
|
}
|
|
117
|
-
img,
|
|
158
|
+
img, video, iframe { display: block; max-width: 100%; height: auto }
|
|
159
|
+
/* svg is left inline: it is as often an icon inside a sentence or a button as
|
|
160
|
+
it is a standalone figure, and display:block drops the icon onto its own row */
|
|
161
|
+
svg { max-width: 100%; vertical-align: middle }
|
|
118
162
|
img, video { border-radius: var(--r); box-shadow: var(--up2) }
|
|
119
163
|
|
|
120
164
|
/* ---- layout: a window on a desktop --------------------------- */
|
|
121
|
-
body
|
|
122
|
-
|
|
165
|
+
/* The shell is body's own children — or the children of the single wrapper a
|
|
166
|
+
framework insists on (#root, #app, #__next, or your own [data-fertig]).
|
|
167
|
+
Without this a React page silently loses the measure, the paper and the
|
|
168
|
+
gutters, and nothing on screen says why. The wrapper list sits inside
|
|
169
|
+
:where() so every selector keeps the specificity it had as `body > *`. */
|
|
170
|
+
body > *,
|
|
171
|
+
body > :where(#root, #app, #__next, [data-fertig]) > * {
|
|
172
|
+
max-width: var(--w); margin-inline: auto; padding-inline: 1.9rem;
|
|
173
|
+
}
|
|
174
|
+
/* the wrapper is not itself a shell element; it just gets out of the way */
|
|
175
|
+
body > :where(#root, #app, #__next, [data-fertig]) { max-width: none; padding-inline: 0 }
|
|
176
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > :is(header, main, footer) {
|
|
123
177
|
background: var(--el); border-inline: 1px solid var(--bd);
|
|
124
178
|
}
|
|
125
|
-
body > header { padding-block: 2.4rem .8rem }
|
|
126
|
-
body > main { padding-bottom: 2.8rem }
|
|
127
|
-
body > footer {
|
|
179
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > header { padding-block: 2.4rem .8rem }
|
|
180
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > main { padding-bottom: 2.8rem }
|
|
181
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > footer {
|
|
128
182
|
padding-block: 1.2rem 2rem; color: var(--mut); font-size: .85em;
|
|
129
183
|
border-top: 1px solid var(--bd); border-bottom: 1px solid var(--bd);
|
|
130
184
|
border-radius: 0 0 var(--rw) var(--rw); margin-bottom: 2.5rem;
|
|
131
|
-
box-shadow: 0 18px 30px -22px
|
|
185
|
+
box-shadow: 0 18px 30px -22px color-mix(in srgb, var(--sh) 50%, transparent);
|
|
132
186
|
}
|
|
133
187
|
/* nav = unified toolbar: full-bleed, vibrant, contents on the measure */
|
|
134
|
-
body > nav {
|
|
188
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav {
|
|
135
189
|
position: sticky; top: 0; z-index: 9; max-width: none;
|
|
136
190
|
/* the toolbar is full-bleed but its contents line up with the page column.
|
|
137
191
|
--nw is that column: it follows --w, so overriding the measure moves the
|
|
@@ -145,10 +199,10 @@ body > nav {
|
|
|
145
199
|
backdrop-filter: saturate(180%) blur(20px);
|
|
146
200
|
border-bottom: 1px solid var(--bd); font-size: .92em;
|
|
147
201
|
}
|
|
148
|
-
body > nav a { color: var(--fg); text-decoration: none; opacity: .75 }
|
|
149
|
-
body > nav a:hover { opacity: 1 }
|
|
150
|
-
body > nav :is(strong, b) a { opacity: 1 } /* the wordmark is not a link among links */
|
|
151
|
-
body > nav ul {
|
|
202
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav a { color: var(--fg); text-decoration: none; opacity: .75 }
|
|
203
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav a:hover { opacity: 1 }
|
|
204
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav :is(strong, b) a { opacity: 1 } /* the wordmark is not a link among links */
|
|
205
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav ul {
|
|
152
206
|
display: flex; flex-wrap: wrap; gap: 1.1rem; list-style: none; padding: 0; margin: 0;
|
|
153
207
|
flex: 1; /* takes the slack, so a trailing control sits at the end */
|
|
154
208
|
}
|
|
@@ -192,11 +246,11 @@ figcaption { font-size: .85em; color: var(--mut); text-align: center }
|
|
|
192
246
|
code, kbd, samp, pre { font-family: var(--fm); font-size: .9em }
|
|
193
247
|
:not(pre) > code, samp {
|
|
194
248
|
background: var(--face); border: 1px solid var(--bd);
|
|
195
|
-
border-radius: var(--
|
|
249
|
+
border-radius: var(--rs); padding: .08em .35em;
|
|
196
250
|
}
|
|
197
251
|
kbd {
|
|
198
252
|
background: var(--face); border: 1px solid var(--bd); border-bottom-width: 2px;
|
|
199
|
-
border-radius: var(--
|
|
253
|
+
border-radius: var(--rs); padding: .08em .4em; box-shadow: var(--up);
|
|
200
254
|
}
|
|
201
255
|
pre {
|
|
202
256
|
background: var(--face); border: 1px solid var(--bd); border-radius: var(--r);
|
|
@@ -279,9 +333,9 @@ textarea { width: 100%; min-height: 5rem; resize: vertical }
|
|
|
279
333
|
@supports (field-sizing: content) {
|
|
280
334
|
textarea { field-sizing: content; max-height: 24rem }
|
|
281
335
|
}
|
|
282
|
-
input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=submit],[type=button],[type=reset]),
|
|
336
|
+
input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=image],[type=submit],[type=button],[type=reset]),
|
|
283
337
|
select, textarea {
|
|
284
|
-
display: block; width: 100%; padding: .
|
|
338
|
+
display: block; width: 100%; padding: .52em .7em; background: var(--el);
|
|
285
339
|
border: 1px solid var(--bd); border-radius: var(--r); box-shadow: var(--dn);
|
|
286
340
|
}
|
|
287
341
|
select {
|
|
@@ -305,14 +359,14 @@ select[multiple] { background-image: none; padding-inline-end: .6em }
|
|
|
305
359
|
content: ""; inline-size: .45em; block-size: .45em; flex: none;
|
|
306
360
|
border: solid currentColor; border-width: 0 1px 1px 0;
|
|
307
361
|
rotate: 45deg; translate: 0 -.15em; opacity: .7;
|
|
308
|
-
transition: rotate
|
|
362
|
+
transition: rotate var(--dur-1) var(--ease), translate var(--dur-1) var(--ease);
|
|
309
363
|
}
|
|
310
364
|
select:open::picker-icon { rotate: 225deg; translate: 0 .1em }
|
|
311
365
|
::picker(select) {
|
|
312
366
|
min-width: anchor-size(width); /* never narrower than the field */
|
|
313
367
|
border: 1px solid var(--bd); border-radius: var(--r); padding: .3rem;
|
|
314
368
|
background: var(--el); margin-block-start: .35rem;
|
|
315
|
-
box-shadow: var(--up2), 0 20px 40px -14px
|
|
369
|
+
box-shadow: var(--up2), 0 20px 40px -14px color-mix(in srgb, var(--sh) 40%, transparent);
|
|
316
370
|
}
|
|
317
371
|
option {
|
|
318
372
|
display: flex; align-items: center; gap: .5em; padding: .4em .6em;
|
|
@@ -363,7 +417,7 @@ legend + * { clear: left }
|
|
|
363
417
|
button, [type=submit], [type=button], [type=reset],
|
|
364
418
|
:where(a.primary, a[role=button]), ::file-selector-button {
|
|
365
419
|
display: inline-block; text-decoration: none; text-align: center;
|
|
366
|
-
padding: .
|
|
420
|
+
padding: .48em 1.1em; background: var(--el); color: var(--fg); cursor: pointer;
|
|
367
421
|
border: 1px solid var(--bd); border-radius: var(--r); box-shadow: var(--up);
|
|
368
422
|
font: inherit; min-width: 5rem;
|
|
369
423
|
}
|
|
@@ -372,22 +426,34 @@ button, [type=submit], [type=button], [type=reset],
|
|
|
372
426
|
box-shadow: var(--dn); translate: 0 .5px;
|
|
373
427
|
}
|
|
374
428
|
[type=submit], .primary { background: var(--ac); border-color: transparent; color: var(--on-ac) }
|
|
429
|
+
/* The filled button lightens on hover. color-mix does that in oklab, which is
|
|
430
|
+
close enough and is Baseline; relative colour does it by adding to L
|
|
431
|
+
directly, which holds the hue exactly when --ac is a wide-gamut override.
|
|
432
|
+
Both derive from --ac, so a custom accent never needs a second declaration. */
|
|
375
433
|
:is([type=submit], .primary, a.primary):hover:not(:disabled) {
|
|
376
|
-
background:
|
|
434
|
+
background: color-mix(in oklab, var(--ac), #fff 9%);
|
|
435
|
+
}
|
|
436
|
+
@supports (color: oklch(from red l c h)) {
|
|
437
|
+
:is([type=submit], .primary, a.primary):hover:not(:disabled) {
|
|
438
|
+
background: oklch(from var(--ac) calc(l + .05) c h);
|
|
439
|
+
}
|
|
377
440
|
}
|
|
378
441
|
|
|
379
442
|
/* button detail: sizes, variants, toggles, icon and block forms */
|
|
380
|
-
|
|
381
|
-
|
|
443
|
+
/* Scoped to controls. data-size, data-block and data-variant are names other
|
|
444
|
+
component libraries and framework wrappers use too, and a bare attribute
|
|
445
|
+
selector would restyle a stranger's <span data-size="sm"> on contact. */
|
|
446
|
+
:is(button, a, input, select, label)[data-size=sm] { padding: .2em .7em; font-size: .85em; min-width: 0 }
|
|
447
|
+
:is(button, a, input, select, label)[data-size=lg] { padding: .55em 1.5em; font-size: 1.05em }
|
|
382
448
|
button[data-icon] { min-width: 0; padding: .38em .62em; line-height: 1 }
|
|
383
|
-
[data-block] { display: block; width: 100%; min-width: 0 }
|
|
384
|
-
[data-variant=ghost] { background: none; border-color: transparent; box-shadow: none }
|
|
385
|
-
[data-variant=ghost]:hover:not(:disabled) { background: var(--face); border-color: var(--bd) }
|
|
386
|
-
[data-variant=link] {
|
|
449
|
+
:is(button, a, input, select, label)[data-block] { display: block; width: 100%; min-width: 0 }
|
|
450
|
+
:is(button, a, input)[data-variant=ghost] { background: none; border-color: transparent; box-shadow: none }
|
|
451
|
+
:is(button, a, input)[data-variant=ghost]:hover:not(:disabled) { background: var(--face); border-color: var(--bd) }
|
|
452
|
+
:is(button, a, input)[data-variant=link] {
|
|
387
453
|
background: none; border: 0; box-shadow: none; min-width: 0; padding-inline: .2em;
|
|
388
454
|
color: var(--ac); text-decoration: underline; text-underline-offset: 3px;
|
|
389
455
|
}
|
|
390
|
-
[data-variant=link]:hover:not(:disabled) { background: none; text-decoration-thickness: 2px }
|
|
456
|
+
:is(button, a, input)[data-variant=link]:hover:not(:disabled) { background: none; text-decoration-thickness: 2px }
|
|
391
457
|
button[aria-pressed=true] { background: var(--face); color: var(--ac); font-weight: 700 }
|
|
392
458
|
button[aria-pressed=true]:hover:not(:disabled) {
|
|
393
459
|
background: color-mix(in srgb, var(--fg) 7%, var(--face));
|
|
@@ -397,7 +463,13 @@ button[aria-pressed=true]:hover:not(:disabled) {
|
|
|
397
463
|
:is([type=submit], .primary):active:not(:disabled) { box-shadow: inset 0 1px 3px #0006 }
|
|
398
464
|
|
|
399
465
|
@supports (corner-shape: squircle) {
|
|
400
|
-
|
|
466
|
+
/* the surfaces this sheet rounds, not `*` — a universal rule would reshape
|
|
467
|
+
your components too, and this one is a taste, not a repair */
|
|
468
|
+
:is(button, [type=submit], [type=button], [type=reset], input, select, textarea,
|
|
469
|
+
fieldset, details, dialog, [popover], pre, kbd, code, samp, img, video,
|
|
470
|
+
menu, aside, [data-avatar], [data-skeleton], .card, .badge) {
|
|
471
|
+
corner-shape: squircle;
|
|
472
|
+
}
|
|
401
473
|
}
|
|
402
474
|
|
|
403
475
|
/* ---- disclosure / misc -------------------------------------- */
|
|
@@ -412,11 +484,11 @@ details[open] > summary { margin-bottom: .6rem }
|
|
|
412
484
|
dialog {
|
|
413
485
|
background: var(--el); color: var(--fg); border: 1px solid var(--bd);
|
|
414
486
|
border-radius: var(--rw); padding: 1.4rem; max-width: min(30rem, 92dvw);
|
|
415
|
-
box-shadow: 0 32px 64px -16px
|
|
487
|
+
box-shadow: 0 32px 64px -16px color-mix(in srgb, var(--sh) 45%, transparent);
|
|
416
488
|
}
|
|
417
489
|
dialog > :last-child:not(footer) { margin-bottom: 0 }
|
|
418
490
|
dialog, [popover] { overscroll-behavior: contain }
|
|
419
|
-
dialog::backdrop { background:
|
|
491
|
+
dialog::backdrop { background: color-mix(in srgb, var(--sh) 35%, transparent); backdrop-filter: blur(3px) }
|
|
420
492
|
|
|
421
493
|
/* dialog anatomy: an optional header and footer, pinned while the body scrolls.
|
|
422
494
|
Sticky rather than a grid, because a classless sheet has no wrapper around
|
|
@@ -445,7 +517,8 @@ dialog > footer > :is(p, span):first-child { margin: 0; margin-inline-end: auto;
|
|
|
445
517
|
/* sheets come up, they do not appear */
|
|
446
518
|
@media (prefers-reduced-motion: no-preference) and (update: fast) {
|
|
447
519
|
dialog, dialog::backdrop {
|
|
448
|
-
transition: opacity
|
|
520
|
+
transition: opacity var(--dur-2) var(--ease-out), translate var(--dur-2) var(--ease-out),
|
|
521
|
+
display var(--dur-2) allow-discrete, overlay var(--dur-2) allow-discrete;
|
|
449
522
|
}
|
|
450
523
|
dialog { opacity: 0; translate: 0 10px }
|
|
451
524
|
dialog[open] { opacity: 1; translate: 0 }
|
|
@@ -455,7 +528,8 @@ dialog > footer > :is(p, span):first-child { margin: 0; margin-inline-end: auto;
|
|
|
455
528
|
@starting-style { dialog[open]::backdrop { opacity: 0 } }
|
|
456
529
|
[popover] {
|
|
457
530
|
opacity: 0; translate: 0 -4px;
|
|
458
|
-
transition: opacity .13s, translate .13s
|
|
531
|
+
transition: opacity .13s var(--ease-out), translate .13s var(--ease-out),
|
|
532
|
+
display .13s allow-discrete, overlay .13s allow-discrete;
|
|
459
533
|
}
|
|
460
534
|
[popover]:popover-open { opacity: 1; translate: 0 }
|
|
461
535
|
@starting-style { [popover]:popover-open { opacity: 0; translate: 0 -4px } }
|
|
@@ -476,10 +550,13 @@ progress::-moz-progress-bar { background: var(--ac); border-radius: calc(infinit
|
|
|
476
550
|
/* details can only animate to auto height once keywords interpolate */
|
|
477
551
|
@supports selector(::details-content) {
|
|
478
552
|
@media (prefers-reduced-motion: no-preference) and (update: fast) {
|
|
479
|
-
|
|
553
|
+
/* on details, not :root: allow-keywords is inherited, and setting it
|
|
554
|
+
document-wide would quietly change how your own height:auto
|
|
555
|
+
transitions behave */
|
|
556
|
+
details { interpolate-size: allow-keywords }
|
|
480
557
|
details::details-content {
|
|
481
558
|
block-size: 0; overflow: clip;
|
|
482
|
-
transition: block-size .28s ease, content-visibility .28s allow-discrete;
|
|
559
|
+
transition: block-size .28s var(--ease), content-visibility .28s allow-discrete;
|
|
483
560
|
}
|
|
484
561
|
details[open]::details-content { block-size: auto }
|
|
485
562
|
}
|
|
@@ -523,7 +600,7 @@ input[type=checkbox][role=switch] {
|
|
|
523
600
|
input[switch]::after, input[role=switch]::after {
|
|
524
601
|
content: ""; position: absolute; inset-block-start: .14em; inset-inline-start: .14em;
|
|
525
602
|
width: 1.02em; height: 1.02em; border-radius: 50%; background: #fff; box-shadow: var(--up);
|
|
526
|
-
transition: translate .16s ease;
|
|
603
|
+
transition: translate .16s var(--ease-out);
|
|
527
604
|
}
|
|
528
605
|
input[switch]:checked, input[role=switch]:checked { background: var(--ac) }
|
|
529
606
|
/* translate is physical, so the knob is told which way to travel */
|
|
@@ -535,7 +612,7 @@ input[switch]:checked::after, input[role=switch]:checked::after { translate: .9e
|
|
|
535
612
|
background: var(--el); color: var(--fg); border: 1px solid var(--bd);
|
|
536
613
|
border-radius: var(--r); padding: .3rem; min-width: 12rem;
|
|
537
614
|
max-block-size: 60dvh; overflow: auto; font-size: .95em;
|
|
538
|
-
box-shadow: var(--up2), 0 20px 40px -14px
|
|
615
|
+
box-shadow: var(--up2), 0 20px 40px -14px color-mix(in srgb, var(--sh) 40%, transparent);
|
|
539
616
|
}
|
|
540
617
|
/* anchor to the invoking button; without support it centres, which is fine.
|
|
541
618
|
Deliberately NOT scoped to :popover-open — the close is transitioned, so a
|
|
@@ -583,14 +660,21 @@ nav[aria-label] ol li + li::before { content: "/"; margin-inline-end: .5em; colo
|
|
|
583
660
|
}
|
|
584
661
|
@keyframes fertig-spin { to { rotate: 360deg } }
|
|
585
662
|
|
|
586
|
-
/* tooltip: [data-tooltip]
|
|
663
|
+
/* tooltip: [data-tooltip]. Decorative only, and deliberately so: generated
|
|
664
|
+
content is not reliably exposed to assistive tech and there is no way to
|
|
665
|
+
dismiss it without script (WCAG 1.4.13), so put the same words in aria-label
|
|
666
|
+
or aria-describedby and never say anything here that is said nowhere else.
|
|
667
|
+
A scroll container clips it — [role=group] is one. */
|
|
587
668
|
[data-tooltip] { position: relative }
|
|
588
|
-
[data-tooltip]:is(:hover, :focus-visible)::after {
|
|
589
|
-
content: attr(data-tooltip); position: absolute;
|
|
669
|
+
[data-tooltip]:is(:hover, :focus-visible, :active)::after {
|
|
670
|
+
content: attr(data-tooltip); position: absolute;
|
|
671
|
+
inset-block-end: 100%; inset-inline-start: 50%;
|
|
590
672
|
translate: -50% -.45em; z-index: 10; white-space: nowrap; pointer-events: none;
|
|
591
673
|
background: light-dark(#1d1d1f, #3a3a40); color: #fff; font-size: .8em;
|
|
592
674
|
padding: .25em .55em; border-radius: var(--r); box-shadow: var(--up2);
|
|
593
675
|
}
|
|
676
|
+
/* translate is physical; the offset has to be told which way to go */
|
|
677
|
+
[data-tooltip]:dir(rtl)::after { translate: 50% -.45em }
|
|
594
678
|
|
|
595
679
|
/* tones: retint any component by redefining its accent */
|
|
596
680
|
[data-tone=ok] { --ac: light-dark(var(--sage-700), oklch(76% .085 155)) }
|
|
@@ -662,6 +746,27 @@ dialog[data-side=bottom] {
|
|
|
662
746
|
position-area: none; inset: auto 1rem 1rem auto; min-width: 16rem;
|
|
663
747
|
}
|
|
664
748
|
}
|
|
749
|
+
/* Two open toasts land on the same corner coordinates and sit on top of each
|
|
750
|
+
other, and an open popover is in the top layer so no ordinary wrapper can
|
|
751
|
+
stack them. Make the region the popover instead and the toasts plain
|
|
752
|
+
children of it: <div popover=manual data-toast-region> with a
|
|
753
|
+
<div data-toast> per message. */
|
|
754
|
+
[popover][data-toast-region] {
|
|
755
|
+
position: fixed; inset: auto 1rem 1rem auto; margin: 0;
|
|
756
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: .6rem;
|
|
757
|
+
background: none; border: 0; box-shadow: none; padding: 0;
|
|
758
|
+
max-block-size: none; overflow: visible; pointer-events: none;
|
|
759
|
+
}
|
|
760
|
+
[popover][data-toast-region] > [data-toast] {
|
|
761
|
+
pointer-events: auto; position: static; inset: auto;
|
|
762
|
+
min-width: 16rem; max-width: min(24rem, 92dvw); padding: .8rem 1rem;
|
|
763
|
+
background: var(--el); color: var(--fg); border: 1px solid var(--bd);
|
|
764
|
+
border-radius: var(--r); font-size: .95em;
|
|
765
|
+
box-shadow: var(--up2), 0 20px 40px -14px color-mix(in srgb, var(--sh) 40%, transparent);
|
|
766
|
+
}
|
|
767
|
+
@supports (position-area: block-end span-inline-end) {
|
|
768
|
+
[popover][data-toast-region] { position-area: none; inset: auto 1rem 1rem auto }
|
|
769
|
+
}
|
|
665
770
|
|
|
666
771
|
/* list group: a plain <menu> outside a popover is a bordered list */
|
|
667
772
|
menu {
|
|
@@ -718,7 +823,7 @@ nav menu :is(a, button)[aria-current] {
|
|
|
718
823
|
/* on the toolbar .wide cannot mean "be 66rem wide" — the bar stays full-bleed
|
|
719
824
|
and its contents move out to the wide column, so an app screen's wordmark
|
|
720
825
|
lines up with the app's own content rather than floating mid-screen. */
|
|
721
|
-
body > nav.wide { max-width: none; --nw: 66rem }
|
|
826
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav.wide { max-width: none; --nw: 66rem }
|
|
722
827
|
.row { display: flex; flex-wrap: wrap; gap: .7rem; align-items: center }
|
|
723
828
|
/* Form controls are full-width blocks by default, which is right in a form and
|
|
724
829
|
wrong in a toolbar. Inside a .row they size to the row instead. */
|
|
@@ -736,7 +841,7 @@ body > nav.wide { max-width: none; --nw: 66rem }
|
|
|
736
841
|
.grid + * { margin-top: 1.1rem }
|
|
737
842
|
.card {
|
|
738
843
|
container-type: inline-size; /* query it: @container (min-width: …) */
|
|
739
|
-
border: 1px solid var(--bd); border-radius: var(--r); padding: .
|
|
844
|
+
border: 1px solid var(--bd); border-radius: var(--r); padding: 1.1rem;
|
|
740
845
|
background: var(--el); box-shadow: var(--up2);
|
|
741
846
|
display: flex; flex-direction: column; /* so footers line up across a row */
|
|
742
847
|
gap: .85rem; /* spacing is the container's job */
|
|
@@ -803,27 +908,33 @@ body > nav.wide { max-width: none; --nw: 66rem }
|
|
|
803
908
|
}
|
|
804
909
|
|
|
805
910
|
/* card anatomy: an optional header, footer, and full-bleed media */
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
911
|
+
/* No fill on the header and footer. A tinted strip across the top of a card is
|
|
912
|
+
the old panel-heading pattern and it is what dates a sheet on sight; a
|
|
913
|
+
hairline does the same separating job and lets the card read as one surface. */
|
|
914
|
+
.card > :is(header, footer) { margin-inline: -1.1rem; padding-inline: 1.1rem }
|
|
809
915
|
.card > header {
|
|
810
|
-
margin-top: -.
|
|
811
|
-
border-bottom: 1px solid var(--bd);
|
|
916
|
+
margin-top: -.25rem; padding-bottom: .7rem; font-weight: 600;
|
|
917
|
+
border-bottom: 1px solid var(--bd);
|
|
812
918
|
}
|
|
813
919
|
.card > footer {
|
|
814
|
-
margin-top: auto; margin-bottom: -.
|
|
920
|
+
margin-top: auto; margin-bottom: -.25rem; padding-top: .7rem;
|
|
815
921
|
color: var(--mut); font-size: .85em;
|
|
816
|
-
border-top: 1px solid var(--bd);
|
|
922
|
+
border-top: 1px solid var(--bd);
|
|
817
923
|
}
|
|
818
924
|
.card > :is(img, video):first-child {
|
|
819
|
-
align-self: stretch; width: auto; max-width: none; margin:
|
|
925
|
+
align-self: stretch; width: auto; max-width: none; margin: -1.1rem -1.1rem 0;
|
|
820
926
|
aspect-ratio: 16 / 9; object-fit: cover;
|
|
821
927
|
border: 0; border-radius: var(--r) var(--r) 0 0; box-shadow: none;
|
|
822
928
|
}
|
|
823
|
-
/*
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
.
|
|
929
|
+
/* A background layer rather than a border or an inset shadow: a border would
|
|
930
|
+
shift the card's geometry, and an inset shadow does not follow the corner
|
|
931
|
+
radius cleanly at this size — it reads as a detached bar with gaps at the
|
|
932
|
+
top corners. A gradient is clipped by the radius, so the tone hugs the curve. */
|
|
933
|
+
.card[data-tone] {
|
|
934
|
+
background: linear-gradient(var(--ac), var(--ac)) top / 100% 3px no-repeat,
|
|
935
|
+
var(--el);
|
|
936
|
+
}
|
|
937
|
+
|
|
827
938
|
/* a card that is itself a link or button reacts like one */
|
|
828
939
|
:is(a, button).card {
|
|
829
940
|
display: flex; width: 100%; /* keep the column from .card */
|
|
@@ -832,7 +943,7 @@ body > nav.wide { max-width: none; --nw: 66rem }
|
|
|
832
943
|
}
|
|
833
944
|
:is(a, button).card:hover:not(:disabled) {
|
|
834
945
|
border-color: color-mix(in srgb, var(--ac) 45%, var(--bd));
|
|
835
|
-
box-shadow: var(--up2), 0 12px 26px -14px
|
|
946
|
+
box-shadow: var(--up2), 0 12px 26px -14px color-mix(in srgb, var(--sh) 40%, transparent);
|
|
836
947
|
translate: 0 -2px;
|
|
837
948
|
}
|
|
838
949
|
:is(a, button).card:active:not(:disabled) { translate: 0 0 }
|
|
@@ -844,7 +955,7 @@ body > nav.wide { max-width: none; --nw: 66rem }
|
|
|
844
955
|
@media (prefers-reduced-motion: no-preference) and (update: fast) { html { scroll-behavior: smooth } }
|
|
845
956
|
:target { scroll-margin-top: 4rem } /* clears the toolbar */
|
|
846
957
|
:is(h1,h2,h3,h4):target { background: color-mix(in srgb, var(--ac) 12%, transparent) }
|
|
847
|
-
a[target=_blank]::after { content: " ↗"; font-size: .85em; opacity: .6 }
|
|
958
|
+
a[target=_blank]:not(:has(> img, > svg))::after { content: " ↗"; font-size: .85em; opacity: .6 }
|
|
848
959
|
a:has(> img), a:has(> code) { text-decoration: none }
|
|
849
960
|
sup, sub { line-height: 0; font-size: .75em } /* stop them stretching lines */
|
|
850
961
|
q { quotes: "“" "”" "‘" "’" }
|
|
@@ -863,13 +974,28 @@ input:user-invalid, textarea:user-invalid { border-color: light-dark(#c0392b, #f
|
|
|
863
974
|
input:user-invalid + small { color: light-dark(#c0392b, #ff6b6b) }
|
|
864
975
|
summary:hover { color: var(--ac) }
|
|
865
976
|
/* thin scrollbars, macOS weight */
|
|
866
|
-
|
|
977
|
+
/* scrollbar-width is Safari 18.2, scrollbar-color Safari 26.2 — both younger
|
|
978
|
+
than the floor, and both left ungated on purpose: there is no fallback to
|
|
979
|
+
write. An engine without them paints its own scrollbar, which is exactly
|
|
980
|
+
what a scrollbar should look like there anyway. Same reasoning for
|
|
981
|
+
accent-color at the top of the sheet. */
|
|
982
|
+
/* Not `*`: the page's own scrollbar keeps the width the OS gave it. Thinning
|
|
983
|
+
every scroller on the page narrows hit targets the sheet never made, which
|
|
984
|
+
is a target-size problem for anyone using a pointer imprecisely. */
|
|
985
|
+
:is(pre, table, textarea, dialog, [popover], menu, fieldset,
|
|
986
|
+
[role=group], [data-carousel], [data-layout=sidebar] > *) {
|
|
987
|
+
scrollbar-width: thin;
|
|
988
|
+
scrollbar-color: color-mix(in srgb, var(--fg) 25%, transparent) transparent;
|
|
989
|
+
}
|
|
867
990
|
|
|
868
991
|
@page { margin: 2cm }
|
|
869
992
|
@media print {
|
|
870
993
|
:root { --bg: #fff; --el: #fff; --fg: #000; --bd: #999 }
|
|
871
|
-
body >
|
|
872
|
-
|
|
994
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > nav { display: none }
|
|
995
|
+
/* the footer stays: the legal line, the contact and the citations live there,
|
|
996
|
+
and a printout without them is missing the part people keep */
|
|
997
|
+
:is(body, body > :where(#root, #app, #__next, [data-fertig])) > footer { box-shadow: none; margin-bottom: 0 }
|
|
998
|
+
body > *, body > :where(#root, #app, #__next, [data-fertig]) > * { border: 0; max-width: none }
|
|
873
999
|
p, blockquote, li { orphans: 3; widows: 3 } /* no stranded single lines */
|
|
874
1000
|
a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .8em; color: #555 }
|
|
875
1001
|
pre, table, figure, details, tr, img { break-inside: avoid }
|
|
@@ -884,6 +1010,9 @@ summary:hover { color: var(--ac) }
|
|
|
884
1010
|
min-height: 44px;
|
|
885
1011
|
}
|
|
886
1012
|
input[type=checkbox], input[type=radio] { width: 1.4em; height: 1.4em }
|
|
1013
|
+
/* 44px is the AAA target (2.5.5). The small variant opts down to the 24px
|
|
1014
|
+
AA floor (2.5.8) rather than stopping being small on a phone. */
|
|
1015
|
+
:is(button, a, input, select, label)[data-size=sm] { min-height: 32px }
|
|
887
1016
|
}
|
|
888
1017
|
|
|
889
1018
|
/* the OS is already inverting; stop images being inverted twice */
|
|
@@ -916,8 +1045,45 @@ summary:hover { color: var(--ac) }
|
|
|
916
1045
|
|
|
917
1046
|
@media (prefers-reduced-motion: no-preference) and (update: fast) {
|
|
918
1047
|
a, button, input, select, textarea, summary, tr,
|
|
919
|
-
[type=submit], [type=button], [type=reset]
|
|
920
|
-
|
|
1048
|
+
[type=submit], [type=button], [type=reset], .card, .badge,
|
|
1049
|
+
[role=tab], [role=group] > *, [popover] :is(a, button) {
|
|
1050
|
+
transition: box-shadow var(--dur-1) var(--ease),
|
|
1051
|
+
background-color var(--dur-1) var(--ease),
|
|
1052
|
+
border-color var(--dur-1) var(--ease),
|
|
1053
|
+
color var(--dur-1) var(--ease),
|
|
1054
|
+
translate .08s var(--ease-out);
|
|
1055
|
+
}
|
|
1056
|
+
/* the ring must never be animated — it has to be there the instant focus
|
|
1057
|
+
lands, not fade in after it */
|
|
1058
|
+
:focus-visible { transition: none }
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
/* ---- below the floor ---------------------------------------
|
|
1063
|
+
Everything above assumes light-dark(). An engine without it does not
|
|
1064
|
+
degrade to "unstyled but readable" — every colour token becomes invalid at
|
|
1065
|
+
computed-value time, so backgrounds go transparent and colour is inherited,
|
|
1066
|
+
and the page can land unreadable rather than plain. These are the same
|
|
1067
|
+
colours flattened to sRGB. Last in the layer, so it wins on order over the
|
|
1068
|
+
token block at the top. Nothing else needs a twin: an invalid --ac on a
|
|
1069
|
+
[data-tone] or [data-accent] element just inherits the one from :root. */
|
|
1070
|
+
@supports not (color: light-dark(#000, #fff)) {
|
|
1071
|
+
:root {
|
|
1072
|
+
--bg: #ece9e5; --el: #fefdfc; --face: #f8f5f2; --fg: #1d1a17;
|
|
1073
|
+
--mut: #5e5a55; --bd: #1d1a1721; --tb: #f8f5f2d9;
|
|
1074
|
+
--ac: #494fcc; --on-ac: #fff;
|
|
1075
|
+
}
|
|
1076
|
+
@media (prefers-color-scheme: dark) {
|
|
1077
|
+
:root:not([data-theme=light]) {
|
|
1078
|
+
--bg: #0f0d0a; --el: #1a1815; --face: #24211e; --fg: #f0eeec;
|
|
1079
|
+
--mut: #aba7a2; --bd: #f0eeec21; --tb: #24211ed9;
|
|
1080
|
+
--ac: #a3b2fe; --on-ac: #06121f;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
[data-theme=dark] {
|
|
1084
|
+
--bg: #0f0d0a; --el: #1a1815; --face: #24211e; --fg: #f0eeec;
|
|
1085
|
+
--mut: #aba7a2; --bd: #f0eeec21; --tb: #24211ed9;
|
|
1086
|
+
--ac: #a3b2fe; --on-ac: #06121f;
|
|
921
1087
|
}
|
|
922
1088
|
}
|
|
923
1089
|
|
|
@@ -932,6 +1098,9 @@ summary:hover { color: var(--ac) }
|
|
|
932
1098
|
.card, .badge, details, dialog, [popover], fieldset, table {
|
|
933
1099
|
border: 1px solid CanvasText;
|
|
934
1100
|
}
|
|
935
|
-
[role=tab]
|
|
1101
|
+
/* [role=tab] sets border:0, so there is nothing for border-color to land
|
|
1102
|
+
on, and the rule above just removed the box-shadow that marked it */
|
|
1103
|
+
[role=tab][aria-selected=true] { border-bottom: 2px solid Highlight }
|
|
1104
|
+
button[aria-pressed=true] { border-color: Highlight }
|
|
936
1105
|
}
|
|
937
1106
|
}
|