@undrr/undrr-mangrove 2.0.1 → 2.1.1

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 +258 -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);
@@ -797,6 +851,12 @@ $search-input-padding-x: mg-rem(44);
797
851
  display: flex;
798
852
  }
799
853
 
854
+ // Match vertical cards' row fill without changing standalone book cards.
855
+ // The book card's existing start alignment keeps cover rows intrinsic.
856
+ .mg-card__book {
857
+ block-size: 100%;
858
+ }
859
+
800
860
  // Remove list-style dividers in grid/card mode
801
861
  .mg-search__result {
802
862
  padding-bottom: 0;
@@ -810,19 +870,25 @@ $search-input-padding-x: mg-rem(44);
810
870
  // Animation classes applied via JS (which checks prefers-reduced-motion).
811
871
  // ========================================
812
872
 
813
- // Initial hidden state - applied before animation
814
- .mg-search__result--pending {
815
- opacity: 0;
816
- transform: translateX(-20px);
817
- }
873
+ // Legacy consumers may still toggle these classes. Gate the hidden state as
874
+ // well as its transition, so reduced motion never strands a result invisible.
875
+ @media (prefers-reduced-motion: no-preference) {
876
+ .mg-search__result--pending {
877
+ opacity: 0;
878
+ transform: translateX(-20px);
818
879
 
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;
880
+ &:dir(rtl) {
881
+ transform: translateX(20px);
882
+ }
883
+ }
884
+
885
+ .mg-search__result--visible {
886
+ opacity: 1;
887
+ transform: translateX(0);
888
+ transition:
889
+ opacity var(--mg-motion-duration-fast) var(--mg-motion-easing-standard),
890
+ transform var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
891
+ }
826
892
  }
827
893
 
828
894
  // Stale state - dim results while loading new data
@@ -831,29 +897,8 @@ $search-input-padding-x: mg-rem(44);
831
897
  pointer-events: none;
832
898
  }
833
899
 
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
- }
900
+ // Refresh feedback is retained/dimmed results, inline Loader and the shared
901
+ // progress strip; an additional pulsing overlay would compete with them.
857
902
 
858
903
  // ========================================
859
904
  // Collapsible Filter Sections
@@ -900,7 +945,11 @@ $search-input-padding-x: mg-rem(44);
900
945
  border-left: 5px solid transparent;
901
946
  border-right: 5px solid transparent;
902
947
  border-top: 6px solid currentColor;
903
- transition: transform 0.2s ease;
948
+
949
+ @media (prefers-reduced-motion: no-preference) {
950
+ transition: transform var(--mg-motion-duration-fast)
951
+ var(--mg-motion-easing-standard);
952
+ }
904
953
 
905
954
  .mg-search__facet-toggle[aria-expanded="false"] & {
906
955
  transform: rotate(-90deg);
@@ -922,13 +971,37 @@ $search-input-padding-x: mg-rem(44);
922
971
  padding: var(--mg-spacing-200);
923
972
  }
924
973
 
974
+ // Legacy public class: keep compatibility for hand-written Drupal markup.
975
+ // New widget code uses shared Loader. One cycle is 1s; this is a documented
976
+ // looping compatibility exception, not interaction timing. Unlike the shared
977
+ // Loader's designed static reduced-motion state, this legacy markup keeps slow
978
+ // rotation until its consumers can be audited and migrated.
979
+ @keyframes mg-search-spin {
980
+ to {
981
+ transform: rotate(360deg);
982
+ }
983
+ }
984
+
925
985
  .mg-search__spinner {
986
+ --mg-search-spinner-duration: 1s;
987
+
926
988
  border: 4px solid $search-border-light;
927
989
  border-top: 4px solid $search-primary;
928
990
  border-radius: 50%;
929
991
  width: 40px;
930
992
  height: 40px;
931
- animation: mg-search-spin 1s linear infinite;
993
+ animation: mg-search-spin var(--mg-search-spinner-duration) linear infinite;
994
+
995
+ @media (prefers-reduced-motion: reduce) {
996
+ animation-duration: calc(var(--mg-search-spinner-duration) * 3) !important;
997
+ animation-iteration-count: infinite !important;
998
+ }
999
+
1000
+ @media (forced-colors: active) {
1001
+ border-color: GrayText;
1002
+ border-top-color: CanvasText;
1003
+ forced-color-adjust: none;
1004
+ }
932
1005
  }
933
1006
 
934
1007
  .mg-search__loader-text {
@@ -963,9 +1036,13 @@ $search-input-padding-x: mg-rem(44);
963
1036
  border: 1px solid rgb(var(--mg-color-neutral-600) / 0.22);
964
1037
  border-radius: var(--mg-radius-form-input);
965
1038
  cursor: pointer;
966
- transition:
967
- border-color 0.15s,
968
- box-shadow 0.15s;
1039
+
1040
+ @media (prefers-reduced-motion: no-preference) {
1041
+ transition:
1042
+ border-color var(--mg-motion-duration-fast)
1043
+ var(--mg-motion-easing-standard),
1044
+ box-shadow var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
1045
+ }
969
1046
 
970
1047
  &:hover {
971
1048
  border-color: $search-primary;
@@ -997,7 +1074,11 @@ $search-input-padding-x: mg-rem(44);
997
1074
  flex-shrink: 0;
998
1075
  margin-inline-start: var(--mg-spacing-50);
999
1076
  color: $search-text-muted;
1000
- transition: transform 0.2s ease;
1077
+
1078
+ @media (prefers-reduced-motion: no-preference) {
1079
+ transition: transform var(--mg-motion-duration-fast)
1080
+ var(--mg-motion-easing-standard);
1081
+ }
1001
1082
 
1002
1083
  .mg-select--open & {
1003
1084
  transform: rotate(180deg);
@@ -1029,7 +1110,11 @@ $search-input-padding-x: mg-rem(44);
1029
1110
  border-top: none;
1030
1111
  border-radius: 0 0 $search-radius $search-radius;
1031
1112
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
1032
- animation: mg-select-fade-in 0.15s ease-out;
1113
+
1114
+ @media (prefers-reduced-motion: no-preference) {
1115
+ animation: mg-select-fade-in var(--mg-motion-duration-fast)
1116
+ var(--mg-motion-easing-standard);
1117
+ }
1033
1118
  }
1034
1119
 
1035
1120
  @keyframes mg-select-fade-in {
@@ -1085,7 +1170,11 @@ $search-input-padding-x: mg-rem(44);
1085
1170
  padding: var(--mg-spacing-50) var(--mg-spacing-75);
1086
1171
  min-height: 40px; // Touch-friendly
1087
1172
  cursor: pointer;
1088
- transition: background-color 0.1s ease;
1173
+
1174
+ @media (prefers-reduced-motion: no-preference) {
1175
+ transition: background-color var(--mg-motion-duration-fast)
1176
+ var(--mg-motion-easing-standard);
1177
+ }
1089
1178
 
1090
1179
  &:hover,
1091
1180
  &--highlighted {
@@ -1101,6 +1190,9 @@ $search-input-padding-x: mg-rem(44);
1101
1190
  }
1102
1191
  }
1103
1192
 
1193
+ // Hover is pure CSS; --highlighted is the keyboard position only, so
1194
+ // moving the pointer over a long list doesn't re-render it.
1195
+ &--selected:hover,
1104
1196
  &--selected#{&}--highlighted {
1105
1197
  background-color: rgb(var(--mg-color-blue-900) / 0.12);
1106
1198
  }
@@ -1189,7 +1281,11 @@ $search-input-padding-x: mg-rem(44);
1189
1281
  border-radius: $search-radius;
1190
1282
  align-items: baseline;
1191
1283
  gap: 0 var(--mg-spacing-25);
1192
- transition: background-color 0.15s ease;
1284
+
1285
+ @media (prefers-reduced-motion: no-preference) {
1286
+ transition: background-color var(--mg-motion-duration-fast)
1287
+ var(--mg-motion-easing-standard);
1288
+ }
1193
1289
 
1194
1290
  &:hover {
1195
1291
  background-color: rgb(var(--mg-color-blue-900) / 0.05);
@@ -1250,7 +1346,12 @@ $search-input-padding-x: mg-rem(44);
1250
1346
  inset: 0;
1251
1347
  background: rgb(0 0 0 / 0.5);
1252
1348
  z-index: calc(var(--mg-z-index-drawer) - 1); // backdrop behind drawer panel
1253
- animation: mg-search-fade-in 0.2s ease-out;
1349
+
1350
+ @media (prefers-reduced-motion: no-preference) {
1351
+ animation: mg-search-fade-in var(--mg-motion-duration-fast)
1352
+ var(--mg-motion-easing-standard);
1353
+ }
1354
+
1254
1355
  cursor: pointer; // Indicate clickable to close
1255
1356
  }
1256
1357
 
@@ -1281,19 +1382,19 @@ $search-input-padding-x: mg-rem(44);
1281
1382
  box-shadow: var(--mg-search-drawer-shadow-offset) 0 20px rgb(0 0 0 / 0.15);
1282
1383
  display: flex;
1283
1384
  flex-direction: column;
1284
- animation: mg-search-slide-in-start 0.3s ease-out;
1285
1385
  z-index: var(--mg-z-index-drawer);
1286
1386
  cursor: default; // Reset cursor inside drawer
1287
1387
 
1388
+ @media (prefers-reduced-motion: no-preference) {
1389
+ animation: mg-search-slide-in-start var(--mg-motion-duration-medium)
1390
+ var(--mg-motion-easing-standard);
1391
+ }
1392
+
1288
1393
  // The shadow points at the page content, so it mirrors with the panel.
1289
1394
  &:dir(rtl) {
1290
1395
  --mg-search-drawer-offset: 100%;
1291
1396
  --mg-search-drawer-shadow-offset: -4px;
1292
1397
  }
1293
-
1294
- @media (prefers-reduced-motion: reduce) {
1295
- animation: none;
1296
- }
1297
1398
  }
1298
1399
 
1299
1400
  @keyframes mg-search-slide-in-start {
@@ -1351,9 +1452,13 @@ $search-input-padding-x: mg-rem(44);
1351
1452
  border: none;
1352
1453
  border-radius: $search-radius;
1353
1454
  cursor: pointer;
1354
- transition:
1355
- background-color 0.15s,
1356
- color 0.15s;
1455
+
1456
+ @media (prefers-reduced-motion: no-preference) {
1457
+ transition:
1458
+ background-color var(--mg-motion-duration-fast)
1459
+ var(--mg-motion-easing-standard),
1460
+ color var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
1461
+ }
1357
1462
 
1358
1463
  &:hover {
1359
1464
  background: $search-chip-bg;
@@ -1395,7 +1500,11 @@ $search-input-padding-x: mg-rem(44);
1395
1500
  border: none;
1396
1501
  border-radius: $search-radius;
1397
1502
  cursor: pointer;
1398
- transition: background-color 0.15s;
1503
+
1504
+ @media (prefers-reduced-motion: no-preference) {
1505
+ transition: background-color var(--mg-motion-duration-fast)
1506
+ var(--mg-motion-easing-standard);
1507
+ }
1399
1508
 
1400
1509
  &:hover {
1401
1510
  background: $search-primary-dark;
@@ -1503,9 +1612,14 @@ $search-input-padding-x: mg-rem(44);
1503
1612
  align-items: center;
1504
1613
  padding: var(--mg-spacing-25) var(--mg-spacing-50);
1505
1614
  cursor: pointer;
1506
- transition:
1507
- color 0.15s,
1508
- border-color 0.15s;
1615
+
1616
+ @media (prefers-reduced-motion: no-preference) {
1617
+ transition:
1618
+ color var(--mg-motion-duration-fast) var(--mg-motion-easing-standard),
1619
+ border-color var(--mg-motion-duration-fast)
1620
+ var(--mg-motion-easing-standard);
1621
+ }
1622
+
1509
1623
  font-weight: 400;
1510
1624
  font-size: var(--mg-font-size-200);
1511
1625
  user-select: none;
@@ -1554,3 +1668,20 @@ $search-input-padding-x: mg-rem(44);
1554
1668
 
1555
1669
  // Pager styles now live in the standalone Pager component.
1556
1670
  // @see stories/Components/Pager/pager.scss
1671
+
1672
+ // Skeleton surfaces must stay identifiable when forced colours removes
1673
+ // author backgrounds. These shapes are decorative; the status owns meaning.
1674
+ @media (forced-colors: active) {
1675
+ .mg-search__skeleton-title,
1676
+ .mg-search__skeleton-meta,
1677
+ .mg-search__skeleton-text,
1678
+ .mg-search__skeleton-card-image,
1679
+ .mg-search__skeleton-card {
1680
+ outline: 1px solid GrayText;
1681
+ outline-offset: -1px;
1682
+ }
1683
+
1684
+ .mg-search__drawer {
1685
+ border-inline-end: 1px solid CanvasText;
1686
+ }
1687
+ }
@@ -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;