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.
Files changed (75) hide show
  1. package/CHANGELOG.md +77 -35
  2. package/dist/actual.css +1 -20
  3. package/dist/actual.full.css +679 -221
  4. package/dist/actual.full.js +1 -1
  5. package/dist/actual.full.min.css +1 -1
  6. package/dist/actual.min.css +1 -1
  7. package/docs/design-notes/app-ui.md +33 -0
  8. package/docs/design-notes/enhancement-contract.md +25 -0
  9. package/docs/design-notes/grid.md +177 -4
  10. package/docs/design-notes/references.md +1 -1
  11. package/docs/navigation.json +7 -1
  12. package/docs/pages/components/alert.md +5 -0
  13. package/docs/pages/components/app-navigation.md +34 -0
  14. package/docs/pages/components/avatar.md +10 -0
  15. package/docs/pages/components/badge.md +2 -0
  16. package/docs/pages/components/button.md +30 -0
  17. package/docs/pages/components/card.md +5 -1
  18. package/docs/pages/components/fab.md +2 -0
  19. package/docs/pages/components/indicator.md +52 -0
  20. package/docs/pages/components/list.md +62 -0
  21. package/docs/pages/components/navbar.md +6 -0
  22. package/docs/pages/components/rating.md +68 -0
  23. package/docs/pages/components/steps.md +57 -0
  24. package/docs/pages/enhancements/flyout.md +2 -2
  25. package/docs/pages/enhancements/tabs.md +1 -2
  26. package/docs/pages/examples/overview.md +8 -0
  27. package/docs/pages/forms/otp.md +32 -0
  28. package/docs/pages/forms/states-and-sizes.md +3 -3
  29. package/docs/pages/guides/integrating-actual-css.md +6 -0
  30. package/docs/pages/layout/app-layout.md +57 -0
  31. package/docs/pages/layout/app-shell.md +28 -7
  32. package/docs/pages/layout/center.md +12 -1
  33. package/docs/pages/layout/choosing.md +15 -0
  34. package/docs/pages/layout/grid.md +41 -8
  35. package/docs/pages/layout/sidebar-layout.md +19 -0
  36. package/docs/pages/layout/stack.md +17 -0
  37. package/docs/pages/patterns/icon-slot.md +61 -0
  38. package/docs/pages/patterns/structured-lists.md +38 -40
  39. package/package.json +3 -2
  40. package/scripts/reserved-classes.json +17 -0
  41. package/src/css/components/app-nav.css +76 -0
  42. package/src/css/components/avatar.css +4 -0
  43. package/src/css/components/badge.css +1 -13
  44. package/src/css/components/button.css +8 -38
  45. package/src/css/components/card.css +11 -11
  46. package/src/css/components/chat.css +0 -6
  47. package/src/css/components/drawer.css +0 -6
  48. package/src/css/components/fab.css +1 -7
  49. package/src/css/components/index.css +5 -0
  50. package/src/css/components/indicator.css +47 -0
  51. package/src/css/components/list.css +83 -0
  52. package/src/css/components/menu.css +0 -7
  53. package/src/css/components/modal.css +0 -17
  54. package/src/css/components/navbar.css +6 -15
  55. package/src/css/components/rating.css +101 -0
  56. package/src/css/components/steps.css +122 -0
  57. package/src/css/components/tab.css +20 -22
  58. package/src/css/components/tooltip.css +1 -10
  59. package/src/css/core/focus.css +0 -8
  60. package/src/css/core/theme.css +0 -10
  61. package/src/css/core/tokens.css +1 -2
  62. package/src/css/effects/aura.css +1 -10
  63. package/src/css/forms/choice-card.css +6 -15
  64. package/src/css/forms/otp.css +7 -2
  65. package/src/css/forms/range.css +14 -4
  66. package/src/css/layout/app-layout.css +102 -0
  67. package/src/css/layout/app-shell.css +1 -1
  68. package/src/css/layout/center.css +1 -0
  69. package/src/css/layout/container-query.css +6 -4
  70. package/src/css/layout/grid.css +25 -11
  71. package/src/css/layout/index.css +4 -2
  72. package/src/css/layout/sidebar-layout.css +22 -1
  73. package/src/css/typography/prose.css +4 -0
  74. package/src/js/tooltip.js +2 -1
  75. package/docs/pages/layout/list.md +0 -41
@@ -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
- * .grid-N structural density of N; wrap in .container-query for
1052
- * balanced subdivision (in grid.css)
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
- /* Local container-query opt-in: establishes an inline-size query context for
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 opt .grid-N into balanced subdivision
1066
- (see grid.css). Layout primitives are responsive without it — this adds
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-type: inline-size;
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 application shell. */
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 last direct children carry the sizing roles.
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 -> wrap it in .container-query
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, opted into by an ancestor query container.
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
- .container-query :where(.grid-2, .grid-3, .grid-4, .grid-6) {
1345
- grid-template-columns: 1fr;
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
- .container-query :where(.grid-2, .grid-4, .grid-6) {
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
- .container-query :where(.grid-3, .grid-6) {
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
- .container-query :where(.grid-4) {
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
- .container-query :where(.grid-6) {
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: map the custom thumb/track to system colors. The focus
2614
- indicator stays visible through the transparent outline on :focus-visible. */
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-thumb-bg: ButtonText;
2618
- --range-track-bg: Canvas;
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
- box-shadow: 0 0 0 2px Canvas;
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
- padding-inline: 0;
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-color: transparent;
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
- forced-color-adjust: auto;
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). Page layout
3898
- * remains the job of markup and layout primitives outside the card.
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
- .card > * {
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
- .fab-label {
5924
- border: var(--border-width) solid CanvasText;
5925
- box-shadow: none;
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
- .fab {
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
- @media (forced-colors: active) {
6028
- .nav-link:where([aria-current]:not([aria-current="false"])) {
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
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
6214
- border-block-end-color: transparent;
6215
- border-inline-end-color: var(--intent, var(--state-selected));
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
- @media (forced-colors: active) {
6219
- .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
6220
- text-decoration: underline;
6221
- text-underline-offset: 0.18em;
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.