@undrr/undrr-mangrove 2.0.0-rc.2 → 2.0.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 (124) hide show
  1. package/README.md +11 -6
  2. package/components/BookCard.js +1 -1
  3. package/components/Breadcrumbs.js +2 -2
  4. package/components/Checkbox.js +1 -1
  5. package/components/Chips.js +1 -1
  6. package/components/CopyButton.js +2 -2
  7. package/components/CtaButton.js +1 -1
  8. package/components/Drawer.js +1 -1
  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 +1 -1
  23. package/components/MegaMenu.js +2 -2
  24. package/components/Notice.js +2 -2
  25. package/components/PageHeader.js +2 -2
  26. package/components/Pager.js +2 -2
  27. package/components/QuoteHighlight.js +2 -2
  28. package/components/QuoteHighlight.js.LICENSE.txt +2 -0
  29. package/components/Radio.js +1 -1
  30. package/components/Range.js +1 -1
  31. package/components/ScrollContainer.js +2 -2
  32. package/components/SegmentedControl.js +9 -0
  33. package/components/SegmentedControl.js.LICENSE.txt +9 -0
  34. package/components/Select.js +1 -1
  35. package/components/ServiceNotice.js +2 -2
  36. package/components/ShareButtons.js +2 -2
  37. package/components/ShowMore.js +2 -2
  38. package/components/SkipLink.js +1 -1
  39. package/components/StatsCard.js +1 -1
  40. package/components/SyndicationSearchWidget.js +2 -2
  41. package/components/Tab.js +2 -2
  42. package/components/TextCta.js +1 -1
  43. package/components/TextInput.js +1 -1
  44. package/components/Textarea.js +1 -1
  45. package/components/Tree.js +1 -1
  46. package/components/UserFeedback.js +1 -1
  47. package/components/VerticalCard.js +1 -1
  48. package/components/hydrate.js +2 -2
  49. package/css/style-all.css +60 -40
  50. package/css/style-all.css.map +1 -1
  51. package/css/style-delta.css +59 -39
  52. package/css/style-delta.css.map +1 -1
  53. package/css/style-gutenberg.css +16 -7
  54. package/css/style-gutenberg.css.map +1 -1
  55. package/css/style-irp.css +60 -40
  56. package/css/style-irp.css.map +1 -1
  57. package/css/style-mcr.css +59 -39
  58. package/css/style-mcr.css.map +1 -1
  59. package/css/style-preventionweb.css +60 -40
  60. package/css/style-preventionweb.css.map +1 -1
  61. package/css/style.css +59 -39
  62. package/css/style.css.map +1 -1
  63. package/error-pages/401.html +3 -3
  64. package/error-pages/403.html +3 -3
  65. package/error-pages/404.html +4 -4
  66. package/error-pages/429.html +3 -3
  67. package/error-pages/500.html +6 -6
  68. package/error-pages/502.html +3 -3
  69. package/error-pages/503.html +3 -3
  70. package/error-pages/504.html +3 -3
  71. package/error-pages/5xx.html +4 -4
  72. package/error-pages/challenge.html +3 -3
  73. package/error-pages/managed-challenge.html +3 -3
  74. package/js/README.md +31 -25
  75. package/js/copy-button.js +17 -0
  76. package/js/drawer.js +752 -0
  77. package/js/hub-header.js +217 -0
  78. package/js/show-more.js +144 -10
  79. package/js/switch-pending.js +195 -46
  80. package/package.json +8 -3
  81. package/scss/Atom/BaseTypography/Blockquote/blockquote.scss +2 -1
  82. package/scss/Atom/BaseTypography/Cite/cite.scss +2 -1
  83. package/scss/Atom/BaseTypography/Code/code.scss +8 -2
  84. package/scss/Atom/Icons/icons.scss +1 -1
  85. package/scss/Atom/Images/AuthorImage/author-image.scss +16 -1
  86. package/scss/Atom/StatusLabel/status-label.scss +331 -8
  87. package/scss/Atom/Table/table.scss +8 -3
  88. package/scss/Atom/Tag/tag.scss +20 -2
  89. package/scss/Components/Breadcrumbs/breadcrumbs.scss +8 -4
  90. package/scss/Components/Buttons/CtaButton/cta-button.scss +15 -0
  91. package/scss/Components/Cards/Card/card.scss +37 -10
  92. package/scss/Components/Cards/StatsCard/stats-card.scss +2 -10
  93. package/scss/Components/DataViz/Legend/legend.scss +2 -1
  94. package/scss/Components/ErrorPages/error-pages.scss +8 -0
  95. package/scss/Components/Forms/SegmentedControl/segmented-control.scss +412 -0
  96. package/scss/Components/Forms/_form-base.scss +109 -18
  97. package/scss/Components/Hero/hero.scss +296 -57
  98. package/scss/Components/MegaMenu/mega-menu.scss +26 -6
  99. package/scss/Components/OnThisPageNav/on-this-page-nav.scss +3 -1
  100. package/scss/Components/PageHeader/page-header.scss +13 -28
  101. package/scss/Components/Pager/pager.scss +3 -1
  102. package/scss/Components/QuoteHighlight/quote-highlight.scss +6 -1
  103. package/scss/Components/ScrollContainer/scroll-container.scss +11 -0
  104. package/scss/Components/SyndicationSearchWidget/syndication-search-widget.scss +51 -24
  105. package/scss/Components/Tab/tab.scss +1 -1
  106. package/scss/Components/TextCta/text-cta.scss +11 -3
  107. package/scss/Patterns/HubHeader/hub-header.scss +14 -3
  108. package/scss/Utilities/FullWidth/full-width.scss +14 -1
  109. package/scss/assets/scss/_components.scss +1 -0
  110. package/scss/assets/scss/_fonts.scss +11 -0
  111. package/scss/assets/scss/_foundational.scss +7 -0
  112. package/scss/assets/scss/_mixins.scss +32 -0
  113. package/scss/assets/scss/_tokens-data-viz.scss +9 -0
  114. package/scss/assets/scss/_variables.scss +16 -3
  115. package/scss/assets/scss/generated/_tokens-irp.scss +20 -4
  116. package/scss/assets/scss/generated/_tokens-mangrove.scss +6 -0
  117. package/scss/assets/scss/generated/_tokens-preventionweb.scss +20 -4
  118. package/js/__tests__/copy-button.test.js +0 -112
  119. package/js/__tests__/show-more.test.js +0 -80
  120. package/js/__tests__/table-of-contents.test.js +0 -196
  121. package/scss/Components/SyndicationSearchWidget/SyndicationSearchWidget.scss +0 -11
  122. package/scss/Components/TableOfContents/TableOfContents.scss +0 -11
  123. package/scss/Utilities/FullWidth/FullWidth.scss +0 -11
  124. package/scss/Utilities/ShowMore/ShowMore.scss +0 -11
@@ -1,5 +1,6 @@
1
1
  // Pending state for .mg-switch: announces progress, ignores presses while a
2
- // save runs, times out, and reverts on failure. See Checkbox.mdx.
2
+ // save runs, times out, and reverts on failure. The announcements are also
3
+ // available on their own, as mgSwitchAnnouncer(). See Checkbox.mdx.
3
4
 
4
5
  /** A request that has not settled by then is aborted and reverts. */
5
6
  export const PENDING_TIMEOUT_MS = 10000;
@@ -115,10 +116,47 @@ function resolveStatus(input, option) {
115
116
  return described || null;
116
117
  }
117
118
 
119
+ // A switch input, from the input itself or a wrapper such as its label. Takes
120
+ // anything, because mgSwitchAnnouncer() is public and a document or a stray
121
+ // value should get the no-op announcer rather than a TypeError.
118
122
  function toInput(element) {
119
- if (!element) return null;
120
- if (element.matches(INPUT_SELECTOR)) return element;
121
- return element.querySelector(INPUT_SELECTOR);
123
+ if (!element || typeof element !== 'object') return null;
124
+ if (
125
+ typeof element.matches === 'function' &&
126
+ element.matches(INPUT_SELECTOR)
127
+ ) {
128
+ return element;
129
+ }
130
+ return element.querySelector?.(INPUT_SELECTOR) ?? null;
131
+ }
132
+
133
+ // A setting written in HTML, on the input or its label. The input wins, as it
134
+ // does for data-mg-switch-labels; an option passed in JavaScript wins over both.
135
+ function settingAttribute(input, name) {
136
+ const own = input.getAttribute(name);
137
+ if (own !== null) return own;
138
+ return input.closest('label')?.getAttribute(name) ?? null;
139
+ }
140
+
141
+ /**
142
+ * Milliseconds before a save is abandoned, or Infinity for no deadline.
143
+ * `0` and `Infinity` both disable it; anything unusable falls back to the
144
+ * default, as it did before the option accepted those two.
145
+ */
146
+ function resolveTimeout(value, input) {
147
+ let setting = value;
148
+ if (setting === undefined || setting === null) {
149
+ const raw = settingAttribute(input, 'data-mg-switch-timeout');
150
+ if (raw !== null && raw.trim() !== '') setting = Number(raw);
151
+ }
152
+ if (setting === 0 || setting === Infinity) return Infinity;
153
+ return Number.isFinite(setting) && setting > 0 ? setting : PENDING_TIMEOUT_MS;
154
+ }
155
+
156
+ /** Whether a failed save moves the switch back. */
157
+ function resolveRevert(value, input) {
158
+ if (typeof value === 'boolean') return value;
159
+ return settingAttribute(input, 'data-mg-switch-revert') !== 'false';
122
160
  }
123
161
 
124
162
  function cancelWrite(status, owner) {
@@ -145,24 +183,11 @@ function noResponderError() {
145
183
  );
146
184
  }
147
185
 
148
- function enhance(input, options, auto) {
149
- const noop = { destroy() {} };
150
- if (!input || typeof input.addEventListener !== 'function') return noop;
151
- const existing = registry.instances.get(input);
152
- if (existing) {
153
- // An explicit call takes over a switch that page-load auto-init enhanced,
154
- // so its options are not silently ignored.
155
- if (!existing.auto || auto) return existing.handle;
156
- existing.handle.destroy();
157
- }
158
- const { signal } = options;
159
- if (signal?.aborted) return noop;
160
-
186
+ // The announcements, on their own: the live region, the label resolution, the
187
+ // re-announce trick and the rate limit, without the save state machine.
188
+ // mgSwitchPending() runs this one, so there is a single implementation.
189
+ function createAnnouncer(input, options) {
161
190
  const view = input.ownerDocument.defaultView || window;
162
- const timeout =
163
- Number.isFinite(options.timeout) && options.timeout > 0
164
- ? options.timeout
165
- : PENDING_TIMEOUT_MS;
166
191
  const labels = {
167
192
  ...SWITCH_PENDING_DEFAULT_LABELS,
168
193
  ...readLabels(input.closest('label')),
@@ -194,9 +219,7 @@ function enhance(input, options, auto) {
194
219
  );
195
220
  }
196
221
 
197
- let current = null;
198
222
  let lastNudge = -Infinity;
199
- let destroyed = false;
200
223
  const owner = {};
201
224
 
202
225
  const write = text => {
@@ -227,23 +250,123 @@ function enhance(input, options, auto) {
227
250
  status.textContent = text;
228
251
  };
229
252
 
253
+ const say = value => write(formatLabel(value, labelText(input), input));
254
+
255
+ return {
256
+ get status() {
257
+ return status;
258
+ },
259
+ get labels() {
260
+ return labels;
261
+ },
262
+ label: () => labelText(input),
263
+ announce: say,
264
+ pending: () => say(labels.saving),
265
+ stillSaving(force = false) {
266
+ const now = Date.now();
267
+ if (!force && now - lastNudge < STILL_SAVING_INTERVAL_MS) return false;
268
+ lastNudge = now;
269
+ say(labels.stillSaving);
270
+ return true;
271
+ },
272
+ settled: checked => say(checked ? labels.on : labels.off),
273
+ failed: () => say(labels.error),
274
+ destroy() {
275
+ cancelWrite(status, owner);
276
+ createdStatus?.remove();
277
+ },
278
+ };
279
+ }
280
+
281
+ const NOOP_ANNOUNCER = Object.freeze({
282
+ status: null,
283
+ labels: SWITCH_PENDING_DEFAULT_LABELS,
284
+ label: () => '',
285
+ announce: () => {},
286
+ pending: () => {},
287
+ stillSaving: () => false,
288
+ settled: () => {},
289
+ failed: () => {},
290
+ destroy: () => {},
291
+ });
292
+
293
+ /**
294
+ * The switch's announcements, without the save state machine: for apps that
295
+ * own the control's position and only want the accessible part.
296
+ *
297
+ * It resolves or creates the live region, resolves the switch's label the way
298
+ * the accessible name is resolved, formats `{label}` and function labels, and
299
+ * re-announces a repeated message. Announcements are skipped for a switch that
300
+ * has left the page, and a region it created is taken off the page with it.
301
+ *
302
+ * @param {HTMLInputElement|Element} element A `.mg-switch__input`, or a
303
+ * wrapper such as its label
304
+ * @param {Object} [options]
305
+ * @param {Element|string} [options.status] Live region, or a selector for one
306
+ * @param {Object} [options.labels] `{ saving, stillSaving, error, on, off }`,
307
+ * merged over `data-mg-switch-labels` and the defaults
308
+ * @returns {{
309
+ * status: Element|null,
310
+ * labels: Object,
311
+ * label: () => string,
312
+ * announce: (text: string|Function) => void,
313
+ * pending: () => void,
314
+ * stillSaving: (force?: boolean) => boolean,
315
+ * settled: (checked: boolean) => void,
316
+ * failed: () => void,
317
+ * destroy: () => void,
318
+ * }}
319
+ */
320
+ export function mgSwitchAnnouncer(element, options = {}) {
321
+ const input = toInput(element);
322
+ if (!input || typeof input.getAttribute !== 'function') return NOOP_ANNOUNCER;
323
+ return createAnnouncer(input, options || {});
324
+ }
325
+
326
+ function enhance(input, options, auto) {
327
+ const noop = { destroy() {} };
328
+ if (!input || typeof input.addEventListener !== 'function') return noop;
329
+ const existing = registry.instances.get(input);
330
+ if (existing) {
331
+ // An explicit call takes over a switch that page-load auto-init enhanced,
332
+ // so its options are not silently ignored.
333
+ if (!existing.auto || auto) return existing.handle;
334
+ existing.handle.destroy();
335
+ }
336
+ const { signal } = options;
337
+ if (signal?.aborted) return noop;
338
+
339
+ const view = input.ownerDocument.defaultView || window;
340
+ const timeout = resolveTimeout(options.timeout, input);
341
+ const revert = resolveRevert(options.revert, input);
342
+ const announcer = createAnnouncer(input, options);
343
+
344
+ let current = null;
345
+ let destroyed = false;
346
+ // Only an aria-invalid this helper set is cleared, and whatever the page had
347
+ // there is put back, so one the page authored for its own reasons survives a
348
+ // failed save rather than being overwritten and then removed.
349
+ let flaggedInvalid = false;
350
+ let invalidBefore = null;
351
+
230
352
  const emit = (type, detail) =>
231
353
  input.dispatchEvent(
232
354
  new CustomEvent(type, { bubbles: true, cancelable: false, detail })
233
355
  );
234
356
 
235
- const stillSaving = () => {
236
- const now = Date.now();
237
- if (now - lastNudge < STILL_SAVING_INTERVAL_MS) return;
238
- lastNudge = now;
239
- write(formatLabel(labels.stillSaving, labelText(input), input));
357
+ const clearInvalid = () => {
358
+ if (!flaggedInvalid) return;
359
+ flaggedInvalid = false;
360
+ if (invalidBefore === null) input.removeAttribute('aria-invalid');
361
+ else input.setAttribute('aria-invalid', invalidBefore);
362
+ invalidBefore = null;
240
363
  };
241
364
 
242
365
  const onClick = event => {
243
366
  if (!current) return;
244
367
  // Cancelling the click keeps checked where it is.
245
368
  event.preventDefault();
246
- stillSaving();
369
+ announcer.stillSaving();
247
370
  };
248
371
 
249
372
  const requestSave = (checked, requestSignal) => {
@@ -295,14 +418,16 @@ function enhance(input, options, auto) {
295
418
  if (current) {
296
419
  // A script changed the switch mid-request: undo it.
297
420
  input.checked = !input.checked;
298
- stillSaving();
421
+ announcer.stillSaving();
299
422
  return;
300
423
  }
301
424
 
302
425
  const requested = input.checked;
303
426
  const request = { controller: new AbortController() };
304
427
  current = request;
305
- write(formatLabel(labels.saving, labelText(input), input));
428
+ // A new attempt: the switch is saving again, not known to be unsaved.
429
+ clearInvalid();
430
+ announcer.pending();
306
431
  // Mark busy a frame later, so the new state is announced first.
307
432
  request.frame = view.requestAnimationFrame(() => {
308
433
  if (current === request) input.setAttribute('aria-busy', 'true');
@@ -315,32 +440,51 @@ function enhance(input, options, auto) {
315
440
  view.clearTimeout(request.timer);
316
441
  view.cancelAnimationFrame(request.frame);
317
442
  input.setAttribute('aria-busy', 'false');
318
- const label = labelText(input);
319
443
  if (ok) {
320
444
  // A form reset or a script may have moved it without a change event.
321
445
  input.checked = requested;
322
- write(formatLabel(requested ? labels.on : labels.off, label, input));
446
+ announcer.settled(requested);
323
447
  emit('mg-switch:settled', { checked: requested, ok: true });
324
448
  return;
325
449
  }
326
450
  request.controller.abort(error);
327
- input.checked = !requested;
328
- write(formatLabel(labels.error, label, input));
451
+ // Without revert the switch keeps what the user asked for, so its
452
+ // position no longer matches what is saved. aria-invalid says so to
453
+ // assistive technology and gives CSS a hook; the page still owns
454
+ // showing the failure and offering a way to try again.
455
+ input.checked = revert ? !requested : requested;
456
+ if (!revert) {
457
+ if (!flaggedInvalid) invalidBefore = input.getAttribute('aria-invalid');
458
+ input.setAttribute('aria-invalid', 'true');
459
+ flaggedInvalid = true;
460
+ }
461
+ announcer.failed();
329
462
  emit('mg-switch:failed', {
330
- checked: !requested,
463
+ checked: input.checked,
331
464
  requested,
465
+ reverted: revert,
332
466
  reason,
333
467
  error,
334
468
  });
335
469
  };
336
470
 
337
- request.timer = view.setTimeout(() => {
338
- const error = new DOMException(
339
- `Saving the switch took longer than ${timeout}ms.`,
340
- 'TimeoutError'
341
- );
342
- settle(false, 'timeout', error);
343
- }, timeout);
471
+ if (timeout === Infinity) {
472
+ // No deadline: the save owns settling, so aria-busy stays true until it
473
+ // does. Say once, where the deadline would have been, that the save is
474
+ // still running, so a screen reader user is not left on "Saving…" with
475
+ // a control marked busy and no further word.
476
+ request.timer = view.setTimeout(() => {
477
+ if (current === request) announcer.stillSaving(true);
478
+ }, PENDING_TIMEOUT_MS);
479
+ } else {
480
+ request.timer = view.setTimeout(() => {
481
+ const error = new DOMException(
482
+ `Saving the switch took longer than ${timeout}ms.`,
483
+ 'TimeoutError'
484
+ );
485
+ settle(false, 'timeout', error);
486
+ }, timeout);
487
+ }
344
488
 
345
489
  emit('mg-switch:pending', { checked: requested });
346
490
  // A pending listener may have destroyed the helper.
@@ -365,7 +509,7 @@ function enhance(input, options, auto) {
365
509
  input.removeEventListener('click', onClick);
366
510
  input.removeEventListener('change', onChange);
367
511
  signal?.removeEventListener('abort', onAbort);
368
- cancelWrite(status, owner);
512
+ clearInvalid();
369
513
  const request = current;
370
514
  current = null;
371
515
  if (request) {
@@ -374,7 +518,7 @@ function enhance(input, options, auto) {
374
518
  request.controller.abort();
375
519
  input.removeAttribute('aria-busy');
376
520
  }
377
- createdStatus?.remove();
521
+ announcer.destroy();
378
522
  if (registry.instances.get(input)?.handle === handle) {
379
523
  registry.instances.delete(input);
380
524
  input.removeAttribute(ENHANCED_ATTR);
@@ -400,7 +544,12 @@ function enhance(input, options, auto) {
400
544
  * Saves the new position; resolving means success. Without it, a cancelable
401
545
  * `mg-switch:save` event is dispatched for a listener to answer.
402
546
  * @param {Element|string} [options.status] Live region, or a selector for one
403
- * @param {number} [options.timeout=10000] Milliseconds before a save fails
547
+ * @param {number} [options.timeout=10000] Milliseconds before a save fails.
548
+ * `0` or `Infinity` means no deadline: the save must settle itself, and
549
+ * `aria-busy` stays true until it does.
550
+ * @param {boolean} [options.revert=true] Whether a failed save moves the
551
+ * switch back. With `false` it keeps the requested position and gets
552
+ * `aria-invalid="true"` instead, for apps that own the control's position.
404
553
  * @param {Object} [options.labels] `{ saving, stillSaving, error, on, off }`
405
554
  * @param {AbortSignal} [options.signal] Aborting it destroys the helper
406
555
  * @returns {{ destroy: () => void }}
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "@undrr/undrr-mangrove",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0",
4
4
  "description": "Mangrove design System for UNDRR",
5
- "main": "dist/index.js",
6
5
  "files": [
7
6
  "components/**/*",
8
7
  "css/**/*",
9
8
  "js/**/*",
10
9
  "scss/**/*.scss",
11
10
  "error-pages/**/*",
12
- "fonts/**/*"
11
+ "fonts/**/*",
12
+ "!**/__tests__/**",
13
+ "!**/__snapshots__/**",
14
+ "!**/__mocks__/**",
15
+ "!**/__fixtures__/**",
16
+ "!**/*.test.*",
17
+ "!**/*.spec.*"
13
18
  ],
14
19
  "repository": {
15
20
  "type": "git",
@@ -6,7 +6,8 @@
6
6
  * Full documentation:
7
7
  * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
- .mg-body blockquote {
9
+ .mg-body blockquote,
10
+ .mg-content blockquote {
10
11
  font-size: var(--mg-font-size-600);
11
12
  font-weight: 600;
12
13
  line-height: 1.16;
@@ -6,7 +6,8 @@
6
6
  * Full documentation:
7
7
  * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
- .mg-body cite {
9
+ .mg-body cite,
10
+ .mg-content cite {
10
11
  display: block;
11
12
  font-style: inherit;
12
13
  font-weight: 400;
@@ -144,12 +144,18 @@ figure.mg-code-block {
144
144
 
145
145
  // Line numbers — emitted by react-syntax-highlighter when showLineNumbers={true}.
146
146
  // Class name is react-syntax-highlighter's API, not a Mangrove BEM class.
147
+ // The gutter colour was neutral-400 (#7e8082), which measures 4.39:1 on the
148
+ // neutral-800 code surface and so failed axe's colour-contrast rule on every
149
+ // number. neutral-300 is the next step up and clears 4.5:1 comfortably; it
150
+ // matches the comment colour, which is the intended de-emphasised tone for
151
+ // this surface anyway. The neutral ramp is defined once in tokens/mangrove.yaml
152
+ // and no theme overrides it, so these ratios hold in every theme.
147
153
  .react-syntax-highlighter-line-number {
148
154
  display: inline-block;
149
155
  min-width: 2.5em;
150
156
  padding-right: var(--mg-spacing-100);
151
157
  text-align: right;
152
- color: rgb(var(--mg-color-neutral-400)); // #808080 — 4.59:1 on neutral-800
158
+ color: rgb(var(--mg-color-neutral-300)); // #96999c — 6.08:1 on neutral-800
153
159
  user-select: none;
154
160
  pointer-events: none;
155
161
  }
@@ -165,7 +171,7 @@ figure.mg-code-block {
165
171
  .token.prolog,
166
172
  .token.doctype,
167
173
  .token.cdata {
168
- color: rgb(var(--mg-color-neutral-300)); // #999 — 6.37:1 on neutral-800
174
+ color: rgb(var(--mg-color-neutral-300)); // #96999c — 6.08:1 on neutral-800
169
175
  }
170
176
 
171
177
  .token.keyword,
@@ -44,7 +44,7 @@
44
44
  }
45
45
 
46
46
  .fa-before:before {
47
- padding-right: 0.5em;
47
+ padding-inline-end: 0.5em;
48
48
  }
49
49
 
50
50
  .fa-2x {
@@ -147,11 +147,26 @@
147
147
  --mg-author-image-accent: rgb(var(--mg-color-interactive));
148
148
  }
149
149
 
150
+ // The accent drives three things: the avatar ring, the focus outline and the
151
+ // title. The Sendai orange is fine as the ring — a band of colour with no copy
152
+ // over it — but it is 2.65 as ink on white, under the 4.5 the title needs, and
153
+ // 2.95 as an outline against white, under the 3.0 a focus indicator needs. So
154
+ // this variant keeps the orange ring and hands those two back to the tokens
155
+ // that are sized for them.
156
+ // See docs/COLOUR-CONTRAST-METHODOLOGY.md#the-orange-accent-carries-no-text
150
157
  .mg-author-image--secondary {
151
158
  --mg-author-image-accent: rgb(var(--mg-color-secondary));
152
159
  --mg-author-image-hover-tint: rgb(var(--mg-color-secondary) / 0.5);
153
160
  }
154
161
 
162
+ .mg-author-image--secondary .mg-author-image__title {
163
+ color: rgb(var(--mg-color-text));
164
+ }
165
+
166
+ .mg-author-image--secondary .mg-author-image__link:focus-visible {
167
+ outline-color: rgb(var(--mg-color-focus-ring));
168
+ }
169
+
155
170
  .mg-author-image--tertiary {
156
171
  --mg-author-image-accent: rgb(var(--mg-color-tertiary));
157
172
  --mg-author-image-hover-tint: rgb(var(--mg-color-tertiary) / 0.5);
@@ -188,7 +203,7 @@
188
203
 
189
204
  .mg-author-image--horizontal .mg-author-image__content {
190
205
  align-content: center;
191
- text-align: left;
206
+ text-align: start;
192
207
  }
193
208
 
194
209
  .mg-author-image--vertical .mg-author-image__meta {