@koehler8/cms 1.0.0-beta.5

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/bin/cms-generate-public-assets.js +27 -0
  4. package/bin/cms-validate-extensions.js +18 -0
  5. package/bin/cms-validate-themes.js +7 -0
  6. package/extensions/manifest.schema.json +125 -0
  7. package/package.json +84 -0
  8. package/public/img/preloaders/preloader-black.svg +1 -0
  9. package/public/img/preloaders/preloader-white.svg +1 -0
  10. package/public/robots.txt +5 -0
  11. package/scripts/check-overflow.mjs +33 -0
  12. package/scripts/generate-public-assets.js +401 -0
  13. package/scripts/patch-lru-cache-tla.js +164 -0
  14. package/scripts/validate-extensions.mjs +392 -0
  15. package/scripts/validate-themes.mjs +64 -0
  16. package/src/App.vue +3 -0
  17. package/src/components/About.vue +481 -0
  18. package/src/components/AboutValue.vue +361 -0
  19. package/src/components/BackToTop.vue +42 -0
  20. package/src/components/ComingSoon.vue +411 -0
  21. package/src/components/ComingSoonModal.vue +230 -0
  22. package/src/components/Contact.vue +518 -0
  23. package/src/components/Footer.vue +65 -0
  24. package/src/components/FooterMinimal.vue +153 -0
  25. package/src/components/Header.vue +583 -0
  26. package/src/components/Hero.vue +327 -0
  27. package/src/components/Home.vue +144 -0
  28. package/src/components/Intro.vue +130 -0
  29. package/src/components/IntroGate.vue +444 -0
  30. package/src/components/Plan.vue +116 -0
  31. package/src/components/Portfolio.vue +459 -0
  32. package/src/components/Preloader.vue +20 -0
  33. package/src/components/Principles.vue +67 -0
  34. package/src/components/Spacer15.vue +9 -0
  35. package/src/components/Spacer30.vue +9 -0
  36. package/src/components/Spacer40.vue +9 -0
  37. package/src/components/Spacer60.vue +9 -0
  38. package/src/components/StickyCTA.vue +263 -0
  39. package/src/components/Team.vue +432 -0
  40. package/src/components/icons/IconLinkedIn.vue +22 -0
  41. package/src/components/icons/IconX.vue +22 -0
  42. package/src/components/ui/SbCard.vue +52 -0
  43. package/src/components/ui/SkeletonPulse.vue +117 -0
  44. package/src/components/ui/UnitChip.vue +69 -0
  45. package/src/composables/useComingSoonConfig.js +120 -0
  46. package/src/composables/useComingSoonInterstitial.js +27 -0
  47. package/src/composables/useComponentResolver.js +196 -0
  48. package/src/composables/useEngagementTracking.js +187 -0
  49. package/src/composables/useIntroGate.js +46 -0
  50. package/src/composables/useLazyImage.js +77 -0
  51. package/src/composables/usePageConfig.js +184 -0
  52. package/src/composables/usePageMeta.js +76 -0
  53. package/src/composables/usePromoBackgroundStyles.js +67 -0
  54. package/src/constants/locales.js +20 -0
  55. package/src/extensions/extensionLoader.js +512 -0
  56. package/src/main.js +175 -0
  57. package/src/router/index.js +112 -0
  58. package/src/styles/base.css +896 -0
  59. package/src/styles/layout.css +342 -0
  60. package/src/styles/theme-base.css +84 -0
  61. package/src/themes/themeLoader.js +124 -0
  62. package/src/themes/themeManager.js +257 -0
  63. package/src/themes/themeValidator.js +380 -0
  64. package/src/utils/analytics.js +100 -0
  65. package/src/utils/appInfo.js +9 -0
  66. package/src/utils/assetResolver.js +162 -0
  67. package/src/utils/componentRegistry.js +46 -0
  68. package/src/utils/contentRequirements.js +67 -0
  69. package/src/utils/cookieConsent.js +281 -0
  70. package/src/utils/ctaCopy.js +58 -0
  71. package/src/utils/formatNumber.js +115 -0
  72. package/src/utils/imageSources.js +179 -0
  73. package/src/utils/inflateFlatConfig.js +30 -0
  74. package/src/utils/loadConfig.js +271 -0
  75. package/src/utils/semver.js +49 -0
  76. package/src/utils/siteStyles.js +40 -0
  77. package/src/utils/themeColors.js +65 -0
  78. package/src/utils/trackingContext.js +142 -0
  79. package/src/utils/unwrapDefault.js +14 -0
  80. package/src/utils/useScrollReveal.js +48 -0
  81. package/templates/index.html +36 -0
  82. package/themes/base/README.md +23 -0
  83. package/themes/base/theme.config.js +214 -0
  84. package/vite-plugin.js +637 -0
@@ -0,0 +1,411 @@
1
+ <template>
2
+ <section
3
+ v-if="shouldRender"
4
+ id="coming-soon"
5
+ class="coming-soon-section section-shell"
6
+ data-analytics-section="coming-soon"
7
+ >
8
+ <div class="container">
9
+ <div class="coming-soon-shell brand-card">
10
+ <div class="coming-soon-preamble">
11
+ <span
12
+ v-if="eyebrow"
13
+ class="coming-soon-eyebrow ui-label-sm text-uppercase"
14
+ >
15
+ {{ eyebrow }}
16
+ </span>
17
+ <span
18
+ v-if="badge.text"
19
+ class="coming-soon-badge"
20
+ >
21
+ {{ badge.text }}
22
+ </span>
23
+ </div>
24
+ <h2 class="coming-soon-title ui-title-md">
25
+ {{ title }}
26
+ </h2>
27
+ <p class="coming-soon-message">
28
+ {{ message }}
29
+ </p>
30
+ <ul
31
+ v-if="highlights.length"
32
+ class="coming-soon-highlights"
33
+ >
34
+ <li
35
+ v-for="(highlight, index) in highlights"
36
+ :key="`coming-soon-highlight-${index}`"
37
+ class="coming-soon-highlight brand-card"
38
+ >
39
+ <span
40
+ v-if="highlight.icon"
41
+ class="coming-soon-highlight-icon"
42
+ aria-hidden="true"
43
+ >
44
+ {{ highlight.icon }}
45
+ </span>
46
+ <span class="coming-soon-highlight-text">
47
+ {{ highlight.text }}
48
+ </span>
49
+ </li>
50
+ </ul>
51
+ <p
52
+ v-if="note"
53
+ class="coming-soon-note"
54
+ >
55
+ {{ note }}
56
+ </p>
57
+ <div
58
+ v-if="actions.length"
59
+ class="coming-soon-actions"
60
+ >
61
+ <a
62
+ v-for="action in actions"
63
+ :key="action.key"
64
+ class="coming-soon-action"
65
+ :class="[
66
+ action.variant === 'secondary'
67
+ ? 'coming-soon-action--secondary'
68
+ : 'coming-soon-action--primary',
69
+ ]"
70
+ :href="action.href"
71
+ :target="action.target"
72
+ :rel="action.rel"
73
+ @click="(event) => handleActionClick(event, action)"
74
+ >
75
+ <span>{{ action.text }}</span>
76
+ </a>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </section>
81
+ </template>
82
+
83
+ <script setup>
84
+ import { computed, inject, ref } from 'vue';
85
+
86
+ import { useComingSoonResolver } from '../composables/useComingSoonConfig.js';
87
+ import { useComingSoonInterstitial } from '../composables/useComingSoonInterstitial.js';
88
+ import { trackEvent } from '../utils/analytics.js';
89
+
90
+ const pageContent = inject('pageContent', ref({}));
91
+ const siteData = inject('siteData', ref({}));
92
+
93
+ const comingSoonSource = computed(() => {
94
+ const pageBlock = pageContent.value?.comingSoon;
95
+ if (pageBlock && typeof pageBlock === 'object') {
96
+ return pageBlock;
97
+ }
98
+ const sharedBlock = siteData.value?.shared?.content?.comingSoon;
99
+ if (sharedBlock && typeof sharedBlock === 'object') {
100
+ return sharedBlock;
101
+ }
102
+ return null;
103
+ });
104
+
105
+ const fallbackTitle = 'Coming Soon';
106
+ const fallbackMessage = "We're putting the finishing touches on this experience. Check back soon!";
107
+
108
+ const stringOrFallback = (value, fallback = '') => {
109
+ if (typeof value === 'string' && value.trim()) {
110
+ return value.trim();
111
+ }
112
+ return fallback;
113
+ };
114
+
115
+ const title = computed(() =>
116
+ stringOrFallback(comingSoonSource.value?.title, fallbackTitle)
117
+ );
118
+ const message = computed(() =>
119
+ stringOrFallback(comingSoonSource.value?.message, fallbackMessage)
120
+ );
121
+ const eyebrow = computed(() => stringOrFallback(comingSoonSource.value?.eyebrow));
122
+ const note = computed(() => stringOrFallback(comingSoonSource.value?.note));
123
+
124
+ const badge = computed(() => {
125
+ const rawBadge = comingSoonSource.value?.badge;
126
+ if (typeof rawBadge === 'string') {
127
+ return { text: rawBadge.trim(), variant: null };
128
+ }
129
+ if (!rawBadge || typeof rawBadge !== 'object') {
130
+ return { text: '', variant: null };
131
+ }
132
+ return {
133
+ text: stringOrFallback(rawBadge.text),
134
+ variant: stringOrFallback(rawBadge.variant),
135
+ };
136
+ });
137
+
138
+ function normalizeHighlights(source) {
139
+ if (!source) return [];
140
+
141
+ const candidates = Array.isArray(source) ? source : [];
142
+ return candidates
143
+ .map((entry) => {
144
+ if (typeof entry === 'string') {
145
+ const trimmed = entry.trim();
146
+ return trimmed ? { text: trimmed, icon: '' } : null;
147
+ }
148
+ if (!entry || typeof entry !== 'object') {
149
+ return null;
150
+ }
151
+ const text = stringOrFallback(entry.text);
152
+ const icon = stringOrFallback(entry.icon);
153
+ if (!text) return null;
154
+ return { text, icon };
155
+ })
156
+ .filter(Boolean);
157
+ }
158
+
159
+ const highlights = computed(() => {
160
+ const sourceHighlights =
161
+ comingSoonSource.value?.highlights ??
162
+ comingSoonSource.value?.bullets ??
163
+ comingSoonSource.value?.items;
164
+ return normalizeHighlights(sourceHighlights);
165
+ });
166
+
167
+ function normalizeAction(raw, fallbackVariant = 'primary', index = 0) {
168
+ if (!raw) return null;
169
+
170
+ let base = raw;
171
+ if (typeof raw === 'string') {
172
+ base = { text: raw, href: raw.startsWith('#') ? raw : 'coming-soon' };
173
+ }
174
+ if (typeof base !== 'object') {
175
+ return null;
176
+ }
177
+
178
+ const text = stringOrFallback(base.text);
179
+ const href = stringOrFallback(base.href, 'coming-soon');
180
+ if (!text || !href) {
181
+ return null;
182
+ }
183
+
184
+ const variant = stringOrFallback(base.variant, fallbackVariant).toLowerCase() === 'secondary'
185
+ ? 'secondary'
186
+ : 'primary';
187
+
188
+ const openInNewTab =
189
+ typeof base.openInNewTab === 'boolean'
190
+ ? base.openInNewTab
191
+ : base.target === '_blank';
192
+
193
+ const rel = openInNewTab ? base.rel || 'noopener noreferrer' : base.rel || null;
194
+ const target = openInNewTab ? '_blank' : base.target || null;
195
+
196
+ return {
197
+ key: `${text}-${href}-${variant}-${index}`,
198
+ text,
199
+ href,
200
+ variant,
201
+ target,
202
+ rel,
203
+ trigger: stringOrFallback(
204
+ base.analyticsTrigger || base.trigger,
205
+ variant === 'secondary' ? 'coming-soon-secondary' : 'coming-soon-primary'
206
+ ),
207
+ };
208
+ }
209
+
210
+ function dedupeActions(items) {
211
+ const map = new Map();
212
+ for (const action of items) {
213
+ if (!action) continue;
214
+ const key = `${action.text}|${action.href}|${action.variant}`;
215
+ if (!map.has(key)) {
216
+ map.set(key, action);
217
+ }
218
+ }
219
+ return Array.from(map.values());
220
+ }
221
+
222
+ const actions = computed(() => {
223
+ if (!comingSoonSource.value) {
224
+ return [];
225
+ }
226
+
227
+ const rawActions = [];
228
+ const source = comingSoonSource.value;
229
+
230
+ if (Array.isArray(source.actions)) {
231
+ rawActions.push(
232
+ ...source.actions.map((action, index) =>
233
+ normalizeAction(action, index === 0 ? 'primary' : 'secondary', index)
234
+ )
235
+ );
236
+ }
237
+
238
+ if (source.cta) {
239
+ rawActions.push(normalizeAction(source.cta, 'primary', rawActions.length));
240
+ }
241
+
242
+ if (source.primaryAction) {
243
+ rawActions.push(normalizeAction(source.primaryAction, 'primary', rawActions.length));
244
+ }
245
+
246
+ if (source.secondaryAction) {
247
+ rawActions.push(normalizeAction(source.secondaryAction, 'secondary', rawActions.length));
248
+ }
249
+
250
+ return dedupeActions(rawActions).filter(Boolean);
251
+ });
252
+
253
+ const shouldRender = computed(() => !!comingSoonSource.value);
254
+
255
+ const { openComingSoon } = useComingSoonInterstitial();
256
+ const { resolve: resolveComingSoon } = useComingSoonResolver(pageContent);
257
+
258
+ function handleActionClick(event, action) {
259
+ if (!event || !action) return;
260
+
261
+ const modalPayload = resolveComingSoon({ href: action.href });
262
+ if (!modalPayload) {
263
+ return;
264
+ }
265
+
266
+ event.preventDefault();
267
+ openComingSoon(modalPayload);
268
+
269
+ trackEvent('coming_soon_interstitial_shown', {
270
+ trigger: action.trigger || 'coming-soon-cta',
271
+ label: action.text ? action.text.toLowerCase() : 'coming-soon',
272
+ });
273
+ }
274
+ </script>
275
+
276
+ <style scoped>
277
+ .coming-soon-section {
278
+ position: relative;
279
+ padding-block: clamp(80px, 12vw, 160px);
280
+ }
281
+
282
+ .coming-soon-shell {
283
+ --coming-soon-text: var(--brand-card-text, var(--ui-text-primary, #ffffff));
284
+ --coming-soon-text-muted: color-mix(in srgb, var(--coming-soon-text) 70%, transparent);
285
+ --coming-soon-text-subtle: color-mix(in srgb, var(--coming-soon-text) 55%, transparent);
286
+ padding: clamp(2rem, 4vw, 3.5rem);
287
+ display: flex;
288
+ flex-direction: column;
289
+ gap: 1.25rem;
290
+ border-radius: var(--brand-surface-card-radius, 28px);
291
+ background: var(--brand-surface-card, rgba(9, 6, 20, 0.92));
292
+ color: var(--coming-soon-text);
293
+ }
294
+
295
+ .coming-soon-preamble {
296
+ display: flex;
297
+ gap: 0.75rem;
298
+ align-items: center;
299
+ flex-wrap: wrap;
300
+ }
301
+
302
+ .coming-soon-eyebrow {
303
+ letter-spacing: 0.12em;
304
+ color: var(--coming-soon-text-muted, var(--ui-text-muted, rgba(255, 255, 255, 0.72)));
305
+ }
306
+
307
+ .coming-soon-badge {
308
+ padding: 0.25rem 0.75rem;
309
+ border-radius: 999px;
310
+ border: 1px solid rgba(255, 255, 255, 0.25);
311
+ font-size: 0.85rem;
312
+ font-weight: 600;
313
+ text-transform: uppercase;
314
+ color: var(--coming-soon-text);
315
+ }
316
+
317
+ .coming-soon-title {
318
+ margin: 0;
319
+ color: var(--coming-soon-text);
320
+ }
321
+
322
+ .coming-soon-message {
323
+ margin: 0;
324
+ font-size: 1.125rem;
325
+ line-height: 1.7;
326
+ color: var(--coming-soon-text-muted, var(--ui-text-muted, rgba(255, 255, 255, 0.86)));
327
+ }
328
+
329
+ .coming-soon-highlights {
330
+ list-style: none;
331
+ padding: 0;
332
+ margin: 0;
333
+ display: grid;
334
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
335
+ gap: 1rem;
336
+ }
337
+
338
+ .coming-soon-highlight {
339
+ display: flex;
340
+ align-items: flex-start;
341
+ gap: 0.75rem;
342
+ padding: 1rem;
343
+ border-radius: clamp(16px, 2vw, 20px);
344
+ background: var(--surface-tabs-bg, rgba(255, 255, 255, 0.04));
345
+ }
346
+
347
+ .coming-soon-highlight-icon {
348
+ font-size: 1.5rem;
349
+ }
350
+
351
+ .coming-soon-highlight-text {
352
+ color: var(--coming-soon-text);
353
+ line-height: 1.4;
354
+ }
355
+
356
+ .coming-soon-note {
357
+ margin: 0;
358
+ font-size: 0.95rem;
359
+ color: var(--coming-soon-text-subtle, var(--ui-text-muted, rgba(255, 255, 255, 0.72)));
360
+ }
361
+
362
+ .coming-soon-actions {
363
+ display: flex;
364
+ flex-wrap: wrap;
365
+ gap: 0.75rem;
366
+ }
367
+
368
+ .coming-soon-action {
369
+ display: inline-flex;
370
+ align-items: center;
371
+ justify-content: center;
372
+ padding: 0.9rem 1.75rem;
373
+ border-radius: var(--brand-button-radius, 999px);
374
+ font-weight: 600;
375
+ text-decoration: none;
376
+ transition: transform 0.2s ease, opacity 0.2s ease;
377
+ }
378
+
379
+ .coming-soon-action--primary {
380
+ background: var(
381
+ --brand-cta-gradient,
382
+ linear-gradient(90deg, #ff42a5 0%, #7f5dff 50%, #48d5ff 100%)
383
+ );
384
+ color: #0b0418;
385
+ }
386
+
387
+ .coming-soon-action--secondary {
388
+ border: 1px solid rgba(255, 255, 255, 0.35);
389
+ color: var(--coming-soon-text);
390
+ }
391
+
392
+ .coming-soon-action:hover,
393
+ .coming-soon-action:focus {
394
+ transform: translateY(-2px);
395
+ opacity: 0.92;
396
+ }
397
+
398
+ @media (max-width: 767px) {
399
+ .coming-soon-shell {
400
+ padding: 1.75rem;
401
+ }
402
+
403
+ .coming-soon-highlights {
404
+ grid-template-columns: 1fr;
405
+ }
406
+
407
+ .coming-soon-action {
408
+ width: 100%;
409
+ }
410
+ }
411
+ </style>
@@ -0,0 +1,230 @@
1
+ <template>
2
+ <teleport to="body">
3
+ <div
4
+ v-if="open"
5
+ class="coming-soon-overlay"
6
+ role="presentation"
7
+ @keydown.esc.prevent="emitClose"
8
+ >
9
+ <div
10
+ class="coming-soon-backdrop"
11
+ @click.self="emitClose"
12
+ >
13
+ <div
14
+ class="coming-soon-dialog"
15
+ role="dialog"
16
+ aria-modal="true"
17
+ :aria-labelledby="dialogTitleId"
18
+ :aria-describedby="dialogDescriptionId"
19
+ ref="dialogRef"
20
+ tabindex="-1"
21
+ @keydown.esc.prevent.stop="emitClose"
22
+ >
23
+ <button
24
+ type="button"
25
+ class="coming-soon-close"
26
+ @click="emitClose"
27
+ aria-label="Close"
28
+ >
29
+ &times;
30
+ </button>
31
+ <h2 class="coming-soon-title" :id="dialogTitleId">
32
+ {{ title }}
33
+ </h2>
34
+ <p class="coming-soon-message" :id="dialogDescriptionId">
35
+ {{ message }}
36
+ </p>
37
+ <button
38
+ type="button"
39
+ class="coming-soon-cta"
40
+ @click="emitClose"
41
+ >
42
+ Got it
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </teleport>
48
+ </template>
49
+
50
+ <script setup>
51
+ import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue';
52
+
53
+ const props = defineProps({
54
+ open: {
55
+ type: Boolean,
56
+ default: false,
57
+ },
58
+ title: {
59
+ type: String,
60
+ default: 'Coming Soon',
61
+ },
62
+ message: {
63
+ type: String,
64
+ default: "We're putting the finishing touches on this experience. Check back soon!",
65
+ },
66
+ });
67
+
68
+ const emit = defineEmits(['close']);
69
+
70
+ const dialogRef = ref(null);
71
+ const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
72
+ const dialogTitleId = computed(() => 'coming-soon-title');
73
+ const dialogDescriptionId = computed(() => 'coming-soon-message');
74
+
75
+ const emitClose = () => {
76
+ emit('close');
77
+ };
78
+
79
+ const trapFocus = (event) => {
80
+ if (!props.open) return;
81
+ if (typeof document === 'undefined') return;
82
+ if (event.key !== 'Tab') return;
83
+ const dialogEl = dialogRef.value;
84
+ if (!dialogEl) return;
85
+ const focusable = dialogEl.querySelectorAll(focusableSelector);
86
+ if (!focusable.length) {
87
+ event.preventDefault();
88
+ return;
89
+ }
90
+ const first = focusable[0];
91
+ const last = focusable[focusable.length - 1];
92
+ if (event.shiftKey && document.activeElement === first) {
93
+ event.preventDefault();
94
+ last.focus();
95
+ } else if (!event.shiftKey && document.activeElement === last) {
96
+ event.preventDefault();
97
+ first.focus();
98
+ }
99
+ };
100
+
101
+ watch(
102
+ () => props.open,
103
+ (isOpen) => {
104
+ if (typeof document === 'undefined') return;
105
+ if (isOpen) {
106
+ if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
107
+ window.requestAnimationFrame(() => {
108
+ dialogRef.value?.focus();
109
+ });
110
+ } else {
111
+ dialogRef.value?.focus();
112
+ }
113
+ document.addEventListener('keydown', trapFocus);
114
+ } else {
115
+ document.removeEventListener('keydown', trapFocus);
116
+ }
117
+ }
118
+ );
119
+
120
+ onMounted(() => {
121
+ if (typeof document === 'undefined') return;
122
+ if (props.open) {
123
+ document.addEventListener('keydown', trapFocus);
124
+ }
125
+ });
126
+
127
+ onBeforeUnmount(() => {
128
+ if (typeof document === 'undefined') return;
129
+ document.removeEventListener('keydown', trapFocus);
130
+ });
131
+ </script>
132
+
133
+ <style scoped>
134
+ .coming-soon-overlay {
135
+ position: fixed;
136
+ top: 0;
137
+ left: 0;
138
+ width: 100vw;
139
+ height: 100vh;
140
+ z-index: 9999;
141
+ }
142
+
143
+ .coming-soon-backdrop {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ width: 100%;
148
+ height: 100%;
149
+ background: rgba(15, 15, 20, 0.85);
150
+ padding: 1.5rem;
151
+ }
152
+
153
+ .coming-soon-dialog {
154
+ position: relative;
155
+ max-width: 28rem;
156
+ width: 100%;
157
+ padding: 2.5rem 2rem 2rem;
158
+ border-radius: var(--brand-modal-radius, 24px);
159
+ background: var(
160
+ --brand-modal-surface,
161
+ linear-gradient(145deg, rgba(255, 255, 255, 0.98), rgba(240, 240, 255, 0.92))
162
+ );
163
+ box-shadow: var(--brand-modal-shadow, 0 18px 48px rgba(8, 4, 18, 0.35));
164
+ text-align: center;
165
+ color: var(--brand-fg-100, #0a0a0d);
166
+ }
167
+
168
+ .coming-soon-title {
169
+ margin: 0 0 1rem;
170
+ font-size: clamp(1.8rem, 4vw, 2.5rem);
171
+ font-family: 'Space Grotesk', 'Inter', sans-serif;
172
+ font-weight: 700;
173
+ color: var(--brand-fg-100, #0a0a0d);
174
+ }
175
+
176
+ .coming-soon-message {
177
+ margin: 0 0 1.75rem;
178
+ font-size: clamp(1rem, 2.4vw, 1.125rem);
179
+ font-family: 'Inter', 'Helvetica Neue', sans-serif;
180
+ color: var(--brand-fg-200, rgba(8, 8, 18, 0.75));
181
+ }
182
+
183
+ .coming-soon-cta {
184
+ display: inline-flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ min-width: 8rem;
188
+ padding: 0.75rem 1.5rem;
189
+ border-radius: var(--brand-button-radius, 14px);
190
+ border: none;
191
+ font-weight: 600;
192
+ font-family: 'Inter', 'Helvetica Neue', sans-serif;
193
+ color: var(--brand-primary-cta-text, #0b0418);
194
+ background: var(
195
+ --brand-primary-cta-gradient,
196
+ linear-gradient(90deg, #ff42a5 0%, #7f5dff 50%, #48d5ff 100%)
197
+ );
198
+ cursor: pointer;
199
+ transition: background 0.2s ease, transform 0.2s ease;
200
+ }
201
+
202
+ .coming-soon-cta:hover,
203
+ .coming-soon-cta:focus {
204
+ opacity: 0.92;
205
+ transform: translateY(-1px);
206
+ }
207
+
208
+ .coming-soon-cta:focus {
209
+ outline: 2px solid #111111;
210
+ outline-offset: 2px;
211
+ }
212
+
213
+ .coming-soon-close {
214
+ position: absolute;
215
+ top: 0.75rem;
216
+ right: 0.75rem;
217
+ background: transparent;
218
+ border: none;
219
+ font-size: 1.5rem;
220
+ line-height: 1;
221
+ cursor: pointer;
222
+ color: #888;
223
+ padding: 0.25rem;
224
+ }
225
+
226
+ .coming-soon-close:hover,
227
+ .coming-soon-close:focus {
228
+ color: #111111;
229
+ }
230
+ </style>