@mundogamernetwork/shared-ui 1.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 (87) hide show
  1. package/README.md +283 -0
  2. package/components/PressKit/AssetGallery.vue +349 -0
  3. package/components/PressKit/Awards.vue +100 -0
  4. package/components/PressKit/Credits.vue +78 -0
  5. package/components/PressKit/FactSheet.vue +204 -0
  6. package/components/PressKit/Hero.vue +143 -0
  7. package/components/PressKit/Quotes.vue +80 -0
  8. package/components/PressKit/VideoPlayer.vue +134 -0
  9. package/components/checkout/MgCartItemList.vue +214 -0
  10. package/components/checkout/MgCartSummary.vue +204 -0
  11. package/components/checkout/MgCheckoutSidebar.vue +230 -0
  12. package/components/checkout/MgGuestEmailForm.vue +97 -0
  13. package/components/checkout/MgPaymentMethodSelector.vue +162 -0
  14. package/components/checkout/MgPixQRCode.vue +222 -0
  15. package/components/indie-wall/IndieWallLeaderboard.vue +208 -0
  16. package/components/indie-wall/MuralCanvas.vue +481 -0
  17. package/components/indie-wall/StepBlock.vue +314 -0
  18. package/components/indie-wall/StepCustomize.vue +530 -0
  19. package/components/indie-wall/StepGoal.vue +169 -0
  20. package/components/indie-wall/StepPackage.vue +145 -0
  21. package/components/indie-wall/StepPay.vue +209 -0
  22. package/components/indie-wall/SupportStepper.vue +372 -0
  23. package/components/invoices/MgInvoiceDownload.vue +50 -0
  24. package/components/pricing/MgBillingToggle.vue +74 -0
  25. package/components/pricing/MgPricingCard.vue +245 -0
  26. package/components/ui/Header/MgMessageCard.vue +147 -0
  27. package/components/ui/Header/MgMessageModal.vue +414 -0
  28. package/components/ui/Header/MgNotificationCard.vue +200 -0
  29. package/components/ui/Header/MgNotificationsModal.vue +125 -0
  30. package/components/ui/MgAnnouncementBanner.vue +147 -0
  31. package/components/ui/MgBanners.vue +23 -0
  32. package/components/ui/MgHeaderComponent.vue +283 -0
  33. package/components/ui/MgHeaderUIConfig.vue +225 -0
  34. package/components/ui/MgHeaderUIUser.vue +301 -0
  35. package/components/ui/MgLoginModal.vue +156 -0
  36. package/components/ui/MgPromotionBanner.vue +185 -0
  37. package/composables/useLogout.ts +42 -0
  38. package/composables/useMgCheckout.ts +287 -0
  39. package/composables/useMgUserNotifications.ts +122 -0
  40. package/composables/usePaymentMethods.ts +75 -0
  41. package/composables/useSubscription.ts +163 -0
  42. package/middleware/auth.global.ts +40 -0
  43. package/nuxt.config.ts +31 -0
  44. package/package.json +40 -0
  45. package/pages/[slug]/index.vue +112 -0
  46. package/pages/about.vue +133 -0
  47. package/pages/blog.vue +430 -0
  48. package/pages/careers.vue +329 -0
  49. package/pages/contact.vue +339 -0
  50. package/pages/faq.vue +317 -0
  51. package/pages/health-check.vue +20 -0
  52. package/pages/icons.vue +58 -0
  53. package/pages/magazine/[slug].vue +209 -0
  54. package/pages/magazine/index.vue +267 -0
  55. package/pages/media-kit/[slug].vue +625 -0
  56. package/pages/mural/[slug].vue +1058 -0
  57. package/pages/partners.vue +290 -0
  58. package/pages/press.vue +237 -0
  59. package/pages/presskit/[slug].vue +191 -0
  60. package/pages/roadmap.vue +355 -0
  61. package/pages/status.vue +199 -0
  62. package/pages/team.vue +266 -0
  63. package/pages/wall/[slug].vue +11 -0
  64. package/plugins/auth.client.ts +17 -0
  65. package/plugins/echo.client.ts +132 -0
  66. package/services/authService.ts +95 -0
  67. package/services/chatService.ts +53 -0
  68. package/services/contactService.ts +35 -0
  69. package/services/documentService.ts +16 -0
  70. package/services/httpService.ts +95 -0
  71. package/services/indieWallService.ts +174 -0
  72. package/services/institutionalService.ts +248 -0
  73. package/services/mediaKitService.ts +51 -0
  74. package/services/notificationsService.ts +20 -0
  75. package/services/pressKitService.ts +55 -0
  76. package/stores/announcement.ts +129 -0
  77. package/stores/auth.ts +86 -0
  78. package/stores/chat.ts +150 -0
  79. package/stores/contact.ts +28 -0
  80. package/stores/document.ts +27 -0
  81. package/stores/index.ts +34 -0
  82. package/stores/institutional.ts +231 -0
  83. package/stores/login.ts +27 -0
  84. package/stores/notifications.ts +133 -0
  85. package/stores/promotion.ts +154 -0
  86. package/types/index.ts +135 -0
  87. package/utils/serialize.ts +29 -0
@@ -0,0 +1,329 @@
1
+ <script setup lang="ts">
2
+ import { storeToRefs } from "pinia";
3
+
4
+ definePageMeta({ layout: "default" });
5
+
6
+ const institutionalStore = useInstitutionalStore();
7
+ const { career, country, department } = storeToRefs(institutionalStore);
8
+
9
+ const config = useRuntimeConfig();
10
+ const systemId = config.public.mgSharedUi?.systemId || import.meta.env.VITE_SYSTEM_ID;
11
+
12
+ const selectedCountry = ref("");
13
+ const selectedDepartment = ref("");
14
+ const currentPage = ref(1);
15
+ const perPage = 10;
16
+
17
+ const showModal = ref(false);
18
+ const modalMessage = ref("");
19
+ const closeModal = () => {
20
+ showModal.value = false;
21
+ };
22
+
23
+ async function fetchCareers() {
24
+ try {
25
+ const params: any = {
26
+ filter: {
27
+ status: 1,
28
+ platform_id: systemId,
29
+ },
30
+ sort: "created_at",
31
+ order: "desc",
32
+ per_page: perPage,
33
+ page: currentPage.value,
34
+ };
35
+ if (selectedCountry.value) params.filter.country_id = selectedCountry.value;
36
+ if (selectedDepartment.value) params.filter.operational_area_id = selectedDepartment.value;
37
+
38
+ await institutionalStore.fetchCareer(params);
39
+ } catch (error: any) {
40
+ modalMessage.value = error.response?.data?.message ?? "Could not load careers";
41
+ showModal.value = true;
42
+ }
43
+ }
44
+
45
+ async function fetchFilters() {
46
+ try {
47
+ await Promise.all([
48
+ institutionalStore.fetchCountry({ per_page: "all" }),
49
+ institutionalStore.fetchDepartment({ per_page: "all" }),
50
+ ]);
51
+ } catch {
52
+ // silent
53
+ }
54
+ }
55
+
56
+ function applyFilters() {
57
+ currentPage.value = 1;
58
+ fetchCareers();
59
+ }
60
+
61
+ function changePage(page: number) {
62
+ currentPage.value = page;
63
+ fetchCareers();
64
+ }
65
+
66
+ onMounted(async () => {
67
+ fetchCareers();
68
+ fetchFilters();
69
+ });
70
+ </script>
71
+
72
+ <template>
73
+ <div id="careers" class="container careers">
74
+ <section>
75
+ <div class="header">
76
+ <div>
77
+ <h1 class="title-4">{{ $t("more.careers.title") }}</h1>
78
+ <p>{{ $t("more.careers.subtitle") }}</p>
79
+ </div>
80
+ </div>
81
+ <div class="container">
82
+ <div class="row">
83
+ <div class="col-8 offset-2">
84
+ <div class="filters">
85
+ <select v-model="selectedCountry" @change="applyFilters">
86
+ <option value="">{{ $t("more.careers.all_countries") }}</option>
87
+ <option
88
+ v-for="c in country?.data?.data"
89
+ :key="c.id"
90
+ :value="c.id"
91
+ >
92
+ {{ c.localized_name || c.name }}
93
+ </option>
94
+ </select>
95
+ <select v-model="selectedDepartment" @change="applyFilters">
96
+ <option value="">{{ $t("more.careers.all_departments") }}</option>
97
+ <option
98
+ v-for="d in department?.data?.data"
99
+ :key="d.id"
100
+ :value="d.id"
101
+ >
102
+ {{ d.localized_name || d.name }}
103
+ </option>
104
+ </select>
105
+ </div>
106
+
107
+ <div v-if="career.status === 'pending'" class="loading">
108
+ <p>{{ $t("components.loading") }}</p>
109
+ </div>
110
+
111
+ <div v-else-if="career.data && career.data.length > 0">
112
+ <div
113
+ v-for="item in career.data"
114
+ :key="item.id"
115
+ class="career-card"
116
+ >
117
+ <div class="career-card__header">
118
+ <h3>{{ item.localized_title }}</h3>
119
+ <div class="career-card__tags">
120
+ <span v-if="item.methodology" class="tag">{{ item.methodology }}</span>
121
+ <span v-if="item.type" class="tag">{{ item.type }}</span>
122
+ <span v-if="item.category" class="tag">{{ item.category }}</span>
123
+ </div>
124
+ </div>
125
+ <p class="career-card__description">{{ item.localized_short_description }}</p>
126
+ <div class="career-card__footer">
127
+ <span v-if="item.country">{{ item.country.localized_name || item.country.name }}</span>
128
+ <span v-if="item.operational_area">{{ item.operational_area.localized_name || item.operational_area.name }}</span>
129
+ </div>
130
+ </div>
131
+
132
+ <div v-if="career.data.meta" class="pagination-controls">
133
+ <button
134
+ :disabled="currentPage <= 1"
135
+ @click="changePage(currentPage - 1)"
136
+ >
137
+ {{ $t("components.previous") }}
138
+ </button>
139
+ <span>{{ currentPage }} / {{ career.data.meta.last_page || 1 }}</span>
140
+ <button
141
+ :disabled="currentPage >= (career.data.meta.last_page || 1)"
142
+ @click="changePage(currentPage + 1)"
143
+ >
144
+ {{ $t("components.next") }}
145
+ </button>
146
+ </div>
147
+ </div>
148
+
149
+ <div v-else class="no-data">
150
+ {{ $t("more.careers.no_data") }}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </section>
156
+
157
+ <div v-if="showModal" class="modal-overlay" @click.self="closeModal">
158
+ <div class="modal-content">
159
+ <p class="modal-message">{{ modalMessage }}</p>
160
+ <button class="modal-close-btn" @click="closeModal">
161
+ {{ $t("more.contact.message_popup.btn_close") }}
162
+ </button>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </template>
167
+
168
+ <style lang="scss" scoped>
169
+ .careers {
170
+ color: var(--inactive);
171
+ font-size: 0.875rem;
172
+
173
+ .header {
174
+ position: relative;
175
+ height: 200px;
176
+ display: flex;
177
+ justify-content: center;
178
+ align-items: center;
179
+ font-size: 0.875rem;
180
+ margin-bottom: 2rem;
181
+
182
+ & > div {
183
+ width: 480px;
184
+ text-align: center;
185
+ }
186
+
187
+ &:before {
188
+ content: " ";
189
+ display: block;
190
+ position: absolute;
191
+ background: url("@/assets/images/bg-careers.png") no-repeat center center / cover;
192
+ width: 100%;
193
+ height: 100%;
194
+ top: 50%;
195
+ transform: translateY(-50%);
196
+ z-index: -1;
197
+ }
198
+ }
199
+
200
+ .filters {
201
+ display: flex;
202
+ gap: 16px;
203
+ margin-bottom: 24px;
204
+
205
+ select {
206
+ flex: 1;
207
+ height: 40px;
208
+ padding: 0 12px;
209
+ border: 1px solid var(--search-bar-border-color);
210
+ background: var(--search-bar-bg);
211
+ color: var(--search-bar-fg);
212
+ font-size: 14px;
213
+ outline: none;
214
+ }
215
+ }
216
+
217
+ .career-card {
218
+ background-color: var(--chip-background-2);
219
+ padding: 16px;
220
+ margin-bottom: 16px;
221
+
222
+ &__header {
223
+ display: flex;
224
+ justify-content: space-between;
225
+ align-items: flex-start;
226
+ margin-bottom: 8px;
227
+
228
+ h3 {
229
+ font-size: 16px;
230
+ font-weight: 600;
231
+ color: var(--card-cover-title);
232
+ }
233
+ }
234
+
235
+ &__tags {
236
+ display: flex;
237
+ gap: 8px;
238
+
239
+ .tag {
240
+ font-size: 12px;
241
+ padding: 2px 8px;
242
+ background: var(--chip-text);
243
+ color: var(--chip-background-2);
244
+ }
245
+ }
246
+
247
+ &__description {
248
+ font-size: 14px;
249
+ line-height: 20px;
250
+ margin-bottom: 12px;
251
+ }
252
+
253
+ &__footer {
254
+ display: flex;
255
+ gap: 16px;
256
+ font-size: 12px;
257
+ color: var(--secondary-info-fg);
258
+ }
259
+ }
260
+
261
+ .no-data {
262
+ text-align: center;
263
+ padding: 32px;
264
+ color: var(--secondary-info-fg);
265
+ }
266
+
267
+ .pagination-controls {
268
+ display: flex;
269
+ justify-content: center;
270
+ align-items: center;
271
+ margin-top: 20px;
272
+ gap: 16px;
273
+
274
+ button {
275
+ padding: 8px 15px;
276
+ background-color: var(--chip-background-2);
277
+ color: var(--card-cover-title);
278
+ border: 1px solid var(--search-bar-border-color);
279
+ cursor: pointer;
280
+ font-size: 14px;
281
+
282
+ &:disabled {
283
+ opacity: 0.5;
284
+ cursor: default;
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ .modal-overlay {
291
+ position: fixed;
292
+ top: 0;
293
+ left: 0;
294
+ right: 0;
295
+ bottom: 0;
296
+ background-color: rgba(0, 0, 0, 0.6);
297
+ display: flex;
298
+ justify-content: center;
299
+ align-items: center;
300
+ z-index: 1000;
301
+
302
+ .modal-content {
303
+ background-color: #0a0a0a;
304
+ padding: 2rem;
305
+ max-width: 500px;
306
+ border-radius: 8px;
307
+ text-align: center;
308
+ }
309
+
310
+ .modal-message {
311
+ margin-bottom: 1.5rem;
312
+ font-size: 1.1rem;
313
+ color: #ffffff;
314
+ }
315
+
316
+ .modal-close-btn {
317
+ padding: 0.5rem 1rem;
318
+ border: 1px solid #060606;
319
+ border-radius: 4px;
320
+ background-color: #060606;
321
+ color: #ffffff;
322
+ cursor: pointer;
323
+
324
+ &:hover {
325
+ border-color: cyan;
326
+ }
327
+ }
328
+ }
329
+ </style>
@@ -0,0 +1,339 @@
1
+ <script setup lang="ts">
2
+ import { storeToRefs } from "pinia";
3
+ import { createMessage } from "../services/contactService";
4
+
5
+ definePageMeta({ layout: "default" });
6
+
7
+ const locale = useNuxtApp().$i18n.locale;
8
+ const config = useRuntimeConfig();
9
+ const systemId = config.public.mgSharedUi?.systemId || import.meta.env.VITE_SYSTEM_ID;
10
+
11
+ const contactStore = useContactStore();
12
+ const { country, subject } = storeToRefs(contactStore);
13
+
14
+ onMounted(async () => {
15
+ contactStore.fetchCountry({ filter: { enabled: 1 }, sort: "name", order: "asc", per_page: "all", lang: locale.value });
16
+ contactStore.fetchSubject({ sort: "id", order: "asc", per_page: "all", lang: locale.value });
17
+ });
18
+
19
+ const initialFormData = {
20
+ name: "",
21
+ email: "",
22
+ subject_id: "",
23
+ language_slug: locale.value,
24
+ country_id: "",
25
+ message: "",
26
+ privacy_policy: 1,
27
+ origin_network_id: systemId,
28
+ status: 0,
29
+ };
30
+ const formData = ref({ ...initialFormData });
31
+
32
+ const maxChars = 1200;
33
+ const remainingChars = computed(() => maxChars - formData.value.message.length);
34
+ const handleMessageInput = (event: Event) => {
35
+ const target = event.target as HTMLTextAreaElement;
36
+ if (target.value.length >= maxChars) {
37
+ target.value = target.value.substring(0, maxChars);
38
+ formData.value.message = target.value;
39
+ }
40
+ };
41
+
42
+ const showModal = ref(false);
43
+ const modalMessage = ref("");
44
+ const closeModal = () => {
45
+ showModal.value = false;
46
+ };
47
+
48
+ interface InputError {
49
+ status: boolean;
50
+ error: string;
51
+ }
52
+
53
+ const inputErrors = ref<Record<string, InputError>>({
54
+ email: { status: false, error: "" },
55
+ name: { status: false, error: "" },
56
+ message: { status: false, error: "" },
57
+ subject_id: { status: false, error: "" },
58
+ country_id: { status: false, error: "" },
59
+ });
60
+
61
+ watchEffect(() => { if (formData.value.email) inputErrors.value.email.status = false; });
62
+ watchEffect(() => { if (formData.value.name) inputErrors.value.name.status = false; });
63
+ watchEffect(() => { if (formData.value.message) inputErrors.value.message.status = false; });
64
+ watchEffect(() => { if (formData.value.subject_id) inputErrors.value.subject_id.status = false; });
65
+ watchEffect(() => { if (formData.value.country_id) inputErrors.value.country_id.status = false; });
66
+
67
+ const submitForm = async () => {
68
+ try {
69
+ const data = {
70
+ ...formData.value,
71
+ subject_id: Number(formData.value.subject_id),
72
+ country_id: Number(formData.value.country_id),
73
+ };
74
+
75
+ await createMessage(data);
76
+ formData.value = { ...initialFormData };
77
+ modalMessage.value = String($t("more.contact.message_popup.success") || "Your message was successfully sent!");
78
+ showModal.value = true;
79
+ for (const key in inputErrors.value) {
80
+ inputErrors.value[key] = { status: false, error: "" };
81
+ }
82
+ } catch (error: any) {
83
+ if (error.response?.status === 422) {
84
+ for (const key in inputErrors.value) {
85
+ inputErrors.value[key] = { status: false, error: "" };
86
+ }
87
+ const errors = error.response.data.errors;
88
+ for (const key in errors) {
89
+ if (key in inputErrors.value) {
90
+ inputErrors.value[key] = { status: true, error: errors[key].join(", ") };
91
+ }
92
+ }
93
+ } else {
94
+ modalMessage.value = String($t("more.contact.message_popup.error") || "There was an error sending your message. Please try again.");
95
+ showModal.value = true;
96
+ }
97
+ }
98
+ };
99
+
100
+ const { $t } = useNuxtApp().$i18n;
101
+ </script>
102
+
103
+ <template>
104
+ <div class="container contact" id="contact">
105
+ <section>
106
+ <div class="header">
107
+ <div>
108
+ <h1 class="title-4">{{ $t("more.contact.title") }}</h1>
109
+ <p>{{ $t("more.contact.subtitle") }}</p>
110
+ </div>
111
+ </div>
112
+ <div class="container">
113
+ <div class="row">
114
+ <div class="col-12">
115
+ <h6 class="title-section">{{ $t("more.contact.subtitle_2") }}</h6>
116
+ </div>
117
+ <div class="col-12 col-md-6 offset-md-3">
118
+ <form class="contact-form" @submit.prevent="submitForm">
119
+ <div class="form-group" :class="{ error: inputErrors.name.status }">
120
+ <label>{{ $t("more.contact.name") }}</label>
121
+ <input v-model="formData.name" type="text" :placeholder="$t('more.contact.name')" />
122
+ <span v-if="inputErrors.name.status" class="error-msg">{{ inputErrors.name.error }}</span>
123
+ </div>
124
+
125
+ <div class="form-group" :class="{ error: inputErrors.email.status }">
126
+ <label>{{ $t("more.contact.email") }}</label>
127
+ <input v-model="formData.email" type="email" :placeholder="$t('more.contact.email')" />
128
+ <span v-if="inputErrors.email.status" class="error-msg">{{ inputErrors.email.error }}</span>
129
+ </div>
130
+
131
+ <div class="form-group" :class="{ error: inputErrors.subject_id.status }">
132
+ <label>{{ $t("more.contact.subject") }}</label>
133
+ <select v-model="formData.subject_id">
134
+ <option value="">{{ $t("more.contact.select_subject") }}</option>
135
+ <option
136
+ v-for="s in subject?.data?.data"
137
+ :key="s.id"
138
+ :value="s.id"
139
+ >
140
+ {{ s.localized_name || s.name }}
141
+ </option>
142
+ </select>
143
+ <span v-if="inputErrors.subject_id.status" class="error-msg">{{ inputErrors.subject_id.error }}</span>
144
+ </div>
145
+
146
+ <div class="form-group" :class="{ error: inputErrors.country_id.status }">
147
+ <label>{{ $t("more.contact.country") }}</label>
148
+ <select v-model="formData.country_id">
149
+ <option value="">{{ $t("more.contact.select_country") }}</option>
150
+ <option
151
+ v-for="c in country?.data?.data"
152
+ :key="c.id"
153
+ :value="c.id"
154
+ >
155
+ {{ c.localized_name || c.name }}
156
+ </option>
157
+ </select>
158
+ <span v-if="inputErrors.country_id.status" class="error-msg">{{ inputErrors.country_id.error }}</span>
159
+ </div>
160
+
161
+ <div class="form-group" :class="{ error: inputErrors.message.status }">
162
+ <label>{{ $t("more.contact.message_title") }}</label>
163
+ <textarea
164
+ v-model="formData.message"
165
+ :placeholder="$t('more.contact.message_title')"
166
+ rows="5"
167
+ @input="handleMessageInput"
168
+ ></textarea>
169
+ <div class="char-counter">{{ remainingChars }} {{ $t("more.contact.characters_remaining") }}</div>
170
+ <span v-if="inputErrors.message.status" class="error-msg">{{ inputErrors.message.error }}</span>
171
+ </div>
172
+
173
+ <button type="submit" class="btn-contact">
174
+ {{ $t("more.contact.btn_send_message") }}
175
+ </button>
176
+ </form>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </section>
181
+
182
+ <div v-if="showModal" class="modal-overlay" @click.self="closeModal">
183
+ <div class="modal-content">
184
+ <p class="modal-message">{{ modalMessage }}</p>
185
+ <button class="modal-close-btn" @click="closeModal">
186
+ {{ $t("more.contact.message_popup.btn_close") }}
187
+ </button>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </template>
192
+
193
+ <style lang="scss" scoped>
194
+ .contact {
195
+ color: var(--inactive);
196
+ font-size: 0.875rem;
197
+
198
+ .header {
199
+ position: relative;
200
+ height: 200px;
201
+ display: flex;
202
+ justify-content: center;
203
+ align-items: center;
204
+ margin-bottom: 2rem;
205
+
206
+ & > div {
207
+ width: 480px;
208
+ text-align: center;
209
+ }
210
+
211
+ &:before {
212
+ content: " ";
213
+ display: block;
214
+ position: absolute;
215
+ background: url("@/assets/images/bg-contact.png") no-repeat center center / cover;
216
+ width: 100%;
217
+ height: 100%;
218
+ top: 50%;
219
+ transform: translateY(-50%);
220
+ z-index: -1;
221
+ }
222
+ }
223
+
224
+ .title-section {
225
+ text-align: center;
226
+ font-size: 16px;
227
+ font-weight: 600;
228
+ color: var(--card-cover-title);
229
+ margin-bottom: 24px;
230
+ }
231
+
232
+ .contact-form {
233
+ display: flex;
234
+ flex-direction: column;
235
+ gap: 16px;
236
+
237
+ .form-group {
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 4px;
241
+
242
+ label {
243
+ font-size: 14px;
244
+ font-weight: 500;
245
+ color: var(--card-cover-title);
246
+ }
247
+
248
+ input, select, textarea {
249
+ padding: 8px 12px;
250
+ border: 1px solid var(--search-bar-border-color);
251
+ background: var(--search-bar-bg);
252
+ color: var(--search-bar-fg);
253
+ font-size: 14px;
254
+ outline: none;
255
+
256
+ &:focus {
257
+ border-color: var(--chip-text);
258
+ }
259
+ }
260
+
261
+ textarea {
262
+ resize: vertical;
263
+ }
264
+
265
+ &.error input,
266
+ &.error select,
267
+ &.error textarea {
268
+ border-color: #d9534f;
269
+ }
270
+
271
+ .error-msg {
272
+ font-size: 12px;
273
+ color: #d9534f;
274
+ }
275
+
276
+ .char-counter {
277
+ font-size: 12px;
278
+ color: var(--secondary-info-fg);
279
+ text-align: right;
280
+ }
281
+ }
282
+
283
+ .btn-contact {
284
+ padding: 10px 24px;
285
+ background: var(--chip-text);
286
+ color: var(--chip-background-2);
287
+ border: none;
288
+ font-size: 14px;
289
+ font-weight: 600;
290
+ cursor: pointer;
291
+ align-self: flex-start;
292
+
293
+ &:hover {
294
+ opacity: 0.9;
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ .modal-overlay {
301
+ position: fixed;
302
+ top: 0;
303
+ left: 0;
304
+ right: 0;
305
+ bottom: 0;
306
+ background-color: rgba(0, 0, 0, 0.6);
307
+ display: flex;
308
+ justify-content: center;
309
+ align-items: center;
310
+ z-index: 1000;
311
+
312
+ .modal-content {
313
+ background-color: #0a0a0a;
314
+ padding: 2rem;
315
+ max-width: 500px;
316
+ border-radius: 8px;
317
+ text-align: center;
318
+ }
319
+
320
+ .modal-message {
321
+ margin-bottom: 1.5rem;
322
+ font-size: 1.1rem;
323
+ color: #ffffff;
324
+ }
325
+
326
+ .modal-close-btn {
327
+ padding: 0.5rem 1rem;
328
+ border: 1px solid #060606;
329
+ border-radius: 4px;
330
+ background-color: #060606;
331
+ color: #ffffff;
332
+ cursor: pointer;
333
+
334
+ &:hover {
335
+ border-color: cyan;
336
+ }
337
+ }
338
+ }
339
+ </style>