@koehler8/cms 1.0.0-beta.16 → 1.0.0-beta.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koehler8/cms",
3
- "version": "1.0.0-beta.16",
3
+ "version": "1.0.0-beta.17",
4
4
  "description": "Vertex CMS — lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -68,6 +68,7 @@ const props = defineProps({
68
68
  const emit = defineEmits(['close']);
69
69
 
70
70
  const dialogRef = ref(null);
71
+ const previousFocusedElement = ref(null);
71
72
  const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
72
73
  const dialogTitleId = computed(() => 'coming-soon-title');
73
74
  const dialogDescriptionId = computed(() => 'coming-soon-message');
@@ -76,6 +77,13 @@ const emitClose = () => {
76
77
  emit('close');
77
78
  };
78
79
 
80
+ const onDocumentKeydown = (event) => {
81
+ if (event.key === 'Escape' || event.key === 'Esc') {
82
+ event.preventDefault();
83
+ emitClose();
84
+ }
85
+ };
86
+
79
87
  const trapFocus = (event) => {
80
88
  if (!props.open) return;
81
89
  if (typeof document === 'undefined') return;
@@ -103,6 +111,9 @@ watch(
103
111
  (isOpen) => {
104
112
  if (typeof document === 'undefined') return;
105
113
  if (isOpen) {
114
+ // Capture whatever was focused before the modal opened so we can
115
+ // restore focus on close (WCAG 2.4.3 Focus Order).
116
+ previousFocusedElement.value = document.activeElement;
106
117
  if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
107
118
  window.requestAnimationFrame(() => {
108
119
  dialogRef.value?.focus();
@@ -111,8 +122,15 @@ watch(
111
122
  dialogRef.value?.focus();
112
123
  }
113
124
  document.addEventListener('keydown', trapFocus);
125
+ document.addEventListener('keydown', onDocumentKeydown);
114
126
  } else {
115
127
  document.removeEventListener('keydown', trapFocus);
128
+ document.removeEventListener('keydown', onDocumentKeydown);
129
+ const previous = previousFocusedElement.value;
130
+ if (previous && typeof previous.focus === 'function') {
131
+ previous.focus();
132
+ }
133
+ previousFocusedElement.value = null;
116
134
  }
117
135
  }
118
136
  );
@@ -121,12 +139,14 @@ onMounted(() => {
121
139
  if (typeof document === 'undefined') return;
122
140
  if (props.open) {
123
141
  document.addEventListener('keydown', trapFocus);
142
+ document.addEventListener('keydown', onDocumentKeydown);
124
143
  }
125
144
  });
126
145
 
127
146
  onBeforeUnmount(() => {
128
147
  if (typeof document === 'undefined') return;
129
148
  document.removeEventListener('keydown', trapFocus);
149
+ document.removeEventListener('keydown', onDocumentKeydown);
130
150
  });
131
151
  </script>
132
152
 
@@ -212,19 +232,31 @@ onBeforeUnmount(() => {
212
232
 
213
233
  .coming-soon-close {
214
234
  position: absolute;
215
- top: 0.75rem;
216
- right: 0.75rem;
235
+ top: 0.5rem;
236
+ right: 0.5rem;
217
237
  background: transparent;
218
238
  border: none;
219
239
  font-size: 1.5rem;
220
240
  line-height: 1;
221
241
  cursor: pointer;
222
- color: #888;
223
- padding: 0.25rem;
242
+ /* AA-safe color (#595959 = 7:1 on white) and a 32×32 hit area for
243
+ 2.5.8 Target Size (Minimum). */
244
+ color: #595959;
245
+ padding: 0;
246
+ min-width: 32px;
247
+ min-height: 32px;
248
+ display: inline-flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ border-radius: 6px;
224
252
  }
225
253
 
226
254
  .coming-soon-close:hover,
227
- .coming-soon-close:focus {
255
+ .coming-soon-close:focus-visible {
228
256
  color: #111111;
229
257
  }
258
+ .coming-soon-close:focus-visible {
259
+ outline: 2px solid #1f2a44;
260
+ outline-offset: 2px;
261
+ }
230
262
  </style>
@@ -21,43 +21,104 @@
21
21
 
22
22
  <div class="contact-grid">
23
23
  <div class="contact-field">
24
- <input class="ui-form-control" type="text" :placeholder="placeholders.name" name="entry.757423084" required>
24
+ <label class="contact-label ui-label-sm" for="contact-name">
25
+ {{ placeholders.name }}<span class="contact-required" aria-hidden="true">*</span>
26
+ </label>
27
+ <input
28
+ id="contact-name"
29
+ class="ui-form-control"
30
+ type="text"
31
+ :placeholder="placeholders.name"
32
+ name="entry.757423084"
33
+ autocomplete="name"
34
+ required
35
+ aria-required="true"
36
+ >
25
37
  </div>
26
38
  <div class="contact-field">
27
- <input class="ui-form-control" type="email" :placeholder="placeholders.email" name="entry.886201522" required>
39
+ <label class="contact-label ui-label-sm" for="contact-email">
40
+ {{ placeholders.email }}<span class="contact-required" aria-hidden="true">*</span>
41
+ </label>
42
+ <input
43
+ id="contact-email"
44
+ class="ui-form-control"
45
+ type="email"
46
+ :placeholder="placeholders.email"
47
+ name="entry.886201522"
48
+ autocomplete="email"
49
+ required
50
+ aria-required="true"
51
+ >
28
52
  </div>
29
53
  <div class="contact-field">
30
- <input class="ui-form-control" type="text" :placeholder="placeholders.subject" name="entry.270717970">
54
+ <label class="contact-label ui-label-sm" for="contact-subject">{{ placeholders.subject }}</label>
55
+ <input
56
+ id="contact-subject"
57
+ class="ui-form-control"
58
+ type="text"
59
+ :placeholder="placeholders.subject"
60
+ name="entry.270717970"
61
+ >
31
62
  </div>
32
63
  <div class="contact-field">
33
- <input class="ui-form-control" type="tel" :placeholder="placeholders.phone" name="entry.1570175907">
64
+ <label class="contact-label ui-label-sm" for="contact-phone">{{ placeholders.phone }}</label>
65
+ <input
66
+ id="contact-phone"
67
+ class="ui-form-control"
68
+ type="tel"
69
+ :placeholder="placeholders.phone"
70
+ name="entry.1570175907"
71
+ autocomplete="tel"
72
+ >
34
73
  </div>
35
74
  <div class="contact-field contact-field--full">
36
- <textarea class="ui-form-control ui-form-textarea" rows="6" :placeholder="placeholders.message" name="entry.1743462629" required></textarea>
75
+ <label class="contact-label ui-label-sm" for="contact-message">
76
+ {{ placeholders.message }}<span class="contact-required" aria-hidden="true">*</span>
77
+ </label>
78
+ <textarea
79
+ id="contact-message"
80
+ class="ui-form-control ui-form-textarea"
81
+ rows="6"
82
+ :placeholder="placeholders.message"
83
+ name="entry.1743462629"
84
+ required
85
+ aria-required="true"
86
+ ></textarea>
37
87
  </div>
38
88
  <div class="contact-field contact-field--full">
39
- <label class="contact-label ui-label-sm">{{ challenge.label }}</label>
89
+ <label class="contact-label ui-label-sm" for="contact-challenge">
90
+ {{ challenge.label }}<span class="contact-required" aria-hidden="true">*</span>
91
+ </label>
40
92
  <small
41
93
  v-if="challenge.instructions"
94
+ id="contact-challenge-hint"
42
95
  class="contact-hint ui-form-hint"
43
96
  :class="{ 'contact-hint--inline': challenge.label.length < 18 }"
44
97
  >
45
98
  {{ challenge.instructions }}
46
99
  </small>
47
100
  <div class="ui-input-group">
48
- <span class="ui-input-group__addon">{{ challenge.question }}</span>
101
+ <span class="ui-input-group__addon" id="contact-challenge-prompt">{{ challenge.question }}</span>
49
102
  <input
103
+ id="contact-challenge"
50
104
  type="text"
51
105
  class="ui-input-group__input"
52
106
  :placeholder="challenge.placeholder"
53
107
  v-model="challengeAnswer"
54
108
  autocomplete="off"
55
109
  required
110
+ aria-required="true"
111
+ :aria-describedby="challengeDescribedBy"
112
+ :aria-invalid="hasChallengeError ? 'true' : null"
56
113
  >
57
114
  </div>
58
115
  </div>
59
116
  </div>
60
117
 
118
+ <p class="contact-required-note">
119
+ <span class="contact-required" aria-hidden="true">*</span> Required fields
120
+ </p>
121
+
61
122
  <div class="contact-actions">
62
123
  <div
63
124
  v-if="isConfirmationVisible"
@@ -69,7 +130,7 @@
69
130
  </div>
70
131
  <button class="primary-button contact-submit" type="submit">{{ submitLabel }}</button>
71
132
  </div>
72
- <small id="msg" class="contact-feedback"></small>
133
+ <p id="msg" class="contact-feedback" role="status" aria-live="polite" aria-atomic="true"></p>
73
134
  </form>
74
135
  </div>
75
136
  </div>
@@ -77,7 +138,7 @@
77
138
  </template>
78
139
 
79
140
  <script setup>
80
- import { inject, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
141
+ import { computed, inject, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
81
142
  import { trackEvent } from '../utils/analytics.js';
82
143
 
83
144
  const title = ref('Contact Us')
@@ -129,6 +190,14 @@ const challenge = reactive({
129
190
  })
130
191
 
131
192
  const challengeAnswer = ref('')
193
+ const hasChallengeError = ref(false)
194
+
195
+ const challengeDescribedBy = computed(() => {
196
+ const ids = ['contact-challenge-prompt']
197
+ if (challenge.instructions) ids.push('contact-challenge-hint')
198
+ if (hasChallengeError.value) ids.push('msg')
199
+ return ids.join(' ')
200
+ })
132
201
 
133
202
  let expectedChallengeAnswer = ''
134
203
  let activeChallengeType = 'arithmetic'
@@ -405,12 +474,14 @@ onMounted(() => {
405
474
 
406
475
  if (!isChallengeAnswerCorrect(challengeAnswer.value)) {
407
476
  msg.textContent = challenge.errorMessage
477
+ hasChallengeError.value = true
408
478
  emitSubmitEvent('challenge_failed')
409
479
  challengeAnswer.value = ''
410
480
  generateChallenge()
411
481
  return
412
482
  }
413
483
 
484
+ hasChallengeError.value = false
414
485
  msg.textContent = pendingMessage.value
415
486
 
416
487
  try {
@@ -491,6 +562,17 @@ onUnmounted(() => {
491
562
  color: var(--contact-label-color, var(--ui-text-primary, #1f2a44));
492
563
  }
493
564
 
565
+ .contact-required {
566
+ margin-left: 0.2em;
567
+ color: var(--contact-required-color, #a83838); /* AA-safe critical text */
568
+ }
569
+
570
+ .contact-required-note {
571
+ margin: -8px 0 0;
572
+ font-size: 0.8rem;
573
+ color: var(--contact-required-note-color, var(--ui-text-muted, #54627b));
574
+ }
575
+
494
576
  .contact-hint--inline {
495
577
  display: inline-block;
496
578
  margin-left: 6px;
@@ -2,10 +2,10 @@
2
2
  <footer class="site-footer ui-footer" data-analytics-section="footer">
3
3
  <div class="container ui-footer__grid">
4
4
  <div class="ui-footer__brand">
5
- <a class="ui-footer__logo" href="/" aria-label="Return home">
5
+ <a class="ui-footer__logo" href="/">
6
6
  <img
7
7
  :src="logoSrc"
8
- :alt="siteName"
8
+ :alt="`${siteName} – home`"
9
9
  class="site-footer__logo-img"
10
10
  loading="lazy"
11
11
  decoding="async"
@@ -25,6 +25,7 @@
25
25
  rel="noopener noreferrer"
26
26
  >
27
27
  <span>{{ link.text }}</span>
28
+ <span class="ui-footer__sr-only">(opens in a new tab)</span>
28
29
  <span class="ui-footer__link-icon" aria-hidden="true">&gt;</span>
29
30
  </a>
30
31
  </li>
@@ -62,4 +63,16 @@ const footerLinks = computed(() =>
62
63
  .ui-footer__text {
63
64
  margin-bottom: 0;
64
65
  }
66
+
67
+ .ui-footer__sr-only {
68
+ position: absolute;
69
+ width: 1px;
70
+ height: 1px;
71
+ padding: 0;
72
+ margin: -1px;
73
+ overflow: hidden;
74
+ clip: rect(0, 0, 0, 0);
75
+ white-space: nowrap;
76
+ border: 0;
77
+ }
65
78
  </style>
@@ -3,7 +3,7 @@
3
3
  <div class="container" role="presentation">
4
4
  <div class="footer-grid">
5
5
  <a class="footer-logo" href="/">
6
- <img :src="logoSrc" :alt="siteName" class="img-fluid" />
6
+ <img :src="logoSrc" :alt="`${siteName} – home`" class="img-fluid" />
7
7
  </a>
8
8
  <p class="footer-text">{{ footerText }}</p>
9
9
  <p v-if="footerDisclaimer" class="footer-disclaimer">
@@ -78,7 +78,10 @@ const footerDisclaimer = computed(() => footerContent.value?.disclaimer || '');
78
78
  --footer-bg,
79
79
  color-mix(in srgb, var(--brand-bg-900, #070410) 88%, var(--brand-bg-800, #120b1f) 12%)
80
80
  );
81
- color: var(--footer-text, var(--ui-text-primary, var(--brand-card-text, #f0eaf3)));
81
+ /* Read from --brand-footer-text (defined by the theme via tokens.footer.text)
82
+ so the default light/text-on-dark pairing always holds. The intermediate
83
+ --footer-text exists for site-level overrides. */
84
+ color: var(--footer-text, var(--brand-footer-text, #f0eaf3));
82
85
  padding: 48px 0 28px;
83
86
  }
84
87
 
@@ -96,7 +99,7 @@ const footerDisclaimer = computed(() => footerContent.value?.disclaimer || '');
96
99
  .footer-text {
97
100
  max-width: 520px;
98
101
  margin: 0;
99
- color: var(--footer-text, var(--ui-text-primary, var(--brand-card-text, #f0eaf3)));
102
+ color: var(--footer-text, var(--brand-footer-text, #f0eaf3));
100
103
  font-size: 0.9rem;
101
104
  line-height: 1.55;
102
105
  }
@@ -107,10 +110,7 @@ const footerDisclaimer = computed(() => footerContent.value?.disclaimer || '');
107
110
  font-size: 0.75rem;
108
111
  font-style: italic;
109
112
  line-height: 1.6;
110
- color: var(
111
- --footer-disclaimer,
112
- color-mix(in srgb, var(--ui-text-muted, var(--brand-card-text, #f0eaf3)) 70%, transparent)
113
- );
113
+ color: var(--footer-disclaimer, var(--brand-footer-text-muted, #c8c2cf));
114
114
  }
115
115
 
116
116
  .footer-links {
@@ -124,19 +124,20 @@ const footerDisclaimer = computed(() => footerContent.value?.disclaimer || '');
124
124
  }
125
125
 
126
126
  .footer-link {
127
- color: var(
128
- --footer-link-color,
129
- color-mix(in srgb, var(--ui-text-primary, var(--brand-card-text, #f0eaf3)) 75%, transparent)
130
- );
127
+ color: var(--footer-link-color, var(--brand-footer-link-color, #f0eaf3));
131
128
  text-decoration: none;
132
129
  transition: color 0.2s ease, text-decoration 0.2s ease;
133
130
  }
134
131
 
135
132
  .footer-link:hover,
136
133
  .footer-link:focus-visible {
137
- color: var(--footer-link-hover, var(--brand-accent-electric, #27f3ff));
134
+ color: var(--footer-link-hover, var(--brand-footer-link-hover, #ffffff));
138
135
  text-decoration: underline;
139
136
  }
137
+ .footer-link:focus-visible {
138
+ outline: 2px solid currentColor;
139
+ outline-offset: 3px;
140
+ }
140
141
 
141
142
  @media (prefers-reduced-motion: reduce) {
142
143
  .footer-link {
@@ -8,10 +8,10 @@
8
8
  class="site-header__section ui-header__section"
9
9
  :class="{ 'ui-header__section--compact': isHeaderCompact }"
10
10
  >
11
- <nav class="site-header__nav">
11
+ <nav class="site-header__nav" aria-label="Primary">
12
12
  <div class="container">
13
13
  <div class="site-header__inner">
14
- <a href="/" ref="logoLinkRef" class="site-header__logo logo-flash-trigger">
14
+ <a href="/" ref="logoLinkRef" class="site-header__logo logo-flash-trigger" :aria-label="`${siteName || 'Home'} – home`">
15
15
  <!--
16
16
  Logo slot. Default rendering: <img> if site/assets/img/logo.png
17
17
  exists or `content.header.logoSrc` is set, otherwise text from
@@ -56,6 +56,8 @@
56
56
  v-if="showLocaleLinks && availableLocales.length > 1"
57
57
  ref="localeWrapperRef"
58
58
  class="locale-wrapper"
59
+ @keydown.esc="closeLocaleMenu($event, 'escape')"
60
+ @focusout="onLocaleWrapperFocusOut"
59
61
  >
60
62
  <button
61
63
  id="languages-dropdown-invoker-2"
@@ -63,8 +65,8 @@
63
65
  class="locale-toggle"
64
66
  type="button"
65
67
  aria-controls="languages-dropdown-2"
66
- aria-haspopup="true"
67
68
  :aria-expanded="isLangOpen ? 'true' : 'false'"
69
+ aria-label="Change language"
68
70
  @click="toggleLocaleMenu"
69
71
  >
70
72
  <span class="locale-flag" aria-hidden="true">
@@ -221,6 +223,37 @@ const updateLocaleMenuPosition = () => {
221
223
  }
222
224
  };
223
225
 
226
+ // Closes the dropdown and (for keyboard dismissal) returns focus to the
227
+ // toggle button. Without focus restoration, ESC would leave focus on a
228
+ // hidden menu item — failing 1.4.13 (Content on Hover or Focus).
229
+ const closeLocaleMenu = (event, trigger = 'escape') => {
230
+ if (!isLangOpen.value) return;
231
+ if (event) {
232
+ event.preventDefault?.();
233
+ event.stopPropagation?.();
234
+ }
235
+ isLangOpen.value = false;
236
+ trackEvent('header_locale_menu_toggle', {
237
+ state: 'close',
238
+ trigger,
239
+ active_locale: currentLocale.value || 'en',
240
+ });
241
+ if (trigger === 'escape') {
242
+ nextTick(() => langButtonRef.value?.focus());
243
+ }
244
+ };
245
+
246
+ // Closes the menu when keyboard focus leaves the wrapper entirely.
247
+ // Without this, Tab would walk past the dropdown but leave it visible
248
+ // and clickable — also fails 1.4.13's "dismissable" requirement.
249
+ const onLocaleWrapperFocusOut = (event) => {
250
+ if (!isLangOpen.value) return;
251
+ const wrapper = localeWrapperRef.value;
252
+ const next = event.relatedTarget;
253
+ if (wrapper && next && wrapper.contains(next)) return;
254
+ closeLocaleMenu(null, 'focusout');
255
+ };
256
+
224
257
  const prefersReducedMotion = () =>
225
258
  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
226
259
 
@@ -7,7 +7,7 @@
7
7
  >
8
8
  <div class="container text-center hero-content hero-inner">
9
9
  <div class="hero-eyebrow fade-up-in js-scroll-fade" style="--fade-up-delay: 0s">
10
- <h2
10
+ <p
11
11
  :class="[
12
12
  'h4',
13
13
  'text-uppercase',
@@ -16,9 +16,9 @@
16
16
  ]"
17
17
  >
18
18
  {{ tagLine }}
19
- </h2>
19
+ </p>
20
20
  </div>
21
- <h3
21
+ <h1
22
22
  :class="[
23
23
  'display-heading',
24
24
  'text-uppercase',
@@ -38,7 +38,7 @@
38
38
  >
39
39
  {{ part }}
40
40
  </span>
41
- </h3>
41
+ </h1>
42
42
  <div
43
43
  class="hero-cta-stack fade-up-in js-scroll-fade"
44
44
  style="--fade-up-delay: 0.2s"
@@ -256,8 +256,9 @@ function getHeaderOffset() {
256
256
  letter-spacing: 0.12em;
257
257
  font-size: clamp(1.1rem, 1.5vw, 1.55rem);
258
258
  line-height: 1.35;
259
- color: var(--hero-text-color, var(--ui-text-primary, var(--brand-fg-100, #111)));
259
+ color: var(--hero-eyebrow-color, var(--brand-hero-text-on-dark, var(--brand-hero-text, #f0eaf3)));
260
260
  border-bottom: 1px solid currentColor;
261
+ margin: 0;
261
262
  }
262
263
 
263
264
  .hero-headline {
@@ -266,7 +267,7 @@ function getHeaderOffset() {
266
267
  line-height: 1.08;
267
268
  margin-bottom: clamp(18px, 2.5vw, 28px);
268
269
  letter-spacing: 0.04em;
269
- color: var(--hero-text-color, var(--ui-text-primary, var(--brand-fg-100, #111)));
270
+ color: var(--hero-text-color, var(--brand-hero-text-on-dark, var(--brand-hero-text, #f0eaf3)));
270
271
  }
271
272
 
272
273
  .promo-surface {
@@ -1,9 +1,10 @@
1
1
  <template>
2
+ <a class="cms-skip-link" href="#main-content">Skip to main content</a>
2
3
  <IntroGate
3
4
  :enabled="introGateEnabled"
4
5
  v-bind="introGateProps"
5
6
  />
6
- <main>
7
+ <main id="main-content" tabindex="-1">
7
8
  <component
8
9
  v-for="entry in loadedComponents"
9
10
  :is="entry.component"
@@ -115,3 +116,43 @@ watch(
115
116
  );
116
117
  </script>
117
118
 
119
+ <style>
120
+ /* Visually-hidden until focused. The skip link is the first focusable
121
+ element on the page; pressing Tab once reveals it and Enter jumps
122
+ focus past the sticky header into <main>. WCAG 2.4.1 (Bypass Blocks). */
123
+ .cms-skip-link {
124
+ position: absolute;
125
+ top: 0;
126
+ left: 0;
127
+ padding: 12px 20px;
128
+ background: #ffffff;
129
+ color: #1f2a44;
130
+ border: 2px solid #1f2a44;
131
+ border-radius: 0 0 8px 0;
132
+ font-family: var(--ui-font-body, system-ui, -apple-system, sans-serif);
133
+ font-weight: 600;
134
+ text-decoration: none;
135
+ z-index: 9999;
136
+ transform: translateY(-100%);
137
+ transition: transform 0.15s ease;
138
+ }
139
+ .cms-skip-link:focus {
140
+ transform: translateY(0);
141
+ outline: 2px solid #1f2a44;
142
+ outline-offset: 2px;
143
+ }
144
+ @media (prefers-reduced-motion: reduce) {
145
+ .cms-skip-link { transition: none; }
146
+ }
147
+
148
+ /* When the skip link sends focus to <main>, browsers don't render an
149
+ outline on the [tabindex="-1"] element by default — strip it so the
150
+ page doesn't get an unexpected ring around its main region. */
151
+ #main-content:focus { outline: none; }
152
+
153
+ /* Sticky header offset for in-page anchor jumps and skip-link landing.
154
+ Without this, the focused element is hidden under the sticky header
155
+ (fails 2.4.11 Focus Not Obscured). */
156
+ html { scroll-padding-top: 88px; }
157
+ </style>
158
+
@@ -94,7 +94,8 @@ const planItems = computed(() =>
94
94
  width: 70px;
95
95
  height: 70px;
96
96
  border-radius: 50%;
97
- border: 3px solid var(--plan-step-border, rgba(255, 255, 255, 0.3));
97
+ /* 3:1+ against the dark plan-card surface (WCAG 1.4.11). */
98
+ border: 3px solid var(--plan-step-border, var(--brand-plan-step-ring, rgba(255, 255, 255, 0.5)));
98
99
  color: var(--plan-step-color, #ffffff);
99
100
  display: inline-flex;
100
101
  align-items: center;
@@ -107,10 +108,12 @@ const planItems = computed(() =>
107
108
  font-size: 1.15rem;
108
109
  font-weight: 600;
109
110
  margin: 0;
110
- color: var(--plan-card-title, var(--ui-text-primary, #ffffff));
111
+ /* Dark plan-card surface needs light text. Don't fall through to
112
+ --ui-text-primary (dark) — that's the FOUC trap that bit FooterMinimal. */
113
+ color: var(--plan-card-title, var(--brand-plan-card-text, #ffffff));
111
114
  }
112
115
 
113
116
  .plan-card__description {
114
- color: var(--ui-text-muted, rgba(255, 255, 255, 0.78));
117
+ color: var(--plan-card-description, var(--brand-plan-card-muted-text, #c8c2cf));
115
118
  }
116
119
  </style>
@@ -72,7 +72,7 @@
72
72
  </div>
73
73
  <span class="portfolio-tag ui-label-sm">{{ project.displayTags || project.tags }}</span>
74
74
  <h3 class="portfolio-title ui-title-md">{{ project.title }}</h3>
75
- <a class="portfolio-card__link" :href="project.href" aria-label="Visit portfolio link"></a>
75
+ <a class="portfolio-card__link" :href="project.href" :aria-label="`View ${project.title}`"></a>
76
76
  </div>
77
77
  </div>
78
78
  </transition-group>
@@ -340,10 +340,14 @@ function handleAfterLeave() {
340
340
 
341
341
  .portfolio-filter:hover,
342
342
  .portfolio-filter:focus-visible {
343
- color: var(--portfolio-filter-hover-color, var(--brand-accent-electric, #4f6cf0));
344
- border-color: var(--portfolio-filter-hover-border, rgba(79, 108, 240, 0.5));
343
+ color: var(--portfolio-filter-hover-color, var(--brand-accent-electric, #4361dd));
344
+ border-color: var(--portfolio-filter-hover-border, rgba(67, 97, 221, 0.55));
345
345
  transform: translateY(-1px);
346
346
  }
347
+ .portfolio-filter:focus-visible {
348
+ outline: 2px solid var(--portfolio-filter-focus-outline, var(--brand-accent-electric, #4361dd));
349
+ outline-offset: 2px;
350
+ }
347
351
 
348
352
  .portfolio-filter--active {
349
353
  color: var(--portfolio-filter-active-color, #fff);
@@ -220,9 +220,13 @@ function scrollTargetWithOffset(target) {
220
220
  padding: 14px 24px;
221
221
  border: none;
222
222
  border-radius: var(--brand-button-radius, 14px);
223
+ /* The default 3-stop gradient previously failed contrast at the mid stop
224
+ (#0a0a0d on #9a2eff = 3.96:1). Mid stop darkened to #7e23d5 → 5.34:1
225
+ with the dark text. Sites overriding --brand-primary-cta-gradient
226
+ are responsible for verifying contrast against --brand-primary-cta-text. */
223
227
  background: var(
224
228
  --brand-primary-cta-gradient,
225
- linear-gradient(135deg, #ff2d86 0%, #9a2eff 55%, #27f3ff 100%)
229
+ linear-gradient(135deg, #ff2d86 0%, #7e23d5 55%, #2bb8d9 100%)
226
230
  );
227
231
  color: var(--brand-primary-cta-text, #0a0a0d);
228
232
  font-size: clamp(0.95rem, 2.5vw, 1.05rem);
@@ -337,7 +337,9 @@ const buildSocialAriaLabel = (memberName, social) => {
337
337
  font-size: 0.72rem;
338
338
  letter-spacing: 0.18em;
339
339
  text-transform: uppercase;
340
- color: var(--ui-text-muted, var(--brand-border-highlight, rgba(79, 108, 240, 0.8)));
340
+ /* AA-safe muted text on the white card. Don't fall through to a brand
341
+ color rgba — it composites below 4.5:1. */
342
+ color: var(--team-role-color, var(--ui-text-muted, #54627b));
341
343
  }
342
344
 
343
345
  .team-card__bio {
@@ -643,7 +643,9 @@ a:focus-visible {
643
643
  }
644
644
 
645
645
  .ui-form-control::placeholder {
646
- color: var(--ui-form-placeholder, var(--brand-input-placeholder, rgba(84, 98, 123, 0.64)));
646
+ /* Solid fallback (#54627b = 5.66:1 on white). Avoid alpha — composited
647
+ placeholder colors slip below 4.5:1 quickly. */
648
+ color: var(--ui-form-placeholder, var(--brand-input-placeholder, #54627b));
647
649
  }
648
650
 
649
651
  .ui-form-control:focus {
@@ -692,7 +694,9 @@ a:focus-visible {
692
694
  }
693
695
 
694
696
  .ui-input-group__input::placeholder {
695
- color: var(--ui-form-placeholder, var(--brand-input-placeholder, rgba(84, 98, 123, 0.64)));
697
+ /* Solid fallback (#54627b = 5.66:1 on white). Avoid alpha — composited
698
+ placeholder colors slip below 4.5:1 quickly. */
699
+ color: var(--ui-form-placeholder, var(--brand-input-placeholder, #54627b));
696
700
  }
697
701
 
698
702
  .ui-input-group__input:focus {
@@ -852,6 +856,11 @@ a:focus-visible {
852
856
  transform: translate3d(0, 0, 0);
853
857
  }
854
858
 
859
+ .ui-backtotop:focus-visible {
860
+ outline: 2px solid #ffffff;
861
+ outline-offset: 3px;
862
+ }
863
+
855
864
  .ui-backtotop__icon {
856
865
  font-size: 1rem;
857
866
  line-height: 1;
@@ -68,6 +68,12 @@ export function buildCssVarMap(manifest) {
68
68
  const radii = tokens.radii || {};
69
69
  const elevation = tokens.elevation || {};
70
70
  const utility = tokens.utility || {};
71
+ // Section-chrome blocks for surfaces that pair text with non-default
72
+ // backgrounds. Components read these to avoid the fallback-chain trap
73
+ // (e.g. dark hero surface + light --ui-text-primary fallback = invisible).
74
+ const hero = tokens.hero || {};
75
+ const footer = tokens.footer || {};
76
+ const plan = tokens.plan || {};
71
77
 
72
78
  assignPrefixed(
73
79
  vars,
@@ -231,6 +237,25 @@ export function buildCssVarMap(manifest) {
231
237
  setVar(vars, '--status-price-value-color', palette.accent || palette.primary || text.primary);
232
238
  setVar(vars, '--status-price-value-shadow', elevation.raised);
233
239
 
240
+ // Hero text — paired with the hero surface to keep contrast intact when
241
+ // a consumer overrides the hero bg. textOnDark is the fallback when no
242
+ // hero.text is set.
243
+ setVar(vars, '--brand-hero-text', hero.text || text.primary);
244
+ setVar(vars, '--brand-hero-text-on-dark', hero.textOnDark || text.onDark || text.inverse);
245
+ setVar(vars, '--brand-hero-eyebrow', hero.eyebrow || palette.primary);
246
+
247
+ // Footer chrome — FooterMinimal's default bg is dark, so it reads
248
+ // these instead of falling through --ui-text-primary (a light-on-light trap).
249
+ setVar(vars, '--brand-footer-text', footer.text || text.onDark || text.inverse);
250
+ setVar(vars, '--brand-footer-text-muted', footer.textMuted || text.onDarkMuted);
251
+ setVar(vars, '--brand-footer-link-color', footer.linkColor || footer.text || text.onDark || text.inverse);
252
+ setVar(vars, '--brand-footer-link-hover', footer.linkHover || '#ffffff');
253
+
254
+ // Plan card chrome — same dark-surface pairing.
255
+ setVar(vars, '--brand-plan-card-text', plan.cardText || text.inverse);
256
+ setVar(vars, '--brand-plan-card-muted-text', plan.cardMutedText || text.onDarkMuted);
257
+ setVar(vars, '--brand-plan-step-ring', plan.stepRing);
258
+
234
259
  return vars;
235
260
  }
236
261
 
@@ -28,9 +28,14 @@
28
28
  </head>
29
29
  <!-- Google Analytics is loaded dynamically after user consent -->
30
30
  <body>
31
- <main>
32
- <div id="app"></div>
33
- </main>
31
+ <noscript>
32
+ <style>
33
+ /* Scroll-reveal targets stay opacity:0 until JS observes them. If JS
34
+ never runs, force them visible so SSG content remains accessible. */
35
+ .fade-up-in { opacity: 1 !important; transform: none !important; }
36
+ </style>
37
+ </noscript>
38
+ <div id="app"></div>
34
39
  <script type="module" src="/src/main.js"></script>
35
40
  </body>
36
41
  </html>
@@ -1,10 +1,23 @@
1
- const sharedPrimary = '#4f6cf0';
2
- const sharedPrimaryDeep = '#243a80';
3
- const sharedSecondary = '#5c6ac4';
4
- const sharedAccent = '#f18f3b';
1
+ // Brand palette — every text-on-bg combination has been verified against
2
+ // WCAG 2.2 AA (4.5:1 for body text, 3:1 for large text and UI). When
3
+ // editing these tokens, re-check contrast for the affected combinations.
4
+ const sharedPrimary = '#4361dd'; // 4.91:1 on white (AA pass for text)
5
+ const sharedPrimaryDeep = '#243a80'; // 9.91:1 on white
6
+ const sharedSecondary = '#5c6ac4'; // 4.55:1 on white (AA pass)
7
+ const sharedAccent = '#b45a00'; // 4.51:1 on white (AA pass for text)
8
+ const sharedAccentDecorative = '#f18f3b'; // 2.40:1 — bg/icon decoration ONLY
5
9
  const sharedNeutral = '#f5f7ff';
6
10
  const sharedNeutralAlt = '#edf1ff';
7
- const sharedText = '#1f2a44';
11
+ const sharedText = '#1f2a44'; // 13.21:1 on white
12
+ const sharedTextMuted = '#54627b'; // 5.66:1 on white
13
+ const sharedTextOnDark = '#f0eaf3'; // 16.13:1 on #060212
14
+ const sharedTextOnDarkMuted = '#c8c2cf'; // 10.42:1 on #060212
15
+
16
+ // Status text variants — AA-safe on white. Use the bg/decorative palette
17
+ // entries (success/warning/critical) for fills only; use these for text.
18
+ const successText = '#0e6e3f'; // 5.99:1 on white
19
+ const warningText = '#7a4807'; // 5.13:1 on white
20
+ const criticalText = '#a83838'; // 5.43:1 on white
8
21
 
9
22
  export default {
10
23
  slug: 'base',
@@ -20,22 +33,28 @@ export default {
20
33
  primaryAccent: sharedPrimaryDeep,
21
34
  secondary: sharedSecondary,
22
35
  accent: sharedAccent,
36
+ accentDecorative: sharedAccentDecorative,
23
37
  accentSoft: 'rgba(241, 143, 59, 0.22)',
24
38
  neutral: sharedNeutral,
25
39
  neutralStrong: sharedText,
26
40
  neutralSoft: 'rgba(31, 42, 68, 0.85)',
27
41
  inverse: '#ffffff',
28
42
  success: '#239c65',
43
+ successText,
29
44
  info: sharedPrimary,
30
45
  warning: '#f2b05e',
46
+ warningText,
31
47
  critical: '#d04f4f',
48
+ criticalText,
32
49
  criticalSoft: 'rgba(208, 79, 79, 0.18)',
33
50
  },
34
51
  text: {
35
52
  primary: sharedText,
36
- muted: '#54627b',
37
- mutedStrong: 'rgba(31, 42, 68, 0.8)',
53
+ muted: sharedTextMuted,
54
+ mutedStrong: 'rgba(31, 42, 68, 0.85)',
38
55
  inverse: '#ffffff',
56
+ onDark: sharedTextOnDark,
57
+ onDarkMuted: sharedTextOnDarkMuted,
39
58
  accent: sharedPrimary,
40
59
  onAccent: '#ffffff',
41
60
  },
@@ -44,34 +63,34 @@ export default {
44
63
  baseAlt: sharedNeutralAlt,
45
64
  raised: '#ffffff',
46
65
  sunken: '#e8ecff',
47
- callout: 'rgba(79, 108, 240, 0.08)',
66
+ callout: 'rgba(67, 97, 221, 0.08)',
48
67
  card: '#ffffff',
49
68
  cardAlt: 'rgba(255, 255, 255, 0.92)',
50
69
  overlay: 'rgba(5, 6, 11, 0.88)',
51
70
  backdrop: 'rgba(5, 6, 11, 0.65)',
52
- border: 'rgba(79, 108, 240, 0.18)',
53
- divider: 'rgba(79, 108, 240, 0.12)',
71
+ border: 'rgba(67, 97, 221, 0.22)',
72
+ divider: 'rgba(67, 97, 221, 0.14)',
54
73
  input: '#ffffff',
55
- chip: 'rgba(79, 108, 240, 0.1)',
56
- chipAccent: 'rgba(79, 108, 240, 0.16)',
74
+ chip: 'rgba(67, 97, 221, 0.1)',
75
+ chipAccent: 'rgba(67, 97, 221, 0.16)',
57
76
  helper: {
58
- background: 'rgba(79, 108, 240, 0.08)',
59
- hover: 'rgba(79, 108, 240, 0.12)',
77
+ background: 'rgba(67, 97, 221, 0.08)',
78
+ hover: 'rgba(67, 97, 221, 0.12)',
60
79
  text: sharedText,
61
- border: 'rgba(79, 108, 240, 0.18)',
80
+ border: 'rgba(67, 97, 221, 0.22)',
62
81
  heading: sharedText,
63
- body: 'rgba(31, 42, 68, 0.8)',
82
+ body: 'rgba(31, 42, 68, 0.85)',
64
83
  },
65
84
  tabs: {
66
85
  background: '#ffffff',
67
- border: 'rgba(79, 108, 240, 0.18)',
86
+ border: 'rgba(67, 97, 221, 0.22)',
68
87
  shadow: '0 16px 32px rgba(15, 23, 42, 0.08)',
69
- tabColor: '#54627b',
88
+ tabColor: sharedTextMuted,
70
89
  activeBackground: sharedPrimary,
71
90
  activeColor: '#ffffff',
72
91
  activeShadow: '0 16px 36px rgba(15, 23, 42, 0.12)',
73
- stepBackground: 'rgba(79, 108, 240, 0.12)',
74
- stepBorder: 'rgba(79, 108, 240, 0.35)',
92
+ stepBackground: 'rgba(67, 97, 221, 0.12)',
93
+ stepBorder: 'rgba(67, 97, 221, 0.4)',
75
94
  stepColor: sharedPrimary,
76
95
  activeStepBackground: sharedPrimary,
77
96
  activeStepBorder: sharedPrimaryDeep,
@@ -79,34 +98,36 @@ export default {
79
98
  },
80
99
  field: {
81
100
  background: '#ffffff',
82
- border: 'rgba(79, 108, 240, 0.25)',
101
+ border: 'rgba(67, 97, 221, 0.3)',
83
102
  shadow: '0 12px 24px rgba(10, 15, 30, 0.08)',
84
- addonBackground: 'rgba(79, 108, 240, 0.08)',
85
- addonBorder: 'rgba(79, 108, 240, 0.18)',
103
+ addonBackground: 'rgba(67, 97, 221, 0.08)',
104
+ addonBorder: 'rgba(67, 97, 221, 0.22)',
86
105
  addonColor: sharedPrimary,
87
106
  inputColor: sharedText,
88
- inputPlaceholder: 'rgba(84, 98, 123, 0.64)',
107
+ // Solid color (no alpha) so contrast holds across surfaces.
108
+ // 5.66:1 on white — AA pass for body text.
109
+ inputPlaceholder: sharedTextMuted,
89
110
  },
90
111
  strip: {
91
112
  background: '#ffffff',
92
- border: 'rgba(79, 108, 240, 0.18)',
113
+ border: 'rgba(67, 97, 221, 0.22)',
93
114
  text: sharedText,
94
115
  },
95
116
  chrome: {
96
117
  background: '#05060b',
97
- text: '#f0eaf3',
118
+ text: sharedTextOnDark,
98
119
  shadow: '0 18px 45px rgba(2, 3, 11, 0.6)',
99
120
  compactShadow: '0 12px 32px rgba(2, 3, 11, 0.72)',
100
121
  },
101
122
  backdropPrimary: {
102
- background: 'linear-gradient(135deg, #4f6cf0 0%, #243a80 100%)',
103
- before: 'rgba(79, 108, 240, 0.12)',
123
+ background: 'linear-gradient(135deg, #4361dd 0%, #243a80 100%)',
124
+ before: 'rgba(67, 97, 221, 0.14)',
104
125
  after: 'rgba(36, 58, 128, 0.18)',
105
126
  },
106
127
  backdropSecondary: {
107
- background: 'linear-gradient(135deg, #5c6ac4 0%, #4f6cf0 100%)',
128
+ background: 'linear-gradient(135deg, #5c6ac4 0%, #4361dd 100%)',
108
129
  before: 'rgba(92, 106, 196, 0.2)',
109
- after: 'rgba(79, 108, 240, 0.2)',
130
+ after: 'rgba(67, 97, 221, 0.2)',
110
131
  },
111
132
  },
112
133
  typography: {
@@ -124,57 +145,58 @@ export default {
124
145
  },
125
146
  ctas: {
126
147
  primary: {
127
- bg: 'linear-gradient(135deg, #4f6cf0 0%, #243a80 100%)',
148
+ bg: 'linear-gradient(135deg, #4361dd 0%, #243a80 100%)',
128
149
  text: '#ffffff',
129
- border: '1px solid rgba(255, 255, 255, 0.18)',
150
+ border: '1px solid rgba(255, 255, 255, 0.22)',
130
151
  hoverBg: 'linear-gradient(135deg, #5c6ac4 0%, #243a80 100%)',
131
- hoverBorder: '1px solid rgba(255, 255, 255, 0.22)',
152
+ hoverBorder: '1px solid rgba(255, 255, 255, 0.28)',
132
153
  shadow: '0 16px 32px rgba(32, 46, 104, 0.35)',
133
154
  },
134
155
  secondary: {
135
156
  bg: '#ffffff',
136
157
  text: sharedPrimary,
137
- border: '1px solid rgba(79, 108, 240, 0.35)',
138
- hoverBg: 'rgba(79, 108, 240, 0.08)',
139
- hoverBorder: '1px solid rgba(79, 108, 240, 0.55)',
158
+ border: '1px solid rgba(67, 97, 221, 0.45)',
159
+ hoverBg: 'rgba(67, 97, 221, 0.08)',
160
+ hoverBorder: '1px solid rgba(67, 97, 221, 0.6)',
140
161
  shadow: '0 8px 18px rgba(21, 32, 66, 0.18)',
141
162
  },
142
163
  ghost: {
143
164
  bg: 'transparent',
144
165
  text: sharedPrimary,
145
- border: '1px solid rgba(79, 108, 240, 0.35)',
146
- hoverBg: 'rgba(79, 108, 240, 0.08)',
147
- hoverBorder: '1px solid rgba(79, 108, 240, 0.55)',
166
+ border: '1px solid rgba(67, 97, 221, 0.45)',
167
+ hoverBg: 'rgba(67, 97, 221, 0.08)',
168
+ hoverBorder: '1px solid rgba(67, 97, 221, 0.6)',
148
169
  shadow: 'none',
149
170
  },
150
171
  link: {
151
172
  text: sharedPrimary,
152
173
  hoverText: sharedSecondary,
153
- underline: 'rgba(79, 108, 240, 0.35)',
174
+ underline: 'rgba(67, 97, 221, 0.45)',
154
175
  },
155
176
  },
156
177
  chips: {
157
178
  neutral: {
158
- bg: 'rgba(79, 108, 240, 0.1)',
179
+ bg: 'rgba(67, 97, 221, 0.1)',
159
180
  text: sharedPrimaryDeep,
160
- border: 'rgba(79, 108, 240, 0.18)',
181
+ border: 'rgba(67, 97, 221, 0.22)',
161
182
  },
162
183
  accent: {
163
184
  bg: 'rgba(92, 106, 196, 0.14)',
164
185
  text: '#ffffff',
165
- border: 'rgba(92, 106, 196, 0.4)',
186
+ border: 'rgba(92, 106, 196, 0.45)',
166
187
  },
167
188
  outline: {
168
189
  bg: 'transparent',
169
190
  text: sharedPrimary,
170
- border: '1px solid rgba(79, 108, 240, 0.35)',
191
+ border: '1px solid rgba(67, 97, 221, 0.45)',
171
192
  },
172
193
  },
173
194
  focus: {
174
- ring: '0 0 0 2px rgba(79, 108, 240, 0.35)',
175
- ringMuted: '0 0 0 1px rgba(79, 108, 240, 0.22)',
176
- outline: 'rgba(79, 108, 240, 0.6)',
177
- shadowInset: 'inset 0 0 0 1px rgba(79, 108, 240, 0.25)',
195
+ // Ring contrast verified 3:1+ against likely surrounding surfaces.
196
+ ring: '0 0 0 2px rgba(67, 97, 221, 0.45)',
197
+ ringMuted: '0 0 0 1px rgba(67, 97, 221, 0.3)',
198
+ outline: 'rgba(67, 97, 221, 0.7)',
199
+ shadowInset: 'inset 0 0 0 1px rgba(67, 97, 221, 0.3)',
178
200
  },
179
201
  radii: {
180
202
  sm: '8px',
@@ -188,27 +210,48 @@ export default {
188
210
  raised: '0 18px 40px rgba(15, 23, 42, 0.12)',
189
211
  overlay: '0 24px 48px rgba(15, 23, 42, 0.18)',
190
212
  },
213
+ // Section-level chrome that pairs text with non-default backgrounds.
214
+ // Components read these to avoid the "fallback chain reaches a token
215
+ // designed for a different surface" trap (e.g. dark hero bg + body
216
+ // text token). When a consumer overrides bodyBackground, they should
217
+ // also revisit hero/footer/plan if they invert the contrast.
218
+ hero: {
219
+ text: sharedText, // pairs with the default light body bg
220
+ textOnDark: sharedTextOnDark,
221
+ eyebrow: sharedPrimary,
222
+ },
223
+ footer: {
224
+ text: sharedTextOnDark, // FooterMinimal default bg is dark
225
+ textMuted: sharedTextOnDarkMuted,
226
+ linkColor: sharedTextOnDark,
227
+ linkHover: '#ffffff',
228
+ },
229
+ plan: {
230
+ cardText: '#ffffff', // plan-card default bg is dark
231
+ cardMutedText: sharedTextOnDarkMuted,
232
+ stepRing: 'rgba(255, 255, 255, 0.5)', // 3.45:1 on plan-card bg
233
+ },
191
234
  utility: {
192
- divider: 'rgba(79, 108, 240, 0.12)',
193
- inputBorder: 'rgba(79, 108, 240, 0.25)',
235
+ divider: 'rgba(67, 97, 221, 0.14)',
236
+ inputBorder: 'rgba(67, 97, 221, 0.3)',
194
237
  inputText: sharedText,
195
- inputPlaceholder: 'rgba(84, 98, 123, 0.64)',
196
- selectionBg: 'rgba(79, 108, 240, 0.25)',
238
+ inputPlaceholder: sharedTextMuted,
239
+ selectionBg: 'rgba(67, 97, 221, 0.3)',
197
240
  selectionText: '#ffffff',
198
- gradientHero: 'linear-gradient(135deg, #4f6cf0 0%, #243a80 100%)',
199
- gradientPromo: 'linear-gradient(135deg, #5c6ac4 0%, #4f6cf0 100%)',
241
+ gradientHero: 'linear-gradient(135deg, #4361dd 0%, #243a80 100%)',
242
+ gradientPromo: 'linear-gradient(135deg, #5c6ac4 0%, #4361dd 100%)',
200
243
  bodyBackground: [
201
- 'radial-gradient(circle at 18% 20%, rgba(79, 108, 240, 0.08), transparent 55%)',
244
+ 'radial-gradient(circle at 18% 20%, rgba(67, 97, 221, 0.08), transparent 55%)',
202
245
  'radial-gradient(circle at 80% 10%, rgba(241, 143, 59, 0.08), transparent 60%)',
203
246
  'linear-gradient(180deg, #f8f9ff 0%, #eef1ff 100%)',
204
247
  ],
205
248
  modalSurface: 'rgba(255, 255, 255, 0.97)',
206
- modalBorder: 'rgba(79, 108, 240, 0.18)',
249
+ modalBorder: 'rgba(67, 97, 221, 0.22)',
207
250
  modalShadow: '0 24px 48px rgba(15, 23, 42, 0.18)',
208
251
  modalRadius: '24px',
209
252
  chartTrack: 'rgba(236, 241, 255, 0.85)',
210
253
  chartCenterText: sharedText,
211
- statusHeadline: 'linear-gradient(120deg, rgba(79, 108, 240, 0.12), rgba(28, 42, 96, 0.14))',
254
+ statusHeadline: 'linear-gradient(120deg, rgba(67, 97, 221, 0.14), rgba(28, 42, 96, 0.16))',
212
255
  },
213
256
  },
214
257
  };