@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,444 @@
1
+ <template>
2
+ <teleport to="body">
3
+ <div
4
+ v-if="isOpen"
5
+ class="intro-gate-overlay"
6
+ role="presentation"
7
+ >
8
+ <div
9
+ class="intro-gate-backdrop"
10
+ @click.self="handleDismiss('backdrop')"
11
+ >
12
+ <div
13
+ class="intro-gate-dialog"
14
+ role="dialog"
15
+ aria-modal="true"
16
+ :aria-labelledby="dialogTitleId"
17
+ :aria-describedby="dialogDescriptionId"
18
+ ref="dialogRef"
19
+ tabindex="-1"
20
+ >
21
+ <span class="intro-gate-glow" aria-hidden="true"></span>
22
+ <button
23
+ type="button"
24
+ class="intro-gate-close"
25
+ @click="handleDismiss('close-button')"
26
+ :aria-label="closeButtonAriaLabel"
27
+ >
28
+ &times;
29
+ </button>
30
+ <p class="intro-gate-eyebrow">{{ eyebrowText }}</p>
31
+ <h2 :id="dialogTitleId" class="intro-gate-title">
32
+ {{ titleText }}
33
+ </h2>
34
+ <p :id="dialogDescriptionId" class="intro-gate-body">
35
+ {{ bodyText }}
36
+ </p>
37
+ <div class="intro-gate-actions">
38
+ <button
39
+ type="button"
40
+ class="intro-gate-primary"
41
+ @click="handlePrimaryClick"
42
+ >
43
+ {{ primaryLabelText }}
44
+ </button>
45
+ <a
46
+ class="intro-gate-secondary"
47
+ :href="secondaryHref"
48
+ @click="handleDismiss('learn-more')"
49
+ >
50
+ {{ secondaryLabelText }}
51
+ </a>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </teleport>
57
+ </template>
58
+
59
+ <script setup>
60
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
61
+ import { trackEvent } from '../utils/analytics.js';
62
+
63
+ const props = defineProps({
64
+ enabled: {
65
+ type: Boolean,
66
+ default: false,
67
+ },
68
+ eyebrow: {
69
+ type: String,
70
+ default: '',
71
+ },
72
+ title: {
73
+ type: String,
74
+ default: '',
75
+ },
76
+ body: {
77
+ type: String,
78
+ default: '',
79
+ },
80
+ primaryLabel: {
81
+ type: String,
82
+ default: '',
83
+ },
84
+ secondaryLabel: {
85
+ type: String,
86
+ default: '',
87
+ },
88
+ secondaryHref: {
89
+ type: String,
90
+ default: '',
91
+ },
92
+ storageKey: {
93
+ type: String,
94
+ default: 'site_intro_gate_seen',
95
+ },
96
+ closeAriaLabel: {
97
+ type: String,
98
+ default: '',
99
+ },
100
+ });
101
+
102
+ const isOpen = ref(false);
103
+ const dialogRef = ref(null);
104
+ const previousFocusedElement = ref(null);
105
+
106
+ const dialogTitleId = computed(() => 'intro-gate-title');
107
+ const dialogDescriptionId = computed(() => 'intro-gate-description');
108
+ const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
109
+ const eyebrowText = computed(() => (typeof props.eyebrow === 'string' ? props.eyebrow.trim() : ''));
110
+ const titleText = computed(() => (typeof props.title === 'string' ? props.title.trim() : ''));
111
+ const bodyText = computed(() => (typeof props.body === 'string' ? props.body.trim() : ''));
112
+
113
+ const primaryLabelText = computed(() => {
114
+ const raw = typeof props.primaryLabel === 'string' ? props.primaryLabel.trim() : '';
115
+ return raw || 'Continue';
116
+ });
117
+ const secondaryLabelText = computed(() => {
118
+ const raw = typeof props.secondaryLabel === 'string' ? props.secondaryLabel.trim() : '';
119
+ return raw || 'Learn more';
120
+ });
121
+ const secondaryHref = computed(() => {
122
+ const raw = typeof props.secondaryHref === 'string' ? props.secondaryHref.trim() : '';
123
+ return raw || '#';
124
+ });
125
+ const closeButtonAriaLabel = computed(() => {
126
+ const raw = typeof props.closeAriaLabel === 'string' ? props.closeAriaLabel.trim() : '';
127
+ return raw || 'Close dialog';
128
+ });
129
+
130
+ function getStorageKey() {
131
+ return props.storageKey || 'intro_gate_seen';
132
+ }
133
+
134
+ function getSessionKey() {
135
+ return `${getStorageKey()}_session`;
136
+ }
137
+
138
+ function hasSeenGate() {
139
+ if (import.meta.env.SSR) return true;
140
+ try {
141
+ const storageKey = getStorageKey();
142
+ const sessionKey = getSessionKey();
143
+ return localStorage.getItem(storageKey) === '1' || sessionStorage.getItem(sessionKey) === '1';
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ function rememberGate() {
150
+ if (import.meta.env.SSR) return;
151
+ try {
152
+ localStorage.setItem(getStorageKey(), '1');
153
+ } catch {}
154
+ try {
155
+ sessionStorage.setItem(getSessionKey(), '1');
156
+ } catch {}
157
+ }
158
+
159
+ function handleDismiss(source) {
160
+ rememberGate();
161
+ isOpen.value = false;
162
+ trackEvent('intro_gate_dismissed', { source });
163
+ }
164
+
165
+ function handlePrimaryClick() {
166
+ handleDismiss('continue');
167
+ }
168
+
169
+ function focusDialog() {
170
+ if (typeof window === 'undefined') return;
171
+ nextTick(() => {
172
+ dialogRef.value?.focus();
173
+ });
174
+ }
175
+
176
+ function restoreFocus() {
177
+ if (!previousFocusedElement.value) return;
178
+ if (typeof previousFocusedElement.value.focus === 'function') {
179
+ previousFocusedElement.value.focus();
180
+ }
181
+ previousFocusedElement.value = null;
182
+ }
183
+
184
+ function trapFocus(event) {
185
+ if (!isOpen.value) return;
186
+ if (typeof document === 'undefined') return;
187
+ if (event.key !== 'Tab') return;
188
+ const dialogEl = dialogRef.value;
189
+ if (!dialogEl) return;
190
+ const focusable = dialogEl.querySelectorAll(focusableSelector);
191
+ if (!focusable.length) {
192
+ event.preventDefault();
193
+ return;
194
+ }
195
+ const first = focusable[0];
196
+ const last = focusable[focusable.length - 1];
197
+ if (event.shiftKey && document.activeElement === first) {
198
+ event.preventDefault();
199
+ last.focus();
200
+ } else if (!event.shiftKey && document.activeElement === last) {
201
+ event.preventDefault();
202
+ first.focus();
203
+ }
204
+ }
205
+
206
+ function handleKeydown(event) {
207
+ if (!isOpen.value) return;
208
+ if (event.key === 'Escape') {
209
+ event.preventDefault();
210
+ handleDismiss('escape');
211
+ } else if (event.key === 'Tab') {
212
+ trapFocus(event);
213
+ }
214
+ }
215
+
216
+ watch(
217
+ () => isOpen.value,
218
+ (open) => {
219
+ if (import.meta.env.SSR) return;
220
+ if (open) {
221
+ previousFocusedElement.value = document.activeElement instanceof HTMLElement ? document.activeElement : null;
222
+ focusDialog();
223
+ document.addEventListener('keydown', handleKeydown);
224
+ } else {
225
+ document.removeEventListener('keydown', handleKeydown);
226
+ restoreFocus();
227
+ }
228
+ }
229
+ );
230
+
231
+ function maybeOpenGate() {
232
+ if (import.meta.env.SSR) return;
233
+ if (!props.enabled) return;
234
+ if (hasSeenGate()) return;
235
+ isOpen.value = true;
236
+ }
237
+
238
+ watch(
239
+ () => props.enabled,
240
+ (enabled) => {
241
+ if (!enabled) {
242
+ isOpen.value = false;
243
+ return;
244
+ }
245
+ maybeOpenGate();
246
+ },
247
+ { immediate: true }
248
+ );
249
+
250
+ onMounted(() => {
251
+ if (import.meta.env.SSR) return;
252
+ maybeOpenGate();
253
+ });
254
+
255
+ onBeforeUnmount(() => {
256
+ if (import.meta.env.SSR) return;
257
+ document.removeEventListener('keydown', handleKeydown);
258
+ });
259
+ </script>
260
+
261
+ <style scoped>
262
+ .intro-gate-overlay {
263
+ position: fixed;
264
+ inset: 0;
265
+ z-index: 10000;
266
+ }
267
+
268
+ .intro-gate-backdrop {
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ width: 100%;
273
+ height: 100%;
274
+ padding: clamp(24px, 6vw, 48px);
275
+ background:
276
+ radial-gradient(circle at 20% 20%, rgba(255, 45, 134, 0.18), transparent 55%),
277
+ radial-gradient(circle at 80% 15%, rgba(39, 243, 255, 0.14), transparent 60%),
278
+ rgba(8, 6, 12, 0.88);
279
+ backdrop-filter: blur(12px);
280
+ }
281
+
282
+ .intro-gate-dialog {
283
+ position: relative;
284
+ max-width: 34rem;
285
+ width: 100%;
286
+ padding: clamp(32px, 5.2vw, 48px);
287
+ border-radius: clamp(18px, 3vw, 26px);
288
+ background: linear-gradient(155deg, rgba(12, 10, 18, 0.95), rgba(18, 12, 26, 0.92) 60%, rgba(26, 14, 44, 0.9));
289
+ color: #f9fafb;
290
+ box-shadow:
291
+ 0 34px 72px rgba(5, 3, 10, 0.7),
292
+ inset 0 0 0 1px rgba(255, 255, 255, 0.04);
293
+ border: 1px solid rgba(255, 255, 255, 0.08);
294
+ outline: none;
295
+ overflow: hidden;
296
+ }
297
+
298
+ .intro-gate-glow {
299
+ position: absolute;
300
+ inset: -2px;
301
+ background: linear-gradient(135deg, rgba(255, 45, 134, 0.45), rgba(39, 243, 255, 0.4));
302
+ filter: blur(32px);
303
+ opacity: 0.3;
304
+ pointer-events: none;
305
+ z-index: 0;
306
+ }
307
+
308
+ .intro-gate-close {
309
+ position: absolute;
310
+ top: 1rem;
311
+ right: 1rem;
312
+ border: none;
313
+ background: transparent;
314
+ color: rgba(248, 250, 252, 0.7);
315
+ font-size: 1.75rem;
316
+ line-height: 1;
317
+ cursor: pointer;
318
+ transition: color 0.2s ease, transform 0.2s ease;
319
+ }
320
+
321
+ .intro-gate-close:hover,
322
+ .intro-gate-close:focus {
323
+ color: #ffffff;
324
+ transform: scale(1.05);
325
+ }
326
+
327
+ .intro-gate-close:focus-visible {
328
+ outline: 2px solid #f9fafb;
329
+ outline-offset: 2px;
330
+ }
331
+
332
+ .intro-gate-eyebrow {
333
+ margin: 0 0 clamp(0.4rem, 1.4vw, 0.75rem);
334
+ letter-spacing: 0.14em;
335
+ text-transform: uppercase;
336
+ font-size: 0.7rem;
337
+ color: rgba(248, 250, 252, 0.72);
338
+ background: rgba(255, 255, 255, 0.08);
339
+ padding: 0.35rem 0.8rem;
340
+ border-radius: 999px;
341
+ display: inline-flex;
342
+ align-items: center;
343
+ gap: 0.4rem;
344
+ }
345
+
346
+ .intro-gate-title {
347
+ margin: clamp(0.35rem, 1.5vw, 0.65rem) 0 clamp(1rem, 2.2vw, 1.4rem);
348
+ font-size: clamp(1.9rem, 3vw, 2.35rem);
349
+ line-height: 1.18;
350
+ font-weight: 700;
351
+ }
352
+
353
+ .intro-gate-body {
354
+ margin: 0 0 clamp(1.5rem, 4vw, 2.2rem);
355
+ font-size: 1.05rem;
356
+ line-height: 1.65;
357
+ color: rgba(248, 250, 252, 0.8);
358
+ }
359
+
360
+ .intro-gate-actions {
361
+ display: grid;
362
+ gap: 0.85rem;
363
+ }
364
+
365
+ .intro-gate-primary,
366
+ .intro-gate-secondary {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ border-radius: var(--brand-button-radius, 14px);
371
+ padding: 0.85rem 1.75rem;
372
+ font-weight: 600;
373
+ font-family: 'Inter', 'Helvetica Neue', sans-serif;
374
+ text-align: center;
375
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease;
376
+ }
377
+
378
+ .intro-gate-primary {
379
+ border: none;
380
+ background: var(
381
+ --brand-primary-cta-gradient,
382
+ linear-gradient(135deg, #ff2d86 0%, #9a2eff 55%, #27f3ff 100%)
383
+ );
384
+ color: var(--brand-primary-cta-text, #0a0a0d);
385
+ cursor: pointer;
386
+ box-shadow: var(--brand-primary-cta-shadow, 0 18px 40px rgba(255, 45, 134, 0.45));
387
+ position: relative;
388
+ overflow: hidden;
389
+ }
390
+
391
+ .intro-gate-primary:hover,
392
+ .intro-gate-primary:focus {
393
+ transform: translateY(-2px);
394
+ box-shadow: var(--brand-primary-cta-hover-shadow, 0 20px 44px rgba(255, 45, 134, 0.55));
395
+ }
396
+
397
+ .intro-gate-primary::after {
398
+ content: '';
399
+ position: absolute;
400
+ inset: 0;
401
+ background: linear-gradient(120deg, rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0));
402
+ transform: translateX(-120%);
403
+ transition: transform 0.35s ease;
404
+ }
405
+
406
+ .intro-gate-primary:hover::after,
407
+ .intro-gate-primary:focus::after {
408
+ transform: translateX(0%);
409
+ }
410
+
411
+ .intro-gate-primary:focus-visible {
412
+ outline: 2px solid rgba(39, 243, 255, 0.6);
413
+ outline-offset: 3px;
414
+ }
415
+
416
+ .intro-gate-secondary {
417
+ border: 1px solid rgba(39, 243, 255, 0.35);
418
+ color: var(--brand-electric-blue, #27f3ff);
419
+ text-decoration: none;
420
+ background: transparent;
421
+ }
422
+
423
+ .intro-gate-secondary:hover,
424
+ .intro-gate-secondary:focus {
425
+ color: var(--brand-neon-pink, #ff2d86);
426
+ border-color: rgba(255, 45, 134, 0.55);
427
+ transform: translateY(-1px);
428
+ }
429
+
430
+ .intro-gate-secondary:focus-visible {
431
+ outline: 2px solid rgba(39, 243, 255, 0.6);
432
+ outline-offset: 3px;
433
+ }
434
+
435
+ @media (min-width: 480px) {
436
+ .intro-gate-actions {
437
+ flex-direction: row;
438
+ }
439
+
440
+ .intro-gate-secondary {
441
+ padding-inline: 1.2rem;
442
+ }
443
+ }
444
+ </style>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <section id="plan" class="section-shell plan-section" data-analytics-section="plan">
3
+ <div class="container">
4
+ <header class="plan-heading text-center">
5
+ <div class="plan-heading__divider" aria-hidden="true"></div>
6
+ <h2 class="plan-heading__title text-uppercase">{{ planTitle }}</h2>
7
+ <p v-if="planSubtitle" class="plan-heading__subtitle mb-0 text-muted">
8
+ {{ planSubtitle }}
9
+ </p>
10
+ </header>
11
+ <div class="plan-grid">
12
+ <div
13
+ v-for="(item, idx) in planItems"
14
+ :key="idx"
15
+ class="plan-column"
16
+ >
17
+ <div class="plan-card text-center">
18
+ <span
19
+ class="plan-step-icon"
20
+ >
21
+ {{ item.step }}
22
+ </span>
23
+ <h3 class="plan-card__title">{{ item.title }}</h3>
24
+ <p class="plan-card__description mb-0">{{ item.description }}</p>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </section>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { computed, inject, ref } from 'vue';
34
+
35
+ const pageContent = inject('pageContent', ref({}));
36
+
37
+ const planData = computed(() => pageContent.value?.plan || {});
38
+
39
+ const planTitle = computed(() => planData.value?.title || 'The Plan');
40
+ const planSubtitle = computed(() => planData.value?.subtitle || '');
41
+
42
+ const planItems = computed(() =>
43
+ Array.isArray(planData.value?.items) ? planData.value.items : []
44
+ );
45
+ </script>
46
+
47
+ <style scoped>
48
+ .plan-section {
49
+ background: var(--plan-section-bg, transparent);
50
+ }
51
+
52
+ .plan-heading {
53
+ max-width: 640px;
54
+ margin: 0 auto var(--ui-space-32, 32px);
55
+ }
56
+
57
+ .plan-heading__divider {
58
+ width: 60px;
59
+ height: 3px;
60
+ margin: 0 auto var(--ui-space-16, 16px);
61
+ background: var(--plan-heading-divider, var(--brand-accent-electric, #4f6cf0));
62
+ border-radius: 999px;
63
+ }
64
+
65
+ .plan-heading__title {
66
+ font-weight: 600;
67
+ font-size: clamp(2rem, 4vw, 2.6rem);
68
+ margin-bottom: var(--ui-space-12, 12px);
69
+ }
70
+
71
+ .plan-heading__subtitle {
72
+ font-size: 1rem;
73
+ }
74
+
75
+ .plan-grid {
76
+ display: grid;
77
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
78
+ gap: var(--ui-space-24, 24px);
79
+ }
80
+
81
+ .plan-card {
82
+ padding: var(--ui-space-24, 24px);
83
+ border-radius: var(--brand-card-radius, 24px);
84
+ border: 1px solid color-mix(in srgb, var(--brand-surface-card-border, rgba(255, 255, 255, 0.12)) 100%, transparent);
85
+ background: var(--plan-card-bg, color-mix(in srgb, var(--brand-bg-900, #05050a) 85%, transparent));
86
+ display: flex;
87
+ flex-direction: column;
88
+ align-items: center;
89
+ gap: var(--ui-space-16, 16px);
90
+ min-height: 100%;
91
+ }
92
+
93
+ .plan-step-icon {
94
+ width: 70px;
95
+ height: 70px;
96
+ border-radius: 50%;
97
+ border: 3px solid var(--plan-step-border, rgba(255, 255, 255, 0.3));
98
+ color: var(--plan-step-color, #ffffff);
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ font-weight: 600;
103
+ font-size: 1.2rem;
104
+ }
105
+
106
+ .plan-card__title {
107
+ font-size: 1.15rem;
108
+ font-weight: 600;
109
+ margin: 0;
110
+ color: var(--plan-card-title, var(--ui-text-primary, #ffffff));
111
+ }
112
+
113
+ .plan-card__description {
114
+ color: var(--ui-text-muted, rgba(255, 255, 255, 0.78));
115
+ }
116
+ </style>