actual-css 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -35
- package/dist/actual.css +1 -20
- package/dist/actual.full.css +679 -221
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/design-notes/app-ui.md +33 -0
- package/docs/design-notes/enhancement-contract.md +25 -0
- package/docs/design-notes/grid.md +177 -4
- package/docs/design-notes/references.md +1 -1
- package/docs/navigation.json +7 -1
- package/docs/pages/components/alert.md +5 -0
- package/docs/pages/components/app-navigation.md +34 -0
- package/docs/pages/components/avatar.md +10 -0
- package/docs/pages/components/badge.md +2 -0
- package/docs/pages/components/button.md +30 -0
- package/docs/pages/components/card.md +5 -1
- package/docs/pages/components/fab.md +2 -0
- package/docs/pages/components/indicator.md +52 -0
- package/docs/pages/components/list.md +62 -0
- package/docs/pages/components/navbar.md +6 -0
- package/docs/pages/components/rating.md +68 -0
- package/docs/pages/components/steps.md +57 -0
- package/docs/pages/enhancements/flyout.md +2 -2
- package/docs/pages/enhancements/tabs.md +1 -2
- package/docs/pages/examples/overview.md +8 -0
- package/docs/pages/forms/otp.md +32 -0
- package/docs/pages/forms/states-and-sizes.md +3 -3
- package/docs/pages/guides/integrating-actual-css.md +6 -0
- package/docs/pages/layout/app-layout.md +57 -0
- package/docs/pages/layout/app-shell.md +28 -7
- package/docs/pages/layout/center.md +12 -1
- package/docs/pages/layout/choosing.md +15 -0
- package/docs/pages/layout/grid.md +41 -8
- package/docs/pages/layout/sidebar-layout.md +19 -0
- package/docs/pages/layout/stack.md +17 -0
- package/docs/pages/patterns/icon-slot.md +61 -0
- package/docs/pages/patterns/structured-lists.md +38 -40
- package/package.json +3 -2
- package/scripts/reserved-classes.json +17 -0
- package/src/css/components/app-nav.css +76 -0
- package/src/css/components/avatar.css +4 -0
- package/src/css/components/badge.css +1 -13
- package/src/css/components/button.css +8 -38
- package/src/css/components/card.css +11 -11
- package/src/css/components/chat.css +0 -6
- package/src/css/components/drawer.css +0 -6
- package/src/css/components/fab.css +1 -7
- package/src/css/components/index.css +5 -0
- package/src/css/components/indicator.css +47 -0
- package/src/css/components/list.css +83 -0
- package/src/css/components/menu.css +0 -7
- package/src/css/components/modal.css +0 -17
- package/src/css/components/navbar.css +6 -15
- package/src/css/components/rating.css +101 -0
- package/src/css/components/steps.css +122 -0
- package/src/css/components/tab.css +20 -22
- package/src/css/components/tooltip.css +1 -10
- package/src/css/core/focus.css +0 -8
- package/src/css/core/theme.css +0 -10
- package/src/css/core/tokens.css +1 -2
- package/src/css/effects/aura.css +1 -10
- package/src/css/forms/choice-card.css +6 -15
- package/src/css/forms/otp.css +7 -2
- package/src/css/forms/range.css +14 -4
- package/src/css/layout/app-layout.css +102 -0
- package/src/css/layout/app-shell.css +1 -1
- package/src/css/layout/center.css +1 -0
- package/src/css/layout/container-query.css +6 -4
- package/src/css/layout/grid.css +25 -11
- package/src/css/layout/index.css +4 -2
- package/src/css/layout/sidebar-layout.css +22 -1
- package/src/css/typography/prose.css +4 -0
- package/src/js/tooltip.js +2 -1
- package/docs/pages/layout/list.md +0 -41
package/dist/actual.full.css
CHANGED
|
@@ -208,9 +208,8 @@ select {
|
|
|
208
208
|
@media (forced-colors: active) {
|
|
209
209
|
:root,
|
|
210
210
|
[data-theme] {
|
|
211
|
-
--state-selected: Highlight;
|
|
212
|
-
--state-selected-fg: HighlightText;
|
|
213
211
|
--state-disabled: GrayText;
|
|
212
|
+
--disabled-opacity: 1;
|
|
214
213
|
}
|
|
215
214
|
}
|
|
216
215
|
|
|
@@ -436,16 +435,6 @@ select {
|
|
|
436
435
|
}
|
|
437
436
|
}
|
|
438
437
|
|
|
439
|
-
@media (forced-colors: active) {
|
|
440
|
-
:root,
|
|
441
|
-
[data-theme] {
|
|
442
|
-
--focus-outline-color: Highlight;
|
|
443
|
-
--focus-ring-shadow: none;
|
|
444
|
-
|
|
445
|
-
--backdrop-fill: Canvas;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
438
|
@media (prefers-contrast: more) {
|
|
450
439
|
:root,
|
|
451
440
|
/* The repeated attribute intentionally outranks single-attribute preset
|
|
@@ -680,14 +669,6 @@ small {
|
|
|
680
669
|
:focus:not(:focus-visible) {
|
|
681
670
|
outline: none;
|
|
682
671
|
}
|
|
683
|
-
|
|
684
|
-
/* Pointer focus must still signal in high-contrast mode, where the shared
|
|
685
|
-
box-shadow rings are disabled (--focus-ring-shadow: none). */
|
|
686
|
-
@media (forced-colors: active) {
|
|
687
|
-
:focus:not(:focus-visible) {
|
|
688
|
-
outline: var(--focus-outline);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
672
|
}
|
|
692
673
|
|
|
693
674
|
@media print {
|
|
@@ -750,6 +731,10 @@ small {
|
|
|
750
731
|
/* Vertical rhythm belongs to the direct block that participates in the
|
|
751
732
|
document, so it must not be injected into the internals of a nested
|
|
752
733
|
component (e.g. a .table-wrap around a .table would trap the margin). */
|
|
734
|
+
.prose > * {
|
|
735
|
+
margin-block-end: 0;
|
|
736
|
+
}
|
|
737
|
+
|
|
753
738
|
.prose > * + * {
|
|
754
739
|
margin-block-start: var(--prose-flow);
|
|
755
740
|
}
|
|
@@ -1044,12 +1029,13 @@ small {
|
|
|
1044
1029
|
* .center centered content column
|
|
1045
1030
|
* .frame fixed-ratio media frame
|
|
1046
1031
|
* .app-shell full-height application shell
|
|
1032
|
+
* .app-layout topbar + scrolling main + bottom/side app navigation
|
|
1047
1033
|
* .media fixed media + flexible content
|
|
1048
1034
|
* .switcher peer regions: all horizontal or all stacked
|
|
1049
1035
|
* .sidebar-layout main content + secondary aside
|
|
1050
1036
|
* .grid responsive collection; item width is the contract
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1037
|
+
* .grid-N structural density of N; balanced inside a container named
|
|
1038
|
+
* actual-grid (in grid.css)
|
|
1053
1039
|
* .column-layout explicit 12-column region composition; the author owns
|
|
1054
1040
|
* narrow-container recomposition
|
|
1055
1041
|
* .scroller theme-aware scrollbar density and colour
|
|
@@ -1060,10 +1046,12 @@ small {
|
|
|
1060
1046
|
*
|
|
1061
1047
|
* Prefer these compositions over viewport breakpoint utilities.
|
|
1062
1048
|
*/
|
|
1063
|
-
/*
|
|
1049
|
+
/* Convenience helper: establishes a query container named actual-grid for
|
|
1064
1050
|
descendants. Use it for a component that changes representation according
|
|
1065
|
-
to its allocated inline size, and to
|
|
1066
|
-
(see grid.css).
|
|
1051
|
+
to its allocated inline size, and to give .grid-N the named context it needs
|
|
1052
|
+
for balanced subdivision (see grid.css). The name matters: grids only respond
|
|
1053
|
+
to an explicitly actual-grid-named container, never to an incidental
|
|
1054
|
+
query container. Layout primitives are responsive without it; this adds
|
|
1067
1055
|
precision, it is never a prerequisite.
|
|
1068
1056
|
|
|
1069
1057
|
The element takes inline-size containment, so put it on a normal
|
|
@@ -1071,7 +1059,7 @@ small {
|
|
|
1071
1059
|
float, an inline-block): its contents would stop contributing to its own
|
|
1072
1060
|
inline size and it would collapse. */
|
|
1073
1061
|
.container-query {
|
|
1074
|
-
container
|
|
1062
|
+
container: actual-grid / inline-size;
|
|
1075
1063
|
}
|
|
1076
1064
|
|
|
1077
1065
|
/* Vertical flow. .stack owns the block axis only: block margins reset, inline
|
|
@@ -1127,6 +1115,7 @@ small {
|
|
|
1127
1115
|
*/
|
|
1128
1116
|
.center {
|
|
1129
1117
|
box-sizing: content-box;
|
|
1118
|
+
inline-size: calc(100% - var(--center-pad, 1rem) * 2);
|
|
1130
1119
|
max-inline-size: var(--center-size, 72rem);
|
|
1131
1120
|
margin-inline: auto;
|
|
1132
1121
|
padding-inline: var(--center-pad, 1rem);
|
|
@@ -1152,13 +1141,116 @@ small {
|
|
|
1152
1141
|
object-fit: cover;
|
|
1153
1142
|
}
|
|
1154
1143
|
|
|
1155
|
-
/* Full-height
|
|
1144
|
+
/* Full-height single-column document shell. */
|
|
1156
1145
|
.app-shell {
|
|
1157
1146
|
min-block-size: var(--viewport-block);
|
|
1158
1147
|
display: flex;
|
|
1159
1148
|
flex-direction: column;
|
|
1160
1149
|
}
|
|
1161
1150
|
|
|
1151
|
+
/*
|
|
1152
|
+
* Specialized application shell recipe: one exact DOM structure uses a
|
|
1153
|
+
* bottom navigation bar when narrow and a labelled side navigation when space
|
|
1154
|
+
* permits. The main region owns scrolling, keeping both pieces of application
|
|
1155
|
+
* chrome available without fixed offsets. This is not a generic page wrapper
|
|
1156
|
+
* or a public-site layout; all three direct regions are part of the contract.
|
|
1157
|
+
*
|
|
1158
|
+
* Public hook:
|
|
1159
|
+
* --app-nav-side-size
|
|
1160
|
+
*/
|
|
1161
|
+
.app-layout {
|
|
1162
|
+
--app-nav-side-size: 12rem;
|
|
1163
|
+
|
|
1164
|
+
display: grid;
|
|
1165
|
+
grid-template:
|
|
1166
|
+
"topbar" auto
|
|
1167
|
+
"main" minmax(0, 1fr)
|
|
1168
|
+
"nav" auto
|
|
1169
|
+
/ minmax(0, 1fr);
|
|
1170
|
+
block-size: var(--viewport-block);
|
|
1171
|
+
min-inline-size: 0;
|
|
1172
|
+
overflow: hidden;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.app-layout > .topbar {
|
|
1176
|
+
grid-area: topbar;
|
|
1177
|
+
padding-block-start: calc(var(--space-30) + env(safe-area-inset-top));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.app-layout > .app-main {
|
|
1181
|
+
grid-area: main;
|
|
1182
|
+
min-inline-size: 0;
|
|
1183
|
+
min-block-size: 0;
|
|
1184
|
+
overflow: auto;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.app-layout > .app-nav {
|
|
1188
|
+
grid-area: nav;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/* The main grid area keeps the FAB clear of bottom navigation without making
|
|
1192
|
+
either component copy the other's geometry. */
|
|
1193
|
+
.app-layout > .fab {
|
|
1194
|
+
position: relative;
|
|
1195
|
+
grid-area: main;
|
|
1196
|
+
place-self: end;
|
|
1197
|
+
inset: auto;
|
|
1198
|
+
margin: var(--fab-offset);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
@media (min-width: 48rem) {
|
|
1202
|
+
.app-layout {
|
|
1203
|
+
grid-template:
|
|
1204
|
+
"nav topbar" auto
|
|
1205
|
+
"nav main" minmax(0, 1fr)
|
|
1206
|
+
/ var(--app-nav-side-size) minmax(0, 1fr);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
.app-layout > .app-nav {
|
|
1210
|
+
position: sticky;
|
|
1211
|
+
inset-block: 0;
|
|
1212
|
+
grid-auto-flow: row;
|
|
1213
|
+
grid-auto-rows: minmax(var(--control-size-lg), auto);
|
|
1214
|
+
grid-auto-columns: auto;
|
|
1215
|
+
align-content: start;
|
|
1216
|
+
gap: var(--space-20);
|
|
1217
|
+
min-block-size: 0;
|
|
1218
|
+
padding-block: max(var(--space-20), env(safe-area-inset-top))
|
|
1219
|
+
max(var(--space-20), env(safe-area-inset-bottom));
|
|
1220
|
+
padding-inline: var(--space-20);
|
|
1221
|
+
border-block-start: 0;
|
|
1222
|
+
border-inline-end: var(--border-width) solid var(--ui-border, var(--border));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.app-layout > .app-nav > a {
|
|
1226
|
+
grid-template-rows: auto;
|
|
1227
|
+
grid-template-columns: 1.5rem minmax(0, 1fr);
|
|
1228
|
+
place-content: center start;
|
|
1229
|
+
align-items: center;
|
|
1230
|
+
justify-items: start;
|
|
1231
|
+
gap: var(--space-30);
|
|
1232
|
+
padding-inline: var(--space-30);
|
|
1233
|
+
text-align: start;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/* Side navigation no longer occupies the viewport's bottom safe area. */
|
|
1237
|
+
.app-layout > .fab {
|
|
1238
|
+
margin-block-end: max(var(--fab-offset), env(safe-area-inset-bottom));
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
@media print {
|
|
1243
|
+
.app-layout {
|
|
1244
|
+
display: block;
|
|
1245
|
+
block-size: auto;
|
|
1246
|
+
overflow: visible;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.app-layout > .app-main {
|
|
1250
|
+
overflow: visible;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1162
1254
|
/* Media object: fixed-width leading element (avatar, icon, image) + flexible
|
|
1163
1255
|
trailing content. Replaces the repeated "auto minmax(0, 1fr)" pattern in
|
|
1164
1256
|
author cards, comments, meta rows, and similar compositions.
|
|
@@ -1206,7 +1298,10 @@ small {
|
|
|
1206
1298
|
* --sidebar-content-min
|
|
1207
1299
|
* --sidebar-layout-size
|
|
1208
1300
|
*
|
|
1209
|
-
* Child contract: the first and
|
|
1301
|
+
* Child contract: the main region is first and the aside second by default
|
|
1302
|
+
* (aside on the inline end). .reverse expects the aside first in the DOM, so
|
|
1303
|
+
* visual, reading and keyboard order all agree — the role rules swap so the
|
|
1304
|
+
* aside still takes its preferred width and the main still reclaims the row.
|
|
1210
1305
|
*/
|
|
1211
1306
|
.sidebar-layout {
|
|
1212
1307
|
display: flex;
|
|
@@ -1215,15 +1310,33 @@ small {
|
|
|
1215
1310
|
align-items: flex-start;
|
|
1216
1311
|
}
|
|
1217
1312
|
|
|
1313
|
+
/* Main region — flexible, reclaims the full row when the aside wraps. */
|
|
1218
1314
|
.sidebar-layout > :first-child {
|
|
1219
1315
|
flex-basis: 0;
|
|
1220
1316
|
flex-grow: 999;
|
|
1221
1317
|
min-inline-size: min(100%, var(--sidebar-content-min, 30rem));
|
|
1222
1318
|
}
|
|
1223
1319
|
|
|
1320
|
+
/* Aside region — preferred width, never forces content min-width. */
|
|
1224
1321
|
.sidebar-layout > :last-child {
|
|
1225
1322
|
flex-basis: var(--sidebar-layout-size, 18rem);
|
|
1226
1323
|
flex-grow: 1;
|
|
1324
|
+
min-inline-size: 0;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/* .reverse: the aside is first in the DOM and the main last, so each role
|
|
1328
|
+
rule above would hit the wrong element. Swap the sizing per child; the
|
|
1329
|
+
higher-specificity reverse rules outrank the positional defaults. */
|
|
1330
|
+
.sidebar-layout.reverse > :last-child {
|
|
1331
|
+
flex-basis: 0;
|
|
1332
|
+
flex-grow: 999;
|
|
1333
|
+
min-inline-size: min(100%, var(--sidebar-content-min, 30rem));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.sidebar-layout.reverse > :first-child {
|
|
1337
|
+
flex-basis: var(--sidebar-layout-size, 18rem);
|
|
1338
|
+
flex-grow: 1;
|
|
1339
|
+
min-inline-size: 0;
|
|
1227
1340
|
}
|
|
1228
1341
|
|
|
1229
1342
|
/*
|
|
@@ -1252,7 +1365,8 @@ small {
|
|
|
1252
1365
|
/* Recipe:
|
|
1253
1366
|
- collection whose item width is the contract -> .grid, tune --grid-min
|
|
1254
1367
|
- collection whose column density is the contract -> .grid-2/3/4/6
|
|
1255
|
-
- balanced subdivision of that density ->
|
|
1368
|
+
- balanced subdivision of that density -> place it inside any size query
|
|
1369
|
+
container (.container-query is the convenience helper for that context)
|
|
1256
1370
|
- peer regions that switch as a group -> .switcher
|
|
1257
1371
|
- content + aside -> .sidebar-layout
|
|
1258
1372
|
- exact custom track templates -> --grid-columns with an explicit collapse
|
|
@@ -1313,7 +1427,13 @@ small {
|
|
|
1313
1427
|
min-inline-size: 0;
|
|
1314
1428
|
}
|
|
1315
1429
|
|
|
1316
|
-
/* Balanced subdivision,
|
|
1430
|
+
/* Balanced subdivision, activated by an ancestor container named actual-grid.
|
|
1431
|
+
* A .grid-N inside such a container enters only divisor states of N. The name
|
|
1432
|
+
* is deliberate: a grid must not key off the nearest anonymous query container,
|
|
1433
|
+
* or a region that sets container-type: inline-size for an unrelated reason
|
|
1434
|
+
* would silently change the grid. The author opts the grid in by naming the
|
|
1435
|
+
* context — .container-query is the official helper, or declare
|
|
1436
|
+
* container: actual-grid / inline-size on your own region.
|
|
1317
1437
|
*
|
|
1318
1438
|
* .grid-2 2 -> 1
|
|
1319
1439
|
* .grid-3 3 -> 1
|
|
@@ -1330,6 +1450,11 @@ small {
|
|
|
1330
1450
|
* inline contribution and collapses it inside .cluster, .topbar, floats, and
|
|
1331
1451
|
* any shrink-to-fit box. See docs/design-notes/grid.md.
|
|
1332
1452
|
*
|
|
1453
|
+
* Without an actual-grid ancestor none of these @container rules resolve, so
|
|
1454
|
+
* the auto-fill baseline above stays. The min-width: 0 rule is the lowest
|
|
1455
|
+
* step: it pins the grid to one column inside a container before any larger
|
|
1456
|
+
* threshold fires (a bare grid would otherwise show a 2 + 1 orphan at 28-48rem).
|
|
1457
|
+
*
|
|
1333
1458
|
* Three shared thresholds, not one calibrated per preset: 28rem opens the
|
|
1334
1459
|
* first horizontal density, 48rem opens three columns, 64rem opens the dense
|
|
1335
1460
|
* 4 and 6. N already carries the author's statement about item size, so
|
|
@@ -1341,28 +1466,30 @@ small {
|
|
|
1341
1466
|
* Ascending min-width only: the last matching step wins by cascade order.
|
|
1342
1467
|
*/
|
|
1343
1468
|
@supports (container-type: inline-size) {
|
|
1344
|
-
|
|
1345
|
-
grid-
|
|
1469
|
+
@container actual-grid (min-width: 0px) {
|
|
1470
|
+
:where(.grid-2, .grid-3, .grid-4, .grid-6) {
|
|
1471
|
+
grid-template-columns: 1fr;
|
|
1472
|
+
}
|
|
1346
1473
|
}
|
|
1347
1474
|
|
|
1348
|
-
@container (min-width: 28rem) {
|
|
1349
|
-
|
|
1475
|
+
@container actual-grid (min-width: 28rem) {
|
|
1476
|
+
:where(.grid-2, .grid-4, .grid-6) {
|
|
1350
1477
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1351
1478
|
}
|
|
1352
1479
|
}
|
|
1353
1480
|
|
|
1354
|
-
@container (min-width: 48rem) {
|
|
1355
|
-
|
|
1481
|
+
@container actual-grid (min-width: 48rem) {
|
|
1482
|
+
:where(.grid-3, .grid-6) {
|
|
1356
1483
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1357
1484
|
}
|
|
1358
1485
|
}
|
|
1359
1486
|
|
|
1360
|
-
@container (min-width: 64rem) {
|
|
1361
|
-
|
|
1487
|
+
@container actual-grid (min-width: 64rem) {
|
|
1488
|
+
:where(.grid-4) {
|
|
1362
1489
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1363
1490
|
}
|
|
1364
1491
|
|
|
1365
|
-
|
|
1492
|
+
:where(.grid-6) {
|
|
1366
1493
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
1367
1494
|
}
|
|
1368
1495
|
}
|
|
@@ -2610,12 +2737,22 @@ small {
|
|
|
2610
2737
|
opacity: var(--disabled-opacity);
|
|
2611
2738
|
}
|
|
2612
2739
|
|
|
2613
|
-
/* Forced colors:
|
|
2614
|
-
|
|
2740
|
+
/* Forced colors: the track must stay a visible line against the page (Canvas
|
|
2741
|
+
would merge with it) and the thumb needs a real boundary to remain
|
|
2742
|
+
distinguishable from the track. Disabled keeps GrayText via :disabled.
|
|
2743
|
+
Focus stays on the transparent outline of :focus-visible. */
|
|
2615
2744
|
@media (forced-colors: active) {
|
|
2616
2745
|
.range {
|
|
2617
|
-
--range-
|
|
2618
|
-
--range-
|
|
2746
|
+
--range-track-bg: CanvasText;
|
|
2747
|
+
--range-thumb-bg: ButtonFace;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
.range::-webkit-slider-thumb {
|
|
2751
|
+
border: var(--border-width) solid ButtonText;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
.range::-moz-range-thumb {
|
|
2755
|
+
border: var(--border-width) solid ButtonText;
|
|
2619
2756
|
}
|
|
2620
2757
|
}
|
|
2621
2758
|
|
|
@@ -2752,28 +2889,19 @@ small {
|
|
|
2752
2889
|
transform: scale(1);
|
|
2753
2890
|
}
|
|
2754
2891
|
|
|
2892
|
+
/* The native input is hidden and the checked marker is custom-drawn: without
|
|
2893
|
+
this repair, forced colors pushes disc and check to the same system color
|
|
2894
|
+
and the state disappears. Highlight/Canvas/HighlightText are preserved as-is,
|
|
2895
|
+
so no forced-color-adjust is needed. The pseudo is not focusable — the
|
|
2896
|
+
outline is a separator against the card, not a focus channel. */
|
|
2755
2897
|
@media (forced-colors: active) {
|
|
2756
|
-
.choice-card {
|
|
2757
|
-
--choice-card-border: ButtonText;
|
|
2758
|
-
--choice-card-bg: Canvas;
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
.choice-card:has(:checked) {
|
|
2762
|
-
--choice-card-border: Highlight;
|
|
2763
|
-
/* Pin the selected surface to the system canvas: only the essential
|
|
2764
|
-
marker pseudos opt out of remapping, never the interactive card host. */
|
|
2765
|
-
--choice-card-bg: Canvas;
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
2898
|
.choice-card:has(:checked)::before {
|
|
2769
2899
|
background: Highlight;
|
|
2770
|
-
|
|
2771
|
-
forced-color-adjust: none;
|
|
2900
|
+
outline: 2px solid Canvas;
|
|
2772
2901
|
}
|
|
2773
2902
|
|
|
2774
2903
|
.choice-card:has(:checked)::after {
|
|
2775
2904
|
background: HighlightText;
|
|
2776
|
-
forced-color-adjust: none;
|
|
2777
2905
|
}
|
|
2778
2906
|
}
|
|
2779
2907
|
|
|
@@ -3058,7 +3186,12 @@ small {
|
|
|
3058
3186
|
but it sits in the clipped input extension once every cell is filled, so
|
|
3059
3187
|
the focused edge must be unambiguous on its own — hence the doubled width,
|
|
3060
3188
|
not only the color swap. Width is safe to change: box-sizing is border-box
|
|
3061
|
-
and the cells have a fixed inline/block size, so nothing reflows.
|
|
3189
|
+
and the cells have a fixed inline/block size, so nothing reflows.
|
|
3190
|
+
|
|
3191
|
+
The <input> must be the first direct child, with one cell <span> per
|
|
3192
|
+
character after it: the state rules select the cells as following siblings
|
|
3193
|
+
(input:focus ~ span, input:disabled ~ span, …), so that order is part of the
|
|
3194
|
+
contract. */
|
|
3062
3195
|
.otp > input:focus ~ span {
|
|
3063
3196
|
--otp-cell-border: var(--focus);
|
|
3064
3197
|
--otp-cell-border-width: calc(var(--border-width) * 2);
|
|
@@ -3082,7 +3215,7 @@ small {
|
|
|
3082
3215
|
|
|
3083
3216
|
.otp > input:disabled ~ span {
|
|
3084
3217
|
--otp-cell-bg: var(--surface-subtle);
|
|
3085
|
-
|
|
3218
|
+
--otp-cell-border: var(--state-disabled);
|
|
3086
3219
|
opacity: var(--disabled-opacity);
|
|
3087
3220
|
}
|
|
3088
3221
|
|
|
@@ -3192,7 +3325,8 @@ small {
|
|
|
3192
3325
|
|
|
3193
3326
|
.btn.icon-only {
|
|
3194
3327
|
inline-size: var(--btn-min-size);
|
|
3195
|
-
|
|
3328
|
+
block-size: var(--btn-min-size);
|
|
3329
|
+
padding: 0;
|
|
3196
3330
|
}
|
|
3197
3331
|
|
|
3198
3332
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
@@ -3264,14 +3398,15 @@ small {
|
|
|
3264
3398
|
--ui-fg: var(--intent, currentColor);
|
|
3265
3399
|
--ui-border: transparent;
|
|
3266
3400
|
--btn-radius: var(--radius-sm);
|
|
3267
|
-
--btn-border-block-end-color: transparent;
|
|
3268
|
-
--btn-border-block-end-width: var(--border-width);
|
|
3269
3401
|
|
|
3270
3402
|
min-block-size: auto;
|
|
3271
3403
|
padding-inline: 0;
|
|
3272
3404
|
inline-size: fit-content;
|
|
3273
3405
|
max-inline-size: 100%;
|
|
3274
|
-
border
|
|
3406
|
+
/* No border at all: a link reads through its underline, and the inherited
|
|
3407
|
+
.btn border made transparent would turn into a visible box in forced
|
|
3408
|
+
colors. :focus-visible keeps its own outline. */
|
|
3409
|
+
border: 0;
|
|
3275
3410
|
text-decoration: underline;
|
|
3276
3411
|
text-underline-offset: 0.18em;
|
|
3277
3412
|
}
|
|
@@ -3332,44 +3467,12 @@ a.btn[aria-disabled="true"] {
|
|
|
3332
3467
|
}
|
|
3333
3468
|
|
|
3334
3469
|
@media (forced-colors: active) {
|
|
3335
|
-
.btn {
|
|
3336
|
-
border-color: ButtonText;
|
|
3337
|
-
background: ButtonFace;
|
|
3338
|
-
color: ButtonText;
|
|
3339
|
-
box-shadow: none;
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
.btn.link {
|
|
3343
|
-
border-color: transparent;
|
|
3344
|
-
background: Canvas;
|
|
3345
|
-
color: LinkText;
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
.btn:is([aria-pressed="true"], [aria-current="page"]):not(.link) {
|
|
3349
|
-
border-color: Highlight;
|
|
3350
|
-
background: Highlight;
|
|
3351
|
-
color: HighlightText;
|
|
3352
|
-
forced-color-adjust: none;
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
.btn:is([aria-pressed="true"], [aria-current="page"]):not(.link):focus-visible {
|
|
3356
|
-
outline: var(--focus-outline);
|
|
3357
|
-
box-shadow: none;
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
.btn:hover:not(:where(:disabled, [aria-disabled="true"], .link)) {
|
|
3361
|
-
outline: var(--border-width) solid ButtonText;
|
|
3362
|
-
outline-offset: 2px;
|
|
3363
|
-
}
|
|
3364
|
-
|
|
3365
|
-
.btn:disabled,
|
|
3366
3470
|
a.btn[aria-disabled="true"] {
|
|
3367
|
-
border-color: GrayText;
|
|
3368
|
-
background: Canvas;
|
|
3369
3471
|
color: GrayText;
|
|
3370
|
-
|
|
3472
|
+
border-color: GrayText;
|
|
3371
3473
|
}
|
|
3372
3474
|
}
|
|
3475
|
+
|
|
3373
3476
|
@media print {
|
|
3374
3477
|
:is(button, input[type="button"], input[type="submit"], input[type="reset"]).btn {
|
|
3375
3478
|
display: none !important;
|
|
@@ -3563,21 +3666,9 @@ a.badge[aria-disabled="true"] {
|
|
|
3563
3666
|
background: ButtonText;
|
|
3564
3667
|
}
|
|
3565
3668
|
|
|
3566
|
-
:where(a.badge, button.badge):hover:not(:where(:disabled, [aria-disabled="true"])) {
|
|
3567
|
-
outline: var(--border-width) solid ButtonText;
|
|
3568
|
-
outline-offset: 2px;
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
.badge > button:not(:disabled):hover {
|
|
3572
|
-
outline: var(--border-width) solid currentColor;
|
|
3573
|
-
outline-offset: 1px;
|
|
3574
|
-
}
|
|
3575
|
-
|
|
3576
|
-
button.badge:disabled,
|
|
3577
3669
|
.badge[aria-disabled="true"] {
|
|
3578
|
-
border-color: GrayText;
|
|
3579
|
-
background: Canvas;
|
|
3580
3670
|
color: GrayText;
|
|
3671
|
+
border-color: GrayText;
|
|
3581
3672
|
}
|
|
3582
3673
|
}
|
|
3583
3674
|
@media print {
|
|
@@ -3894,11 +3985,14 @@ a.badge[aria-disabled="true"] {
|
|
|
3894
3985
|
* Surface, border, radius, padding, density, and elevation stay local here.
|
|
3895
3986
|
* Direct header/footer children are structural slots; a direct footer is
|
|
3896
3987
|
* anchored to the card's bottom edge when extra block space is available (so
|
|
3897
|
-
* price/action rows align across equal-height cards in a grid).
|
|
3898
|
-
*
|
|
3988
|
+
* price/action rows align across equal-height cards in a grid). A bare card
|
|
3989
|
+
* owns its direct-child rhythm; a layout primitive on the same element takes
|
|
3990
|
+
* over that layout. Page layout remains the job of markup and layout
|
|
3991
|
+
* primitives outside the card.
|
|
3899
3992
|
*
|
|
3900
3993
|
* Public hooks:
|
|
3901
3994
|
* --card-pad
|
|
3995
|
+
* --card-gap
|
|
3902
3996
|
* --card-radius
|
|
3903
3997
|
* --card-max-inline-size
|
|
3904
3998
|
*
|
|
@@ -3922,6 +4016,7 @@ a.badge[aria-disabled="true"] {
|
|
|
3922
4016
|
|
|
3923
4017
|
.card {
|
|
3924
4018
|
--card-pad: var(--space-40);
|
|
4019
|
+
--card-gap: var(--space-30);
|
|
3925
4020
|
--card-radius: var(--radius-lg);
|
|
3926
4021
|
--card-max-inline-size: none;
|
|
3927
4022
|
--card-bg: var(--ui-bg, var(--surface-raised));
|
|
@@ -3948,10 +4043,14 @@ a.badge[aria-disabled="true"] {
|
|
|
3948
4043
|
:where(.card) {
|
|
3949
4044
|
display: flex;
|
|
3950
4045
|
flex-direction: column;
|
|
4046
|
+
gap: var(--card-gap);
|
|
3951
4047
|
}
|
|
3952
4048
|
|
|
3953
|
-
|
|
4049
|
+
/* Native block margins would add to flex gap because flex-item margins never
|
|
4050
|
+
collapse. Zero specificity keeps intentional child utilities available. */
|
|
4051
|
+
:where(.card) > * {
|
|
3954
4052
|
min-inline-size: 0;
|
|
4053
|
+
margin-block: 0;
|
|
3955
4054
|
}
|
|
3956
4055
|
|
|
3957
4056
|
.card > :where(header, footer) {
|
|
@@ -3978,14 +4077,6 @@ a.badge[aria-disabled="true"] {
|
|
|
3978
4077
|
margin-block-start: auto;
|
|
3979
4078
|
}
|
|
3980
4079
|
|
|
3981
|
-
.card > :first-child {
|
|
3982
|
-
margin-block-start: 0;
|
|
3983
|
-
}
|
|
3984
|
-
|
|
3985
|
-
.card > :last-child {
|
|
3986
|
-
margin-block-end: 0;
|
|
3987
|
-
}
|
|
3988
|
-
|
|
3989
4080
|
.card.raised {
|
|
3990
4081
|
box-shadow: var(--shadow);
|
|
3991
4082
|
}
|
|
@@ -4331,23 +4422,6 @@ html.has-modal-open.had-scrollbar {
|
|
|
4331
4422
|
}
|
|
4332
4423
|
}
|
|
4333
4424
|
|
|
4334
|
-
@media (forced-colors: active) {
|
|
4335
|
-
dialog.modal {
|
|
4336
|
-
border-color: ButtonText;
|
|
4337
|
-
background: Canvas;
|
|
4338
|
-
color: CanvasText;
|
|
4339
|
-
box-shadow: none;
|
|
4340
|
-
}
|
|
4341
|
-
|
|
4342
|
-
dialog.modal::backdrop {
|
|
4343
|
-
background: Canvas;
|
|
4344
|
-
}
|
|
4345
|
-
|
|
4346
|
-
.dialog-icon {
|
|
4347
|
-
border: var(--border-width) solid CanvasText;
|
|
4348
|
-
}
|
|
4349
|
-
}
|
|
4350
|
-
|
|
4351
4425
|
/* Close controls. A close button is an action attached to a surface, so it
|
|
4352
4426
|
never participates in the content flow. The modal and the drawer share the
|
|
4353
4427
|
same simple placement — absolute at the panel's top inline-end, inside the
|
|
@@ -4553,12 +4627,6 @@ dialog.drawer nav {
|
|
|
4553
4627
|
}
|
|
4554
4628
|
}
|
|
4555
4629
|
|
|
4556
|
-
@media (forced-colors: active) {
|
|
4557
|
-
dialog.drawer::backdrop {
|
|
4558
|
-
background: Canvas;
|
|
4559
|
-
}
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
4630
|
@media (prefers-reduced-motion: no-preference) {
|
|
4563
4631
|
dialog.drawer.is-static {
|
|
4564
4632
|
animation: actual-dialog-static var(--duration-slow) var(--ease);
|
|
@@ -4887,13 +4955,6 @@ dialog.drawer > form > header {
|
|
|
4887
4955
|
border-block-start: var(--border-width) solid var(--border);
|
|
4888
4956
|
}
|
|
4889
4957
|
|
|
4890
|
-
@media (forced-colors: active) {
|
|
4891
|
-
.menu-item:focus-visible {
|
|
4892
|
-
outline: var(--border-width) solid ButtonText;
|
|
4893
|
-
outline-offset: -1px;
|
|
4894
|
-
}
|
|
4895
|
-
}
|
|
4896
|
-
|
|
4897
4958
|
/*
|
|
4898
4959
|
* Tooltip — supplemental label overlaid near a trigger.
|
|
4899
4960
|
*
|
|
@@ -4920,6 +4981,7 @@ dialog.drawer > form > header {
|
|
|
4920
4981
|
max-inline-size: min(24rem, calc(var(--viewport-inline) - 2rem));
|
|
4921
4982
|
padding: 0.25em 0.6em;
|
|
4922
4983
|
border-radius: var(--radius-sm);
|
|
4984
|
+
border: var(--border-width) solid transparent;
|
|
4923
4985
|
background: var(--tooltip-bg);
|
|
4924
4986
|
color: var(--tooltip-fg);
|
|
4925
4987
|
font-size: var(--font-size-xs);
|
|
@@ -4998,16 +5060,6 @@ dialog.drawer > form > header {
|
|
|
4998
5060
|
clip-path: polygon(0 50%, 100% 0, 100% 100%);
|
|
4999
5061
|
}
|
|
5000
5062
|
|
|
5001
|
-
@media (forced-colors: active) {
|
|
5002
|
-
.tooltip {
|
|
5003
|
-
--tooltip-bg: Canvas;
|
|
5004
|
-
--tooltip-fg: CanvasText;
|
|
5005
|
-
}
|
|
5006
|
-
|
|
5007
|
-
.tooltip::before {
|
|
5008
|
-
display: none;
|
|
5009
|
-
}
|
|
5010
|
-
}
|
|
5011
5063
|
@media print {
|
|
5012
5064
|
.tooltip {
|
|
5013
5065
|
display: none !important;
|
|
@@ -5556,6 +5608,9 @@ dialog.drawer > form > header {
|
|
|
5556
5608
|
}
|
|
5557
5609
|
}
|
|
5558
5610
|
|
|
5611
|
+
/* The initials element (a span or an accessible <abbr title="…">) fills the box.
|
|
5612
|
+
text-decoration: none neutralizes the UA dotted underline on <abbr title="…">
|
|
5613
|
+
inside the circular avatar. */
|
|
5559
5614
|
.avatar > :first-child:not(:where(img, picture, .badge)) {
|
|
5560
5615
|
display: inline-grid;
|
|
5561
5616
|
place-items: center;
|
|
@@ -5563,6 +5618,7 @@ dialog.drawer > form > header {
|
|
|
5563
5618
|
block-size: 100%;
|
|
5564
5619
|
overflow: hidden;
|
|
5565
5620
|
border-radius: inherit;
|
|
5621
|
+
text-decoration: none;
|
|
5566
5622
|
}
|
|
5567
5623
|
|
|
5568
5624
|
.avatar > img,
|
|
@@ -5691,6 +5747,54 @@ a.avatar[aria-disabled="true"] {
|
|
|
5691
5747
|
z-index: 1;
|
|
5692
5748
|
}
|
|
5693
5749
|
|
|
5750
|
+
/*
|
|
5751
|
+
* Indicator — generic corner positioning for overlaid component content.
|
|
5752
|
+
*
|
|
5753
|
+
* The container only establishes the containing block. The item owns no
|
|
5754
|
+
* paint, size, or ring so badges and other components keep their own visual
|
|
5755
|
+
* contract.
|
|
5756
|
+
*
|
|
5757
|
+
* Internal:
|
|
5758
|
+
* --indicator-translate-x
|
|
5759
|
+
* --indicator-translate-y
|
|
5760
|
+
*/
|
|
5761
|
+
.indicator {
|
|
5762
|
+
position: relative;
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
.indicator-item {
|
|
5766
|
+
--indicator-translate-x: 50%;
|
|
5767
|
+
--indicator-translate-y: -50%;
|
|
5768
|
+
|
|
5769
|
+
position: absolute;
|
|
5770
|
+
inset-block-start: 0;
|
|
5771
|
+
inset-inline-end: 0;
|
|
5772
|
+
z-index: 1;
|
|
5773
|
+
transform: translate(var(--indicator-translate-x), var(--indicator-translate-y));
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5776
|
+
.indicator-item.start {
|
|
5777
|
+
--indicator-translate-x: -50%;
|
|
5778
|
+
|
|
5779
|
+
inset-inline-start: 0;
|
|
5780
|
+
inset-inline-end: auto;
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
.indicator-item.bottom {
|
|
5784
|
+
--indicator-translate-y: 50%;
|
|
5785
|
+
|
|
5786
|
+
inset-block-start: auto;
|
|
5787
|
+
inset-block-end: 0;
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
.indicator-item:dir(rtl) {
|
|
5791
|
+
--indicator-translate-x: -50%;
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
.indicator-item.start:dir(rtl) {
|
|
5795
|
+
--indicator-translate-x: 50%;
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5694
5798
|
/*
|
|
5695
5799
|
* Key — keyboard keys and shortcut tokens for UI chrome.
|
|
5696
5800
|
*
|
|
@@ -5821,12 +5925,6 @@ a.avatar[aria-disabled="true"] {
|
|
|
5821
5925
|
}
|
|
5822
5926
|
}
|
|
5823
5927
|
|
|
5824
|
-
@media (forced-colors: active) {
|
|
5825
|
-
.chat-bubble {
|
|
5826
|
-
border-color: CanvasText;
|
|
5827
|
-
}
|
|
5828
|
-
}
|
|
5829
|
-
|
|
5830
5928
|
/*
|
|
5831
5929
|
* Fixed floating action placement and a native-details speed dial.
|
|
5832
5930
|
* Button owns the action's size, shape, and intent; FAB only owns placement.
|
|
@@ -5897,6 +5995,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
5897
5995
|
max-inline-size: 16rem;
|
|
5898
5996
|
padding: var(--space-10) var(--space-20);
|
|
5899
5997
|
border-radius: var(--radius);
|
|
5998
|
+
border: var(--border-width) solid transparent;
|
|
5900
5999
|
background: var(--surface);
|
|
5901
6000
|
color: var(--text);
|
|
5902
6001
|
box-shadow: var(--shadow);
|
|
@@ -5919,21 +6018,316 @@ a.avatar[aria-disabled="true"] {
|
|
|
5919
6018
|
transform: rotate(45deg);
|
|
5920
6019
|
}
|
|
5921
6020
|
|
|
6021
|
+
@media print {
|
|
6022
|
+
.fab {
|
|
6023
|
+
display: none;
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
|
|
6027
|
+
/*
|
|
6028
|
+
* Rating — integer star rating drawn on native radio inputs.
|
|
6029
|
+
*
|
|
6030
|
+
* The checked radio remains the functional baseline. :has() progressively
|
|
6031
|
+
* fills preceding stars without reversing DOM or keyboard order. Forced
|
|
6032
|
+
* colors restores the platform radio presentation.
|
|
6033
|
+
*
|
|
6034
|
+
* Public hooks:
|
|
6035
|
+
* --rating-size
|
|
6036
|
+
* --rating-gap
|
|
6037
|
+
* --rating-color
|
|
6038
|
+
* --rating-empty
|
|
6039
|
+
* --rating-star
|
|
6040
|
+
*/
|
|
6041
|
+
:where(.rating) {
|
|
6042
|
+
--intent: initial;
|
|
6043
|
+
}
|
|
6044
|
+
|
|
6045
|
+
.rating {
|
|
6046
|
+
--rating-size: 1.25rem;
|
|
6047
|
+
--rating-gap: 0.1rem;
|
|
6048
|
+
--rating-color: var(--intent, var(--warning));
|
|
6049
|
+
--rating-empty: var(--border);
|
|
6050
|
+
--rating-star: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='2 2 16 16'%3E%3Cpath d='M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 0 0 .95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 0 0-.364 1.118l1.07 3.292c.3.922-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 0 0-1.175 0l-2.8 2.034c-.783.57-1.838-.196-1.539-1.118l1.07-3.292a1 1 0 0 0-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 0 0 .951-.69l1.07-3.292Z'/%3E%3C/svg%3E");
|
|
6051
|
+
|
|
6052
|
+
display: inline-flex;
|
|
6053
|
+
gap: 0;
|
|
6054
|
+
color: var(--rating-color);
|
|
6055
|
+
}
|
|
6056
|
+
|
|
6057
|
+
.rating > input[type="radio"] {
|
|
6058
|
+
appearance: none;
|
|
6059
|
+
flex: 0 0 auto;
|
|
6060
|
+
inline-size: calc(var(--rating-size) + var(--rating-gap));
|
|
6061
|
+
block-size: var(--rating-size);
|
|
6062
|
+
margin: 0;
|
|
6063
|
+
border: 0;
|
|
6064
|
+
background: var(--rating-empty);
|
|
6065
|
+
/* Contiguous radio boxes own hit-testing; the centered mask preserves the
|
|
6066
|
+
star's proportions while leaving a purely visual gap between values. */
|
|
6067
|
+
-webkit-mask: var(--rating-star) center / var(--rating-size) var(--rating-size) no-repeat;
|
|
6068
|
+
mask: var(--rating-star) center / var(--rating-size) var(--rating-size) no-repeat;
|
|
6069
|
+
cursor: pointer;
|
|
6070
|
+
transition: background-color var(--duration);
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
.rating > input[type="radio"]:checked {
|
|
6074
|
+
background: var(--rating-color);
|
|
6075
|
+
}
|
|
6076
|
+
|
|
6077
|
+
.rating > input[type="radio"]:disabled {
|
|
6078
|
+
cursor: not-allowed;
|
|
6079
|
+
opacity: var(--disabled-opacity);
|
|
6080
|
+
}
|
|
6081
|
+
|
|
6082
|
+
.rating > input[type="radio"]:focus-visible {
|
|
6083
|
+
outline: var(--focus-outline);
|
|
6084
|
+
outline-offset: var(--focus-outline-offset);
|
|
6085
|
+
background: var(--rating-color);
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
/* Browsers without :has() still show the selected native radio as one filled
|
|
6089
|
+
star. The enhancement only adds cumulative checked and hover previews. */
|
|
6090
|
+
@supports selector(:has(*)) {
|
|
6091
|
+
.rating > input[type="radio"]:has(~ input[type="radio"]:checked) {
|
|
6092
|
+
background: var(--rating-color);
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
@media (hover: hover) {
|
|
6096
|
+
.rating > input[type="radio"]:not(:disabled):hover,
|
|
6097
|
+
.rating > input[type="radio"]:has(~ input[type="radio"]:not(:disabled):hover) {
|
|
6098
|
+
background: var(--rating-color);
|
|
6099
|
+
}
|
|
6100
|
+
|
|
6101
|
+
/* A hovered value previews exactly that value: later stars are emptied,
|
|
6102
|
+
including a previously checked star beyond the pointer. */
|
|
6103
|
+
.rating > input[type="radio"]:not(:disabled):hover ~ input[type="radio"] {
|
|
6104
|
+
background: var(--rating-empty);
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
|
|
6109
|
+
.rating.sm {
|
|
6110
|
+
--rating-size: 1rem;
|
|
6111
|
+
}
|
|
6112
|
+
|
|
6113
|
+
.rating.lg {
|
|
6114
|
+
--rating-size: 1.75rem;
|
|
6115
|
+
}
|
|
6116
|
+
|
|
5922
6117
|
@media (forced-colors: active) {
|
|
5923
|
-
.
|
|
5924
|
-
|
|
5925
|
-
|
|
6118
|
+
.rating > input[type="radio"] {
|
|
6119
|
+
appearance: auto;
|
|
6120
|
+
inline-size: auto;
|
|
6121
|
+
block-size: auto;
|
|
6122
|
+
background: Canvas;
|
|
6123
|
+
-webkit-mask: none;
|
|
6124
|
+
mask: none;
|
|
6125
|
+
opacity: 1;
|
|
5926
6126
|
}
|
|
5927
6127
|
}
|
|
5928
6128
|
|
|
6129
|
+
/*
|
|
6130
|
+
* Steps — semantic ordered-list progression. One intrinsic layout: items fill
|
|
6131
|
+
* the row when they fit and hold a minimum width otherwise, so the sequence
|
|
6132
|
+
* scrolls horizontally only when it genuinely cannot fit. No breakpoint decides
|
|
6133
|
+
* this — the item minimum tracks the real content, not an arbitrary width.
|
|
6134
|
+
*
|
|
6135
|
+
* Markers come from the list counter; state stays in .complete and
|
|
6136
|
+
* aria-current="step", so the component needs no runtime.
|
|
6137
|
+
*
|
|
6138
|
+
* Public hooks:
|
|
6139
|
+
* --step-size
|
|
6140
|
+
* --step-min
|
|
6141
|
+
* --step-gap
|
|
6142
|
+
* --step-line-size
|
|
6143
|
+
* --step-marker-radius
|
|
6144
|
+
* --step-complete-mark
|
|
6145
|
+
* --step-connector
|
|
6146
|
+
*
|
|
6147
|
+
* Internal:
|
|
6148
|
+
* --step-marker-bg
|
|
6149
|
+
* --step-marker-border
|
|
6150
|
+
* --step-marker-fg
|
|
6151
|
+
* --step-line
|
|
6152
|
+
*/
|
|
6153
|
+
.steps {
|
|
6154
|
+
--step-size: 1.75rem;
|
|
6155
|
+
--step-min: 7rem;
|
|
6156
|
+
--step-gap: var(--space-20);
|
|
6157
|
+
--step-line-size: var(--border-width);
|
|
6158
|
+
--step-marker-bg: var(--surface);
|
|
6159
|
+
--step-marker-border: var(--border);
|
|
6160
|
+
--step-marker-fg: var(--text-muted);
|
|
6161
|
+
--step-marker-radius: var(--radius-full);
|
|
6162
|
+
--step-line: var(--border);
|
|
6163
|
+
|
|
6164
|
+
counter-reset: actual-step;
|
|
6165
|
+
display: flex;
|
|
6166
|
+
inline-size: 100%;
|
|
6167
|
+
margin: 0;
|
|
6168
|
+
padding: 0;
|
|
6169
|
+
list-style: none;
|
|
6170
|
+
/* Fill when it fits, scroll when it cannot: items never shrink below
|
|
6171
|
+
--step-min, so labels keep a readable width and the sequence scrolls
|
|
6172
|
+
horizontally instead of squishing. overscroll-behavior keeps the scroll
|
|
6173
|
+
from chaining to the page. */
|
|
6174
|
+
overflow-x: auto;
|
|
6175
|
+
overscroll-behavior-inline: contain;
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
.steps > li {
|
|
6179
|
+
position: relative;
|
|
6180
|
+
display: grid;
|
|
6181
|
+
flex: 1 0 var(--step-min);
|
|
6182
|
+
grid-template-rows: var(--step-size) auto;
|
|
6183
|
+
gap: var(--step-gap);
|
|
6184
|
+
justify-items: center;
|
|
6185
|
+
min-inline-size: max(var(--step-size), var(--step-min));
|
|
6186
|
+
color: var(--text-muted);
|
|
6187
|
+
text-align: center;
|
|
6188
|
+
counter-increment: actual-step;
|
|
6189
|
+
}
|
|
6190
|
+
|
|
6191
|
+
.steps > li::before {
|
|
6192
|
+
content: counter(actual-step);
|
|
6193
|
+
position: relative;
|
|
6194
|
+
z-index: 1;
|
|
6195
|
+
display: inline-grid;
|
|
6196
|
+
place-items: center;
|
|
6197
|
+
inline-size: var(--step-size);
|
|
6198
|
+
block-size: var(--step-size);
|
|
6199
|
+
border: var(--border-width) solid var(--step-marker-border);
|
|
6200
|
+
border-radius: var(--step-marker-radius);
|
|
6201
|
+
background: var(--step-marker-bg);
|
|
6202
|
+
color: var(--step-marker-fg);
|
|
6203
|
+
font-size: var(--font-size-sm);
|
|
6204
|
+
font-weight: var(--font-weight-strong);
|
|
6205
|
+
line-height: 1;
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
.steps > li::after {
|
|
6209
|
+
content: "";
|
|
6210
|
+
position: absolute;
|
|
6211
|
+
inset-block-start: calc((var(--step-size) - var(--step-line-size)) / 2);
|
|
6212
|
+
inset-inline-start: calc(50% + var(--step-size) / 2);
|
|
6213
|
+
inline-size: calc(100% - var(--step-size));
|
|
6214
|
+
block-size: var(--step-line-size);
|
|
6215
|
+
/* A custom property resolves its var() before inheritance, so a default
|
|
6216
|
+
--step-connector: var(--step-line) on .steps would freeze the parent line
|
|
6217
|
+
color and a .complete child's connector would never follow its own line.
|
|
6218
|
+
Falling back to the local --step-line here keeps each connector in sync;
|
|
6219
|
+
an explicit --step-connector still wins when the author sets one. */
|
|
6220
|
+
background: var(--step-connector, var(--step-line));
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
.steps > li:last-child::after {
|
|
6224
|
+
content: none;
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
.steps > :is(.complete, [aria-current="step"]) {
|
|
6228
|
+
--step-marker-bg: var(--state-selected);
|
|
6229
|
+
--step-marker-border: var(--state-selected);
|
|
6230
|
+
--step-marker-fg: var(--state-selected-fg);
|
|
6231
|
+
|
|
6232
|
+
color: var(--text);
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
/* Thicker border is the selected affordance in every mode — structural, and it
|
|
6236
|
+
never competes with the outline channel owned by focus. */
|
|
6237
|
+
.steps > :is(.complete, [aria-current="step"])::before {
|
|
6238
|
+
border-width: calc(var(--border-width) * 2);
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
.steps > .complete {
|
|
6242
|
+
--step-line: var(--state-selected);
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6245
|
+
/* A finished step may replace its number with a mark (e.g. a check glyph):
|
|
6246
|
+
--step-complete-mark accepts any content value and falls back to the
|
|
6247
|
+
counter, so existing numbered steppers are unchanged. */
|
|
6248
|
+
.steps > .complete::before {
|
|
6249
|
+
content: var(--step-complete-mark, counter(actual-step));
|
|
6250
|
+
}
|
|
6251
|
+
|
|
6252
|
+
|
|
6253
|
+
/* Navigation */
|
|
6254
|
+
/*
|
|
6255
|
+
* Primary application navigation. The component owns the navigation surface
|
|
6256
|
+
* and item states; app-layout owns whether that surface is a bottom bar or
|
|
6257
|
+
* labelled side navigation.
|
|
6258
|
+
*
|
|
6259
|
+
* Public hooks:
|
|
6260
|
+
* --app-nav-size
|
|
6261
|
+
* --app-nav-gap
|
|
6262
|
+
*/
|
|
6263
|
+
:where(.app-nav) {
|
|
6264
|
+
--ui-bg: initial;
|
|
6265
|
+
--ui-fg: initial;
|
|
6266
|
+
--ui-border: initial;
|
|
6267
|
+
}
|
|
6268
|
+
|
|
6269
|
+
.app-nav {
|
|
6270
|
+
--app-nav-size: 4rem;
|
|
6271
|
+
--app-nav-gap: var(--space-10);
|
|
6272
|
+
|
|
6273
|
+
position: sticky;
|
|
6274
|
+
inset-block-end: 0;
|
|
6275
|
+
z-index: var(--z-sticky);
|
|
6276
|
+
display: grid;
|
|
6277
|
+
grid-auto-flow: column;
|
|
6278
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
6279
|
+
gap: var(--app-nav-gap);
|
|
6280
|
+
min-block-size: calc(var(--app-nav-size) + env(safe-area-inset-bottom));
|
|
6281
|
+
padding-block: var(--space-10) max(var(--space-10), env(safe-area-inset-bottom));
|
|
6282
|
+
padding-inline: var(--space-10);
|
|
6283
|
+
border-block-start: var(--border-width) solid var(--ui-border, var(--border));
|
|
6284
|
+
background: var(--ui-bg, var(--surface));
|
|
6285
|
+
color: var(--ui-fg, var(--text-muted));
|
|
6286
|
+
}
|
|
6287
|
+
|
|
6288
|
+
.app-nav > a {
|
|
6289
|
+
display: grid;
|
|
6290
|
+
grid-template-rows: auto auto;
|
|
6291
|
+
place-content: center;
|
|
6292
|
+
justify-items: center;
|
|
6293
|
+
gap: var(--space-10);
|
|
6294
|
+
min-inline-size: 0;
|
|
6295
|
+
min-block-size: var(--control-size-lg);
|
|
6296
|
+
padding-inline: var(--space-10);
|
|
6297
|
+
border-radius: var(--radius);
|
|
6298
|
+
color: inherit;
|
|
6299
|
+
font-size: var(--font-size-xs);
|
|
6300
|
+
font-weight: var(--font-weight-medium);
|
|
6301
|
+
line-height: var(--line-height-tight);
|
|
6302
|
+
text-align: center;
|
|
6303
|
+
text-decoration: none;
|
|
6304
|
+
overflow-wrap: anywhere;
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
.app-nav > a > :where(svg, img, [aria-hidden="true"]) {
|
|
6308
|
+
inline-size: 1.5rem;
|
|
6309
|
+
block-size: 1.5rem;
|
|
6310
|
+
font-size: 1.5rem;
|
|
6311
|
+
line-height: 1;
|
|
6312
|
+
}
|
|
6313
|
+
|
|
6314
|
+
.app-nav > a:hover {
|
|
6315
|
+
background: var(--surface-subtle);
|
|
6316
|
+
color: var(--text);
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6319
|
+
.app-nav > a:where([aria-current]:not([aria-current="false"])) {
|
|
6320
|
+
background: var(--state-selected);
|
|
6321
|
+
color: var(--state-selected-fg);
|
|
6322
|
+
font-weight: var(--font-weight-bold);
|
|
6323
|
+
}
|
|
6324
|
+
|
|
5929
6325
|
@media print {
|
|
5930
|
-
.
|
|
6326
|
+
.app-nav {
|
|
5931
6327
|
display: none;
|
|
5932
6328
|
}
|
|
5933
6329
|
}
|
|
5934
6330
|
|
|
5935
|
-
|
|
5936
|
-
/* Navigation */
|
|
5937
6331
|
/*
|
|
5938
6332
|
* Navbar — top-level navigation bar.
|
|
5939
6333
|
*
|
|
@@ -6003,6 +6397,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6003
6397
|
.nav-link:where([aria-current]:not([aria-current="false"])) {
|
|
6004
6398
|
background: var(--surface-subtle);
|
|
6005
6399
|
color: var(--text);
|
|
6400
|
+
font-weight: var(--font-weight-bold);
|
|
6006
6401
|
}
|
|
6007
6402
|
|
|
6008
6403
|
.nav-link:hover {
|
|
@@ -6010,7 +6405,9 @@ a.avatar[aria-disabled="true"] {
|
|
|
6010
6405
|
color: var(--text);
|
|
6011
6406
|
}
|
|
6012
6407
|
|
|
6013
|
-
/* Nav list — a vertical navigation list. Used by .drawer for stacked links.
|
|
6408
|
+
/* Nav list — a vertical navigation list. Used by .drawer for stacked links.
|
|
6409
|
+
Links fill each row so the whole line is an interactive target; the
|
|
6410
|
+
horizontal .navbar-nav links keep their intrinsic size. */
|
|
6014
6411
|
.nav-list {
|
|
6015
6412
|
display: grid;
|
|
6016
6413
|
--gap: var(--space-20);
|
|
@@ -6024,20 +6421,8 @@ a.avatar[aria-disabled="true"] {
|
|
|
6024
6421
|
margin: 0;
|
|
6025
6422
|
}
|
|
6026
6423
|
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
outline: var(--border-width) solid Highlight;
|
|
6030
|
-
background: Highlight;
|
|
6031
|
-
color: HighlightText;
|
|
6032
|
-
forced-color-adjust: none;
|
|
6033
|
-
}
|
|
6034
|
-
|
|
6035
|
-
.nav-link:hover:not(:where([aria-current]:not([aria-current="false"]))) {
|
|
6036
|
-
outline: var(--border-width) solid ButtonText;
|
|
6037
|
-
outline-offset: 2px;
|
|
6038
|
-
background: Canvas;
|
|
6039
|
-
color: CanvasText;
|
|
6040
|
-
}
|
|
6424
|
+
.nav-list .nav-link {
|
|
6425
|
+
inline-size: 100%;
|
|
6041
6426
|
}
|
|
6042
6427
|
|
|
6043
6428
|
/*
|
|
@@ -6166,13 +6551,14 @@ a.avatar[aria-disabled="true"] {
|
|
|
6166
6551
|
}
|
|
6167
6552
|
|
|
6168
6553
|
.tab {
|
|
6554
|
+
/* Positioning context for the selected indicator pseudo. */
|
|
6555
|
+
position: relative;
|
|
6169
6556
|
display: inline-flex;
|
|
6170
6557
|
align-items: center;
|
|
6171
6558
|
gap: var(--tab-gap, 0.375em);
|
|
6172
6559
|
min-block-size: var(--control-size);
|
|
6173
6560
|
padding-inline: var(--space-30);
|
|
6174
6561
|
border: 0;
|
|
6175
|
-
border-block-end: 2px solid transparent;
|
|
6176
6562
|
border-radius: 0;
|
|
6177
6563
|
background: transparent;
|
|
6178
6564
|
color: var(--text-muted);
|
|
@@ -6184,8 +6570,6 @@ a.avatar[aria-disabled="true"] {
|
|
|
6184
6570
|
|
|
6185
6571
|
.tabs[aria-orientation="vertical"] .tab {
|
|
6186
6572
|
justify-content: flex-start;
|
|
6187
|
-
border-block-end: 0;
|
|
6188
|
-
border-inline-end: 2px solid transparent;
|
|
6189
6573
|
padding-inline-end: var(--space-40);
|
|
6190
6574
|
}
|
|
6191
6575
|
|
|
@@ -6206,30 +6590,29 @@ a.avatar[aria-disabled="true"] {
|
|
|
6206
6590
|
}
|
|
6207
6591
|
|
|
6208
6592
|
.tab[aria-selected="true"] {
|
|
6209
|
-
border-block-end-color: var(--intent, var(--state-selected));
|
|
6210
6593
|
color: var(--intent, var(--state-selected));
|
|
6594
|
+
/* Second, layout-free selected cue — the underline alone is color-only in
|
|
6595
|
+
forced colors. */
|
|
6596
|
+
font-weight: var(--font-weight-bold);
|
|
6211
6597
|
}
|
|
6212
6598
|
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6599
|
+
/* The selected indicator lives on a positioned pseudo so that only the active
|
|
6600
|
+
tab owns an underline: a transparent border on every tab would surface in
|
|
6601
|
+
forced colors and erase the distinction. currentColor follows the palette the
|
|
6602
|
+
UA forces, so no forced-colors block is needed. */
|
|
6603
|
+
.tab[aria-selected="true"]::after {
|
|
6604
|
+
content: "";
|
|
6605
|
+
position: absolute;
|
|
6606
|
+
inset-inline: 0;
|
|
6607
|
+
inset-block-end: 0;
|
|
6608
|
+
border-block-end: 2px solid currentColor;
|
|
6216
6609
|
}
|
|
6217
6610
|
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
.tab[aria-selected="true"] {
|
|
6225
|
-
border-block-end-color: Highlight;
|
|
6226
|
-
color: Highlight;
|
|
6227
|
-
}
|
|
6228
|
-
|
|
6229
|
-
.tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
|
|
6230
|
-
border-block-end-color: transparent;
|
|
6231
|
-
border-inline-end-color: Highlight;
|
|
6232
|
-
}
|
|
6611
|
+
.tabs[aria-orientation="vertical"] .tab[aria-selected="true"]::after {
|
|
6612
|
+
inset-block: 0;
|
|
6613
|
+
inset-inline: auto 0;
|
|
6614
|
+
border-block-end: 0;
|
|
6615
|
+
border-inline-end: 2px solid currentColor;
|
|
6233
6616
|
}
|
|
6234
6617
|
|
|
6235
6618
|
/*
|
|
@@ -6262,6 +6645,90 @@ a.avatar[aria-disabled="true"] {
|
|
|
6262
6645
|
|
|
6263
6646
|
|
|
6264
6647
|
/* Data */
|
|
6648
|
+
/*
|
|
6649
|
+
* Application list — repeated rows with optional leading and trailing regions.
|
|
6650
|
+
* Content determines the number of lines; the component only owns the row
|
|
6651
|
+
* geometry, dividers, and supporting-text treatment.
|
|
6652
|
+
*
|
|
6653
|
+
* Public hooks:
|
|
6654
|
+
* --list-item-min-size
|
|
6655
|
+
* --list-item-pad-block
|
|
6656
|
+
* --list-item-pad-inline
|
|
6657
|
+
* --list-item-gap
|
|
6658
|
+
* --list-divider
|
|
6659
|
+
*/
|
|
6660
|
+
.list {
|
|
6661
|
+
--list-item-min-size: var(--control-size-lg);
|
|
6662
|
+
--list-item-pad-block: var(--space-30);
|
|
6663
|
+
--list-item-pad-inline: 0;
|
|
6664
|
+
--list-item-gap: var(--gap);
|
|
6665
|
+
--list-divider: var(--border-width) solid var(--border);
|
|
6666
|
+
|
|
6667
|
+
display: grid;
|
|
6668
|
+
gap: 0;
|
|
6669
|
+
margin: 0;
|
|
6670
|
+
padding: 0;
|
|
6671
|
+
border-block-start: var(--list-divider);
|
|
6672
|
+
list-style: none;
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
.list > li {
|
|
6676
|
+
margin: 0;
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6679
|
+
.list > :where(.list-item),
|
|
6680
|
+
.list > li > .list-item {
|
|
6681
|
+
display: grid;
|
|
6682
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
6683
|
+
align-items: center;
|
|
6684
|
+
gap: var(--list-item-gap);
|
|
6685
|
+
min-block-size: var(--list-item-min-size);
|
|
6686
|
+
padding-block: var(--list-item-pad-block);
|
|
6687
|
+
padding-inline: var(--list-item-pad-inline);
|
|
6688
|
+
}
|
|
6689
|
+
|
|
6690
|
+
.list > .list-item + .list-item,
|
|
6691
|
+
.list > li + li {
|
|
6692
|
+
border-block-start: var(--list-divider);
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6695
|
+
a.list-item {
|
|
6696
|
+
color: inherit;
|
|
6697
|
+
text-decoration: none;
|
|
6698
|
+
}
|
|
6699
|
+
|
|
6700
|
+
a.list-item:hover {
|
|
6701
|
+
background: var(--surface-subtle);
|
|
6702
|
+
}
|
|
6703
|
+
|
|
6704
|
+
.list-item-content {
|
|
6705
|
+
min-inline-size: 0;
|
|
6706
|
+
}
|
|
6707
|
+
|
|
6708
|
+
.list-item-title,
|
|
6709
|
+
.list-item-text {
|
|
6710
|
+
display: block;
|
|
6711
|
+
margin: 0;
|
|
6712
|
+
}
|
|
6713
|
+
|
|
6714
|
+
.list-item-text {
|
|
6715
|
+
color: var(--text-muted);
|
|
6716
|
+
font-size: var(--font-size-sm);
|
|
6717
|
+
overflow-wrap: anywhere;
|
|
6718
|
+
}
|
|
6719
|
+
|
|
6720
|
+
.list-item-trailing {
|
|
6721
|
+
justify-self: end;
|
|
6722
|
+
min-inline-size: 0;
|
|
6723
|
+
}
|
|
6724
|
+
|
|
6725
|
+
@media print {
|
|
6726
|
+
.list > :where(.list-item),
|
|
6727
|
+
.list > li > .list-item {
|
|
6728
|
+
break-inside: avoid;
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6265
6732
|
/*
|
|
6266
6733
|
* Table — structured tabular data.
|
|
6267
6734
|
*
|
|
@@ -6560,6 +7027,7 @@ a.avatar[aria-disabled="true"] {
|
|
|
6560
7027
|
display: inline-grid;
|
|
6561
7028
|
padding: var(--aura-width);
|
|
6562
7029
|
border-radius: calc(var(--aura-radius) + var(--aura-width));
|
|
7030
|
+
border: var(--border-width) solid transparent;
|
|
6563
7031
|
background: var(--aura-color);
|
|
6564
7032
|
background: conic-gradient(from var(--aura-angle), transparent 0 65%, var(--aura-color));
|
|
6565
7033
|
}
|
|
@@ -6610,16 +7078,6 @@ a.avatar[aria-disabled="true"] {
|
|
|
6610
7078
|
}
|
|
6611
7079
|
}
|
|
6612
7080
|
|
|
6613
|
-
@media (forced-colors: active) {
|
|
6614
|
-
.aura {
|
|
6615
|
-
background: CanvasText;
|
|
6616
|
-
}
|
|
6617
|
-
|
|
6618
|
-
.aura::before {
|
|
6619
|
-
display: none;
|
|
6620
|
-
}
|
|
6621
|
-
}
|
|
6622
|
-
|
|
6623
7081
|
/* Utilities — small, single-purpose escape hatches. */
|
|
6624
7082
|
/*
|
|
6625
7083
|
* Small, single-purpose utilities for common escape hatches.
|