actual-css 0.4.0 → 0.4.2

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 (83) hide show
  1. package/CHANGELOG.md +84 -35
  2. package/dist/actual.css +1 -20
  3. package/dist/actual.full.css +692 -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/actions.css +1 -0
  42. package/src/css/components/app-nav.css +76 -0
  43. package/src/css/components/avatar.css +4 -0
  44. package/src/css/components/badge.css +1 -13
  45. package/src/css/components/breadcrumb.css +1 -0
  46. package/src/css/components/button.css +8 -38
  47. package/src/css/components/card.css +11 -11
  48. package/src/css/components/chat.css +0 -6
  49. package/src/css/components/drawer.css +0 -6
  50. package/src/css/components/fab.css +1 -7
  51. package/src/css/components/index.css +5 -0
  52. package/src/css/components/indicator.css +47 -0
  53. package/src/css/components/list.css +83 -0
  54. package/src/css/components/menu.css +0 -7
  55. package/src/css/components/modal.css +0 -17
  56. package/src/css/components/navbar.css +8 -15
  57. package/src/css/components/pagination.css +1 -0
  58. package/src/css/components/rating.css +101 -0
  59. package/src/css/components/steps.css +123 -0
  60. package/src/css/components/tab.css +21 -22
  61. package/src/css/components/tooltip.css +1 -10
  62. package/src/css/core/focus.css +0 -8
  63. package/src/css/core/theme.css +0 -10
  64. package/src/css/core/tokens.css +1 -2
  65. package/src/css/effects/aura.css +1 -10
  66. package/src/css/forms/choice-card.css +6 -15
  67. package/src/css/forms/form-actions.css +1 -0
  68. package/src/css/forms/otp.css +7 -2
  69. package/src/css/forms/range.css +14 -4
  70. package/src/css/layout/app-layout.css +102 -0
  71. package/src/css/layout/app-shell.css +1 -1
  72. package/src/css/layout/center.css +1 -0
  73. package/src/css/layout/cluster.css +1 -0
  74. package/src/css/layout/container-query.css +6 -4
  75. package/src/css/layout/grid.css +25 -11
  76. package/src/css/layout/index.css +4 -2
  77. package/src/css/layout/scroll-snap.css +1 -0
  78. package/src/css/layout/sidebar-layout.css +23 -1
  79. package/src/css/layout/switcher.css +1 -0
  80. package/src/css/layout/topbar.css +1 -0
  81. package/src/css/typography/prose.css +4 -0
  82. package/src/js/tooltip.js +2 -1
  83. 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
@@ -1108,6 +1096,7 @@ small {
1108
1096
  */
1109
1097
  .cluster {
1110
1098
  display: flex;
1099
+ flex-direction: row;
1111
1100
  flex-wrap: var(--cluster-wrap, wrap);
1112
1101
  align-items: var(--cluster-align, center);
1113
1102
  justify-content: var(--cluster-justify, flex-start);
@@ -1127,6 +1116,7 @@ small {
1127
1116
  */
1128
1117
  .center {
1129
1118
  box-sizing: content-box;
1119
+ inline-size: calc(100% - var(--center-pad, 1rem) * 2);
1130
1120
  max-inline-size: var(--center-size, 72rem);
1131
1121
  margin-inline: auto;
1132
1122
  padding-inline: var(--center-pad, 1rem);
@@ -1152,13 +1142,116 @@ small {
1152
1142
  object-fit: cover;
1153
1143
  }
1154
1144
 
1155
- /* Full-height application shell. */
1145
+ /* Full-height single-column document shell. */
1156
1146
  .app-shell {
1157
1147
  min-block-size: var(--viewport-block);
1158
1148
  display: flex;
1159
1149
  flex-direction: column;
1160
1150
  }
1161
1151
 
1152
+ /*
1153
+ * Specialized application shell recipe: one exact DOM structure uses a
1154
+ * bottom navigation bar when narrow and a labelled side navigation when space
1155
+ * permits. The main region owns scrolling, keeping both pieces of application
1156
+ * chrome available without fixed offsets. This is not a generic page wrapper
1157
+ * or a public-site layout; all three direct regions are part of the contract.
1158
+ *
1159
+ * Public hook:
1160
+ * --app-nav-side-size
1161
+ */
1162
+ .app-layout {
1163
+ --app-nav-side-size: 12rem;
1164
+
1165
+ display: grid;
1166
+ grid-template:
1167
+ "topbar" auto
1168
+ "main" minmax(0, 1fr)
1169
+ "nav" auto
1170
+ / minmax(0, 1fr);
1171
+ block-size: var(--viewport-block);
1172
+ min-inline-size: 0;
1173
+ overflow: hidden;
1174
+ }
1175
+
1176
+ .app-layout > .topbar {
1177
+ grid-area: topbar;
1178
+ padding-block-start: calc(var(--space-30) + env(safe-area-inset-top));
1179
+ }
1180
+
1181
+ .app-layout > .app-main {
1182
+ grid-area: main;
1183
+ min-inline-size: 0;
1184
+ min-block-size: 0;
1185
+ overflow: auto;
1186
+ }
1187
+
1188
+ .app-layout > .app-nav {
1189
+ grid-area: nav;
1190
+ }
1191
+
1192
+ /* The main grid area keeps the FAB clear of bottom navigation without making
1193
+ either component copy the other's geometry. */
1194
+ .app-layout > .fab {
1195
+ position: relative;
1196
+ grid-area: main;
1197
+ place-self: end;
1198
+ inset: auto;
1199
+ margin: var(--fab-offset);
1200
+ }
1201
+
1202
+ @media (min-width: 48rem) {
1203
+ .app-layout {
1204
+ grid-template:
1205
+ "nav topbar" auto
1206
+ "nav main" minmax(0, 1fr)
1207
+ / var(--app-nav-side-size) minmax(0, 1fr);
1208
+ }
1209
+
1210
+ .app-layout > .app-nav {
1211
+ position: sticky;
1212
+ inset-block: 0;
1213
+ grid-auto-flow: row;
1214
+ grid-auto-rows: minmax(var(--control-size-lg), auto);
1215
+ grid-auto-columns: auto;
1216
+ align-content: start;
1217
+ gap: var(--space-20);
1218
+ min-block-size: 0;
1219
+ padding-block: max(var(--space-20), env(safe-area-inset-top))
1220
+ max(var(--space-20), env(safe-area-inset-bottom));
1221
+ padding-inline: var(--space-20);
1222
+ border-block-start: 0;
1223
+ border-inline-end: var(--border-width) solid var(--ui-border, var(--border));
1224
+ }
1225
+
1226
+ .app-layout > .app-nav > a {
1227
+ grid-template-rows: auto;
1228
+ grid-template-columns: 1.5rem minmax(0, 1fr);
1229
+ place-content: center start;
1230
+ align-items: center;
1231
+ justify-items: start;
1232
+ gap: var(--space-30);
1233
+ padding-inline: var(--space-30);
1234
+ text-align: start;
1235
+ }
1236
+
1237
+ /* Side navigation no longer occupies the viewport's bottom safe area. */
1238
+ .app-layout > .fab {
1239
+ margin-block-end: max(var(--fab-offset), env(safe-area-inset-bottom));
1240
+ }
1241
+ }
1242
+
1243
+ @media print {
1244
+ .app-layout {
1245
+ display: block;
1246
+ block-size: auto;
1247
+ overflow: visible;
1248
+ }
1249
+
1250
+ .app-layout > .app-main {
1251
+ overflow: visible;
1252
+ }
1253
+ }
1254
+
1162
1255
  /* Media object: fixed-width leading element (avatar, icon, image) + flexible
1163
1256
  trailing content. Replaces the repeated "auto minmax(0, 1fr)" pattern in
1164
1257
  author cards, comments, meta rows, and similar compositions.
@@ -1184,6 +1277,7 @@ small {
1184
1277
  */
1185
1278
  .switcher {
1186
1279
  display: flex;
1280
+ flex-direction: row;
1187
1281
  flex-wrap: wrap;
1188
1282
  gap: var(--gap);
1189
1283
  }
@@ -1206,24 +1300,46 @@ small {
1206
1300
  * --sidebar-content-min
1207
1301
  * --sidebar-layout-size
1208
1302
  *
1209
- * Child contract: the first and last direct children carry the sizing roles.
1303
+ * Child contract: the main region is first and the aside second by default
1304
+ * (aside on the inline end). .reverse expects the aside first in the DOM, so
1305
+ * visual, reading and keyboard order all agree — the role rules swap so the
1306
+ * aside still takes its preferred width and the main still reclaims the row.
1210
1307
  */
1211
1308
  .sidebar-layout {
1212
1309
  display: flex;
1310
+ flex-direction: row;
1213
1311
  flex-wrap: wrap;
1214
1312
  gap: var(--gap);
1215
1313
  align-items: flex-start;
1216
1314
  }
1217
1315
 
1316
+ /* Main region — flexible, reclaims the full row when the aside wraps. */
1218
1317
  .sidebar-layout > :first-child {
1219
1318
  flex-basis: 0;
1220
1319
  flex-grow: 999;
1221
1320
  min-inline-size: min(100%, var(--sidebar-content-min, 30rem));
1222
1321
  }
1223
1322
 
1323
+ /* Aside region — preferred width, never forces content min-width. */
1224
1324
  .sidebar-layout > :last-child {
1225
1325
  flex-basis: var(--sidebar-layout-size, 18rem);
1226
1326
  flex-grow: 1;
1327
+ min-inline-size: 0;
1328
+ }
1329
+
1330
+ /* .reverse: the aside is first in the DOM and the main last, so each role
1331
+ rule above would hit the wrong element. Swap the sizing per child; the
1332
+ higher-specificity reverse rules outrank the positional defaults. */
1333
+ .sidebar-layout.reverse > :last-child {
1334
+ flex-basis: 0;
1335
+ flex-grow: 999;
1336
+ min-inline-size: min(100%, var(--sidebar-content-min, 30rem));
1337
+ }
1338
+
1339
+ .sidebar-layout.reverse > :first-child {
1340
+ flex-basis: var(--sidebar-layout-size, 18rem);
1341
+ flex-grow: 1;
1342
+ min-inline-size: 0;
1227
1343
  }
1228
1344
 
1229
1345
  /*
@@ -1252,7 +1368,8 @@ small {
1252
1368
  /* Recipe:
1253
1369
  - collection whose item width is the contract -> .grid, tune --grid-min
1254
1370
  - collection whose column density is the contract -> .grid-2/3/4/6
1255
- - balanced subdivision of that density -> wrap it in .container-query
1371
+ - balanced subdivision of that density -> place it inside any size query
1372
+ container (.container-query is the convenience helper for that context)
1256
1373
  - peer regions that switch as a group -> .switcher
1257
1374
  - content + aside -> .sidebar-layout
1258
1375
  - exact custom track templates -> --grid-columns with an explicit collapse
@@ -1313,7 +1430,13 @@ small {
1313
1430
  min-inline-size: 0;
1314
1431
  }
1315
1432
 
1316
- /* Balanced subdivision, opted into by an ancestor query container.
1433
+ /* Balanced subdivision, activated by an ancestor container named actual-grid.
1434
+ * A .grid-N inside such a container enters only divisor states of N. The name
1435
+ * is deliberate: a grid must not key off the nearest anonymous query container,
1436
+ * or a region that sets container-type: inline-size for an unrelated reason
1437
+ * would silently change the grid. The author opts the grid in by naming the
1438
+ * context — .container-query is the official helper, or declare
1439
+ * container: actual-grid / inline-size on your own region.
1317
1440
  *
1318
1441
  * .grid-2 2 -> 1
1319
1442
  * .grid-3 3 -> 1
@@ -1330,6 +1453,11 @@ small {
1330
1453
  * inline contribution and collapses it inside .cluster, .topbar, floats, and
1331
1454
  * any shrink-to-fit box. See docs/design-notes/grid.md.
1332
1455
  *
1456
+ * Without an actual-grid ancestor none of these @container rules resolve, so
1457
+ * the auto-fill baseline above stays. The min-width: 0 rule is the lowest
1458
+ * step: it pins the grid to one column inside a container before any larger
1459
+ * threshold fires (a bare grid would otherwise show a 2 + 1 orphan at 28-48rem).
1460
+ *
1333
1461
  * Three shared thresholds, not one calibrated per preset: 28rem opens the
1334
1462
  * first horizontal density, 48rem opens three columns, 64rem opens the dense
1335
1463
  * 4 and 6. N already carries the author's statement about item size, so
@@ -1341,28 +1469,30 @@ small {
1341
1469
  * Ascending min-width only: the last matching step wins by cascade order.
1342
1470
  */
1343
1471
  @supports (container-type: inline-size) {
1344
- .container-query :where(.grid-2, .grid-3, .grid-4, .grid-6) {
1345
- grid-template-columns: 1fr;
1472
+ @container actual-grid (min-width: 0px) {
1473
+ :where(.grid-2, .grid-3, .grid-4, .grid-6) {
1474
+ grid-template-columns: 1fr;
1475
+ }
1346
1476
  }
1347
1477
 
1348
- @container (min-width: 28rem) {
1349
- .container-query :where(.grid-2, .grid-4, .grid-6) {
1478
+ @container actual-grid (min-width: 28rem) {
1479
+ :where(.grid-2, .grid-4, .grid-6) {
1350
1480
  grid-template-columns: repeat(2, minmax(0, 1fr));
1351
1481
  }
1352
1482
  }
1353
1483
 
1354
- @container (min-width: 48rem) {
1355
- .container-query :where(.grid-3, .grid-6) {
1484
+ @container actual-grid (min-width: 48rem) {
1485
+ :where(.grid-3, .grid-6) {
1356
1486
  grid-template-columns: repeat(3, minmax(0, 1fr));
1357
1487
  }
1358
1488
  }
1359
1489
 
1360
- @container (min-width: 64rem) {
1361
- .container-query :where(.grid-4) {
1490
+ @container actual-grid (min-width: 64rem) {
1491
+ :where(.grid-4) {
1362
1492
  grid-template-columns: repeat(4, minmax(0, 1fr));
1363
1493
  }
1364
1494
 
1365
- .container-query :where(.grid-6) {
1495
+ :where(.grid-6) {
1366
1496
  grid-template-columns: repeat(6, minmax(0, 1fr));
1367
1497
  }
1368
1498
  }
@@ -1575,6 +1705,7 @@ small {
1575
1705
  --scroll-snap-item-size: auto;
1576
1706
 
1577
1707
  display: flex;
1708
+ flex-direction: row;
1578
1709
  gap: var(--scroll-snap-gap);
1579
1710
  overflow-x: auto;
1580
1711
  }
@@ -1646,6 +1777,7 @@ small {
1646
1777
  z-index: var(--z-sticky);
1647
1778
  isolation: isolate;
1648
1779
  display: flex;
1780
+ flex-direction: row;
1649
1781
  align-items: center;
1650
1782
  gap: var(--gap);
1651
1783
  padding-block: var(--space-30);
@@ -2293,6 +2425,7 @@ small {
2293
2425
  */
2294
2426
  .form-actions {
2295
2427
  display: flex;
2428
+ flex-direction: row;
2296
2429
  flex-wrap: wrap;
2297
2430
  gap: var(--gap);
2298
2431
  align-items: var(--form-actions-align, center);
@@ -2610,12 +2743,22 @@ small {
2610
2743
  opacity: var(--disabled-opacity);
2611
2744
  }
2612
2745
 
2613
- /* Forced colors: map the custom thumb/track to system colors. The focus
2614
- indicator stays visible through the transparent outline on :focus-visible. */
2746
+ /* Forced colors: the track must stay a visible line against the page (Canvas
2747
+ would merge with it) and the thumb needs a real boundary to remain
2748
+ distinguishable from the track. Disabled keeps GrayText via :disabled.
2749
+ Focus stays on the transparent outline of :focus-visible. */
2615
2750
  @media (forced-colors: active) {
2616
2751
  .range {
2617
- --range-thumb-bg: ButtonText;
2618
- --range-track-bg: Canvas;
2752
+ --range-track-bg: CanvasText;
2753
+ --range-thumb-bg: ButtonFace;
2754
+ }
2755
+
2756
+ .range::-webkit-slider-thumb {
2757
+ border: var(--border-width) solid ButtonText;
2758
+ }
2759
+
2760
+ .range::-moz-range-thumb {
2761
+ border: var(--border-width) solid ButtonText;
2619
2762
  }
2620
2763
  }
2621
2764
 
@@ -2752,28 +2895,19 @@ small {
2752
2895
  transform: scale(1);
2753
2896
  }
2754
2897
 
2898
+ /* The native input is hidden and the checked marker is custom-drawn: without
2899
+ this repair, forced colors pushes disc and check to the same system color
2900
+ and the state disappears. Highlight/Canvas/HighlightText are preserved as-is,
2901
+ so no forced-color-adjust is needed. The pseudo is not focusable — the
2902
+ outline is a separator against the card, not a focus channel. */
2755
2903
  @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
2904
  .choice-card:has(:checked)::before {
2769
2905
  background: Highlight;
2770
- box-shadow: 0 0 0 2px Canvas;
2771
- forced-color-adjust: none;
2906
+ outline: 2px solid Canvas;
2772
2907
  }
2773
2908
 
2774
2909
  .choice-card:has(:checked)::after {
2775
2910
  background: HighlightText;
2776
- forced-color-adjust: none;
2777
2911
  }
2778
2912
  }
2779
2913
 
@@ -3058,7 +3192,12 @@ small {
3058
3192
  but it sits in the clipped input extension once every cell is filled, so
3059
3193
  the focused edge must be unambiguous on its own — hence the doubled width,
3060
3194
  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. */
3195
+ and the cells have a fixed inline/block size, so nothing reflows.
3196
+
3197
+ The <input> must be the first direct child, with one cell <span> per
3198
+ character after it: the state rules select the cells as following siblings
3199
+ (input:focus ~ span, input:disabled ~ span, …), so that order is part of the
3200
+ contract. */
3062
3201
  .otp > input:focus ~ span {
3063
3202
  --otp-cell-border: var(--focus);
3064
3203
  --otp-cell-border-width: calc(var(--border-width) * 2);
@@ -3082,7 +3221,7 @@ small {
3082
3221
 
3083
3222
  .otp > input:disabled ~ span {
3084
3223
  --otp-cell-bg: var(--surface-subtle);
3085
-
3224
+ --otp-cell-border: var(--state-disabled);
3086
3225
  opacity: var(--disabled-opacity);
3087
3226
  }
3088
3227
 
@@ -3192,7 +3331,8 @@ small {
3192
3331
 
3193
3332
  .btn.icon-only {
3194
3333
  inline-size: var(--btn-min-size);
3195
- padding-inline: 0;
3334
+ block-size: var(--btn-min-size);
3335
+ padding: 0;
3196
3336
  }
3197
3337
 
3198
3338
  @supports (color: color-mix(in oklch, red, white)) {
@@ -3264,14 +3404,15 @@ small {
3264
3404
  --ui-fg: var(--intent, currentColor);
3265
3405
  --ui-border: transparent;
3266
3406
  --btn-radius: var(--radius-sm);
3267
- --btn-border-block-end-color: transparent;
3268
- --btn-border-block-end-width: var(--border-width);
3269
3407
 
3270
3408
  min-block-size: auto;
3271
3409
  padding-inline: 0;
3272
3410
  inline-size: fit-content;
3273
3411
  max-inline-size: 100%;
3274
- border-color: transparent;
3412
+ /* No border at all: a link reads through its underline, and the inherited
3413
+ .btn border made transparent would turn into a visible box in forced
3414
+ colors. :focus-visible keeps its own outline. */
3415
+ border: 0;
3275
3416
  text-decoration: underline;
3276
3417
  text-underline-offset: 0.18em;
3277
3418
  }
@@ -3332,44 +3473,12 @@ a.btn[aria-disabled="true"] {
3332
3473
  }
3333
3474
 
3334
3475
  @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
3476
  a.btn[aria-disabled="true"] {
3367
- border-color: GrayText;
3368
- background: Canvas;
3369
3477
  color: GrayText;
3370
- forced-color-adjust: auto;
3478
+ border-color: GrayText;
3371
3479
  }
3372
3480
  }
3481
+
3373
3482
  @media print {
3374
3483
  :is(button, input[type="button"], input[type="submit"], input[type="reset"]).btn {
3375
3484
  display: none !important;
@@ -3563,21 +3672,9 @@ a.badge[aria-disabled="true"] {
3563
3672
  background: ButtonText;
3564
3673
  }
3565
3674
 
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
3675
  .badge[aria-disabled="true"] {
3578
- border-color: GrayText;
3579
- background: Canvas;
3580
3676
  color: GrayText;
3677
+ border-color: GrayText;
3581
3678
  }
3582
3679
  }
3583
3680
  @media print {
@@ -3874,6 +3971,7 @@ a.badge[aria-disabled="true"] {
3874
3971
  --gap: var(--space-20);
3875
3972
 
3876
3973
  display: flex;
3974
+ flex-direction: row;
3877
3975
  flex-wrap: wrap;
3878
3976
  align-items: center;
3879
3977
  gap: var(--gap);
@@ -3894,11 +3992,14 @@ a.badge[aria-disabled="true"] {
3894
3992
  * Surface, border, radius, padding, density, and elevation stay local here.
3895
3993
  * Direct header/footer children are structural slots; a direct footer is
3896
3994
  * 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.
3995
+ * price/action rows align across equal-height cards in a grid). A bare card
3996
+ * owns its direct-child rhythm; a layout primitive on the same element takes
3997
+ * over that layout. Page layout remains the job of markup and layout
3998
+ * primitives outside the card.
3899
3999
  *
3900
4000
  * Public hooks:
3901
4001
  * --card-pad
4002
+ * --card-gap
3902
4003
  * --card-radius
3903
4004
  * --card-max-inline-size
3904
4005
  *
@@ -3922,6 +4023,7 @@ a.badge[aria-disabled="true"] {
3922
4023
 
3923
4024
  .card {
3924
4025
  --card-pad: var(--space-40);
4026
+ --card-gap: var(--space-30);
3925
4027
  --card-radius: var(--radius-lg);
3926
4028
  --card-max-inline-size: none;
3927
4029
  --card-bg: var(--ui-bg, var(--surface-raised));
@@ -3948,10 +4050,14 @@ a.badge[aria-disabled="true"] {
3948
4050
  :where(.card) {
3949
4051
  display: flex;
3950
4052
  flex-direction: column;
4053
+ gap: var(--card-gap);
3951
4054
  }
3952
4055
 
3953
- .card > * {
4056
+ /* Native block margins would add to flex gap because flex-item margins never
4057
+ collapse. Zero specificity keeps intentional child utilities available. */
4058
+ :where(.card) > * {
3954
4059
  min-inline-size: 0;
4060
+ margin-block: 0;
3955
4061
  }
3956
4062
 
3957
4063
  .card > :where(header, footer) {
@@ -3978,14 +4084,6 @@ a.badge[aria-disabled="true"] {
3978
4084
  margin-block-start: auto;
3979
4085
  }
3980
4086
 
3981
- .card > :first-child {
3982
- margin-block-start: 0;
3983
- }
3984
-
3985
- .card > :last-child {
3986
- margin-block-end: 0;
3987
- }
3988
-
3989
4087
  .card.raised {
3990
4088
  box-shadow: var(--shadow);
3991
4089
  }
@@ -4331,23 +4429,6 @@ html.has-modal-open.had-scrollbar {
4331
4429
  }
4332
4430
  }
4333
4431
 
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
4432
  /* Close controls. A close button is an action attached to a surface, so it
4352
4433
  never participates in the content flow. The modal and the drawer share the
4353
4434
  same simple placement — absolute at the panel's top inline-end, inside the
@@ -4553,12 +4634,6 @@ dialog.drawer nav {
4553
4634
  }
4554
4635
  }
4555
4636
 
4556
- @media (forced-colors: active) {
4557
- dialog.drawer::backdrop {
4558
- background: Canvas;
4559
- }
4560
- }
4561
-
4562
4637
  @media (prefers-reduced-motion: no-preference) {
4563
4638
  dialog.drawer.is-static {
4564
4639
  animation: actual-dialog-static var(--duration-slow) var(--ease);
@@ -4887,13 +4962,6 @@ dialog.drawer > form > header {
4887
4962
  border-block-start: var(--border-width) solid var(--border);
4888
4963
  }
4889
4964
 
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
4965
  /*
4898
4966
  * Tooltip — supplemental label overlaid near a trigger.
4899
4967
  *
@@ -4920,6 +4988,7 @@ dialog.drawer > form > header {
4920
4988
  max-inline-size: min(24rem, calc(var(--viewport-inline) - 2rem));
4921
4989
  padding: 0.25em 0.6em;
4922
4990
  border-radius: var(--radius-sm);
4991
+ border: var(--border-width) solid transparent;
4923
4992
  background: var(--tooltip-bg);
4924
4993
  color: var(--tooltip-fg);
4925
4994
  font-size: var(--font-size-xs);
@@ -4998,16 +5067,6 @@ dialog.drawer > form > header {
4998
5067
  clip-path: polygon(0 50%, 100% 0, 100% 100%);
4999
5068
  }
5000
5069
 
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
5070
  @media print {
5012
5071
  .tooltip {
5013
5072
  display: none !important;
@@ -5556,6 +5615,9 @@ dialog.drawer > form > header {
5556
5615
  }
5557
5616
  }
5558
5617
 
5618
+ /* The initials element (a span or an accessible <abbr title="…">) fills the box.
5619
+ text-decoration: none neutralizes the UA dotted underline on <abbr title="…">
5620
+ inside the circular avatar. */
5559
5621
  .avatar > :first-child:not(:where(img, picture, .badge)) {
5560
5622
  display: inline-grid;
5561
5623
  place-items: center;
@@ -5563,6 +5625,7 @@ dialog.drawer > form > header {
5563
5625
  block-size: 100%;
5564
5626
  overflow: hidden;
5565
5627
  border-radius: inherit;
5628
+ text-decoration: none;
5566
5629
  }
5567
5630
 
5568
5631
  .avatar > img,
@@ -5691,6 +5754,54 @@ a.avatar[aria-disabled="true"] {
5691
5754
  z-index: 1;
5692
5755
  }
5693
5756
 
5757
+ /*
5758
+ * Indicator — generic corner positioning for overlaid component content.
5759
+ *
5760
+ * The container only establishes the containing block. The item owns no
5761
+ * paint, size, or ring so badges and other components keep their own visual
5762
+ * contract.
5763
+ *
5764
+ * Internal:
5765
+ * --indicator-translate-x
5766
+ * --indicator-translate-y
5767
+ */
5768
+ .indicator {
5769
+ position: relative;
5770
+ }
5771
+
5772
+ .indicator-item {
5773
+ --indicator-translate-x: 50%;
5774
+ --indicator-translate-y: -50%;
5775
+
5776
+ position: absolute;
5777
+ inset-block-start: 0;
5778
+ inset-inline-end: 0;
5779
+ z-index: 1;
5780
+ transform: translate(var(--indicator-translate-x), var(--indicator-translate-y));
5781
+ }
5782
+
5783
+ .indicator-item.start {
5784
+ --indicator-translate-x: -50%;
5785
+
5786
+ inset-inline-start: 0;
5787
+ inset-inline-end: auto;
5788
+ }
5789
+
5790
+ .indicator-item.bottom {
5791
+ --indicator-translate-y: 50%;
5792
+
5793
+ inset-block-start: auto;
5794
+ inset-block-end: 0;
5795
+ }
5796
+
5797
+ .indicator-item:dir(rtl) {
5798
+ --indicator-translate-x: -50%;
5799
+ }
5800
+
5801
+ .indicator-item.start:dir(rtl) {
5802
+ --indicator-translate-x: 50%;
5803
+ }
5804
+
5694
5805
  /*
5695
5806
  * Key — keyboard keys and shortcut tokens for UI chrome.
5696
5807
  *
@@ -5821,12 +5932,6 @@ a.avatar[aria-disabled="true"] {
5821
5932
  }
5822
5933
  }
5823
5934
 
5824
- @media (forced-colors: active) {
5825
- .chat-bubble {
5826
- border-color: CanvasText;
5827
- }
5828
- }
5829
-
5830
5935
  /*
5831
5936
  * Fixed floating action placement and a native-details speed dial.
5832
5937
  * Button owns the action's size, shape, and intent; FAB only owns placement.
@@ -5897,6 +6002,7 @@ a.avatar[aria-disabled="true"] {
5897
6002
  max-inline-size: 16rem;
5898
6003
  padding: var(--space-10) var(--space-20);
5899
6004
  border-radius: var(--radius);
6005
+ border: var(--border-width) solid transparent;
5900
6006
  background: var(--surface);
5901
6007
  color: var(--text);
5902
6008
  box-shadow: var(--shadow);
@@ -5919,21 +6025,317 @@ a.avatar[aria-disabled="true"] {
5919
6025
  transform: rotate(45deg);
5920
6026
  }
5921
6027
 
6028
+ @media print {
6029
+ .fab {
6030
+ display: none;
6031
+ }
6032
+ }
6033
+
6034
+ /*
6035
+ * Rating — integer star rating drawn on native radio inputs.
6036
+ *
6037
+ * The checked radio remains the functional baseline. :has() progressively
6038
+ * fills preceding stars without reversing DOM or keyboard order. Forced
6039
+ * colors restores the platform radio presentation.
6040
+ *
6041
+ * Public hooks:
6042
+ * --rating-size
6043
+ * --rating-gap
6044
+ * --rating-color
6045
+ * --rating-empty
6046
+ * --rating-star
6047
+ */
6048
+ :where(.rating) {
6049
+ --intent: initial;
6050
+ }
6051
+
6052
+ .rating {
6053
+ --rating-size: 1.25rem;
6054
+ --rating-gap: 0.1rem;
6055
+ --rating-color: var(--intent, var(--warning));
6056
+ --rating-empty: var(--border);
6057
+ --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");
6058
+
6059
+ display: inline-flex;
6060
+ gap: 0;
6061
+ color: var(--rating-color);
6062
+ }
6063
+
6064
+ .rating > input[type="radio"] {
6065
+ appearance: none;
6066
+ flex: 0 0 auto;
6067
+ inline-size: calc(var(--rating-size) + var(--rating-gap));
6068
+ block-size: var(--rating-size);
6069
+ margin: 0;
6070
+ border: 0;
6071
+ background: var(--rating-empty);
6072
+ /* Contiguous radio boxes own hit-testing; the centered mask preserves the
6073
+ star's proportions while leaving a purely visual gap between values. */
6074
+ -webkit-mask: var(--rating-star) center / var(--rating-size) var(--rating-size) no-repeat;
6075
+ mask: var(--rating-star) center / var(--rating-size) var(--rating-size) no-repeat;
6076
+ cursor: pointer;
6077
+ transition: background-color var(--duration);
6078
+ }
6079
+
6080
+ .rating > input[type="radio"]:checked {
6081
+ background: var(--rating-color);
6082
+ }
6083
+
6084
+ .rating > input[type="radio"]:disabled {
6085
+ cursor: not-allowed;
6086
+ opacity: var(--disabled-opacity);
6087
+ }
6088
+
6089
+ .rating > input[type="radio"]:focus-visible {
6090
+ outline: var(--focus-outline);
6091
+ outline-offset: var(--focus-outline-offset);
6092
+ background: var(--rating-color);
6093
+ }
6094
+
6095
+ /* Browsers without :has() still show the selected native radio as one filled
6096
+ star. The enhancement only adds cumulative checked and hover previews. */
6097
+ @supports selector(:has(*)) {
6098
+ .rating > input[type="radio"]:has(~ input[type="radio"]:checked) {
6099
+ background: var(--rating-color);
6100
+ }
6101
+
6102
+ @media (hover: hover) {
6103
+ .rating > input[type="radio"]:not(:disabled):hover,
6104
+ .rating > input[type="radio"]:has(~ input[type="radio"]:not(:disabled):hover) {
6105
+ background: var(--rating-color);
6106
+ }
6107
+
6108
+ /* A hovered value previews exactly that value: later stars are emptied,
6109
+ including a previously checked star beyond the pointer. */
6110
+ .rating > input[type="radio"]:not(:disabled):hover ~ input[type="radio"] {
6111
+ background: var(--rating-empty);
6112
+ }
6113
+ }
6114
+ }
6115
+
6116
+ .rating.sm {
6117
+ --rating-size: 1rem;
6118
+ }
6119
+
6120
+ .rating.lg {
6121
+ --rating-size: 1.75rem;
6122
+ }
6123
+
5922
6124
  @media (forced-colors: active) {
5923
- .fab-label {
5924
- border: var(--border-width) solid CanvasText;
5925
- box-shadow: none;
6125
+ .rating > input[type="radio"] {
6126
+ appearance: auto;
6127
+ inline-size: auto;
6128
+ block-size: auto;
6129
+ background: Canvas;
6130
+ -webkit-mask: none;
6131
+ mask: none;
6132
+ opacity: 1;
5926
6133
  }
5927
6134
  }
5928
6135
 
6136
+ /*
6137
+ * Steps — semantic ordered-list progression. One intrinsic layout: items fill
6138
+ * the row when they fit and hold a minimum width otherwise, so the sequence
6139
+ * scrolls horizontally only when it genuinely cannot fit. No breakpoint decides
6140
+ * this — the item minimum tracks the real content, not an arbitrary width.
6141
+ *
6142
+ * Markers come from the list counter; state stays in .complete and
6143
+ * aria-current="step", so the component needs no runtime.
6144
+ *
6145
+ * Public hooks:
6146
+ * --step-size
6147
+ * --step-min
6148
+ * --step-gap
6149
+ * --step-line-size
6150
+ * --step-marker-radius
6151
+ * --step-complete-mark
6152
+ * --step-connector
6153
+ *
6154
+ * Internal:
6155
+ * --step-marker-bg
6156
+ * --step-marker-border
6157
+ * --step-marker-fg
6158
+ * --step-line
6159
+ */
6160
+ .steps {
6161
+ --step-size: 1.75rem;
6162
+ --step-min: 7rem;
6163
+ --step-gap: var(--space-20);
6164
+ --step-line-size: var(--border-width);
6165
+ --step-marker-bg: var(--surface);
6166
+ --step-marker-border: var(--border);
6167
+ --step-marker-fg: var(--text-muted);
6168
+ --step-marker-radius: var(--radius-full);
6169
+ --step-line: var(--border);
6170
+
6171
+ counter-reset: actual-step;
6172
+ display: flex;
6173
+ flex-direction: row;
6174
+ inline-size: 100%;
6175
+ margin: 0;
6176
+ padding: 0;
6177
+ list-style: none;
6178
+ /* Fill when it fits, scroll when it cannot: items never shrink below
6179
+ --step-min, so labels keep a readable width and the sequence scrolls
6180
+ horizontally instead of squishing. overscroll-behavior keeps the scroll
6181
+ from chaining to the page. */
6182
+ overflow-x: auto;
6183
+ overscroll-behavior-inline: contain;
6184
+ }
6185
+
6186
+ .steps > li {
6187
+ position: relative;
6188
+ display: grid;
6189
+ flex: 1 0 var(--step-min);
6190
+ grid-template-rows: var(--step-size) auto;
6191
+ gap: var(--step-gap);
6192
+ justify-items: center;
6193
+ min-inline-size: max(var(--step-size), var(--step-min));
6194
+ color: var(--text-muted);
6195
+ text-align: center;
6196
+ counter-increment: actual-step;
6197
+ }
6198
+
6199
+ .steps > li::before {
6200
+ content: counter(actual-step);
6201
+ position: relative;
6202
+ z-index: 1;
6203
+ display: inline-grid;
6204
+ place-items: center;
6205
+ inline-size: var(--step-size);
6206
+ block-size: var(--step-size);
6207
+ border: var(--border-width) solid var(--step-marker-border);
6208
+ border-radius: var(--step-marker-radius);
6209
+ background: var(--step-marker-bg);
6210
+ color: var(--step-marker-fg);
6211
+ font-size: var(--font-size-sm);
6212
+ font-weight: var(--font-weight-strong);
6213
+ line-height: 1;
6214
+ }
6215
+
6216
+ .steps > li::after {
6217
+ content: "";
6218
+ position: absolute;
6219
+ inset-block-start: calc((var(--step-size) - var(--step-line-size)) / 2);
6220
+ inset-inline-start: calc(50% + var(--step-size) / 2);
6221
+ inline-size: calc(100% - var(--step-size));
6222
+ block-size: var(--step-line-size);
6223
+ /* A custom property resolves its var() before inheritance, so a default
6224
+ --step-connector: var(--step-line) on .steps would freeze the parent line
6225
+ color and a .complete child's connector would never follow its own line.
6226
+ Falling back to the local --step-line here keeps each connector in sync;
6227
+ an explicit --step-connector still wins when the author sets one. */
6228
+ background: var(--step-connector, var(--step-line));
6229
+ }
6230
+
6231
+ .steps > li:last-child::after {
6232
+ content: none;
6233
+ }
6234
+
6235
+ .steps > :is(.complete, [aria-current="step"]) {
6236
+ --step-marker-bg: var(--state-selected);
6237
+ --step-marker-border: var(--state-selected);
6238
+ --step-marker-fg: var(--state-selected-fg);
6239
+
6240
+ color: var(--text);
6241
+ }
6242
+
6243
+ /* Thicker border is the selected affordance in every mode — structural, and it
6244
+ never competes with the outline channel owned by focus. */
6245
+ .steps > :is(.complete, [aria-current="step"])::before {
6246
+ border-width: calc(var(--border-width) * 2);
6247
+ }
6248
+
6249
+ .steps > .complete {
6250
+ --step-line: var(--state-selected);
6251
+ }
6252
+
6253
+ /* A finished step may replace its number with a mark (e.g. a check glyph):
6254
+ --step-complete-mark accepts any content value and falls back to the
6255
+ counter, so existing numbered steppers are unchanged. */
6256
+ .steps > .complete::before {
6257
+ content: var(--step-complete-mark, counter(actual-step));
6258
+ }
6259
+
6260
+
6261
+ /* Navigation */
6262
+ /*
6263
+ * Primary application navigation. The component owns the navigation surface
6264
+ * and item states; app-layout owns whether that surface is a bottom bar or
6265
+ * labelled side navigation.
6266
+ *
6267
+ * Public hooks:
6268
+ * --app-nav-size
6269
+ * --app-nav-gap
6270
+ */
6271
+ :where(.app-nav) {
6272
+ --ui-bg: initial;
6273
+ --ui-fg: initial;
6274
+ --ui-border: initial;
6275
+ }
6276
+
6277
+ .app-nav {
6278
+ --app-nav-size: 4rem;
6279
+ --app-nav-gap: var(--space-10);
6280
+
6281
+ position: sticky;
6282
+ inset-block-end: 0;
6283
+ z-index: var(--z-sticky);
6284
+ display: grid;
6285
+ grid-auto-flow: column;
6286
+ grid-auto-columns: minmax(0, 1fr);
6287
+ gap: var(--app-nav-gap);
6288
+ min-block-size: calc(var(--app-nav-size) + env(safe-area-inset-bottom));
6289
+ padding-block: var(--space-10) max(var(--space-10), env(safe-area-inset-bottom));
6290
+ padding-inline: var(--space-10);
6291
+ border-block-start: var(--border-width) solid var(--ui-border, var(--border));
6292
+ background: var(--ui-bg, var(--surface));
6293
+ color: var(--ui-fg, var(--text-muted));
6294
+ }
6295
+
6296
+ .app-nav > a {
6297
+ display: grid;
6298
+ grid-template-rows: auto auto;
6299
+ place-content: center;
6300
+ justify-items: center;
6301
+ gap: var(--space-10);
6302
+ min-inline-size: 0;
6303
+ min-block-size: var(--control-size-lg);
6304
+ padding-inline: var(--space-10);
6305
+ border-radius: var(--radius);
6306
+ color: inherit;
6307
+ font-size: var(--font-size-xs);
6308
+ font-weight: var(--font-weight-medium);
6309
+ line-height: var(--line-height-tight);
6310
+ text-align: center;
6311
+ text-decoration: none;
6312
+ overflow-wrap: anywhere;
6313
+ }
6314
+
6315
+ .app-nav > a > :where(svg, img, [aria-hidden="true"]) {
6316
+ inline-size: 1.5rem;
6317
+ block-size: 1.5rem;
6318
+ font-size: 1.5rem;
6319
+ line-height: 1;
6320
+ }
6321
+
6322
+ .app-nav > a:hover {
6323
+ background: var(--surface-subtle);
6324
+ color: var(--text);
6325
+ }
6326
+
6327
+ .app-nav > a:where([aria-current]:not([aria-current="false"])) {
6328
+ background: var(--state-selected);
6329
+ color: var(--state-selected-fg);
6330
+ font-weight: var(--font-weight-bold);
6331
+ }
6332
+
5929
6333
  @media print {
5930
- .fab {
6334
+ .app-nav {
5931
6335
  display: none;
5932
6336
  }
5933
6337
  }
5934
6338
 
5935
-
5936
- /* Navigation */
5937
6339
  /*
5938
6340
  * Navbar — top-level navigation bar.
5939
6341
  *
@@ -5954,6 +6356,7 @@ a.avatar[aria-disabled="true"] {
5954
6356
 
5955
6357
  .navbar {
5956
6358
  display: flex;
6359
+ flex-direction: row;
5957
6360
  flex-wrap: wrap;
5958
6361
  align-items: center;
5959
6362
  justify-content: space-between;
@@ -5981,6 +6384,7 @@ a.avatar[aria-disabled="true"] {
5981
6384
 
5982
6385
  .navbar-nav {
5983
6386
  display: flex;
6387
+ flex-direction: row;
5984
6388
  flex-wrap: wrap;
5985
6389
  align-items: center;
5986
6390
  gap: var(--space-10);
@@ -6003,6 +6407,7 @@ a.avatar[aria-disabled="true"] {
6003
6407
  .nav-link:where([aria-current]:not([aria-current="false"])) {
6004
6408
  background: var(--surface-subtle);
6005
6409
  color: var(--text);
6410
+ font-weight: var(--font-weight-bold);
6006
6411
  }
6007
6412
 
6008
6413
  .nav-link:hover {
@@ -6010,7 +6415,9 @@ a.avatar[aria-disabled="true"] {
6010
6415
  color: var(--text);
6011
6416
  }
6012
6417
 
6013
- /* Nav list — a vertical navigation list. Used by .drawer for stacked links. */
6418
+ /* Nav list — a vertical navigation list. Used by .drawer for stacked links.
6419
+ Links fill each row so the whole line is an interactive target; the
6420
+ horizontal .navbar-nav links keep their intrinsic size. */
6014
6421
  .nav-list {
6015
6422
  display: grid;
6016
6423
  --gap: var(--space-20);
@@ -6024,20 +6431,8 @@ a.avatar[aria-disabled="true"] {
6024
6431
  margin: 0;
6025
6432
  }
6026
6433
 
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
- }
6434
+ .nav-list .nav-link {
6435
+ inline-size: 100%;
6041
6436
  }
6042
6437
 
6043
6438
  /*
@@ -6048,6 +6443,7 @@ a.avatar[aria-disabled="true"] {
6048
6443
  */
6049
6444
  .breadcrumb {
6050
6445
  display: flex;
6446
+ flex-direction: row;
6051
6447
  flex-wrap: wrap;
6052
6448
  align-items: center;
6053
6449
  gap: var(--space-10);
@@ -6101,6 +6497,7 @@ a.avatar[aria-disabled="true"] {
6101
6497
  */
6102
6498
  .pagination {
6103
6499
  display: flex;
6500
+ flex-direction: row;
6104
6501
  flex-wrap: wrap;
6105
6502
  align-items: center;
6106
6503
  gap: var(--space-10);
@@ -6149,6 +6546,7 @@ a.avatar[aria-disabled="true"] {
6149
6546
 
6150
6547
  .tabs {
6151
6548
  display: flex;
6549
+ flex-direction: row;
6152
6550
  flex-wrap: wrap;
6153
6551
  gap: var(--space-10);
6154
6552
  margin: 0;
@@ -6166,13 +6564,14 @@ a.avatar[aria-disabled="true"] {
6166
6564
  }
6167
6565
 
6168
6566
  .tab {
6567
+ /* Positioning context for the selected indicator pseudo. */
6568
+ position: relative;
6169
6569
  display: inline-flex;
6170
6570
  align-items: center;
6171
6571
  gap: var(--tab-gap, 0.375em);
6172
6572
  min-block-size: var(--control-size);
6173
6573
  padding-inline: var(--space-30);
6174
6574
  border: 0;
6175
- border-block-end: 2px solid transparent;
6176
6575
  border-radius: 0;
6177
6576
  background: transparent;
6178
6577
  color: var(--text-muted);
@@ -6184,8 +6583,6 @@ a.avatar[aria-disabled="true"] {
6184
6583
 
6185
6584
  .tabs[aria-orientation="vertical"] .tab {
6186
6585
  justify-content: flex-start;
6187
- border-block-end: 0;
6188
- border-inline-end: 2px solid transparent;
6189
6586
  padding-inline-end: var(--space-40);
6190
6587
  }
6191
6588
 
@@ -6206,30 +6603,29 @@ a.avatar[aria-disabled="true"] {
6206
6603
  }
6207
6604
 
6208
6605
  .tab[aria-selected="true"] {
6209
- border-block-end-color: var(--intent, var(--state-selected));
6210
6606
  color: var(--intent, var(--state-selected));
6607
+ /* Second, layout-free selected cue — the underline alone is color-only in
6608
+ forced colors. */
6609
+ font-weight: var(--font-weight-bold);
6211
6610
  }
6212
6611
 
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));
6612
+ /* The selected indicator lives on a positioned pseudo so that only the active
6613
+ tab owns an underline: a transparent border on every tab would surface in
6614
+ forced colors and erase the distinction. currentColor follows the palette the
6615
+ UA forces, so no forced-colors block is needed. */
6616
+ .tab[aria-selected="true"]::after {
6617
+ content: "";
6618
+ position: absolute;
6619
+ inset-inline: 0;
6620
+ inset-block-end: 0;
6621
+ border-block-end: 2px solid currentColor;
6216
6622
  }
6217
6623
 
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
- }
6624
+ .tabs[aria-orientation="vertical"] .tab[aria-selected="true"]::after {
6625
+ inset-block: 0;
6626
+ inset-inline: auto 0;
6627
+ border-block-end: 0;
6628
+ border-inline-end: 2px solid currentColor;
6233
6629
  }
6234
6630
 
6235
6631
  /*
@@ -6262,6 +6658,90 @@ a.avatar[aria-disabled="true"] {
6262
6658
 
6263
6659
 
6264
6660
  /* Data */
6661
+ /*
6662
+ * Application list — repeated rows with optional leading and trailing regions.
6663
+ * Content determines the number of lines; the component only owns the row
6664
+ * geometry, dividers, and supporting-text treatment.
6665
+ *
6666
+ * Public hooks:
6667
+ * --list-item-min-size
6668
+ * --list-item-pad-block
6669
+ * --list-item-pad-inline
6670
+ * --list-item-gap
6671
+ * --list-divider
6672
+ */
6673
+ .list {
6674
+ --list-item-min-size: var(--control-size-lg);
6675
+ --list-item-pad-block: var(--space-30);
6676
+ --list-item-pad-inline: 0;
6677
+ --list-item-gap: var(--gap);
6678
+ --list-divider: var(--border-width) solid var(--border);
6679
+
6680
+ display: grid;
6681
+ gap: 0;
6682
+ margin: 0;
6683
+ padding: 0;
6684
+ border-block-start: var(--list-divider);
6685
+ list-style: none;
6686
+ }
6687
+
6688
+ .list > li {
6689
+ margin: 0;
6690
+ }
6691
+
6692
+ .list > :where(.list-item),
6693
+ .list > li > .list-item {
6694
+ display: grid;
6695
+ grid-template-columns: auto minmax(0, 1fr) auto;
6696
+ align-items: center;
6697
+ gap: var(--list-item-gap);
6698
+ min-block-size: var(--list-item-min-size);
6699
+ padding-block: var(--list-item-pad-block);
6700
+ padding-inline: var(--list-item-pad-inline);
6701
+ }
6702
+
6703
+ .list > .list-item + .list-item,
6704
+ .list > li + li {
6705
+ border-block-start: var(--list-divider);
6706
+ }
6707
+
6708
+ a.list-item {
6709
+ color: inherit;
6710
+ text-decoration: none;
6711
+ }
6712
+
6713
+ a.list-item:hover {
6714
+ background: var(--surface-subtle);
6715
+ }
6716
+
6717
+ .list-item-content {
6718
+ min-inline-size: 0;
6719
+ }
6720
+
6721
+ .list-item-title,
6722
+ .list-item-text {
6723
+ display: block;
6724
+ margin: 0;
6725
+ }
6726
+
6727
+ .list-item-text {
6728
+ color: var(--text-muted);
6729
+ font-size: var(--font-size-sm);
6730
+ overflow-wrap: anywhere;
6731
+ }
6732
+
6733
+ .list-item-trailing {
6734
+ justify-self: end;
6735
+ min-inline-size: 0;
6736
+ }
6737
+
6738
+ @media print {
6739
+ .list > :where(.list-item),
6740
+ .list > li > .list-item {
6741
+ break-inside: avoid;
6742
+ }
6743
+ }
6744
+
6265
6745
  /*
6266
6746
  * Table — structured tabular data.
6267
6747
  *
@@ -6560,6 +7040,7 @@ a.avatar[aria-disabled="true"] {
6560
7040
  display: inline-grid;
6561
7041
  padding: var(--aura-width);
6562
7042
  border-radius: calc(var(--aura-radius) + var(--aura-width));
7043
+ border: var(--border-width) solid transparent;
6563
7044
  background: var(--aura-color);
6564
7045
  background: conic-gradient(from var(--aura-angle), transparent 0 65%, var(--aura-color));
6565
7046
  }
@@ -6610,16 +7091,6 @@ a.avatar[aria-disabled="true"] {
6610
7091
  }
6611
7092
  }
6612
7093
 
6613
- @media (forced-colors: active) {
6614
- .aura {
6615
- background: CanvasText;
6616
- }
6617
-
6618
- .aura::before {
6619
- display: none;
6620
- }
6621
- }
6622
-
6623
7094
  /* Utilities — small, single-purpose escape hatches. */
6624
7095
  /*
6625
7096
  * Small, single-purpose utilities for common escape hatches.