@undrr/undrr-mangrove 2.0.1 → 2.1.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 (98) hide show
  1. package/README.md +1 -1
  2. package/components/BookCard.js +1 -1
  3. package/components/Breadcrumbs.js +1 -1
  4. package/components/Checkbox.js +1 -1
  5. package/components/Chips.js +1 -1
  6. package/components/CopyButton.js +1 -1
  7. package/components/CtaButton.js +1 -1
  8. package/components/Drawer.js +2 -2
  9. package/components/EmbedContainer.js +1 -1
  10. package/components/Footer.js +1 -1
  11. package/components/FormAction.js +1 -1
  12. package/components/FormErrorSummary.js +1 -1
  13. package/components/FormGroup.js +1 -1
  14. package/components/FullWidth.js +1 -1
  15. package/components/Gallery.js +2 -2
  16. package/components/Hero.js +2 -2
  17. package/components/HighlightBox.js +1 -1
  18. package/components/HorizontalBookCard.js +1 -1
  19. package/components/HorizontalCard.js +1 -1
  20. package/components/IconCard.js +1 -1
  21. package/components/Legend.js +1 -1
  22. package/components/Loader.js +2 -2
  23. package/components/MegaMenu.js +2 -2
  24. package/components/Notice.js +1 -1
  25. package/components/PageHeader.js +1 -1
  26. package/components/Pager.js +1 -1
  27. package/components/ProgressBar.js +2 -2
  28. package/components/QuoteHighlight.js +1 -1
  29. package/components/Radio.js +1 -1
  30. package/components/Range.js +1 -1
  31. package/components/ScrollContainer.js +1 -1
  32. package/components/SegmentedControl.js +1 -1
  33. package/components/Select.js +1 -1
  34. package/components/ServiceNotice.js +1 -1
  35. package/components/ShareButtons.js +1 -1
  36. package/components/ShowMore.js +2 -2
  37. package/components/SkipLink.js +1 -1
  38. package/components/StatsCard.js +1 -1
  39. package/components/SyndicationSearchWidget.js +2 -2
  40. package/components/Tab.js +1 -1
  41. package/components/TextCta.js +1 -1
  42. package/components/TextInput.js +1 -1
  43. package/components/Textarea.js +1 -1
  44. package/components/Tree.js +1 -1
  45. package/components/UserFeedback.js +1 -1
  46. package/components/VerticalCard.js +1 -1
  47. package/components/hydrate.js +2 -2
  48. package/css/style-all.css +41 -42
  49. package/css/style-all.css.map +1 -1
  50. package/css/style-delta.css +41 -42
  51. package/css/style-delta.css.map +1 -1
  52. package/css/style-gutenberg.css +4 -4
  53. package/css/style-gutenberg.css.map +1 -1
  54. package/css/style-irp.css +41 -42
  55. package/css/style-irp.css.map +1 -1
  56. package/css/style-mcr.css +41 -42
  57. package/css/style-mcr.css.map +1 -1
  58. package/css/style-preventionweb.css +41 -42
  59. package/css/style-preventionweb.css.map +1 -1
  60. package/css/style.css +41 -42
  61. package/css/style.css.map +1 -1
  62. package/docs/EDITORIAL-MANUAL.md +1 -1
  63. package/docs/llms-motion.txt +125 -0
  64. package/error-pages/401.html +1 -1
  65. package/error-pages/403.html +1 -1
  66. package/error-pages/404.html +1 -1
  67. package/error-pages/429.html +1 -1
  68. package/error-pages/500.html +1 -1
  69. package/error-pages/502.html +1 -1
  70. package/error-pages/503.html +1 -1
  71. package/error-pages/504.html +1 -1
  72. package/error-pages/5xx.html +1 -1
  73. package/error-pages/challenge.html +1 -1
  74. package/error-pages/managed-challenge.html +1 -1
  75. package/js/cite-this.js +685 -43
  76. package/js/on-this-page-nav.js +80 -1
  77. package/js/show-more.js +149 -21
  78. package/package.json +1 -1
  79. package/scss/Atom/Images/AuthorImage/author-image.scss +9 -2
  80. package/scss/Atom/ReachElement/Details/details.scss +4 -2
  81. package/scss/Components/Cards/Card/card.scss +8 -2
  82. package/scss/Components/Cards/StatsCard/stats-card.scss +8 -1
  83. package/scss/Components/CiteThis/cite-this.scss +162 -9
  84. package/scss/Components/Gallery/gallery.scss +40 -15
  85. package/scss/Components/Hero/hero.scss +165 -0
  86. package/scss/Components/MegaMenu/mega-menu.scss +42 -121
  87. package/scss/Components/Navigation/Drawer/drawer.scss +10 -0
  88. package/scss/Components/OnThisPageNav/on-this-page-nav.scss +108 -3
  89. package/scss/Components/SyndicationSearchWidget/syndication-search-widget.scss +252 -127
  90. package/scss/Components/Tab/tab.scss +2 -1
  91. package/scss/Patterns/RichContent/rich-content.scss +177 -0
  92. package/scss/Patterns/_shared/pattern-demo.scss +5 -0
  93. package/scss/Utilities/Loader/loader.scss +89 -11
  94. package/scss/Utilities/ShowMore/show-more.scss +6 -0
  95. package/scss/assets/scss/_components.scss +3 -0
  96. package/scss/assets/scss/_foundational.scss +23 -8
  97. package/scss/assets/scss/_mixins.scss +5 -0
  98. package/scss/assets/scss/_variables.scss +1 -1
@@ -57,34 +57,13 @@ $search-input-padding-x: mg-rem(44);
57
57
  // Loading Progress Bar
58
58
  // ========================================
59
59
 
60
+ // Shared ProgressBar owns track, animation, RTL and accessibility modes.
61
+ // Keep the historical wrapper class as the positioning hook.
60
62
  .mg-search__progress {
61
63
  position: absolute;
62
- top: 0;
63
- left: 0;
64
- right: 0;
65
- height: 3px;
66
- background: $search-border-light;
67
- overflow: hidden;
68
- z-index: 10;
69
- }
70
-
71
- .mg-search__progress-bar {
72
- height: 100%;
73
- width: 30%;
74
- background: linear-gradient(90deg, $search-primary, $search-primary-dark);
75
- animation: mg-search-progress 1.2s ease-in-out infinite;
76
- }
77
-
78
- @keyframes mg-search-progress {
79
- 0% {
80
- transform: translateX(-100%);
81
- }
82
- 50% {
83
- transform: translateX(200%);
84
- }
85
- 100% {
86
- transform: translateX(400%);
87
- }
64
+ inset-block-start: 0;
65
+ inset-inline: 0;
66
+ z-index: 1; // local stacking within the positioned results region
88
67
  }
89
68
 
90
69
  // Search form (used by both the in-widget SearchForm and the standalone
@@ -109,9 +88,13 @@ $search-input-padding-x: mg-rem(44);
109
88
 
110
89
  // Override the inline-end padding to leave room for the clear button
111
90
  padding-inline-end: $search-input-padding-x;
112
- transition:
113
- border-color 0.2s,
114
- box-shadow 0.2s;
91
+
92
+ @media (prefers-reduced-motion: no-preference) {
93
+ transition:
94
+ border-color var(--mg-motion-duration-fast)
95
+ var(--mg-motion-easing-standard),
96
+ box-shadow var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
97
+ }
115
98
 
116
99
  &::placeholder {
117
100
  color: $search-text-muted;
@@ -143,12 +126,6 @@ $search-input-padding-x: mg-rem(44);
143
126
  }
144
127
  }
145
128
 
146
- @keyframes mg-search-spin {
147
- to {
148
- transform: rotate(360deg);
149
- }
150
- }
151
-
152
129
  // Submit button: visual styling comes from .mg-button / .mg-button-primary.
153
130
  // Only layout-specific spacing for the icon + label is defined here.
154
131
  // Loading state is conveyed by the widget-level progress strip
@@ -202,9 +179,14 @@ $search-input-padding-x: mg-rem(44);
202
179
  border: 1px solid $search-border;
203
180
  border-radius: $search-radius;
204
181
  cursor: pointer;
205
- transition:
206
- background-color 0.2s,
207
- border-color 0.2s;
182
+
183
+ @media (prefers-reduced-motion: no-preference) {
184
+ transition:
185
+ background-color var(--mg-motion-duration-fast)
186
+ var(--mg-motion-easing-standard),
187
+ border-color var(--mg-motion-duration-fast)
188
+ var(--mg-motion-easing-standard);
189
+ }
208
190
 
209
191
  &:hover {
210
192
  background: $search-chip-hover;
@@ -231,9 +213,13 @@ $search-input-padding-x: mg-rem(44);
231
213
  border: 1px solid $search-primary;
232
214
  border-radius: $search-radius;
233
215
  cursor: pointer;
234
- transition:
235
- background-color 0.2s,
236
- color 0.2s;
216
+
217
+ @media (prefers-reduced-motion: no-preference) {
218
+ transition:
219
+ background-color var(--mg-motion-duration-fast)
220
+ var(--mg-motion-easing-standard),
221
+ color var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
222
+ }
237
223
 
238
224
  &:hover {
239
225
  color: #fff;
@@ -247,7 +233,23 @@ $search-input-padding-x: mg-rem(44);
247
233
  gap: var(--mg-spacing-200);
248
234
  }
249
235
 
236
+ // Results column: the search box and filter chips (sidebar layout) and the
237
+ // results.
238
+ .mg-search__column {
239
+ flex: 1;
240
+ min-width: 0;
241
+ }
242
+
243
+ // The in-widget search box is capped at a comfortable reading width, start
244
+ // aligned, in every layout; on narrow screens it is full width anyway. A
245
+ // form portaled into a searchTarget is outside the widget and left to the
246
+ // host page.
247
+ .mg-search-widget .mg-search__form {
248
+ max-inline-size: mg-rem(768);
249
+ }
250
+
250
251
  .mg-search__main {
252
+ position: relative;
251
253
  flex: 1;
252
254
  min-width: 0;
253
255
  }
@@ -304,7 +306,63 @@ $search-input-padding-x: mg-rem(44);
304
306
 
305
307
  // Results
306
308
  .mg-search__results {
307
- transition: opacity 0.2s ease-out;
309
+ @media (prefers-reduced-motion: no-preference) {
310
+ transition: opacity var(--mg-motion-duration-fast)
311
+ var(--mg-motion-easing-standard);
312
+ }
313
+ }
314
+
315
+ // A new search with results on screen: keep them, dimmed, with an inline
316
+ // loader in the header, instead of swapping in the skeleton.
317
+ // Keep active status text at full contrast; only the retained hits are stale.
318
+ .mg-search__results--refreshing {
319
+ .mg-search__results-list,
320
+ .mg-search__results-grid {
321
+ opacity: 0.6;
322
+ }
323
+ }
324
+
325
+ .mg-search__results-loader {
326
+ display: inline-flex;
327
+ align-items: center;
328
+ gap: var(--mg-spacing-50);
329
+ min-inline-size: 0;
330
+ overflow-wrap: anywhere;
331
+ margin-inline-start: auto;
332
+ color: $search-primary;
333
+ }
334
+
335
+ // One short fade for a completed set. No per-item delays or movement:
336
+ // results are immediately usable and the base state is fully visible.
337
+ .mg-search__enter {
338
+ @media (prefers-reduced-motion: no-preference) {
339
+ animation: mg-search-results-in var(--mg-motion-duration-fast)
340
+ var(--mg-motion-easing-standard);
341
+ }
342
+ }
343
+
344
+ @keyframes mg-search-results-in {
345
+ from {
346
+ opacity: 0;
347
+ }
348
+ to {
349
+ opacity: 1;
350
+ }
351
+ }
352
+
353
+ // Server-rendered results ([data-mg-search-ssr], moved into the results
354
+ // region until the first client results). The recommended markup reuses the
355
+ // client classes (.mg-search__results-list, .mg-search__result), which get
356
+ // containment and block images from their own rules; these cover markup
357
+ // that doesn't, so a malformed teaser can't cover the page here either.
358
+ // undrr/web-backlog#3145.
359
+ [data-mg-search-ssr] {
360
+ position: relative;
361
+ isolation: isolate;
362
+
363
+ :where(.mg-card__visual) img {
364
+ display: block;
365
+ }
308
366
  }
309
367
 
310
368
  .mg-search__results-header {
@@ -342,9 +400,15 @@ $search-input-padding-x: mg-rem(44);
342
400
  border: 1px solid $search-border;
343
401
  border-radius: $search-radius;
344
402
  cursor: pointer;
345
- transition:
346
- background-color 0.15s,
347
- border-color 0.15s;
403
+
404
+ @media (prefers-reduced-motion: no-preference) {
405
+ transition:
406
+ background-color var(--mg-motion-duration-fast)
407
+ var(--mg-motion-easing-standard),
408
+ border-color var(--mg-motion-duration-fast)
409
+ var(--mg-motion-easing-standard);
410
+ }
411
+
348
412
  white-space: nowrap;
349
413
 
350
414
  &:hover {
@@ -423,6 +487,12 @@ $search-input-padding-x: mg-rem(44);
423
487
 
424
488
  // Result item
425
489
  .mg-search__result {
490
+ // Contain each result: teaser HTML comes from the consuming site's index,
491
+ // and a malformed teaser's absolutely positioned pieces (e.g. a stretched
492
+ // link's ::after) otherwise anchor to the page and cover it.
493
+ // undrr/web-backlog#3145.
494
+ position: relative;
495
+ isolation: isolate;
426
496
  padding-bottom: var(--mg-spacing-150);
427
497
  border-bottom: 1px solid $search-border-light;
428
498
 
@@ -432,6 +502,13 @@ $search-input-padding-x: mg-rem(44);
432
502
  }
433
503
  }
434
504
 
505
+ // Teaser images are inline by default, and the teaser's field wrappers set
506
+ // a 24px line height, which left a few pixels of descender space under every
507
+ // result image. Block images sit flush in list, card and book cover modes.
508
+ .mg-search__result :where(.mg-card__visual) img {
509
+ display: block;
510
+ }
511
+
435
512
  .mg-search__result-content {
436
513
  display: flex;
437
514
  gap: var(--mg-spacing-100);
@@ -549,14 +626,7 @@ $search-input-padding-x: mg-rem(44);
549
626
  .mg-search__skeleton-title,
550
627
  .mg-search__skeleton-meta,
551
628
  .mg-search__skeleton-text {
552
- background: linear-gradient(
553
- 90deg,
554
- $search-skeleton-bg 25%,
555
- #e8e8e8 50%,
556
- $search-skeleton-bg 75%
557
- );
558
- background-size: 200% 100%;
559
- animation: mg-search-shimmer 1.5s infinite;
629
+ background: $search-skeleton-bg;
560
630
  border-radius: $search-radius;
561
631
  }
562
632
 
@@ -599,14 +669,7 @@ $search-input-padding-x: mg-rem(44);
599
669
 
600
670
  .mg-search__skeleton-card-image {
601
671
  aspect-ratio: 16 / 9;
602
- background: linear-gradient(
603
- 90deg,
604
- $search-skeleton-bg 25%,
605
- #e8e8e8 50%,
606
- $search-skeleton-bg 75%
607
- );
608
- background-size: 200% 100%;
609
- animation: mg-search-shimmer 1.5s infinite;
672
+ background: $search-skeleton-bg;
610
673
  }
611
674
 
612
675
  .mg-search__skeleton-card-body {
@@ -622,15 +685,6 @@ $search-input-padding-x: mg-rem(44);
622
685
  }
623
686
  }
624
687
 
625
- @keyframes mg-search-shimmer {
626
- 0% {
627
- background-position: 200% 0;
628
- }
629
- 100% {
630
- background-position: -200% 0;
631
- }
632
- }
633
-
634
688
  // Facets placeholder
635
689
  .mg-search__facets-placeholder {
636
690
  padding: var(--mg-spacing-100);
@@ -810,19 +864,25 @@ $search-input-padding-x: mg-rem(44);
810
864
  // Animation classes applied via JS (which checks prefers-reduced-motion).
811
865
  // ========================================
812
866
 
813
- // Initial hidden state - applied before animation
814
- .mg-search__result--pending {
815
- opacity: 0;
816
- transform: translateX(-20px);
817
- }
867
+ // Legacy consumers may still toggle these classes. Gate the hidden state as
868
+ // well as its transition, so reduced motion never strands a result invisible.
869
+ @media (prefers-reduced-motion: no-preference) {
870
+ .mg-search__result--pending {
871
+ opacity: 0;
872
+ transform: translateX(-20px);
818
873
 
819
- // Animated state - triggers the transition
820
- .mg-search__result--visible {
821
- opacity: 1;
822
- transform: translateX(0);
823
- transition:
824
- opacity 0.3s ease-out,
825
- transform 0.3s ease-out;
874
+ &:dir(rtl) {
875
+ transform: translateX(20px);
876
+ }
877
+ }
878
+
879
+ .mg-search__result--visible {
880
+ opacity: 1;
881
+ transform: translateX(0);
882
+ transition:
883
+ opacity var(--mg-motion-duration-fast) var(--mg-motion-easing-standard),
884
+ transform var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
885
+ }
826
886
  }
827
887
 
828
888
  // Stale state - dim results while loading new data
@@ -831,29 +891,8 @@ $search-input-padding-x: mg-rem(44);
831
891
  pointer-events: none;
832
892
  }
833
893
 
834
- // Stale main area with more visible feedback
835
- .mg-search__main--stale {
836
- position: relative;
837
-
838
- &::after {
839
- content: "";
840
- position: absolute;
841
- inset: 0;
842
- background: rgb(255 255 255 / 0.4);
843
- pointer-events: none;
844
- animation: mg-search-pulse 1.5s ease-in-out infinite;
845
- }
846
- }
847
-
848
- @keyframes mg-search-pulse {
849
- 0%,
850
- 100% {
851
- opacity: 0.3;
852
- }
853
- 50% {
854
- opacity: 0.6;
855
- }
856
- }
894
+ // Refresh feedback is retained/dimmed results, inline Loader and the shared
895
+ // progress strip; an additional pulsing overlay would compete with them.
857
896
 
858
897
  // ========================================
859
898
  // Collapsible Filter Sections
@@ -900,7 +939,11 @@ $search-input-padding-x: mg-rem(44);
900
939
  border-left: 5px solid transparent;
901
940
  border-right: 5px solid transparent;
902
941
  border-top: 6px solid currentColor;
903
- transition: transform 0.2s ease;
942
+
943
+ @media (prefers-reduced-motion: no-preference) {
944
+ transition: transform var(--mg-motion-duration-fast)
945
+ var(--mg-motion-easing-standard);
946
+ }
904
947
 
905
948
  .mg-search__facet-toggle[aria-expanded="false"] & {
906
949
  transform: rotate(-90deg);
@@ -922,13 +965,37 @@ $search-input-padding-x: mg-rem(44);
922
965
  padding: var(--mg-spacing-200);
923
966
  }
924
967
 
968
+ // Legacy public class: keep compatibility for hand-written Drupal markup.
969
+ // New widget code uses shared Loader. One cycle is 1s; this is a documented
970
+ // looping compatibility exception, not interaction timing. Unlike the shared
971
+ // Loader's designed static reduced-motion state, this legacy markup keeps slow
972
+ // rotation until its consumers can be audited and migrated.
973
+ @keyframes mg-search-spin {
974
+ to {
975
+ transform: rotate(360deg);
976
+ }
977
+ }
978
+
925
979
  .mg-search__spinner {
980
+ --mg-search-spinner-duration: 1s;
981
+
926
982
  border: 4px solid $search-border-light;
927
983
  border-top: 4px solid $search-primary;
928
984
  border-radius: 50%;
929
985
  width: 40px;
930
986
  height: 40px;
931
- animation: mg-search-spin 1s linear infinite;
987
+ animation: mg-search-spin var(--mg-search-spinner-duration) linear infinite;
988
+
989
+ @media (prefers-reduced-motion: reduce) {
990
+ animation-duration: calc(var(--mg-search-spinner-duration) * 3) !important;
991
+ animation-iteration-count: infinite !important;
992
+ }
993
+
994
+ @media (forced-colors: active) {
995
+ border-color: GrayText;
996
+ border-top-color: CanvasText;
997
+ forced-color-adjust: none;
998
+ }
932
999
  }
933
1000
 
934
1001
  .mg-search__loader-text {
@@ -963,9 +1030,13 @@ $search-input-padding-x: mg-rem(44);
963
1030
  border: 1px solid rgb(var(--mg-color-neutral-600) / 0.22);
964
1031
  border-radius: var(--mg-radius-form-input);
965
1032
  cursor: pointer;
966
- transition:
967
- border-color 0.15s,
968
- box-shadow 0.15s;
1033
+
1034
+ @media (prefers-reduced-motion: no-preference) {
1035
+ transition:
1036
+ border-color var(--mg-motion-duration-fast)
1037
+ var(--mg-motion-easing-standard),
1038
+ box-shadow var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
1039
+ }
969
1040
 
970
1041
  &:hover {
971
1042
  border-color: $search-primary;
@@ -997,7 +1068,11 @@ $search-input-padding-x: mg-rem(44);
997
1068
  flex-shrink: 0;
998
1069
  margin-inline-start: var(--mg-spacing-50);
999
1070
  color: $search-text-muted;
1000
- transition: transform 0.2s ease;
1071
+
1072
+ @media (prefers-reduced-motion: no-preference) {
1073
+ transition: transform var(--mg-motion-duration-fast)
1074
+ var(--mg-motion-easing-standard);
1075
+ }
1001
1076
 
1002
1077
  .mg-select--open & {
1003
1078
  transform: rotate(180deg);
@@ -1029,7 +1104,11 @@ $search-input-padding-x: mg-rem(44);
1029
1104
  border-top: none;
1030
1105
  border-radius: 0 0 $search-radius $search-radius;
1031
1106
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
1032
- animation: mg-select-fade-in 0.15s ease-out;
1107
+
1108
+ @media (prefers-reduced-motion: no-preference) {
1109
+ animation: mg-select-fade-in var(--mg-motion-duration-fast)
1110
+ var(--mg-motion-easing-standard);
1111
+ }
1033
1112
  }
1034
1113
 
1035
1114
  @keyframes mg-select-fade-in {
@@ -1085,7 +1164,11 @@ $search-input-padding-x: mg-rem(44);
1085
1164
  padding: var(--mg-spacing-50) var(--mg-spacing-75);
1086
1165
  min-height: 40px; // Touch-friendly
1087
1166
  cursor: pointer;
1088
- transition: background-color 0.1s ease;
1167
+
1168
+ @media (prefers-reduced-motion: no-preference) {
1169
+ transition: background-color var(--mg-motion-duration-fast)
1170
+ var(--mg-motion-easing-standard);
1171
+ }
1089
1172
 
1090
1173
  &:hover,
1091
1174
  &--highlighted {
@@ -1101,6 +1184,9 @@ $search-input-padding-x: mg-rem(44);
1101
1184
  }
1102
1185
  }
1103
1186
 
1187
+ // Hover is pure CSS; --highlighted is the keyboard position only, so
1188
+ // moving the pointer over a long list doesn't re-render it.
1189
+ &--selected:hover,
1104
1190
  &--selected#{&}--highlighted {
1105
1191
  background-color: rgb(var(--mg-color-blue-900) / 0.12);
1106
1192
  }
@@ -1189,7 +1275,11 @@ $search-input-padding-x: mg-rem(44);
1189
1275
  border-radius: $search-radius;
1190
1276
  align-items: baseline;
1191
1277
  gap: 0 var(--mg-spacing-25);
1192
- transition: background-color 0.15s ease;
1278
+
1279
+ @media (prefers-reduced-motion: no-preference) {
1280
+ transition: background-color var(--mg-motion-duration-fast)
1281
+ var(--mg-motion-easing-standard);
1282
+ }
1193
1283
 
1194
1284
  &:hover {
1195
1285
  background-color: rgb(var(--mg-color-blue-900) / 0.05);
@@ -1250,7 +1340,12 @@ $search-input-padding-x: mg-rem(44);
1250
1340
  inset: 0;
1251
1341
  background: rgb(0 0 0 / 0.5);
1252
1342
  z-index: calc(var(--mg-z-index-drawer) - 1); // backdrop behind drawer panel
1253
- animation: mg-search-fade-in 0.2s ease-out;
1343
+
1344
+ @media (prefers-reduced-motion: no-preference) {
1345
+ animation: mg-search-fade-in var(--mg-motion-duration-fast)
1346
+ var(--mg-motion-easing-standard);
1347
+ }
1348
+
1254
1349
  cursor: pointer; // Indicate clickable to close
1255
1350
  }
1256
1351
 
@@ -1281,19 +1376,19 @@ $search-input-padding-x: mg-rem(44);
1281
1376
  box-shadow: var(--mg-search-drawer-shadow-offset) 0 20px rgb(0 0 0 / 0.15);
1282
1377
  display: flex;
1283
1378
  flex-direction: column;
1284
- animation: mg-search-slide-in-start 0.3s ease-out;
1285
1379
  z-index: var(--mg-z-index-drawer);
1286
1380
  cursor: default; // Reset cursor inside drawer
1287
1381
 
1382
+ @media (prefers-reduced-motion: no-preference) {
1383
+ animation: mg-search-slide-in-start var(--mg-motion-duration-medium)
1384
+ var(--mg-motion-easing-standard);
1385
+ }
1386
+
1288
1387
  // The shadow points at the page content, so it mirrors with the panel.
1289
1388
  &:dir(rtl) {
1290
1389
  --mg-search-drawer-offset: 100%;
1291
1390
  --mg-search-drawer-shadow-offset: -4px;
1292
1391
  }
1293
-
1294
- @media (prefers-reduced-motion: reduce) {
1295
- animation: none;
1296
- }
1297
1392
  }
1298
1393
 
1299
1394
  @keyframes mg-search-slide-in-start {
@@ -1351,9 +1446,13 @@ $search-input-padding-x: mg-rem(44);
1351
1446
  border: none;
1352
1447
  border-radius: $search-radius;
1353
1448
  cursor: pointer;
1354
- transition:
1355
- background-color 0.15s,
1356
- color 0.15s;
1449
+
1450
+ @media (prefers-reduced-motion: no-preference) {
1451
+ transition:
1452
+ background-color var(--mg-motion-duration-fast)
1453
+ var(--mg-motion-easing-standard),
1454
+ color var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
1455
+ }
1357
1456
 
1358
1457
  &:hover {
1359
1458
  background: $search-chip-bg;
@@ -1395,7 +1494,11 @@ $search-input-padding-x: mg-rem(44);
1395
1494
  border: none;
1396
1495
  border-radius: $search-radius;
1397
1496
  cursor: pointer;
1398
- transition: background-color 0.15s;
1497
+
1498
+ @media (prefers-reduced-motion: no-preference) {
1499
+ transition: background-color var(--mg-motion-duration-fast)
1500
+ var(--mg-motion-easing-standard);
1501
+ }
1399
1502
 
1400
1503
  &:hover {
1401
1504
  background: $search-primary-dark;
@@ -1503,9 +1606,14 @@ $search-input-padding-x: mg-rem(44);
1503
1606
  align-items: center;
1504
1607
  padding: var(--mg-spacing-25) var(--mg-spacing-50);
1505
1608
  cursor: pointer;
1506
- transition:
1507
- color 0.15s,
1508
- border-color 0.15s;
1609
+
1610
+ @media (prefers-reduced-motion: no-preference) {
1611
+ transition:
1612
+ color var(--mg-motion-duration-fast) var(--mg-motion-easing-standard),
1613
+ border-color var(--mg-motion-duration-fast)
1614
+ var(--mg-motion-easing-standard);
1615
+ }
1616
+
1509
1617
  font-weight: 400;
1510
1618
  font-size: var(--mg-font-size-200);
1511
1619
  user-select: none;
@@ -1554,3 +1662,20 @@ $search-input-padding-x: mg-rem(44);
1554
1662
 
1555
1663
  // Pager styles now live in the standalone Pager component.
1556
1664
  // @see stories/Components/Pager/pager.scss
1665
+
1666
+ // Skeleton surfaces must stay identifiable when forced colours removes
1667
+ // author backgrounds. These shapes are decorative; the status owns meaning.
1668
+ @media (forced-colors: active) {
1669
+ .mg-search__skeleton-title,
1670
+ .mg-search__skeleton-meta,
1671
+ .mg-search__skeleton-text,
1672
+ .mg-search__skeleton-card-image,
1673
+ .mg-search__skeleton-card {
1674
+ outline: 1px solid GrayText;
1675
+ outline-offset: -1px;
1676
+ }
1677
+
1678
+ .mg-search__drawer {
1679
+ border-inline-end: 1px solid CanvasText;
1680
+ }
1681
+ }
@@ -107,7 +107,8 @@
107
107
  transform: rotate(-45deg);
108
108
  vertical-align: top;
109
109
  width: 0.45em;
110
- transition: transform 0.3s ease;
110
+ transition: transform var(--mg-motion-duration-medium)
111
+ var(--mg-motion-easing-standard);
111
112
 
112
113
  @media (prefers-reduced-motion: reduce) {
113
114
  transition: none;