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
@@ -18,6 +18,7 @@
18
18
 
19
19
  .navbar {
20
20
  display: flex;
21
+ flex-direction: row;
21
22
  flex-wrap: wrap;
22
23
  align-items: center;
23
24
  justify-content: space-between;
@@ -45,6 +46,7 @@
45
46
 
46
47
  .navbar-nav {
47
48
  display: flex;
49
+ flex-direction: row;
48
50
  flex-wrap: wrap;
49
51
  align-items: center;
50
52
  gap: var(--space-10);
@@ -67,6 +69,7 @@
67
69
  .nav-link:where([aria-current]:not([aria-current="false"])) {
68
70
  background: var(--surface-subtle);
69
71
  color: var(--text);
72
+ font-weight: var(--font-weight-bold);
70
73
  }
71
74
 
72
75
  .nav-link:hover {
@@ -74,7 +77,9 @@
74
77
  color: var(--text);
75
78
  }
76
79
 
77
- /* Nav list — a vertical navigation list. Used by .drawer for stacked links. */
80
+ /* Nav list — a vertical navigation list. Used by .drawer for stacked links.
81
+ Links fill each row so the whole line is an interactive target; the
82
+ horizontal .navbar-nav links keep their intrinsic size. */
78
83
  .nav-list {
79
84
  display: grid;
80
85
  --gap: var(--space-20);
@@ -88,18 +93,6 @@
88
93
  margin: 0;
89
94
  }
90
95
 
91
- @media (forced-colors: active) {
92
- .nav-link:where([aria-current]:not([aria-current="false"])) {
93
- outline: var(--border-width) solid Highlight;
94
- background: Highlight;
95
- color: HighlightText;
96
- forced-color-adjust: none;
97
- }
98
-
99
- .nav-link:hover:not(:where([aria-current]:not([aria-current="false"]))) {
100
- outline: var(--border-width) solid ButtonText;
101
- outline-offset: 2px;
102
- background: Canvas;
103
- color: CanvasText;
104
- }
96
+ .nav-list .nav-link {
97
+ inline-size: 100%;
105
98
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  .pagination {
12
12
  display: flex;
13
+ flex-direction: row;
13
14
  flex-wrap: wrap;
14
15
  align-items: center;
15
16
  gap: var(--space-10);
@@ -0,0 +1,101 @@
1
+ /*
2
+ * Rating — integer star rating drawn on native radio inputs.
3
+ *
4
+ * The checked radio remains the functional baseline. :has() progressively
5
+ * fills preceding stars without reversing DOM or keyboard order. Forced
6
+ * colors restores the platform radio presentation.
7
+ *
8
+ * Public hooks:
9
+ * --rating-size
10
+ * --rating-gap
11
+ * --rating-color
12
+ * --rating-empty
13
+ * --rating-star
14
+ */
15
+ :where(.rating) {
16
+ --intent: initial;
17
+ }
18
+
19
+ .rating {
20
+ --rating-size: 1.25rem;
21
+ --rating-gap: 0.1rem;
22
+ --rating-color: var(--intent, var(--warning));
23
+ --rating-empty: var(--border);
24
+ --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");
25
+
26
+ display: inline-flex;
27
+ gap: 0;
28
+ color: var(--rating-color);
29
+ }
30
+
31
+ .rating > input[type="radio"] {
32
+ appearance: none;
33
+ flex: 0 0 auto;
34
+ inline-size: calc(var(--rating-size) + var(--rating-gap));
35
+ block-size: var(--rating-size);
36
+ margin: 0;
37
+ border: 0;
38
+ background: var(--rating-empty);
39
+ /* Contiguous radio boxes own hit-testing; the centered mask preserves the
40
+ star's proportions while leaving a purely visual gap between values. */
41
+ -webkit-mask: var(--rating-star) center / var(--rating-size) var(--rating-size) no-repeat;
42
+ mask: var(--rating-star) center / var(--rating-size) var(--rating-size) no-repeat;
43
+ cursor: pointer;
44
+ transition: background-color var(--duration);
45
+ }
46
+
47
+ .rating > input[type="radio"]:checked {
48
+ background: var(--rating-color);
49
+ }
50
+
51
+ .rating > input[type="radio"]:disabled {
52
+ cursor: not-allowed;
53
+ opacity: var(--disabled-opacity);
54
+ }
55
+
56
+ .rating > input[type="radio"]:focus-visible {
57
+ outline: var(--focus-outline);
58
+ outline-offset: var(--focus-outline-offset);
59
+ background: var(--rating-color);
60
+ }
61
+
62
+ /* Browsers without :has() still show the selected native radio as one filled
63
+ star. The enhancement only adds cumulative checked and hover previews. */
64
+ @supports selector(:has(*)) {
65
+ .rating > input[type="radio"]:has(~ input[type="radio"]:checked) {
66
+ background: var(--rating-color);
67
+ }
68
+
69
+ @media (hover: hover) {
70
+ .rating > input[type="radio"]:not(:disabled):hover,
71
+ .rating > input[type="radio"]:has(~ input[type="radio"]:not(:disabled):hover) {
72
+ background: var(--rating-color);
73
+ }
74
+
75
+ /* A hovered value previews exactly that value: later stars are emptied,
76
+ including a previously checked star beyond the pointer. */
77
+ .rating > input[type="radio"]:not(:disabled):hover ~ input[type="radio"] {
78
+ background: var(--rating-empty);
79
+ }
80
+ }
81
+ }
82
+
83
+ .rating.sm {
84
+ --rating-size: 1rem;
85
+ }
86
+
87
+ .rating.lg {
88
+ --rating-size: 1.75rem;
89
+ }
90
+
91
+ @media (forced-colors: active) {
92
+ .rating > input[type="radio"] {
93
+ appearance: auto;
94
+ inline-size: auto;
95
+ block-size: auto;
96
+ background: Canvas;
97
+ -webkit-mask: none;
98
+ mask: none;
99
+ opacity: 1;
100
+ }
101
+ }
@@ -0,0 +1,123 @@
1
+ /*
2
+ * Steps — semantic ordered-list progression. One intrinsic layout: items fill
3
+ * the row when they fit and hold a minimum width otherwise, so the sequence
4
+ * scrolls horizontally only when it genuinely cannot fit. No breakpoint decides
5
+ * this — the item minimum tracks the real content, not an arbitrary width.
6
+ *
7
+ * Markers come from the list counter; state stays in .complete and
8
+ * aria-current="step", so the component needs no runtime.
9
+ *
10
+ * Public hooks:
11
+ * --step-size
12
+ * --step-min
13
+ * --step-gap
14
+ * --step-line-size
15
+ * --step-marker-radius
16
+ * --step-complete-mark
17
+ * --step-connector
18
+ *
19
+ * Internal:
20
+ * --step-marker-bg
21
+ * --step-marker-border
22
+ * --step-marker-fg
23
+ * --step-line
24
+ */
25
+ .steps {
26
+ --step-size: 1.75rem;
27
+ --step-min: 7rem;
28
+ --step-gap: var(--space-20);
29
+ --step-line-size: var(--border-width);
30
+ --step-marker-bg: var(--surface);
31
+ --step-marker-border: var(--border);
32
+ --step-marker-fg: var(--text-muted);
33
+ --step-marker-radius: var(--radius-full);
34
+ --step-line: var(--border);
35
+
36
+ counter-reset: actual-step;
37
+ display: flex;
38
+ flex-direction: row;
39
+ inline-size: 100%;
40
+ margin: 0;
41
+ padding: 0;
42
+ list-style: none;
43
+ /* Fill when it fits, scroll when it cannot: items never shrink below
44
+ --step-min, so labels keep a readable width and the sequence scrolls
45
+ horizontally instead of squishing. overscroll-behavior keeps the scroll
46
+ from chaining to the page. */
47
+ overflow-x: auto;
48
+ overscroll-behavior-inline: contain;
49
+ }
50
+
51
+ .steps > li {
52
+ position: relative;
53
+ display: grid;
54
+ flex: 1 0 var(--step-min);
55
+ grid-template-rows: var(--step-size) auto;
56
+ gap: var(--step-gap);
57
+ justify-items: center;
58
+ min-inline-size: max(var(--step-size), var(--step-min));
59
+ color: var(--text-muted);
60
+ text-align: center;
61
+ counter-increment: actual-step;
62
+ }
63
+
64
+ .steps > li::before {
65
+ content: counter(actual-step);
66
+ position: relative;
67
+ z-index: 1;
68
+ display: inline-grid;
69
+ place-items: center;
70
+ inline-size: var(--step-size);
71
+ block-size: var(--step-size);
72
+ border: var(--border-width) solid var(--step-marker-border);
73
+ border-radius: var(--step-marker-radius);
74
+ background: var(--step-marker-bg);
75
+ color: var(--step-marker-fg);
76
+ font-size: var(--font-size-sm);
77
+ font-weight: var(--font-weight-strong);
78
+ line-height: 1;
79
+ }
80
+
81
+ .steps > li::after {
82
+ content: "";
83
+ position: absolute;
84
+ inset-block-start: calc((var(--step-size) - var(--step-line-size)) / 2);
85
+ inset-inline-start: calc(50% + var(--step-size) / 2);
86
+ inline-size: calc(100% - var(--step-size));
87
+ block-size: var(--step-line-size);
88
+ /* A custom property resolves its var() before inheritance, so a default
89
+ --step-connector: var(--step-line) on .steps would freeze the parent line
90
+ color and a .complete child's connector would never follow its own line.
91
+ Falling back to the local --step-line here keeps each connector in sync;
92
+ an explicit --step-connector still wins when the author sets one. */
93
+ background: var(--step-connector, var(--step-line));
94
+ }
95
+
96
+ .steps > li:last-child::after {
97
+ content: none;
98
+ }
99
+
100
+ .steps > :is(.complete, [aria-current="step"]) {
101
+ --step-marker-bg: var(--state-selected);
102
+ --step-marker-border: var(--state-selected);
103
+ --step-marker-fg: var(--state-selected-fg);
104
+
105
+ color: var(--text);
106
+ }
107
+
108
+ /* Thicker border is the selected affordance in every mode — structural, and it
109
+ never competes with the outline channel owned by focus. */
110
+ .steps > :is(.complete, [aria-current="step"])::before {
111
+ border-width: calc(var(--border-width) * 2);
112
+ }
113
+
114
+ .steps > .complete {
115
+ --step-line: var(--state-selected);
116
+ }
117
+
118
+ /* A finished step may replace its number with a mark (e.g. a check glyph):
119
+ --step-complete-mark accepts any content value and falls back to the
120
+ counter, so existing numbered steppers are unchanged. */
121
+ .steps > .complete::before {
122
+ content: var(--step-complete-mark, counter(actual-step));
123
+ }
@@ -16,6 +16,7 @@
16
16
 
17
17
  .tabs {
18
18
  display: flex;
19
+ flex-direction: row;
19
20
  flex-wrap: wrap;
20
21
  gap: var(--space-10);
21
22
  margin: 0;
@@ -33,13 +34,14 @@
33
34
  }
34
35
 
35
36
  .tab {
37
+ /* Positioning context for the selected indicator pseudo. */
38
+ position: relative;
36
39
  display: inline-flex;
37
40
  align-items: center;
38
41
  gap: var(--tab-gap, 0.375em);
39
42
  min-block-size: var(--control-size);
40
43
  padding-inline: var(--space-30);
41
44
  border: 0;
42
- border-block-end: 2px solid transparent;
43
45
  border-radius: 0;
44
46
  background: transparent;
45
47
  color: var(--text-muted);
@@ -51,8 +53,6 @@
51
53
 
52
54
  .tabs[aria-orientation="vertical"] .tab {
53
55
  justify-content: flex-start;
54
- border-block-end: 0;
55
- border-inline-end: 2px solid transparent;
56
56
  padding-inline-end: var(--space-40);
57
57
  }
58
58
 
@@ -73,28 +73,27 @@
73
73
  }
74
74
 
75
75
  .tab[aria-selected="true"] {
76
- border-block-end-color: var(--intent, var(--state-selected));
77
76
  color: var(--intent, var(--state-selected));
77
+ /* Second, layout-free selected cue — the underline alone is color-only in
78
+ forced colors. */
79
+ font-weight: var(--font-weight-bold);
78
80
  }
79
81
 
80
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
81
- border-block-end-color: transparent;
82
- border-inline-end-color: var(--intent, var(--state-selected));
82
+ /* The selected indicator lives on a positioned pseudo so that only the active
83
+ tab owns an underline: a transparent border on every tab would surface in
84
+ forced colors and erase the distinction. currentColor follows the palette the
85
+ UA forces, so no forced-colors block is needed. */
86
+ .tab[aria-selected="true"]::after {
87
+ content: "";
88
+ position: absolute;
89
+ inset-inline: 0;
90
+ inset-block-end: 0;
91
+ border-block-end: 2px solid currentColor;
83
92
  }
84
93
 
85
- @media (forced-colors: active) {
86
- .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
87
- text-decoration: underline;
88
- text-underline-offset: 0.18em;
89
- }
90
-
91
- .tab[aria-selected="true"] {
92
- border-block-end-color: Highlight;
93
- color: Highlight;
94
- }
95
-
96
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
97
- border-block-end-color: transparent;
98
- border-inline-end-color: Highlight;
99
- }
94
+ .tabs[aria-orientation="vertical"] .tab[aria-selected="true"]::after {
95
+ inset-block: 0;
96
+ inset-inline: auto 0;
97
+ border-block-end: 0;
98
+ border-inline-end: 2px solid currentColor;
100
99
  }
@@ -24,6 +24,7 @@
24
24
  max-inline-size: min(24rem, calc(var(--viewport-inline) - 2rem));
25
25
  padding: 0.25em 0.6em;
26
26
  border-radius: var(--radius-sm);
27
+ border: var(--border-width) solid transparent;
27
28
  background: var(--tooltip-bg);
28
29
  color: var(--tooltip-fg);
29
30
  font-size: var(--font-size-xs);
@@ -102,16 +103,6 @@
102
103
  clip-path: polygon(0 50%, 100% 0, 100% 100%);
103
104
  }
104
105
 
105
- @media (forced-colors: active) {
106
- .tooltip {
107
- --tooltip-bg: Canvas;
108
- --tooltip-fg: CanvasText;
109
- }
110
-
111
- .tooltip::before {
112
- display: none;
113
- }
114
- }
115
106
  @media print {
116
107
  .tooltip {
117
108
  display: none !important;
@@ -15,12 +15,4 @@
15
15
  :focus:not(:focus-visible) {
16
16
  outline: none;
17
17
  }
18
-
19
- /* Pointer focus must still signal in high-contrast mode, where the shared
20
- box-shadow rings are disabled (--focus-ring-shadow: none). */
21
- @media (forced-colors: active) {
22
- :focus:not(:focus-visible) {
23
- outline: var(--focus-outline);
24
- }
25
- }
26
18
  }
@@ -188,16 +188,6 @@
188
188
  }
189
189
  }
190
190
 
191
- @media (forced-colors: active) {
192
- :root,
193
- [data-theme] {
194
- --focus-outline-color: Highlight;
195
- --focus-ring-shadow: none;
196
-
197
- --backdrop-fill: Canvas;
198
- }
199
- }
200
-
201
191
  @media (prefers-contrast: more) {
202
192
  :root,
203
193
  /* The repeated attribute intentionally outranks single-attribute preset
@@ -156,9 +156,8 @@
156
156
  @media (forced-colors: active) {
157
157
  :root,
158
158
  [data-theme] {
159
- --state-selected: Highlight;
160
- --state-selected-fg: HighlightText;
161
159
  --state-disabled: GrayText;
160
+ --disabled-opacity: 1;
162
161
  }
163
162
  }
164
163
 
@@ -32,6 +32,7 @@
32
32
  display: inline-grid;
33
33
  padding: var(--aura-width);
34
34
  border-radius: calc(var(--aura-radius) + var(--aura-width));
35
+ border: var(--border-width) solid transparent;
35
36
  background: var(--aura-color);
36
37
  background: conic-gradient(from var(--aura-angle), transparent 0 65%, var(--aura-color));
37
38
  }
@@ -81,13 +82,3 @@
81
82
  --aura-radius: var(--radius-full);
82
83
  }
83
84
  }
84
-
85
- @media (forced-colors: active) {
86
- .aura {
87
- background: CanvasText;
88
- }
89
-
90
- .aura::before {
91
- display: none;
92
- }
93
- }
@@ -131,27 +131,18 @@
131
131
  transform: scale(1);
132
132
  }
133
133
 
134
+ /* The native input is hidden and the checked marker is custom-drawn: without
135
+ this repair, forced colors pushes disc and check to the same system color
136
+ and the state disappears. Highlight/Canvas/HighlightText are preserved as-is,
137
+ so no forced-color-adjust is needed. The pseudo is not focusable — the
138
+ outline is a separator against the card, not a focus channel. */
134
139
  @media (forced-colors: active) {
135
- .choice-card {
136
- --choice-card-border: ButtonText;
137
- --choice-card-bg: Canvas;
138
- }
139
-
140
- .choice-card:has(:checked) {
141
- --choice-card-border: Highlight;
142
- /* Pin the selected surface to the system canvas: only the essential
143
- marker pseudos opt out of remapping, never the interactive card host. */
144
- --choice-card-bg: Canvas;
145
- }
146
-
147
140
  .choice-card:has(:checked)::before {
148
141
  background: Highlight;
149
- box-shadow: 0 0 0 2px Canvas;
150
- forced-color-adjust: none;
142
+ outline: 2px solid Canvas;
151
143
  }
152
144
 
153
145
  .choice-card:has(:checked)::after {
154
146
  background: HighlightText;
155
- forced-color-adjust: none;
156
147
  }
157
148
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  .form-actions {
19
19
  display: flex;
20
+ flex-direction: row;
20
21
  flex-wrap: wrap;
21
22
  gap: var(--gap);
22
23
  align-items: var(--form-actions-align, center);
@@ -79,7 +79,12 @@
79
79
  but it sits in the clipped input extension once every cell is filled, so
80
80
  the focused edge must be unambiguous on its own — hence the doubled width,
81
81
  not only the color swap. Width is safe to change: box-sizing is border-box
82
- and the cells have a fixed inline/block size, so nothing reflows. */
82
+ and the cells have a fixed inline/block size, so nothing reflows.
83
+
84
+ The <input> must be the first direct child, with one cell <span> per
85
+ character after it: the state rules select the cells as following siblings
86
+ (input:focus ~ span, input:disabled ~ span, …), so that order is part of the
87
+ contract. */
83
88
  .otp > input:focus ~ span {
84
89
  --otp-cell-border: var(--focus);
85
90
  --otp-cell-border-width: calc(var(--border-width) * 2);
@@ -103,7 +108,7 @@
103
108
 
104
109
  .otp > input:disabled ~ span {
105
110
  --otp-cell-bg: var(--surface-subtle);
106
-
111
+ --otp-cell-border: var(--state-disabled);
107
112
  opacity: var(--disabled-opacity);
108
113
  }
109
114
 
@@ -93,11 +93,21 @@
93
93
  opacity: var(--disabled-opacity);
94
94
  }
95
95
 
96
- /* Forced colors: map the custom thumb/track to system colors. The focus
97
- indicator stays visible through the transparent outline on :focus-visible. */
96
+ /* Forced colors: the track must stay a visible line against the page (Canvas
97
+ would merge with it) and the thumb needs a real boundary to remain
98
+ distinguishable from the track. Disabled keeps GrayText via :disabled.
99
+ Focus stays on the transparent outline of :focus-visible. */
98
100
  @media (forced-colors: active) {
99
101
  .range {
100
- --range-thumb-bg: ButtonText;
101
- --range-track-bg: Canvas;
102
+ --range-track-bg: CanvasText;
103
+ --range-thumb-bg: ButtonFace;
104
+ }
105
+
106
+ .range::-webkit-slider-thumb {
107
+ border: var(--border-width) solid ButtonText;
108
+ }
109
+
110
+ .range::-moz-range-thumb {
111
+ border: var(--border-width) solid ButtonText;
102
112
  }
103
113
  }
@@ -0,0 +1,102 @@
1
+ /*
2
+ * Specialized application shell recipe: one exact DOM structure uses a
3
+ * bottom navigation bar when narrow and a labelled side navigation when space
4
+ * permits. The main region owns scrolling, keeping both pieces of application
5
+ * chrome available without fixed offsets. This is not a generic page wrapper
6
+ * or a public-site layout; all three direct regions are part of the contract.
7
+ *
8
+ * Public hook:
9
+ * --app-nav-side-size
10
+ */
11
+ .app-layout {
12
+ --app-nav-side-size: 12rem;
13
+
14
+ display: grid;
15
+ grid-template:
16
+ "topbar" auto
17
+ "main" minmax(0, 1fr)
18
+ "nav" auto
19
+ / minmax(0, 1fr);
20
+ block-size: var(--viewport-block);
21
+ min-inline-size: 0;
22
+ overflow: hidden;
23
+ }
24
+
25
+ .app-layout > .topbar {
26
+ grid-area: topbar;
27
+ padding-block-start: calc(var(--space-30) + env(safe-area-inset-top));
28
+ }
29
+
30
+ .app-layout > .app-main {
31
+ grid-area: main;
32
+ min-inline-size: 0;
33
+ min-block-size: 0;
34
+ overflow: auto;
35
+ }
36
+
37
+ .app-layout > .app-nav {
38
+ grid-area: nav;
39
+ }
40
+
41
+ /* The main grid area keeps the FAB clear of bottom navigation without making
42
+ either component copy the other's geometry. */
43
+ .app-layout > .fab {
44
+ position: relative;
45
+ grid-area: main;
46
+ place-self: end;
47
+ inset: auto;
48
+ margin: var(--fab-offset);
49
+ }
50
+
51
+ @media (min-width: 48rem) {
52
+ .app-layout {
53
+ grid-template:
54
+ "nav topbar" auto
55
+ "nav main" minmax(0, 1fr)
56
+ / var(--app-nav-side-size) minmax(0, 1fr);
57
+ }
58
+
59
+ .app-layout > .app-nav {
60
+ position: sticky;
61
+ inset-block: 0;
62
+ grid-auto-flow: row;
63
+ grid-auto-rows: minmax(var(--control-size-lg), auto);
64
+ grid-auto-columns: auto;
65
+ align-content: start;
66
+ gap: var(--space-20);
67
+ min-block-size: 0;
68
+ padding-block: max(var(--space-20), env(safe-area-inset-top))
69
+ max(var(--space-20), env(safe-area-inset-bottom));
70
+ padding-inline: var(--space-20);
71
+ border-block-start: 0;
72
+ border-inline-end: var(--border-width) solid var(--ui-border, var(--border));
73
+ }
74
+
75
+ .app-layout > .app-nav > a {
76
+ grid-template-rows: auto;
77
+ grid-template-columns: 1.5rem minmax(0, 1fr);
78
+ place-content: center start;
79
+ align-items: center;
80
+ justify-items: start;
81
+ gap: var(--space-30);
82
+ padding-inline: var(--space-30);
83
+ text-align: start;
84
+ }
85
+
86
+ /* Side navigation no longer occupies the viewport's bottom safe area. */
87
+ .app-layout > .fab {
88
+ margin-block-end: max(var(--fab-offset), env(safe-area-inset-bottom));
89
+ }
90
+ }
91
+
92
+ @media print {
93
+ .app-layout {
94
+ display: block;
95
+ block-size: auto;
96
+ overflow: visible;
97
+ }
98
+
99
+ .app-layout > .app-main {
100
+ overflow: visible;
101
+ }
102
+ }
@@ -1,4 +1,4 @@
1
- /* Full-height application shell. */
1
+ /* Full-height single-column document shell. */
2
2
  .app-shell {
3
3
  min-block-size: var(--viewport-block);
4
4
  display: flex;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  .center {
9
9
  box-sizing: content-box;
10
+ inline-size: calc(100% - var(--center-pad, 1rem) * 2);
10
11
  max-inline-size: var(--center-size, 72rem);
11
12
  margin-inline: auto;
12
13
  padding-inline: var(--center-pad, 1rem);
@@ -10,6 +10,7 @@
10
10
  */
11
11
  .cluster {
12
12
  display: flex;
13
+ flex-direction: row;
13
14
  flex-wrap: var(--cluster-wrap, wrap);
14
15
  align-items: var(--cluster-align, center);
15
16
  justify-content: var(--cluster-justify, flex-start);