actual-css 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +1087 -643
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +62 -30
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +53 -7
  18. package/docs/design-notes/popover-manual-poc.md +105 -25
  19. package/docs/design-notes/surface-reparenting.md +56 -47
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +7 -4
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +85 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +21 -6
  30. package/docs/pages/components/drawer.md +13 -0
  31. package/docs/pages/components/fab.md +4 -3
  32. package/docs/pages/components/list.md +42 -4
  33. package/docs/pages/components/navbar.md +17 -1
  34. package/docs/pages/components/pagination.md +2 -2
  35. package/docs/pages/components/skeleton.md +3 -1
  36. package/docs/pages/components/steps.md +19 -0
  37. package/docs/pages/components/table.md +3 -2
  38. package/docs/pages/enhancements/flyout.md +82 -38
  39. package/docs/pages/enhancements/overview.md +25 -0
  40. package/docs/pages/enhancements/status-bar.md +1 -1
  41. package/docs/pages/enhancements/tabs.md +45 -37
  42. package/docs/pages/enhancements/tooltip.md +36 -10
  43. package/docs/pages/examples/overview.md +38 -5
  44. package/docs/pages/forms/choice-cards.md +11 -6
  45. package/docs/pages/forms/combobox.md +1 -1
  46. package/docs/pages/forms/floating-field.md +4 -3
  47. package/docs/pages/forms/joined-controls.md +22 -0
  48. package/docs/pages/forms/otp.md +2 -1
  49. package/docs/pages/forms/overview.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +16 -18
  51. package/docs/pages/forms/sticky-actions.md +2 -2
  52. package/docs/pages/forms/switch.md +1 -1
  53. package/docs/pages/foundations/philosophy.md +57 -26
  54. package/docs/pages/foundations/theming.md +2 -2
  55. package/docs/pages/foundations/tokens.md +118 -21
  56. package/docs/pages/foundations/typography.md +35 -14
  57. package/docs/pages/guides/introduction.md +11 -9
  58. package/docs/pages/guides/progressive-enhancement.md +115 -30
  59. package/docs/pages/guides/tailwind.md +3 -2
  60. package/docs/pages/layout/media.md +1 -1
  61. package/docs/pages/layout/overview.md +11 -1
  62. package/docs/pages/layout/scroll-snap.md +3 -3
  63. package/docs/pages/layout/stack.md +17 -9
  64. package/docs/pages/patterns/icon-input.md +4 -2
  65. package/docs/pages/patterns/icon-slot.md +2 -0
  66. package/docs/pages/patterns/intent-nesting.md +1 -1
  67. package/docs/pages/utilities/overview.md +23 -2
  68. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  69. package/docs/pages/utilities/spacing.md +10 -8
  70. package/llms.txt +2 -1
  71. package/package.json +159 -149
  72. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  73. package/src/css/components/accordion.css +42 -3
  74. package/src/css/components/alert.css +25 -13
  75. package/src/css/components/app-nav.css +18 -8
  76. package/src/css/components/avatar.css +13 -1
  77. package/src/css/components/badge.css +78 -12
  78. package/src/css/components/breadcrumb.css +2 -3
  79. package/src/css/components/button.css +9 -16
  80. package/src/css/components/card.css +9 -2
  81. package/src/css/components/chat.css +1 -0
  82. package/src/css/components/data-list.css +1 -1
  83. package/src/css/components/drawer.css +101 -32
  84. package/src/css/components/fab.css +0 -6
  85. package/src/css/components/flyout.css +35 -98
  86. package/src/css/components/index.css +0 -1
  87. package/src/css/components/indicator.css +21 -0
  88. package/src/css/components/key.css +10 -0
  89. package/src/css/components/list.css +40 -4
  90. package/src/css/components/menu.css +2 -1
  91. package/src/css/components/modal.css +48 -33
  92. package/src/css/components/navbar.css +37 -4
  93. package/src/css/components/pagination.css +1 -0
  94. package/src/css/components/skeleton.css +15 -4
  95. package/src/css/components/spinner.css +6 -6
  96. package/src/css/components/status-bar.css +0 -5
  97. package/src/css/components/steps.css +45 -59
  98. package/src/css/components/tab.css +29 -9
  99. package/src/css/components/table.css +13 -5
  100. package/src/css/components/tooltip.css +36 -40
  101. package/src/css/core/base.css +23 -1
  102. package/src/css/core/intents.css +17 -6
  103. package/src/css/core/theme.css +27 -0
  104. package/src/css/core/tokens.css +35 -15
  105. package/src/css/core/variants.css +64 -15
  106. package/src/css/forms/choice-card.css +4 -4
  107. package/src/css/forms/choice.css +76 -29
  108. package/src/css/forms/control.css +2 -1
  109. package/src/css/forms/floating-field.css +3 -3
  110. package/src/css/forms/form.css +4 -3
  111. package/src/css/forms/input-icon.css +20 -14
  112. package/src/css/forms/native.css +6 -1
  113. package/src/css/forms/otp.css +9 -1
  114. package/src/css/forms/range.css +2 -0
  115. package/src/css/forms/select.css +29 -9
  116. package/src/css/forms/switch.css +28 -8
  117. package/src/css/layout/app-layout.css +1 -1
  118. package/src/css/layout/grid.css +50 -76
  119. package/src/css/layout/scroll-snap.css +11 -7
  120. package/src/css/layout/scroller.css +3 -4
  121. package/src/css/layout/stack.css +2 -2
  122. package/src/css/typography/fluid.css +4 -3
  123. package/src/css/typography/prose.css +6 -5
  124. package/src/css/utilities/base.css +2 -1
  125. package/src/css/utilities/extra.css +20 -17
  126. package/src/js/context-menu.js +27 -26
  127. package/src/js/dialog.js +2 -1
  128. package/src/js/dismiss.js +5 -0
  129. package/src/js/enhance.js +34 -2
  130. package/src/js/enhancement-loader.js +36 -43
  131. package/src/js/events.js +0 -3
  132. package/src/js/filter.js +0 -1
  133. package/src/js/floating-space.js +42 -0
  134. package/src/js/flyout.js +9 -44
  135. package/src/js/focus-group.js +7 -0
  136. package/src/js/focus.js +6 -0
  137. package/src/js/menu.js +10 -1
  138. package/src/js/selectors.js +5 -9
  139. package/src/js/status.js +6 -5
  140. package/src/js/surface.js +97 -139
  141. package/src/js/tab.js +6 -1
  142. package/src/js/tooltip.js +150 -51
  143. package/src/js/validation.js +1 -1
  144. package/src/css/components/surface.css +0 -49
@@ -10,7 +10,7 @@
10
10
  * geometry and states. switch.css is imported after this file.
11
11
  *
12
12
  * Public hooks:
13
- * --choice-control-offset
13
+ * --choice-control-nudge
14
14
  * --choice-border
15
15
  * --choice-bg
16
16
  * --choice-mark
@@ -42,45 +42,89 @@
42
42
  cursor: pointer;
43
43
  }
44
44
 
45
- /* Optical alignment: nudge the control down so it sits visually centered with
46
- the first line of label text, not flush with the top of the line-box. Every
47
- inline choice runs the same formula — center the control's own block size on
48
- the first line box, then drop it 0.0625em so it reads level with the
49
- cap-height rather than the em-box. A checkbox and a switch therefore stay
50
- level with each other under any line-height or --choice-control-size, which
51
- a per-control constant cannot do (a taller switch and a shorter checkbox
52
- need different offsets to land on the same optical line).
53
-
54
- The constants are what the formula yields at the default 1.5 line-height:
55
- 0.25em (4px) for a checkbox, 0.1875em (3px) for the taller switch track.
56
- They serve browsers without lh units. Multi-line labels still keep the
57
- control at the top thanks to align-items: flex-start.
58
-
59
- --choice-control-offset stays fallback-only rather than declared here, so an
60
- override placed anywhere up the tree — :root included — reaches the control
61
- instead of losing to a component-level declaration. */
45
+ /* Optical alignment: the control reads level with the first line's cap height,
46
+ not with the top of its line box. Every inline choice runs the same formula
47
+ over its own block size, so a checkbox and a taller switch stay level with
48
+ each other under any line-height or --choice-control-size — something a
49
+ per-control constant cannot do.
50
+
51
+ Three tiers, each the best the engine can express:
52
+
53
+ - `cap` unit: exact, and the only font-independent answer. Where the cap
54
+ center sits inside a line box depends on the font's ascent/descent/cap
55
+ ratios, so no constant can serve both Segoe UI and Roboto — the same rule
56
+ was a pixel high on Windows and two pixels low on Android. Baseline
57
+ alignment puts the control's border-box bottom on the text baseline, and
58
+ one inset drops it by half the difference between its own height and the
59
+ cap height. Both classed controls synthesize that baseline from their
60
+ border box because their mark and knob are out of flow.
61
+ - `lh` only: center the control on the line box, which is within a pixel of
62
+ the cap center for the usual UI faces.
63
+ - neither: the constants that formula yields at the default 1.5
64
+ line-height, 0.1875em for a checkbox and 0.125em for the switch track.
65
+
66
+ --choice-control-nudge adds to whichever tier applies, rather than replacing
67
+ it: the derived value is exact now, so a design has a correction to make,
68
+ not a formula to restate — and one number cannot stand in for three
69
+ different references anyway. It stays fallback-only rather than declared
70
+ here, so an override placed anywhere up the tree — :root included — reaches
71
+ the control instead of losing to a component-level declaration. */
62
72
  .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
63
73
  flex: 0 0 auto;
64
- margin-block-start: var(--choice-control-offset, 0.25em);
74
+ margin-block-start: calc(0.1875em + var(--choice-control-nudge, 0px));
65
75
  }
66
76
 
67
77
  .choice > .switch {
68
78
  flex: 0 0 auto;
69
- margin-block-start: var(--choice-control-offset, 0.1875em);
79
+ margin-block-start: calc(0.125em + var(--choice-control-nudge, 0px));
70
80
  }
71
81
 
72
82
  @supports (margin-block-start: 1lh) {
73
83
  .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
74
- margin-block-start: var(
75
- --choice-control-offset,
76
- calc((1lh - var(--choice-control-size)) / 2 + 0.0625em)
84
+ margin-block-start: calc(
85
+ (1lh - var(--choice-control-size)) /
86
+ 2 +
87
+ var(--choice-control-nudge, 0px)
88
+ );
89
+ }
90
+
91
+ .choice > .switch {
92
+ margin-block-start: calc(
93
+ (1lh - var(--switch-block-size)) /
94
+ 2 +
95
+ var(--choice-control-nudge, 0px)
96
+ );
97
+ }
98
+ }
99
+
100
+ @supports (margin-block-start: 1cap) {
101
+ .choice {
102
+ align-items: baseline;
103
+ }
104
+
105
+ /* Baseline alignment owns the block position now; the tier above would only
106
+ grow the row. An unclassed native input keeps the UA's own baseline, which
107
+ is already tuned for text — the formula below describes the framework's
108
+ geometry and would not fit it. */
109
+ .choice > :is(input[type="checkbox"], input[type="radio"]) {
110
+ margin-block-start: 0;
111
+ }
112
+
113
+ /* inset-block-start rides the `position: relative` these controls already
114
+ carry for their mark and knob. */
115
+ .choice > :is(.check, .radio) {
116
+ inset-block-start: calc(
117
+ (var(--choice-control-size) - 1cap) /
118
+ 2 +
119
+ var(--choice-control-nudge, 0px)
77
120
  );
78
121
  }
79
122
 
80
123
  .choice > .switch {
81
- margin-block-start: var(
82
- --choice-control-offset,
83
- calc((1lh - var(--switch-block-size)) / 2 + 0.0625em)
124
+ inset-block-start: calc(
125
+ (var(--switch-block-size) - 1cap) /
126
+ 2 +
127
+ var(--choice-control-nudge, 0px)
84
128
  );
85
129
  }
86
130
  }
@@ -115,6 +159,7 @@
115
159
  background: var(--choice-bg);
116
160
  color: var(--choice-mark);
117
161
  appearance: none;
162
+ -webkit-appearance: none;
118
163
  cursor: pointer;
119
164
  }
120
165
 
@@ -153,9 +198,9 @@
153
198
 
154
199
  .check:is(:checked, :indeterminate),
155
200
  .radio:checked {
156
- --choice-border: var(--intent, var(--primary));
157
- --choice-bg: var(--intent, var(--primary));
158
- --choice-mark: var(--intent-fg, var(--primary-fg));
201
+ --choice-border: var(--intent, var(--state-selected));
202
+ --choice-bg: var(--intent, var(--state-selected));
203
+ --choice-mark: var(--intent-fg, var(--state-selected-fg));
159
204
  }
160
205
 
161
206
  .check:checked::before {
@@ -174,6 +219,8 @@
174
219
  transform: scale(1);
175
220
  }
176
221
 
222
+ /* Progressive: without :not() selector lists, the normal unchecked border
223
+ remains visible and the control stays usable. */
177
224
  .check:hover:not(:checked, :indeterminate, :disabled),
178
225
  .radio:hover:not(:checked, :indeterminate, :disabled) {
179
226
  --choice-border: var(--text-subtle);
@@ -32,9 +32,10 @@
32
32
  --control-cursor: auto;
33
33
 
34
34
  appearance: none;
35
+ -webkit-appearance: none;
35
36
  inline-size: 100%;
36
37
  min-block-size: var(--control-size);
37
- padding-inline: var(--space-30);
38
+ padding-inline: var(--control-pad-x);
38
39
  border: var(--border-width) solid var(--form-invalid-border, var(--control-border));
39
40
  border-block-end-color: var(
40
41
  --form-invalid-border,
@@ -14,8 +14,8 @@
14
14
  * placeholder=" " to expose a reliable empty state — without it the label
15
15
  * only reacts to focus.
16
16
  *
17
- * Density flows through --control-size: .sm/.lg shrink the control geometry
18
- * and the reserved headroom with it, leaving typography untouched.
17
+ * Density flows through --control-size: .compact/.spacious change the control
18
+ * geometry and the reserved headroom with it, leaving typography untouched.
19
19
  *
20
20
  * Public hooks:
21
21
  * --floating-label-top
@@ -46,7 +46,7 @@
46
46
  inset-inline-start: calc(var(--space-30) + var(--border-width));
47
47
  color: var(--text-muted);
48
48
  font-size: var(--control-font-size);
49
- font-weight: var(--font-weight);
49
+ font-weight: var(--font-weight-normal);
50
50
  opacity: var(--floating-label-opacity);
51
51
  pointer-events: none;
52
52
  transition:
@@ -18,6 +18,7 @@
18
18
  display: grid;
19
19
  gap: var(--space-10);
20
20
  color: var(--text);
21
+ font-size: var(--control-font-size);
21
22
  }
22
23
 
23
24
  .field-group {
@@ -31,7 +32,7 @@
31
32
  .field-label,
32
33
  .field-group > legend {
33
34
  color: var(--text);
34
- font-weight: var(--font-weight-strong);
35
+ font-weight: var(--font-weight-semibold);
35
36
  }
36
37
 
37
38
  .field-group > legend {
@@ -42,13 +43,13 @@
42
43
 
43
44
  .field-help {
44
45
  color: var(--text-muted);
45
- font-size: var(--font-size-sm);
46
+ font-size: 0.875em;
46
47
  }
47
48
 
48
49
  .field-error {
49
50
  display: none;
50
51
  color: var(--danger);
51
- font-size: var(--font-size-sm);
52
+ font-size: 0.875em;
52
53
  }
53
54
 
54
55
  .required-mark {
@@ -2,18 +2,19 @@
2
2
  * Input Icon — decorative icon inside a relative wrapper, leading or trailing.
3
3
  *
4
4
  * Icon-library agnostic: detects position by DOM order. First child that is
5
- * not .input = leading icon; icon after .input = trailing. The icon keeps a
6
- * fixed size; density contexts shrink the control geometry around it, not
7
- * the pictogram.
5
+ * not .input = leading icon; icon after .input = trailing. The icon follows
6
+ * local typography; density contexts shrink the control geometry around it
7
+ * without changing the pictogram.
8
8
  *
9
9
  * Public hooks:
10
10
  * --input-icon-size
11
11
  */
12
12
  .input-icon {
13
- --input-icon-size: 1.25rem;
13
+ --input-icon-size: 1.25em;
14
14
 
15
15
  display: block;
16
16
  position: relative;
17
+ font-size: var(--control-font-size);
17
18
  }
18
19
 
19
20
  /* Icon — shared by leading and trailing */
@@ -28,27 +29,32 @@
28
29
 
29
30
  /* Leading: icon is the first child */
30
31
  .input-icon > :first-child:where(:not(.input)) {
31
- inset-inline-start: var(--space-30);
32
+ inset-inline-start: var(--control-pad-x);
32
33
  }
33
34
 
34
- /* Trailing: input is the first child, icon follows */
35
- .input-icon > .input:first-child ~ :where(:not(.input)) {
36
- inset-inline-end: var(--space-30);
35
+ /* Trailing: an icon follows the input, with or without a leading icon. */
36
+ .input-icon > .input ~ :where(:not(.input)) {
37
+ inset-inline-end: var(--control-pad-x);
37
38
  }
38
39
 
39
40
  /* font-size only sizes text-metric icons (webfont <i>, <span>). An inline
40
41
  <svg> with just a viewBox ignores it, so give it explicit dimensions. */
41
42
  .input-icon > svg:where(:not(.input)) {
42
- inline-size: var(--input-icon-size);
43
- block-size: var(--input-icon-size);
43
+ /* The shared rule already maps --input-icon-size onto font-size. One em is
44
+ therefore the requested optical size; reusing the em-valued hook here
45
+ would apply its factor a second time. */
46
+ inline-size: 1em;
47
+ block-size: 1em;
44
48
  }
45
49
 
46
50
  /* Leading: input gets start padding to clear the icon */
47
51
  .input-icon > :first-child:where(:not(.input)) ~ .input {
48
- padding-inline-start: calc(var(--space-30) * 2 + var(--input-icon-size));
52
+ padding-inline-start: calc(var(--control-pad-x) * 2 + var(--input-icon-size));
49
53
  }
50
54
 
51
- /* Trailing: input is first child and has a sibling → end padding */
52
- .input-icon > .input:first-child:where(:not(:last-child)) {
53
- padding-inline-end: calc(var(--space-30) * 2 + var(--input-icon-size));
55
+ /* Trailing: the input has a following sibling → end padding. Not tied to the
56
+ input being first child, so a leading icon does not disable the trailing
57
+ icon's reserved space. */
58
+ .input-icon > .input:where(:not(:last-child)) {
59
+ padding-inline-end: calc(var(--control-pad-x) * 2 + var(--input-icon-size));
54
60
  }
@@ -8,18 +8,23 @@
8
8
  */
9
9
  .file {
10
10
  color: var(--text-muted);
11
+ font-size: var(--control-font-size);
11
12
  }
12
13
 
13
14
  .file::file-selector-button {
14
15
  margin-inline-end: var(--space-30);
16
+ /* Consume the shared control scale like .btn, so .sm/.lg and density resize
17
+ the whole control, not just the filename text. */
18
+ min-block-size: var(--control-size);
15
19
  padding-block: var(--space-10);
20
+ padding-block: max(var(--space-10), calc((var(--control-size) - 1lh) / 2));
16
21
  padding-inline: var(--space-20);
17
22
  border: var(--border-width) solid var(--border);
18
23
  border-radius: var(--radius-sm);
19
24
  background: var(--surface-subtle);
20
25
  color: var(--text);
21
26
  font: inherit;
22
- font-size: var(--font-size-sm);
27
+ font-size: var(--control-font-size);
23
28
  font-weight: var(--font-weight-medium);
24
29
  cursor: pointer;
25
30
  transition:
@@ -18,7 +18,7 @@
18
18
  .otp {
19
19
  --otp-cell-size: var(--control-size);
20
20
  --otp-gap: var(--space-10);
21
- --otp-font-size: 1.5rem;
21
+ --otp-font-size: 1.5em;
22
22
  --otp-cell-border: var(--border);
23
23
  --otp-cell-bg: var(--surface);
24
24
  --otp-caret-space: calc((var(--otp-cell-size) - 1ch) / 2 + var(--otp-gap) + 1px);
@@ -31,6 +31,7 @@
31
31
  max-inline-size: 100%;
32
32
  gap: var(--otp-gap);
33
33
  border-radius: var(--radius);
34
+ font-size: var(--control-font-size);
34
35
  /* Unlike hidden, clip cannot become a scroll container when the caret moves
35
36
  into the input extension. */
36
37
  overflow: clip;
@@ -72,6 +73,7 @@
72
73
  letter-spacing: calc(var(--otp-cell-size) + var(--otp-gap) - 1ch);
73
74
  text-align: start;
74
75
  appearance: none;
76
+ -webkit-appearance: none;
75
77
  }
76
78
 
77
79
  /* Deliberately no group ring: a box around all the cells reads as a second
@@ -106,6 +108,12 @@
106
108
  cursor: not-allowed;
107
109
  }
108
110
 
111
+ /* biome-ignore lint/style/noDescendingSpecificity: disabled must come last of
112
+ the cell states, and the reorder the rule asks for would invert that. A
113
+ disabled input is barred from constraint validation, so `:invalid` and
114
+ `:focus` cannot reach it at all; the only overlap left is an authored
115
+ aria-invalid, where equal specificity and source order are what make
116
+ disabled — not danger — the colour a dead field reads as. */
109
117
  .otp > input:disabled ~ span {
110
118
  --otp-cell-bg: var(--surface-subtle);
111
119
  --otp-cell-border: var(--state-disabled);
@@ -26,6 +26,7 @@
26
26
  --range-track-bg: var(--border);
27
27
 
28
28
  appearance: none;
29
+ -webkit-appearance: none;
29
30
  inline-size: 100%;
30
31
  block-size: var(--range-thumb-size);
31
32
  background: transparent;
@@ -46,6 +47,7 @@
46
47
 
47
48
  .range::-webkit-slider-thumb {
48
49
  appearance: none;
50
+ -webkit-appearance: none;
49
51
  inline-size: var(--range-thumb-size);
50
52
  block-size: var(--range-thumb-size);
51
53
  border-radius: var(--radius-full);
@@ -7,20 +7,40 @@
7
7
  * the CSS chevron, so the picker (appearance: base-select) enhancement in
8
8
  * custom-select.css can be opted out independently. Add .native to a specific
9
9
  * select to opt out of the enhancement.
10
+ *
11
+ * Public hooks:
12
+ * --select-icon-size
10
13
  */
11
- .select:not([multiple], [size]:not([size="1"])) {
14
+ /* --select-icon-size is typographic (em), so the reserved slot and the
15
+ pictogram scale with the control's font size, not a fixed rem sliver. */
16
+ .select:not(:is([multiple], [size]:not([size="1"]))) {
17
+ --select-icon-size: 1em;
18
+
12
19
  appearance: none;
13
- padding-inline-end: calc(var(--space-30) + var(--space-60));
20
+ -webkit-appearance: none;
21
+ padding-inline-end: calc(var(--control-pad-x) * 2 + var(--select-icon-size));
14
22
  background-image: var(--icon-chevron);
15
- background-position: center right var(--space-30);
16
- background-size: 1rem auto;
23
+ background-position: center right var(--control-pad-x);
24
+ background-size: var(--select-icon-size) auto;
17
25
  background-repeat: no-repeat;
18
26
  }
19
27
 
20
28
  /* The chevron sits on the inline-end side; flip it in RTL so it follows
21
- the padding-inline-end space reserved for it. */
22
- .select:not([multiple], [size]:not([size="1"])):dir(rtl) {
23
- background-position: center left var(--space-30);
29
+ the padding-inline-end space reserved for it. Safari before 16.4 has no
30
+ :dir(); the ancestor-attribute path is the Degraded-tier fallback and the
31
+ :dir() rules are dropped on their own there. :dir() follows inherited
32
+ directionality, so the explicit ltr rule neutralizes an outer RTL fallback
33
+ for a nested direction change. */
34
+ [dir="rtl"] .select:not(:is([multiple], [size]:not([size="1"]))) {
35
+ background-position: center left var(--control-pad-x);
36
+ }
37
+
38
+ .select:not(:is([multiple], [size]:not([size="1"]))):dir(ltr) {
39
+ background-position: center right var(--control-pad-x);
40
+ }
41
+
42
+ .select:not(:is([multiple], [size]:not([size="1"]))):dir(rtl) {
43
+ background-position: center left var(--control-pad-x);
24
44
  }
25
45
 
26
46
  /* Both forced-colors and prefers-contrast hand the arrow back to the
@@ -28,9 +48,9 @@
28
48
  simpler than maintaining contrast variants, and native rendering already
29
49
  matches whatever contrast mode the user picked. */
30
50
  @media (forced-colors: active), (prefers-contrast: more) {
31
- .select:not([multiple], [size]:not([size="1"])) {
51
+ .select:not(:is([multiple], [size]:not([size="1"]))) {
32
52
  appearance: auto;
33
53
  background-image: none;
34
- padding-inline-end: var(--space-30);
54
+ padding-inline-end: var(--control-pad-x);
35
55
  }
36
56
  }
@@ -56,8 +56,6 @@
56
56
 
57
57
  position: relative;
58
58
  display: inline-grid;
59
- align-items: center;
60
- justify-content: start;
61
59
  inline-size: var(--switch-width);
62
60
  block-size: var(--switch-block-size);
63
61
  margin: 0;
@@ -65,6 +63,7 @@
65
63
  border-radius: var(--radius-full);
66
64
  background: var(--switch-bg);
67
65
  appearance: none;
66
+ -webkit-appearance: none;
68
67
  cursor: pointer;
69
68
  }
70
69
 
@@ -75,9 +74,17 @@
75
74
  against the track is what makes the knob legible. */
76
75
  .switch::before {
77
76
  content: "";
77
+ /* Out of flow like the checkbox mark, so the track carries no in-flow item
78
+ and the switch synthesizes its baseline from its own border box. A knob
79
+ laid out as a grid item makes the baseline the knob's bottom edge instead,
80
+ which is what .choice measures its cap-height offset from. The inset is
81
+ the same --switch-knob-margin on both axes: it resolves against the
82
+ padding box, so the border is already out of the way. */
83
+ position: absolute;
84
+ inset-block-start: var(--switch-knob-margin);
85
+ inset-inline-start: var(--switch-knob-margin);
78
86
  inline-size: var(--switch-knob-size);
79
87
  block-size: var(--switch-knob-size);
80
- margin-inline: var(--switch-knob-margin);
81
88
  border-radius: var(--radius-full);
82
89
  background: var(--switch-knob);
83
90
  transition:
@@ -85,14 +92,17 @@
85
92
  background-color var(--duration);
86
93
  }
87
94
 
88
- .switch:hover:not(:disabled) {
95
+ /* Hover is not selected: an unchecked switch keeps the brand fallback on hover,
96
+ so a theme that separates --primary from --state-selected never tints an
97
+ OFF control with the selection accent before it is checked. */
98
+ .switch:not(:checked):hover:not(:disabled) {
89
99
  --switch-border: var(--intent, var(--primary));
90
100
  }
91
101
 
92
102
  .switch:checked {
93
- --switch-bg: var(--intent, var(--primary));
94
- --switch-border: var(--intent, var(--primary));
95
- --switch-knob: var(--intent-fg, var(--primary-fg));
103
+ --switch-bg: var(--intent, var(--state-selected));
104
+ --switch-border: var(--intent, var(--state-selected));
105
+ --switch-knob: var(--intent-fg, var(--state-selected-fg));
96
106
  }
97
107
 
98
108
  .switch:checked::before {
@@ -100,7 +110,17 @@
100
110
  }
101
111
 
102
112
  /* The knob travels toward inline-end; in RTL that is leftward, so the
103
- translateX sign flips. translateX is physical, not logical. */
113
+ translateX sign flips. translateX is physical, not logical. Safari before
114
+ 16.4 has no :dir(); the ancestor-attribute path is the Degraded-tier
115
+ fallback and the :dir() rules are dropped on their own there. */
116
+ [dir="rtl"] .switch:checked::before {
117
+ transform: translateX(calc(var(--switch-travel) * -1));
118
+ }
119
+
120
+ .switch:checked:dir(ltr)::before {
121
+ transform: translateX(var(--switch-travel));
122
+ }
123
+
104
124
  .switch:checked:dir(rtl)::before {
105
125
  transform: translateX(calc(var(--switch-travel) * -1));
106
126
  }
@@ -83,7 +83,7 @@
83
83
  text-align: start;
84
84
  }
85
85
 
86
- /* Side navigation no longer occupies the viewport's bottom safe area. */
86
+ /* In the side layout the FAB owns the viewport's bottom safe-area inset. */
87
87
  .app-layout > .fab {
88
88
  margin-block-end: max(var(--fab-offset), env(safe-area-inset-bottom));
89
89
  }