@expressivecss/expressive 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 (122) hide show
  1. package/CHANGELOG.md +1313 -1
  2. package/README.md +76 -30
  3. package/dist/css/expressive.css +4896 -1777
  4. package/dist/css/expressive.css.map +1 -1
  5. package/dist/css/expressive.min.css +1 -1
  6. package/dist/css/expressive.min.css.map +1 -1
  7. package/dist/fonts/LICENSE-material-symbols +93 -0
  8. package/dist/fonts/LICENSE-noto-sans +93 -0
  9. package/dist/fonts/LICENSE-roboto +93 -0
  10. package/dist/fonts/material-symbols-outlined.woff2 +0 -0
  11. package/dist/fonts/material-symbols-rounded.woff2 +0 -0
  12. package/dist/fonts/material-symbols-sharp.woff2 +0 -0
  13. package/dist/fonts/noto-sans-latin-400.woff2 +0 -0
  14. package/dist/fonts/noto-sans-latin-500.woff2 +0 -0
  15. package/dist/fonts/roboto-latin-400.woff2 +0 -0
  16. package/dist/fonts/roboto-latin-500.woff2 +0 -0
  17. package/dist/js/expressive.cjs +1673 -1338
  18. package/dist/js/expressive.cjs.map +4 -4
  19. package/dist/js/expressive.js +1673 -1338
  20. package/dist/js/expressive.js.map +4 -4
  21. package/dist/js/expressive.min.js +13 -13
  22. package/dist/js/expressive.min.js.map +4 -4
  23. package/dist/js/expressive.mjs +1673 -1338
  24. package/dist/js/expressive.mjs.map +4 -4
  25. package/dist/types/behaviors/bottomSheets.d.ts +19 -11
  26. package/dist/types/behaviors/sheetDrag.d.ts +40 -0
  27. package/dist/types/behaviors/sideSheets.d.ts +2 -9
  28. package/dist/types/components/appBar.d.ts +26 -0
  29. package/dist/types/components/autocomplete.d.ts +15 -3
  30. package/dist/types/components/buttonGroup.d.ts +39 -0
  31. package/dist/types/components/buttons.d.ts +5 -1
  32. package/dist/types/components/cards.d.ts +11 -13
  33. package/dist/types/components/carousel.d.ts +87 -53
  34. package/dist/types/components/chips.d.ts +21 -1
  35. package/dist/types/components/expandingCard.d.ts +31 -0
  36. package/dist/types/components/index.d.ts +23 -20
  37. package/dist/types/components/lightbox.d.ts +1 -0
  38. package/dist/types/components/menu.d.ts +11 -1
  39. package/dist/types/components/{sidenav.d.ts → navigationDrawer.d.ts} +16 -16
  40. package/dist/types/components/registry.d.ts +18 -8
  41. package/dist/types/components/slider.d.ts +26 -85
  42. package/dist/types/components/snackbar.d.ts +20 -5
  43. package/dist/types/components/tabs.d.ts +12 -0
  44. package/dist/types/components/timepicker.d.ts +12 -10
  45. package/dist/types/components/tooltip.d.ts +1 -0
  46. package/dist/types/core/utils.d.ts +64 -53
  47. package/dist/types/index.d.ts +8 -9
  48. package/package.json +39 -3
  49. package/src/sass/README.md +61 -11
  50. package/src/sass/abstracts/_breakpoints.scss +17 -19
  51. package/src/sass/abstracts/_mixins.scss +155 -21
  52. package/src/sass/abstracts/_variables.scss +69 -5
  53. package/src/sass/base/_fonts.scss +63 -0
  54. package/src/sass/base/_global.scss +6 -24
  55. package/src/sass/base/_grid.scss +15 -12
  56. package/src/sass/base/_normalize.scss +34 -38
  57. package/src/sass/base/_typography.scss +4 -18
  58. package/src/sass/components/_badges.scss +3 -5
  59. package/src/sass/components/_banners.scss +298 -0
  60. package/src/sass/components/_bottom-app-bar.scss +180 -0
  61. package/src/sass/components/_bottom-sheet.scss +12 -4
  62. package/src/sass/components/_button-groups.scss +358 -0
  63. package/src/sass/components/_buttons.scss +245 -92
  64. package/src/sass/components/_cards.scss +514 -78
  65. package/src/sass/components/_carousel.scss +562 -68
  66. package/src/sass/components/_chips.scss +64 -5
  67. package/src/sass/components/_datepicker.scss +24 -24
  68. package/src/sass/components/_dialog.scss +17 -11
  69. package/src/sass/components/_drag-handle.scss +130 -0
  70. package/src/sass/components/_expanding-card.scss +226 -0
  71. package/src/sass/components/_fab-menu.scss +232 -0
  72. package/src/sass/components/_floating-sheet.scss +86 -0
  73. package/src/sass/components/_icon-buttons.scss +172 -0
  74. package/src/sass/components/_icons-material-design.scss +22 -10
  75. package/src/sass/components/_index.scss +22 -6
  76. package/src/sass/components/_list.scss +16 -13
  77. package/src/sass/components/_loading-indicator.scss +106 -0
  78. package/src/sass/components/_menu.scss +335 -61
  79. package/src/sass/components/_navbar.scss +245 -45
  80. package/src/sass/components/_navigation-bar.scss +22 -11
  81. package/src/sass/components/{_sidenav.scss → _navigation-drawer.scss} +123 -33
  82. package/src/sass/components/_navigation-rail.scss +31 -23
  83. package/src/sass/components/_page-footer.scss +19 -16
  84. package/src/sass/components/_pagination.scss +24 -6
  85. package/src/sass/components/_panes.scss +52 -11
  86. package/src/sass/components/{_preloader.scss → _progress.scss} +11 -3
  87. package/src/sass/components/_scrim.scss +37 -0
  88. package/src/sass/components/_search.scss +259 -0
  89. package/src/sass/components/_segmented-buttons.scss +188 -0
  90. package/src/sass/components/_side-sheet.scss +2 -2
  91. package/src/sass/components/_snackbar.scss +20 -15
  92. package/src/sass/components/_split-button.scss +196 -0
  93. package/src/sass/components/_tabs.scss +7 -8
  94. package/src/sass/components/_timepicker.scss +67 -58
  95. package/src/sass/components/_toolbar.scss +206 -80
  96. package/src/sass/components/_transitions.scss +8 -1
  97. package/src/sass/components/forms/_checkboxes.scss +49 -12
  98. package/src/sass/components/forms/_fieldset.scss +4 -4
  99. package/src/sass/components/forms/_file-input.scss +8 -1
  100. package/src/sass/components/forms/_index.scss +1 -1
  101. package/src/sass/components/forms/_input-fields.scss +50 -44
  102. package/src/sass/components/forms/_radio-buttons.scss +42 -10
  103. package/src/sass/components/forms/_select.scss +38 -1
  104. package/src/sass/components/forms/{_range.scss → _slider.scss} +8 -9
  105. package/src/sass/components/forms/_switches.scss +6 -7
  106. package/src/sass/expressive.scss +5 -0
  107. package/src/sass/tokens/_index.scss +4 -1
  108. package/src/sass/tokens/_reference.scss +70 -61
  109. package/src/sass/tokens/_state.scss +24 -0
  110. package/src/sass/tokens/_theme.scss +6 -3
  111. package/src/sass/tokens/_vibrant.scss +76 -0
  112. package/src/sass/utilities/_colors.scss +14 -1
  113. package/src/sass/utilities/_helpers.scss +5 -5
  114. package/src/sass/utilities/_visibility.scss +48 -26
  115. package/dist/types/behaviors/waves.d.ts +0 -15
  116. package/dist/types/components/parallax.d.ts +0 -16
  117. package/dist/types/components/range.d.ts +0 -43
  118. package/dist/types/core/bounding.d.ts +0 -6
  119. package/dist/types/core/edges.d.ts +0 -6
  120. package/src/sass/components/_parallax.scss +0 -104
  121. package/src/sass/components/_pulse.scss +0 -37
  122. package/src/sass/components/_slider.scss +0 -129
@@ -3,16 +3,16 @@
3
3
  // Cards.
4
4
  //
5
5
  // Anatomy is the HTML. An <article> is an elevated card. A heading is the
6
- // headline, a <p> is supporting text, a direct <nav> (not .tabs) is the
7
- // action row,
8
- // <img> / <figure> is media. Helpers switch the M3 variant (filled, outlined)
9
- // or the layout (horizontal).
6
+ // headline, .subhead is the subhead, a <p> is supporting text, direct media
7
+ // is <img> / <picture> / <figure>, and direct .actions is the action row.
8
+ // Helpers switch the M3 variant (filled, outlined) or the layout
9
+ // (horizontal).
10
10
  //
11
11
  // Tokens follow https://m3.material.io/components/cards/specs
12
12
  // container surface-container-low, 12dp corners
13
- // elevated level 1, hover level 2
14
- // filled surface-container-highest, level 0
15
- // outlined surface + 1dp outline, level 0
13
+ // elevated level 1; interactive hover level 2
14
+ // filled surface-container-highest, level 0; interactive hover level 1
15
+ // outlined surface + 1dp outline, level 0; interactive hover level 1
16
16
  // headline title-medium / on-surface
17
17
  // body body-medium / on-surface-variant
18
18
  // padding 16dp
@@ -21,65 +21,240 @@ $_card: "article";
21
21
 
22
22
  :where(#{$_card}) {
23
23
  --md-comp-elevated-card-container-color: var(--md-sys-color-surface-container-low);
24
- --md-comp-elevated-card-container-shape: 12px;
24
+ --md-comp-filled-card-container-color: var(--md-sys-color-surface-container-highest);
25
+ --md-comp-outlined-card-container-color: var(--md-sys-color-surface);
26
+ --md-comp-outlined-card-outline-color: var(--md-sys-color-outline-variant);
27
+ --md-comp-outlined-card-disabled-outline-color: var(--md-sys-color-outline);
28
+ --md-comp-card-container-color: var(--md-comp-elevated-card-container-color);
29
+ --md-comp-card-container-shape: 12px;
30
+ --md-comp-card-media-shape: var(--md-comp-card-container-shape);
31
+ --md-comp-card-media-overlay-container-color: var(--md-sys-color-surface);
32
+ --md-comp-card-media-overlay-ink-color: var(--md-sys-color-on-surface);
33
+ --md-comp-card-media-overlay-shape: 8px;
34
+ --md-comp-card-headline-color: var(--md-sys-color-on-surface);
35
+ --md-comp-card-subhead-color: var(--md-sys-color-on-surface);
36
+ --md-comp-card-supporting-text-color: var(--md-sys-color-on-surface-variant);
37
+ --md-comp-card-content-padding: 16px;
38
+ --md-comp-card-between-space: 8px;
39
+ --md-comp-card-icon-color: var(--md-sys-color-primary);
40
+ --md-comp-card-icon-size: 24px;
41
+ --md-comp-card-state-layer-color: var(--md-sys-color-on-surface);
42
+ --md-comp-card-hover-state-layer-opacity: var(--md-sys-state-hover-state-layer-opacity);
43
+ --md-comp-card-focus-state-layer-opacity: var(--md-sys-state-focus-state-layer-opacity);
44
+ --md-comp-card-pressed-state-layer-opacity: var(--md-sys-state-pressed-state-layer-opacity);
45
+ --md-comp-card-dragged-state-layer-opacity: var(--md-sys-state-dragged-state-layer-opacity);
46
+ --md-comp-card-disabled-container-opacity: 38%;
47
+ --md-comp-outlined-card-disabled-outline-opacity: 12%;
48
+ --md-comp-card-focus-indicator-color: var(--md-sys-color-secondary);
49
+ --md-comp-card-focus-indicator-thickness: 3px;
50
+ --md-comp-card-focus-indicator-offset: 2px;
51
+ --md-comp-card-horizontal-media-width: 40%;
52
+ --md-comp-card-horizontal-min-height: 240px;
25
53
 
26
54
  position: relative;
27
55
  box-sizing: border-box;
28
- background-color: var(--md-comp-elevated-card-container-color);
56
+ background-color: var(--md-comp-card-container-color);
29
57
  color: var(--md-sys-color-on-surface);
30
- border-radius: var(--md-comp-elevated-card-container-shape);
58
+ border-radius: var(--md-comp-card-container-shape);
31
59
  overflow: hidden;
32
- transition: box-shadow .2s ease-out;
60
+ transition: background-color .2s ease-out, box-shadow .2s ease-out;
61
+ }
62
+
63
+ // M3 state layers belong only to an interactive card. A direct
64
+ // `.primary-action` link supplies the card's large action area without making
65
+ // secondary buttons invalid nested controls.
66
+ :where(#{$_card})::after {
67
+ content: "";
68
+ position: absolute;
69
+ inset: 0;
70
+ z-index: 2;
71
+ border-radius: inherit;
72
+ background-color: var(--md-comp-card-state-layer-color);
73
+ opacity: 0;
74
+ pointer-events: none;
75
+ transition: opacity .2s ease-out;
76
+ }
77
+
78
+ :where(#{$_card}) > .primary-action {
79
+ display: block;
80
+ color: inherit;
81
+ border-radius: inherit;
82
+ outline: 0;
33
83
  }
34
84
 
35
85
  // Default is elevated. :where() keeps specificity at zero so .outlined wins.
36
86
  :where(#{$_card}):not(.filled, .outlined, .border) {
37
87
  @include z-depth("1");
38
-
39
- &:hover {
40
- @include z-depth("2");
41
- }
42
88
  }
43
89
 
44
90
  :where(#{$_card}).filled {
45
- @include z-depth("0");
46
- background-color: var(--md-sys-color-surface-container-highest);
91
+ --md-comp-card-container-color: var(--md-comp-filled-card-container-color);
92
+ box-shadow: none;
47
93
  }
48
94
 
49
95
  :where(#{$_card}).outlined,
50
96
  :where(#{$_card}).border {
97
+ --md-comp-card-container-color: var(--md-comp-outlined-card-container-color);
98
+ box-shadow: none;
99
+ border: 1px solid var(--md-comp-outlined-card-outline-color);
100
+ }
101
+
102
+ // Hovered cards lift one level and apply an 8% on-surface state layer. Dragged
103
+ // and picked-up states take precedence over retained pointer state.
104
+ :where(#{$_card}):not(.dragged, .picked-up):not(.filled, .outlined, .border):has(> .primary-action:hover) {
105
+ @include z-depth("2");
106
+ }
107
+
108
+ :where(#{$_card}):not(.dragged, .picked-up):is(.filled, .outlined, .border):has(> .primary-action:hover) {
109
+ @include z-depth("1");
110
+ }
111
+
112
+ :where(#{$_card}):not(.dragged, .picked-up):has(> .primary-action:hover)::after {
113
+ opacity: var(--md-comp-card-hover-state-layer-opacity);
114
+ }
115
+
116
+ // Focus and press retain their variant elevation. Focus gets M3's external
117
+ // 3dp indicator at a 2dp offset; the outlined variant also darkens its stroke.
118
+ :where(#{$_card}):has(> .primary-action:focus-visible) {
119
+ outline: var(--md-comp-card-focus-indicator-thickness) solid var(--md-comp-card-focus-indicator-color);
120
+ outline-offset: var(--md-comp-card-focus-indicator-offset);
121
+ }
122
+
123
+ :where(#{$_card}):not(.dragged, .picked-up):has(> .primary-action:focus-visible)::after {
124
+ opacity: var(--md-comp-card-focus-state-layer-opacity);
125
+ }
126
+
127
+ :where(#{$_card}):is(.outlined, .border):has(> .primary-action:focus-visible) {
128
+ border-color: var(--md-sys-color-on-surface);
129
+ }
130
+
131
+ :where(#{$_card}):not(.dragged, .picked-up):has(> .primary-action:active)::after {
132
+ opacity: var(--md-comp-card-pressed-state-layer-opacity);
133
+ }
134
+
135
+ :where(#{$_card}):not(.dragged, .picked-up):not(.filled, .outlined, .border):has(> .primary-action:active) {
136
+ @include z-depth("1");
137
+ }
138
+
139
+ :where(#{$_card}):not(.dragged, .picked-up):is(.filled, .outlined, .border):has(> .primary-action:active) {
51
140
  @include z-depth("0");
52
- border: 1px solid var(--md-sys-color-outline-variant);
141
+ }
142
+
143
+ // A reorder interaction toggles `.dragged` or `.picked-up`. The elevated
144
+ // variant rises to level 4; filled and outlined variants rise to level 3.
145
+ :where(#{$_card}):not(.filled, .outlined, .border):is(.dragged, .picked-up) {
146
+ z-index: 4;
147
+ @include z-depth("4");
148
+ }
149
+
150
+ :where(#{$_card}):is(.filled, .outlined, .border):is(.dragged, .picked-up) {
151
+ z-index: 4;
152
+ @include z-depth("3");
153
+ }
154
+
155
+ :where(#{$_card}):is(.dragged, .picked-up)::after {
156
+ opacity: var(--md-comp-card-dragged-state-layer-opacity);
157
+ }
158
+
159
+ // Disabled tokens differ by variant in M3. `aria-disabled` belongs on the
160
+ // primary action; `.disabled` / article[aria-disabled] cover app-controlled
161
+ // cards that do not use a link.
162
+ :where(#{$_card}):is(.disabled, [aria-disabled="true"]),
163
+ :where(#{$_card}):has(> .primary-action[aria-disabled="true"]) {
164
+ outline: 0;
165
+ }
166
+
167
+ :where(#{$_card}):not(.filled, .outlined, .border):is(.disabled, [aria-disabled="true"]),
168
+ :where(#{$_card}):not(.filled, .outlined, .border):has(> .primary-action[aria-disabled="true"]) {
169
+ @include z-depth("1");
170
+ background-color: color-mix(
171
+ in oklab,
172
+ var(--md-sys-color-surface) var(--md-comp-card-disabled-container-opacity),
173
+ transparent
174
+ );
175
+ }
176
+
177
+ :where(#{$_card}).filled:is(.disabled, [aria-disabled="true"]),
178
+ :where(#{$_card}).filled:has(> .primary-action[aria-disabled="true"]) {
179
+ @include z-depth("0");
180
+ background-color: color-mix(
181
+ in oklab,
182
+ var(--md-sys-color-surface-variant) var(--md-comp-card-disabled-container-opacity),
183
+ transparent
184
+ );
185
+ }
186
+
187
+ :where(#{$_card}):is(.outlined, .border):is(.disabled, [aria-disabled="true"]),
188
+ :where(#{$_card}):is(.outlined, .border):has(> .primary-action[aria-disabled="true"]) {
189
+ @include z-depth("0");
190
+ background-color: var(--md-comp-outlined-card-container-color);
191
+ border-color: color-mix(
192
+ in oklab,
193
+ var(--md-comp-outlined-card-disabled-outline-color) var(--md-comp-outlined-card-disabled-outline-opacity),
194
+ transparent
195
+ );
196
+ }
197
+
198
+ :where(#{$_card}):is(.disabled, [aria-disabled="true"])::after,
199
+ :where(#{$_card}):has(> .primary-action[aria-disabled="true"])::after {
200
+ opacity: 0;
201
+ }
202
+
203
+ :where(#{$_card}).disabled,
204
+ :where(#{$_card})[aria-disabled="true"],
205
+ :where(#{$_card}) > .primary-action[aria-disabled="true"] {
206
+ pointer-events: none;
207
+ }
208
+
209
+ :where(#{$_card}) .card-icon {
210
+ width: var(--md-comp-card-icon-size);
211
+ height: var(--md-comp-card-icon-size);
212
+ color: var(--md-comp-card-icon-color);
213
+ font-size: var(--md-comp-card-icon-size);
53
214
  }
54
215
 
55
216
  // --- Semantic children ----------------------------------------------------
56
217
 
57
218
  article > :is(h1, h2, h3, h4, h5, h6),
58
- article > header > :is(h1, h2, h3, h4, h5, h6) {
219
+ article > :is(header, .primary-action) > :is(h1, h2, h3, h4, h5, h6) {
59
220
  margin: 0;
60
- padding: 16px 16px 0;
221
+ padding: var(--md-comp-card-content-padding) var(--md-comp-card-content-padding) 0;
61
222
  font-family: var(--md-sys-typescale-title-medium-font-family-name);
62
223
  font-size: var(--md-sys-typescale-title-medium-font-size);
63
224
  font-weight: var(--md-sys-typescale-title-medium-font-weight);
64
225
  line-height: var(--md-sys-typescale-title-medium-line-height);
65
226
  letter-spacing: var(--md-sys-typescale-title-medium-letter-spacing);
66
- color: var(--md-sys-color-on-surface);
227
+ color: var(--md-comp-card-headline-color);
67
228
  }
68
229
 
69
230
  article > p,
70
- article > header > p {
231
+ article > :is(header, .primary-action) > p {
71
232
  margin: 0;
72
- padding: 8px 16px 16px;
233
+ padding: var(--md-comp-card-between-space) var(--md-comp-card-content-padding) var(--md-comp-card-content-padding);
73
234
  font-family: var(--md-sys-typescale-body-medium-font-family-name);
74
235
  font-size: var(--md-sys-typescale-body-medium-font-size);
75
236
  font-weight: var(--md-sys-typescale-body-medium-font-weight);
76
237
  line-height: var(--md-sys-typescale-body-medium-line-height);
77
238
  letter-spacing: var(--md-sys-typescale-body-medium-letter-spacing);
78
- color: color-mix(in oklab, currentColor 72%, transparent);
239
+ color: var(--md-comp-card-supporting-text-color);
240
+ }
241
+
242
+ // Optional subhead between the headline and supporting copy. Keep the class
243
+ // on a paragraph so the document retains useful reading semantics.
244
+ article > p.subhead,
245
+ article > :is(header, .primary-action) > p.subhead {
246
+ margin: 0;
247
+ padding: 4px var(--md-comp-card-content-padding) 0;
248
+ font-family: var(--md-sys-typescale-title-small-font-family-name);
249
+ font-size: var(--md-sys-typescale-title-small-font-size);
250
+ font-weight: var(--md-sys-typescale-title-small-font-weight);
251
+ line-height: var(--md-sys-typescale-title-small-line-height);
252
+ letter-spacing: var(--md-sys-typescale-title-small-letter-spacing);
253
+ color: var(--md-comp-card-subhead-color);
79
254
  }
80
255
 
81
256
  article > :is(h1, h2, h3, h4, h5, h6) + p,
82
- article > header > :is(h1, h2, h3, h4, h5, h6) + p {
257
+ article > :is(header, .primary-action) > :is(h1, h2, h3, h4, h5, h6) + p {
83
258
  padding-top: 0;
84
259
  }
85
260
 
@@ -89,63 +264,189 @@ article > header {
89
264
  color: inherit;
90
265
  }
91
266
 
92
- // Action row. nav.tabs is a tab bar, not actions — leave it to _tabs.scss.
93
- article > nav:not(.tabs) {
267
+ // Action row. A row of buttons, not destinations.
268
+ article > .actions {
269
+ position: relative;
270
+ z-index: 3;
94
271
  display: flex;
95
272
  flex-wrap: wrap;
96
273
  align-items: center;
97
- gap: 8px;
274
+ gap: var(--md-comp-card-between-space);
98
275
  padding: 4px 8px 8px 8px;
99
276
  margin: 0;
100
277
  }
101
278
 
102
279
  article > img,
103
280
  article > picture,
104
- article > figure {
281
+ article > figure,
282
+ article > .primary-action > img,
283
+ article > .primary-action > picture,
284
+ article > .primary-action > figure {
105
285
  display: block;
106
286
  width: 100%;
107
287
  margin: 0;
288
+ border-radius: var(--md-comp-card-media-shape);
289
+ overflow: hidden;
108
290
  }
109
291
 
110
292
  article > img,
111
293
  article > picture img,
112
- article > figure img {
294
+ article > figure img,
295
+ article > .primary-action > img,
296
+ article > .primary-action > picture img,
297
+ article > .primary-action > figure img {
113
298
  display: block;
114
299
  width: 100%;
115
300
  object-fit: cover;
301
+ border-radius: var(--md-comp-card-media-shape);
116
302
  }
117
303
 
118
304
  article > figure {
119
305
  position: relative;
120
306
  }
121
307
 
122
- article > figure > figcaption {
308
+ article > .primary-action > figure {
309
+ position: relative;
310
+ }
311
+
312
+ article > figure > figcaption,
313
+ article > .primary-action > figure > figcaption {
123
314
  position: absolute;
124
- inset-inline: 0;
125
- bottom: 0;
315
+ inset-inline-start: var(--md-comp-card-content-padding);
316
+ inset-inline-end: auto;
317
+ bottom: var(--md-comp-card-content-padding);
318
+ display: inline-flex;
319
+ align-items: center;
320
+ gap: var(--md-comp-card-between-space);
321
+ box-sizing: border-box;
322
+ width: fit-content;
323
+ max-width: calc(100% - (2 * var(--md-comp-card-content-padding)));
126
324
  margin: 0;
127
- padding: 16px;
128
- color: var(--md-sys-color-on-surface);
129
- background: color-mix(in oklab, var(--md-sys-color-surface) 72%, transparent);
325
+ padding: 8px 12px;
326
+ color: var(--md-comp-card-media-overlay-ink-color);
327
+ background-color: var(--md-comp-card-media-overlay-container-color);
328
+ border-radius: var(--md-comp-card-media-overlay-shape);
130
329
  font-family: var(--md-sys-typescale-title-medium-font-family-name);
131
330
  font-size: var(--md-sys-typescale-title-medium-font-size);
132
331
  font-weight: var(--md-sys-typescale-title-medium-font-weight);
133
332
  line-height: var(--md-sys-typescale-title-medium-line-height);
134
333
  }
135
334
 
136
- // Horizontal: media on the start, body stacked beside it.
137
- article.horizontal {
138
- display: flex;
139
- flex-direction: row;
335
+ // Keep meaningful icons on the same opaque role pair as caption text. The
336
+ // surface/on-surface pairing isolates both from unpredictable image colors.
337
+ article > figure > figcaption > :is(.material-symbols, .card-icon, svg),
338
+ article > .primary-action > figure > figcaption > :is(.material-symbols, .card-icon, svg) {
339
+ flex: 0 0 auto;
340
+ color: inherit;
341
+ font-size: var(--md-comp-card-icon-size);
342
+ line-height: 1;
343
+ }
344
+
345
+ article > figure > figcaption > svg,
346
+ article > .primary-action > figure > figcaption > svg {
347
+ fill: currentColor;
348
+ }
349
+
350
+ // Collections are a grid by default. The full track definition is a token so
351
+ // applications can supply fixed, responsive, or asymmetric columns without
352
+ // fighting component selectors. Each card keeps its variant-specific resting
353
+ // elevation; picked-up / dragged state raises each variant above its rest.
354
+ :where(.card-collection) {
355
+ --md-comp-card-collection-gap: 8px;
356
+ --md-comp-card-collection-min-card-width: 240px;
357
+ --md-comp-card-collection-grid-template-columns: repeat(
358
+ auto-fit,
359
+ minmax(min(100%, var(--md-comp-card-collection-min-card-width)), 1fr)
360
+ );
361
+ --md-comp-card-collection-mosaic-row-size: 80px;
362
+ --md-comp-card-collection-carousel-height: 320px;
363
+ --md-comp-card-collection-carousel-card-width: min(82%, 360px);
364
+
365
+ display: grid;
366
+ grid-template-columns: var(--md-comp-card-collection-grid-template-columns);
367
+ gap: var(--md-comp-card-collection-gap);
140
368
  align-items: stretch;
369
+ margin: 0;
370
+ padding: 0;
371
+ }
372
+
373
+ :where(.card-collection article):not(.horizontal) {
374
+ display: flex;
375
+ flex-direction: column;
376
+ min-width: 0;
377
+ }
378
+
379
+ :where(.card-collection article):not(.horizontal) > .actions {
380
+ margin-block-start: auto;
381
+ }
382
+
383
+ // A list is the same collection with one column. The cards retain their own
384
+ // vertical / horizontal orientation.
385
+ .card-collection.list {
386
+ --md-comp-card-collection-grid-template-columns: minmax(0, 1fr);
387
+
388
+ margin: 0;
389
+ }
390
+
391
+ // Staggered uses multicolumn flow for masonry-like intrinsic card heights.
392
+ :where(.card-collection.staggered) {
393
+ display: block;
394
+ columns: var(
395
+ --md-comp-card-collection-staggered-column-width,
396
+ var(--md-comp-card-collection-min-card-width)
397
+ );
398
+ column-gap: var(--md-comp-card-collection-gap);
399
+ }
400
+
401
+ :where(.card-collection.staggered) > article {
402
+ break-inside: avoid;
403
+ width: 100%;
404
+ margin-block: 0 var(--md-comp-card-collection-gap);
405
+ }
406
+
407
+ // Mosaic keeps source order in a dense grid. Per-card custom properties make
408
+ // spans data-driven instead of baking a decorative nth-child pattern into the
409
+ // framework.
410
+ :where(.card-collection.mosaic) {
411
+ grid-auto-flow: dense;
412
+ grid-auto-rows: var(--md-comp-card-collection-mosaic-row-size);
413
+ }
414
+
415
+ :where(.card-collection.mosaic) > article {
416
+ grid-column: span var(--md-comp-card-collection-column-span, 1);
417
+ grid-row: span var(--md-comp-card-collection-row-span, 2);
418
+ }
419
+
420
+ // Reuse the M3 uncontained Carousel for a horizontally scrolling collection.
421
+ // Carousel.Init()/AutoInit() supplies the track and interaction behavior.
422
+ .card-collection.carousel {
423
+ --carousel-height: var(--md-comp-card-collection-carousel-height);
424
+ --md-comp-carousel-gap: var(--md-comp-card-collection-gap);
425
+ --md-comp-carousel-uncontained-item-width: var(
426
+ --md-comp-card-collection-carousel-card-width
427
+ );
428
+
429
+ display: block;
430
+ }
431
+
432
+ // Horizontal uses the same semantic slots as the vertical card. They may be
433
+ // direct children or children of the direct primary-action link.
434
+ @mixin horizontal-card-layout($with-actions: true) {
435
+ display: grid;
436
+ grid-template-columns:
437
+ minmax(0, var(--md-comp-card-horizontal-media-width))
438
+ minmax(0, 1fr);
439
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
440
+ min-height: var(--md-comp-card-horizontal-min-height);
141
441
  overflow: hidden;
142
442
 
143
443
  > img,
144
444
  > picture,
145
445
  > figure {
146
- flex: 0 0 40%;
147
- max-width: 40%;
148
- width: 40%;
446
+ grid-column: 1;
447
+ grid-row: 1 / -1;
448
+ width: 100%;
449
+ height: 100%;
149
450
  align-self: stretch;
150
451
  }
151
452
 
@@ -156,12 +457,45 @@ article.horizontal {
156
457
  object-fit: cover;
157
458
  }
158
459
 
159
- > div {
160
- flex: 1 1 auto;
161
- min-width: 0;
162
- display: flex;
163
- flex-direction: column;
460
+ > :is(h1, h2, h3, h4, h5, h6),
461
+ > header {
462
+ grid-column: 2;
463
+ grid-row: 1;
164
464
  }
465
+
466
+ > p.subhead {
467
+ grid-column: 2;
468
+ grid-row: 2;
469
+ }
470
+
471
+ > p:not(.subhead) {
472
+ grid-column: 2;
473
+ grid-row: 3;
474
+ }
475
+
476
+ @if $with-actions {
477
+ > .actions {
478
+ grid-column: 2;
479
+ grid-row: 4;
480
+ align-self: end;
481
+ justify-content: flex-end;
482
+ }
483
+ }
484
+ }
485
+
486
+ article.horizontal {
487
+ @include horizontal-card-layout;
488
+ }
489
+
490
+ article.horizontal:has(> .primary-action) {
491
+ display: block;
492
+ }
493
+
494
+ article.horizontal > .primary-action {
495
+ @include horizontal-card-layout(false);
496
+
497
+ width: 100%;
498
+ min-height: inherit;
165
499
  }
166
500
 
167
501
  // Fixed heights for uniform grids.
@@ -179,7 +513,7 @@ article.small, article.medium, article.large {
179
513
  overflow: hidden;
180
514
  }
181
515
 
182
- > img, > figure img {
516
+ > img, > picture img, > figure img {
183
517
  height: 100%;
184
518
  }
185
519
 
@@ -189,7 +523,7 @@ article.small, article.medium, article.large {
189
523
  overflow: hidden;
190
524
  }
191
525
 
192
- > nav:not(.tabs) {
526
+ > .actions {
193
527
  margin-top: auto;
194
528
  }
195
529
  }
@@ -197,59 +531,161 @@ article.small, article.medium, article.large {
197
531
  article.horizontal.small,
198
532
  article.horizontal.medium,
199
533
  article.horizontal.large {
200
- flex-direction: row;
534
+ display: grid;
201
535
 
202
536
  > img, > picture, > figure {
203
- flex: 0 0 40%;
204
537
  max-height: none;
205
538
  }
206
539
  }
207
540
 
208
- // Reveal overlay. aside is the panel. Parked under the card (translateY 100%)
209
- // and clipped by overflow:hidden. Open is aria-expanded="true" → slide up
210
- // to cover. Do not use inset:0 as the closed geometry and then
211
- // translateY(-100%): that slides the panel *off* the card, and isOpen stays
212
- // true so a second click is a no-op.
213
- article > aside {
214
- --md-comp-card-reveal-duration: 300ms;
215
- --md-comp-card-reveal-easing: ease;
541
+ article.horizontal:is(.small, .medium, .large):has(> .primary-action) {
542
+ display: block;
543
+ }
216
544
 
217
- position: absolute;
218
- inset: 0;
219
- z-index: 3;
545
+ article.horizontal:is(.small, .medium, .large) > .primary-action {
546
+ height: 100%;
547
+ }
548
+
549
+ // Compact windows use the vertical spelling. This keeps media and controls
550
+ // large enough to scan and operate without changing their source order.
551
+ @include bp-down("medium") {
552
+ article.horizontal,
553
+ article.horizontal.small,
554
+ article.horizontal.medium,
555
+ article.horizontal.large {
556
+ display: flex;
557
+ flex-direction: column;
558
+ height: auto;
559
+ min-height: auto;
560
+ }
561
+
562
+ article.horizontal > .primary-action,
563
+ article.horizontal:is(.small, .medium, .large) > .primary-action {
564
+ display: flex;
565
+ flex-direction: column;
566
+ height: auto;
567
+ min-height: auto;
568
+ }
569
+ }
570
+
571
+ // Reveal panel. Panels stay available until Cards confirms the disclosure
572
+ // contract and adds its runtime marker. The summary and media remain in place
573
+ // while the initialized card grows downward to expose additional actions.
574
+ // `interpolate-size` lets intrinsic content height animate without a fixed cap;
575
+ // the @supports fallback keeps the interaction functional.
576
+ :where(#{$_card}.card-reveal-initialized) {
577
+ interpolate-size: allow-keywords;
578
+ }
579
+
580
+ article.card-reveal-initialized > aside {
581
+ --md-comp-card-reveal-duration: 500ms;
582
+ --md-comp-card-reveal-easing: cubic-bezier(0.2, 0, 0, 1);
583
+
584
+ position: relative;
585
+ inset: auto;
586
+ z-index: 1;
220
587
  box-sizing: border-box;
221
- padding: 16px;
222
- overflow-y: auto;
223
- background-color: var(--md-sys-color-surface);
588
+ height: 0;
589
+ min-height: 0;
590
+ padding: 0 var(--md-comp-card-content-padding);
591
+ overflow: clip;
592
+ background-color: inherit;
224
593
  color: var(--md-sys-color-on-surface);
225
- transform: translateY(100%);
594
+ opacity: 0;
226
595
  visibility: hidden;
227
596
  pointer-events: none;
228
597
  transition:
229
- transform var(--md-comp-card-reveal-duration) var(--md-comp-card-reveal-easing),
598
+ height var(--md-comp-card-reveal-duration) var(--md-comp-card-reveal-easing),
599
+ padding-block var(--md-comp-card-reveal-duration) var(--md-comp-card-reveal-easing),
600
+ opacity 150ms ease-out,
230
601
  visibility 0s var(--md-comp-card-reveal-duration);
231
602
  }
232
603
 
233
- article > aside[aria-expanded="true"] {
234
- transform: translateY(0);
604
+ article.card-reveal-initialized > aside.open {
605
+ height: auto;
606
+ padding-block: 0 var(--md-comp-card-content-padding);
607
+ opacity: 1;
235
608
  visibility: visible;
236
609
  pointer-events: auto;
237
- transition: transform var(--md-comp-card-reveal-duration) var(--md-comp-card-reveal-easing);
610
+ transition:
611
+ height var(--md-comp-card-reveal-duration) var(--md-comp-card-reveal-easing),
612
+ padding-block var(--md-comp-card-reveal-duration) var(--md-comp-card-reveal-easing),
613
+ opacity 200ms 100ms ease-out,
614
+ visibility 0s;
615
+ }
616
+
617
+ @supports not (interpolate-size: allow-keywords) {
618
+ article.card-reveal-initialized > aside {
619
+ display: none;
620
+ height: auto;
621
+ transition: none;
622
+ }
623
+
624
+ article.card-reveal-initialized > aside.open {
625
+ display: block;
626
+ }
627
+ }
628
+
629
+ // A media-sized button supplies the repeated open/close target and carries the
630
+ // state layer for the tap feedback shown by the M3 example.
631
+ article > figure > .card-reveal-trigger {
632
+ @include card-media-trigger;
633
+ }
634
+
635
+ article > header.card-reveal-summary {
636
+ padding-bottom: var(--md-comp-card-content-padding);
637
+ }
638
+
639
+ article > aside > .reveal-actions {
640
+ display: grid;
641
+ gap: 0;
642
+ margin: 0;
643
+ font-style: normal;
644
+ }
645
+
646
+ article > aside > .reveal-actions > .reveal-action {
647
+ display: grid;
648
+ grid-template-columns: var(--md-comp-card-icon-size) minmax(0, 1fr);
649
+ align-items: center;
650
+ gap: var(--md-comp-card-content-padding);
651
+ min-height: 48px;
652
+ padding-inline: 0;
653
+ color: var(--md-sys-color-on-surface);
654
+ border-radius: 8px;
655
+ font-family: var(--md-sys-typescale-body-medium-font-family-name);
656
+ font-size: var(--md-sys-typescale-body-medium-font-size);
657
+ font-weight: var(--md-sys-typescale-body-medium-font-weight);
658
+ line-height: var(--md-sys-typescale-body-medium-line-height);
659
+ letter-spacing: var(--md-sys-typescale-body-medium-letter-spacing);
660
+ }
661
+
662
+ article > aside > .reveal-actions > .reveal-action:hover {
663
+ background-color: color-mix(
664
+ in oklab,
665
+ var(--md-sys-color-on-surface) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%),
666
+ transparent
667
+ );
668
+ }
669
+
670
+ article > aside > .reveal-actions > .reveal-action > :is(.material-symbols, .card-icon, svg) {
671
+ width: var(--md-comp-card-icon-size);
672
+ height: var(--md-comp-card-icon-size);
673
+ color: var(--md-sys-color-on-surface-variant);
674
+ font-size: var(--md-comp-card-icon-size);
675
+ fill: currentColor;
238
676
  }
239
677
 
240
678
  @media (prefers-reduced-motion: reduce) {
241
- article > aside {
679
+ :where(#{$_card}),
680
+ :where(#{$_card})::after,
681
+ article > figure > .card-reveal-trigger,
682
+ article.card-reveal-initialized > aside {
242
683
  transition: none;
243
684
  }
244
685
  }
245
686
 
246
- article.sticky > nav {
687
+ article.sticky > .actions {
247
688
  position: relative;
248
689
  z-index: 4;
249
690
  background-color: inherit;
250
691
  }
251
-
252
- article.sticky > aside {
253
- bottom: auto;
254
- height: calc(100% - 52px);
255
- }