@mundogamernetwork/shared-ui 1.8.6 → 1.8.8

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.
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <client-only>
3
+ <div ref="mapContainer" class="mk-map-container">
4
+ <l-map v-if="mapReady" :zoom="0.5" :center="[0, 0]" :use-global-leaflet="false">
5
+ <l-tile-layer url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" layer-type="base"
6
+ attribution="OpenStreetMap" />
7
+
8
+ <l-circle-marker v-for="(country, index) in countries" :key="index"
9
+ :lat-lng="[country.latitude, country.longitude]"
10
+ :radius="calculateRadius(country.percent)" color="#FDB215" fill-opacity="0.6">
11
+ <l-tooltip :content="`${country.name}: ${country.percent}%`" />
12
+ </l-circle-marker>
13
+ </l-map>
14
+ </div>
15
+ </client-only>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ // Ported from agency-frontend/components/MapCountries.vue (the old
20
+ // casting-details page's audience map) — same lib/tile source/marker
21
+ // styling, adapted to the media-kit's audience_geo field names
22
+ // (name/percent instead of localized_name/percentage).
23
+ import 'leaflet/dist/leaflet.css';
24
+ import { shallowRef, ref, onMounted } from 'vue';
25
+
26
+ defineProps<{
27
+ countries: {
28
+ latitude: number;
29
+ longitude: number;
30
+ percent: number;
31
+ name: string;
32
+ }[];
33
+ }>();
34
+
35
+ const mapContainer = ref<HTMLElement | null>(null);
36
+ const mapReady = ref(false);
37
+ const LMap = shallowRef();
38
+ const LTileLayer = shallowRef();
39
+ const LCircleMarker = shallowRef();
40
+ const LTooltip = shallowRef();
41
+
42
+ const calculateRadius = (percent: number) => {
43
+ return Math.max(2, percent * 0.3);
44
+ };
45
+
46
+ if (import.meta.client) {
47
+ const { LMap: LMapComponent, LTileLayer: LTileLayerComponent, LCircleMarker: LCircleMarkerComponent, LTooltip: LTooltipComponent } = await import('@vue-leaflet/vue-leaflet');
48
+ LMap.value = LMapComponent;
49
+ LTileLayer.value = LTileLayerComponent;
50
+ LCircleMarker.value = LCircleMarkerComponent;
51
+ LTooltip.value = LTooltipComponent;
52
+ }
53
+
54
+ onMounted(() => {
55
+ mapReady.value = true;
56
+ });
57
+ </script>
58
+
59
+ <style scoped>
60
+ .mk-map-container {
61
+ height: 220px;
62
+ width: 100%;
63
+ position: relative;
64
+ }
65
+
66
+ :deep(.leaflet-container) {
67
+ height: 100% !important;
68
+ width: 100% !important;
69
+ background: #0a0a0f;
70
+ }
71
+
72
+ :deep(.leaflet-bottom) {
73
+ bottom: 0;
74
+ display: none !important;
75
+ }
76
+ </style>
@@ -172,7 +172,7 @@ const detailRoute = computed(() =>
172
172
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 16 17" fill="none">
173
173
  <path d="M13.3333 4.1112L12.8666 3.64453L11.9333 4.57786L10.9333 3.64453L8.33329 4.1112L2.66663 9.77786L7.19996 14.3112L12.8666 8.64453L13.3333 6.04453L12.4 5.1112L13.3333 4.1112ZM12.6 6.24453L12.2666 8.3112L7.19996 13.3779L3.59996 9.77786L8.66663 4.7112L10.7333 4.3112L11.4666 5.04453L11 5.5112L10.5333 5.04453L9.99996 5.57786L11.4 6.97786L11.8666 6.5112L11.4 6.04453L11.8666 5.57786L12.6 6.24453Z" fill="#D297FF"/>
174
174
  </svg>
175
- {{ card.type }}
175
+ {{ typeof card.type === "string" ? card.type : card.type.name }}
176
176
  </span>
177
177
  </div>
178
178
  </div>
package/nuxt.config.ts CHANGED
@@ -61,6 +61,13 @@ try {
61
61
  }
62
62
 
63
63
  export default defineNuxtConfig({
64
+ // Nuxt/c12 merges extended-layer config via defu (arrays concatenate), so
65
+ // this reaches every consuming app automatically — matches the storesDirs
66
+ // pattern below. @vue-leaflet/vue-leaflet ships untranspiled ESM that
67
+ // breaks Nuxt's build without this, needed by MediaKit/MapCountries.vue.
68
+ build: {
69
+ transpile: ['@vue-leaflet/vue-leaflet'],
70
+ },
64
71
  components: [
65
72
  {
66
73
  path: `${packageRoot}/components`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.8.6",
3
+ "version": "1.8.8",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -30,9 +30,11 @@
30
30
  "peerDependencies": {
31
31
  "@pinia-plugin-persistedstate/nuxt": ">=1.0.0",
32
32
  "@pinia/nuxt": ">=0.5.0",
33
+ "@vue-leaflet/vue-leaflet": ">=0.10.0",
33
34
  "axios": ">=1.0.0",
34
35
  "hls.js": ">=1.5.0",
35
36
  "laravel-echo": ">=1.15.0",
37
+ "leaflet": ">=1.9.0",
36
38
  "nuxt": ">=3.13.0",
37
39
  "pinia": ">=2.1.0",
38
40
  "pusher-js": ">=8.0.0",
@@ -41,9 +43,12 @@
41
43
  "devDependencies": {
42
44
  "@pinia-plugin-persistedstate/nuxt": "^1.2.1",
43
45
  "@pinia/nuxt": "^0.5.5",
46
+ "@types/leaflet": "^1.9.18",
47
+ "@vue-leaflet/vue-leaflet": "^0.10.1",
44
48
  "axios": "^1.18.1",
45
49
  "hls.js": "^1.6.16",
46
50
  "laravel-echo": "^2.3.7",
51
+ "leaflet": "^1.9.4",
47
52
  "nuxt": "^3.15.4",
48
53
  "pinia": "^2.3.1",
49
54
  "pusher-js": "^8.5.0",
@@ -130,7 +130,9 @@
130
130
  <div
131
131
  class="keys-badge"
132
132
  v-if="card.available_count > 0 && card.user_can_access && !card.isExpired && !card.isUpcoming"
133
+ :title="$t('keys.campaigns.keys_left', { n: card.available_count })"
133
134
  >
135
+ <i class="fa-solid fa-key" />
134
136
  {{ card.available_count }}
135
137
  </div>
136
138
 
@@ -221,10 +223,6 @@
221
223
  +{{ card.platforms.length - 4 }}
222
224
  </span>
223
225
  </div>
224
- <div v-if="card.available_count > 0 && card.user_can_access && !card.isExpired && !card.isUpcoming" class="card-availability">
225
- {{ $t("keys.campaigns.keys_left", { n: card.available_count }) }}
226
- </div>
227
- <div v-if="card.gameName && card.gameName !== card.text" class="card-campaign-name">{{ card.text }}</div>
228
226
 
229
227
  <div class="buttons">
230
228
  <template v-if="!card.user_can_access">
@@ -1237,7 +1235,7 @@ onUnmounted(() => {
1237
1235
  }
1238
1236
 
1239
1237
  .text {
1240
- min-height: 210px;
1238
+ min-height: 170px;
1241
1239
 
1242
1240
  .card-name {
1243
1241
  font-size: 16px;
@@ -1254,18 +1252,6 @@ onUnmounted(() => {
1254
1252
  gap: 6px;
1255
1253
  }
1256
1254
 
1257
- .card-availability {
1258
- font-size: 12px;
1259
- color: var(--key-accent, var(--primary, #D297FF));
1260
- font-weight: 600;
1261
- }
1262
-
1263
- .card-campaign-name {
1264
- font-size: 13px;
1265
- font-weight: 400;
1266
- color: var(--secondary-info-fg);
1267
- }
1268
-
1269
1255
  .card-meta-row {
1270
1256
  display: flex;
1271
1257
  align-items: center;
@@ -3,6 +3,7 @@ import { fetchPublicMediaKit, trackMediaKitEvent } from '../../services/mediaKit
3
3
  import { sendMediaKitProposal } from '../../services/mediaKitProposalService';
4
4
  import MediaKitWallBlock from '../../components/indie-wall/MediaKitWallBlock.vue';
5
5
  import MgMediaKitStore from '../../components/MediaKit/MgMediaKitStore.vue';
6
+ import MapCountries from '../../components/MediaKit/MapCountries.vue';
6
7
 
7
8
  const route = useRoute();
8
9
  const slug = computed(() => route.params.slug as string);
@@ -78,10 +79,32 @@ const websiteUrl = computed<string | null>(() => streamer.value?.website ?? null
78
79
 
79
80
  // ── D2 ecosystem data ──
80
81
  const selectedGames = computed<string[]>(() => kit.value?.selected_games ?? []);
82
+ const languages = computed<any[]>(() => kit.value?.languages ?? []);
81
83
  const audienceGeo = computed<any[]>(() => kit.value?.audience_geo ?? []);
84
+ // A handful of countries.latitude/longitude rows are historically unset —
85
+ // the list still renders those (name/percent don't need coordinates), the
86
+ // map just skips them.
87
+ const audienceGeoMappable = computed(() => audienceGeo.value.filter((c: any) => c.latitude != null && c.longitude != null));
82
88
  const platformsPlayed = computed<any[]>(() => kit.value?.platforms_played ?? []);
83
89
  const academyMentor = computed(() => kit.value?.academy_mentor ?? null);
84
90
  const communityStats = computed(() => kit.value?.community ?? null);
91
+ // Business signals collected by the /trails-form onboarding wizard, exposed
92
+ // via PublicMediaKitController::getBusinessSignals() (2026-07-23 audit).
93
+ const businessSignals = computed<Record<string, any> | null>(() => kit.value?.business_signals ?? null);
94
+ const hasAvailabilitySection = computed(() =>
95
+ !!businessSignals.value && (
96
+ !!businessSignals.value.contact_type
97
+ || !!businessSignals.value.availability_hour
98
+ || !!businessSignals.value.availability_days
99
+ ),
100
+ );
101
+ const hasSelfReportedMetrics = computed(() =>
102
+ !!businessSignals.value && (
103
+ !!businessSignals.value.avg_views_per_content
104
+ || !!businessSignals.value.avg_monthly_views
105
+ || !!businessSignals.value.audience_profile
106
+ ),
107
+ );
85
108
  const tvChannelLogin = computed<string | null>(() => streamer.value?.tv_channel_login ?? null);
86
109
  const tvSiteUrl = import.meta.env.VITE_TV_SITE_URL || 'https://mundogamer.tv';
87
110
  const tvChannelUrl = computed(() => tvChannelLogin.value
@@ -98,6 +121,14 @@ const PLATFORM_LABEL: Record<string, string> = {
98
121
  linkedin: 'LinkedIn', bluesky: 'Bluesky',
99
122
  };
100
123
 
124
+ // Font Awesome brand icon per platform — no entry means no official FA glyph
125
+ // exists yet (e.g. Kick), the template falls back to the 2-letter label.
126
+ const PLATFORM_ICON: Record<string, string> = {
127
+ instagram: 'fa-instagram', tiktok: 'fa-tiktok', youtube: 'fa-youtube', twitch: 'fa-twitch',
128
+ x: 'fa-x-twitter', facebook: 'fa-facebook', discord: 'fa-discord', telegram: 'fa-telegram',
129
+ linkedin: 'fa-linkedin', bluesky: 'fa-bluesky',
130
+ };
131
+
101
132
  const socials = computed(() => {
102
133
  const s = streamer.value?.social_links || {};
103
134
  return Object.entries(s).filter(([, v]) => v).map(([k, v]) => ({ platform: k, url: v as string }));
@@ -110,6 +141,14 @@ const scheduleByDay = computed(() => {
110
141
  return [0, 1, 2, 3, 4, 5, 6].map((d) => ({ d, key: DOW[d], slot: map[d] || null }));
111
142
  });
112
143
 
144
+ // Sidebar "hire" CTA jumps to the rates tab — on mobile the tabs are further
145
+ // down the stacked layout, so switching activeTab alone can leave the change
146
+ // off-screen; scroll it into view too.
147
+ function goToRates() {
148
+ activeTab.value = 1;
149
+ document.getElementById('mk-tabs')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
150
+ }
151
+
113
152
  // ── Tabs: template picks which one opens first, casting-details-style ──
114
153
  const TAB_KEYS = ['profile', 'rates', 'performance'] as const;
115
154
  const TEMPLATE_DEFAULT_TAB: Record<string, number> = {
@@ -272,6 +311,10 @@ function onDownload() {
272
311
  <div class="mk-badges">
273
312
  <span v-if="streamer.platform" class="mk-badge plat">{{ streamer.platform }}</span>
274
313
  <span v-if="streamer.tier && streamer.tier !== 'free'" class="mk-badge tier">{{ String(streamer.tier).toUpperCase() }}</span>
314
+ <span v-if="businessSignals?.available_for_campaigns === true" class="mk-badge available">{{ $t('kit.media.available_for_campaigns') }}</span>
315
+ <span v-else-if="businessSignals?.available_for_campaigns === false" class="mk-badge unavailable">{{ $t('kit.media.unavailable_for_campaigns') }}</span>
316
+ <span v-if="businessSignals?.accept_proposals === false" class="mk-badge muted">{{ $t('kit.media.not_accepting_proposals') }}</span>
317
+ <span v-if="businessSignals?.participated_in_campaigns" class="mk-badge muted">{{ $t('kit.media.past_campaigns') }}</span>
275
318
  </div>
276
319
 
277
320
  <div v-if="showShortlistBar" class="mk-internal-bar no-print">
@@ -284,7 +327,8 @@ function onDownload() {
284
327
  </div>
285
328
 
286
329
  <div class="mk-actions no-print">
287
- <a class="kit-btn kit-btn-primary" @click="onDownload">{{ $t('kit.media.download_pdf') }}</a>
330
+ <a v-if="!isOwnKit" class="kit-btn kit-btn-primary" @click="goToRates">{{ $t('kit.media.hire') }}</a>
331
+ <a class="kit-btn" :class="isOwnKit ? 'kit-btn-primary' : 'kit-btn-ghost'" @click="onDownload">{{ $t('kit.media.download_pdf') }}</a>
288
332
  <a v-if="streamer.contact_email" class="kit-btn kit-btn-ghost" :href="`mailto:${streamer.contact_email}`" @click="track('contact')">{{ $t('kit.media.contact') }}</a>
289
333
  </div>
290
334
 
@@ -314,22 +358,26 @@ function onDownload() {
314
358
 
315
359
  <div v-if="socials.length" class="mk-socials no-print">
316
360
  <a v-for="s in socials" :key="s.platform" class="mk-soc" :href="s.url" target="_blank" rel="noopener" :title="PLATFORM_LABEL[s.platform] || s.platform" @click="track('click')">
317
- {{ (PLATFORM_LABEL[s.platform] || s.platform).slice(0, 2).toUpperCase() }}
361
+ <i v-if="PLATFORM_ICON[s.platform]" class="fa-brands" :class="PLATFORM_ICON[s.platform]" />
362
+ <span v-else class="mk-soc-fallback">{{ (PLATFORM_LABEL[s.platform] || s.platform).slice(0, 2).toUpperCase() }}</span>
318
363
  </a>
319
364
  </div>
320
365
  </aside>
321
366
 
322
367
  <!-- ══════════════ MAIN: TABS ══════════════ -->
323
368
  <main class="mk-main">
324
- <div class="mk-tabs no-print">
369
+ <div id="mk-tabs" class="mk-tabs no-print">
325
370
  <button class="mk-tab" :class="{ active: activeTab === 0 }" @click="activeTab = 0">{{ $t('kit.media.tab_profile') }}</button>
326
- <button v-if="hasRatesTab" class="mk-tab" :class="{ active: activeTab === 1 }" @click="activeTab = 1">{{ $t('kit.media.tab_rates') }}</button>
371
+ <button class="mk-tab" :class="{ active: activeTab === 1 }" @click="activeTab = 1">{{ $t('kit.media.tab_rates') }}</button>
327
372
  <button v-if="hasPerformanceTab" class="mk-tab" :class="{ active: activeTab === 2 }" @click="activeTab = 2">{{ $t('kit.media.tab_performance') }}</button>
328
373
  </div>
329
374
 
330
375
  <!-- ── TAB: PERFIL ── -->
331
376
  <div class="mk-tabpanel" :class="{ active: activeTab === 0 }" data-print-tab="profile">
332
- <p v-if="streamer.bio" class="mk-bio">{{ streamer.bio }}</p>
377
+ <section v-if="streamer.bio" class="kit-block" data-sec="about">
378
+ <h2>{{ $t('kit.media.about_title', { name: streamer.name }) }}</h2>
379
+ <p class="mk-bio">{{ streamer.bio }}</p>
380
+ </section>
333
381
 
334
382
  <section v-if="academyMentor" class="kit-block" data-sec="academy">
335
383
  <h2>{{ $t('kit.media.academy_mentor_title') }}</h2>
@@ -347,7 +395,10 @@ function onDownload() {
347
395
  <div class="mk-platforms">
348
396
  <a v-for="p in platforms" :key="p.platform" class="mk-platform" :href="p.url || undefined" target="_blank" rel="noopener" @click="track('click')">
349
397
  <div class="pl-head">
350
- <span class="pl-name">{{ PLATFORM_LABEL[p.platform] || p.platform }}</span>
398
+ <span class="pl-name-group">
399
+ <i v-if="PLATFORM_ICON[p.platform]" class="fa-brands pl-icon" :class="PLATFORM_ICON[p.platform]" />
400
+ <span class="pl-name">{{ PLATFORM_LABEL[p.platform] || p.platform }}</span>
401
+ </span>
351
402
  <span v-if="p.verified" class="pl-verified" :title="$t('kit.media.verified')">✓</span>
352
403
  </div>
353
404
  <div class="pl-followers">{{ fmt(p.followers) }}</div>
@@ -359,8 +410,27 @@ function onDownload() {
359
410
  </div>
360
411
  </section>
361
412
 
413
+ <section v-if="hasSelfReportedMetrics" class="kit-block" data-sec="self-reported-metrics">
414
+ <h2>{{ $t('kit.media.self_reported_metrics') }}</h2>
415
+ <div class="mk-metrics">
416
+ <div v-if="businessSignals.avg_views_per_content" class="mk-metric">
417
+ <span class="val">{{ businessSignals.avg_views_per_content }}</span>
418
+ <span class="lab">{{ $t('kit.media.avg_views_per_content') }}</span>
419
+ </div>
420
+ <div v-if="businessSignals.avg_monthly_views" class="mk-metric">
421
+ <span class="val">{{ businessSignals.avg_monthly_views }}</span>
422
+ <span class="lab">{{ $t('kit.media.avg_monthly_views') }}</span>
423
+ </div>
424
+ <div v-if="businessSignals.audience_profile" class="mk-metric">
425
+ <span class="val val-sm">{{ businessSignals.audience_profile }}</span>
426
+ <span class="lab">{{ $t('kit.media.audience_profile') }}</span>
427
+ </div>
428
+ </div>
429
+ </section>
430
+
362
431
  <section v-if="audienceGeo.length" class="kit-block" data-sec="geo">
363
432
  <h2>{{ $t('kit.media.audience_geo') }}</h2>
433
+ <MapCountries v-if="audienceGeoMappable.length" :countries="audienceGeoMappable" class="mk-geo-map no-print" />
364
434
  <div class="mk-geo">
365
435
  <div v-for="c in audienceGeo" :key="c.isocode" class="mk-bar">
366
436
  <div class="top">
@@ -408,6 +478,30 @@ function onDownload() {
408
478
  </div>
409
479
  </section>
410
480
 
481
+ <!-- Languages the creator works in — was on the old casting-details
482
+ page, dropped when it was consolidated into this shared builder. -->
483
+ <section v-if="languages.length" class="kit-block" data-sec="languages">
484
+ <h2>{{ $t('kit.media.languages') }}</h2>
485
+ <div class="mk-tags">
486
+ <span v-for="l in languages" :key="l.id" class="mk-tag">{{ l.name }}</span>
487
+ </div>
488
+ </section>
489
+
490
+ <section v-if="hasAvailabilitySection" class="kit-block" data-sec="availability-preference">
491
+ <h2>{{ $t('kit.media.availability_title') }}</h2>
492
+ <div class="mk-cols2">
493
+ <div v-if="businessSignals.contact_type" class="kit-card mk-pad">
494
+ <h3>{{ $t('kit.media.contact_type') }}</h3>
495
+ <span class="mk-tag">{{ businessSignals.contact_type }}</span>
496
+ </div>
497
+ <div v-if="businessSignals.availability_hour || businessSignals.availability_days" class="kit-card mk-pad">
498
+ <h3>{{ $t('kit.media.best_time') }}</h3>
499
+ <span v-if="businessSignals.availability_hour" class="mk-tag">{{ businessSignals.availability_hour }}</span>
500
+ <span v-if="businessSignals.availability_days" class="mk-tag">{{ businessSignals.availability_days }}</span>
501
+ </div>
502
+ </div>
503
+ </section>
504
+
411
505
  <section v-if="platformsPlayed.length" class="kit-block" data-sec="platforms-played">
412
506
  <h2>{{ $t('kit.media.platforms_played') }}</h2>
413
507
  <div class="mk-platforms-played">
@@ -452,8 +546,12 @@ function onDownload() {
452
546
  </div>
453
547
 
454
548
  <!-- ── TAB: RATE CARDS ── -->
455
- <div v-if="hasRatesTab" class="mk-tabpanel" :class="{ active: activeTab === 1 }" data-print-tab="rates">
456
- <div class="mk-rc-layout">
549
+ <div class="mk-tabpanel" :class="{ active: activeTab === 1 }" data-print-tab="rates">
550
+ <div v-if="!hasRatesTab" class="mk-rc-empty">
551
+ <p>{{ isOwnKit ? $t('kit.media.rates_empty_own') : $t('kit.media.rates_empty_viewer') }}</p>
552
+ <a v-if="!isOwnKit && streamer.contact_email" class="kit-btn kit-btn-primary" :href="`mailto:${streamer.contact_email}`" @click="track('contact')">{{ $t('kit.media.contact') }}</a>
553
+ </div>
554
+ <div v-else class="mk-rc-layout">
457
555
  <div class="mk-rc-main">
458
556
  <div v-if="bundles.length" class="mk-rc-group">
459
557
  <h2>{{ $t('kit.media.rates_bundles') }}</h2>
@@ -665,6 +763,9 @@ function onDownload() {
665
763
 
666
764
  &.plat { background: var(--kit-surface-2); border: 1px solid var(--kit-line); }
667
765
  &.tier { background: var(--kit-accent); color: var(--kit-accent-ink); }
766
+ &.available { background: rgba(74, 222, 128, .12); color: #4ade80; border: 1px solid rgba(74, 222, 128, .3); }
767
+ &.unavailable { background: rgba(239, 68, 68, .12); color: #ef4444; border: 1px solid rgba(239, 68, 68, .3); }
768
+ &.muted { background: rgba(139, 146, 160, .1); color: var(--kit-muted); border: 1px solid rgba(139, 146, 160, .2); }
668
769
  }
669
770
 
670
771
  .mk-internal-bar {
@@ -715,11 +816,12 @@ function onDownload() {
715
816
  display: flex; align-items: center; justify-content: center;
716
817
  border: 1px solid var(--kit-line);
717
818
  color: var(--kit-muted);
718
- font-size: 11px; font-weight: 700;
819
+ font-size: 15px;
719
820
  text-decoration: none;
720
821
 
721
822
  &:hover { border-color: var(--kit-accent); color: var(--kit-accent); }
722
823
  }
824
+ .mk-soc-fallback { font-size: 11px; font-weight: 700; }
723
825
 
724
826
  /* Buttons (shared) */
725
827
  .kit-btn {
@@ -823,6 +925,8 @@ function onDownload() {
823
925
 
824
926
  &:hover { border-color: var(--kit-accent); }
825
927
  .pl-head { display: flex; align-items: center; justify-content: space-between; }
928
+ .pl-name-group { display: flex; align-items: center; gap: 6px; }
929
+ .pl-icon { font-size: 13px; color: var(--kit-accent); }
826
930
  .pl-name { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--kit-muted); }
827
931
  .pl-verified { color: #27ae60; font-weight: 700; }
828
932
  .pl-followers { font-family: 'Rajdhani', sans-serif; font-weight: 700; font-size: 24px; }
@@ -830,6 +934,7 @@ function onDownload() {
830
934
  }
831
935
 
832
936
  /* Geo */
937
+ .mk-geo-map { margin-bottom: 16px; border: 1px solid var(--kit-line); }
833
938
  .mk-geo { display: flex; flex-direction: column; gap: 10px; }
834
939
  .mk-flag { width: 18px; height: 13px; object-fit: cover; margin-right: 8px; vertical-align: middle; }
835
940
 
@@ -909,6 +1014,15 @@ function onDownload() {
909
1014
  }
910
1015
 
911
1016
  /* Rate cards / builder */
1017
+ .mk-rc-empty {
1018
+ display: flex;
1019
+ flex-direction: column;
1020
+ align-items: center;
1021
+ gap: 16px;
1022
+ text-align: center;
1023
+ padding: 60px 24px;
1024
+ color: var(--kit-muted);
1025
+ }
912
1026
  .mk-rc-layout { display: grid; grid-template-columns: 1fr 260px; gap: 20px; align-items: start; }
913
1027
  @media (max-width: 760px) { .mk-rc-layout { grid-template-columns: 1fr; } }
914
1028
  .mk-rc-group { display: flex; flex-direction: column; gap: 1px; background: var(--kit-line); margin-bottom: 24px; }
@@ -949,7 +1063,7 @@ function onDownload() {
949
1063
  .mk-metric {
950
1064
  background: var(--kit-surface); border: 1px solid var(--kit-line); padding: 20px 16px; text-align: center; position: relative; overflow: hidden;
951
1065
  &::before { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 2px; background: var(--kit-accent); }
952
- .val { font-family: 'Rajdhani', sans-serif; font-weight: 700; font-size: 30px; line-height: 1; &.accent { color: var(--kit-accent); } }
1066
+ .val { font-family: 'Rajdhani', sans-serif; font-weight: 700; font-size: 30px; line-height: 1; &.accent { color: var(--kit-accent); } &.val-sm { font-size: 16px; } }
953
1067
  .lab { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--kit-muted); margin-top: 8px; }
954
1068
  }
955
1069
  .mk-period { color: var(--kit-muted); font-size: 13px; margin: -8px 0 16px; }