@eox/pages-theme-eox 1.9.0 → 2.0.0-alpha.10

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 (53) hide show
  1. package/.gitlab-ci.yml +4 -1
  2. package/README.md +88 -11
  3. package/cypress/support/mocks/authors.data.js +14 -0
  4. package/cypress/support/mocks/helpers.js +65 -4
  5. package/cypress/support/mocks/vitepress.js +8 -1
  6. package/cypress.config.js +14 -0
  7. package/package.json +3 -1
  8. package/src/Layout.vue +69 -1
  9. package/src/authors.data.js +14 -0
  10. package/src/components/BlogList.vue +655 -0
  11. package/src/components/BlogPost.vue +472 -0
  12. package/src/components/CTASection.vue +18 -4
  13. package/src/components/FeatureCard/Index.vue +462 -31
  14. package/src/components/FeatureCard/Poster.vue +25 -3
  15. package/src/components/FeatureSection.cy.js +16 -0
  16. package/src/components/FeatureSection.vue +3 -8
  17. package/src/components/FeaturesGallery.vue +34 -2
  18. package/src/components/Footer.cy.js +52 -2
  19. package/src/components/Footer.vue +17 -5
  20. package/src/components/HeroSection.cy.js +20 -0
  21. package/src/components/HeroSection.vue +140 -25
  22. package/src/components/ImageCarousel.vue +11 -2
  23. package/src/components/ImageElement.vue +16 -4
  24. package/src/components/ImageGallery.vue +29 -4
  25. package/src/components/LogoSection.vue +26 -3
  26. package/src/components/MobileNavDropdown.vue +13 -3
  27. package/src/components/NavBar.cy.js +15 -0
  28. package/src/components/NavBar.vue +149 -21
  29. package/src/components/NavDropdown.vue +12 -2
  30. package/src/components/OpenPositions.vue +292 -0
  31. package/src/components/SectionHeader.cy.js +54 -0
  32. package/src/components/SectionHeader.vue +115 -0
  33. package/src/components/TagList.vue +311 -0
  34. package/src/components/TeamPage.vue +347 -0
  35. package/src/components/TestimonialsSection.cy.js +76 -0
  36. package/src/components/TestimonialsSection.vue +368 -0
  37. package/src/components/TextSection.cy.js +115 -0
  38. package/src/components/TextSection.vue +396 -0
  39. package/src/helpers.js +182 -37
  40. package/src/helpers.test.js +181 -0
  41. package/src/index.js +16 -0
  42. package/src/pages/author/[author].md +5 -0
  43. package/src/pages/author/[author].paths.js +60 -0
  44. package/src/pages/blog.md +5 -0
  45. package/src/pages/tag/[tag].md +5 -0
  46. package/src/pages/tag/[tag].paths.js +49 -0
  47. package/src/pages/tag.md +5 -0
  48. package/src/pages/team.md +5 -0
  49. package/src/posts.data.js +134 -0
  50. package/src/style.css +389 -8
  51. package/src/tagUtils.js +148 -0
  52. package/src/team.data.js +1 -1
  53. package/src/vitepressConfig.mjs +101 -1
@@ -0,0 +1,655 @@
1
+ <template>
2
+ <div
3
+ class="blog-landing max-w-[1200px] mx-auto px-6 md:px-8"
4
+ :class="{ 'has-hero': Boolean(frontmatter.hero) }"
5
+ >
6
+ <!-- Page Header (shown only when hero is not present) -->
7
+ <div v-if="!frontmatter.hero" class="blog-landing-header" role="banner">
8
+ <div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
9
+ <div>
10
+ <h1 class="blog-landing-title">EOX Blog</h1>
11
+ <p class="blog-landing-subtitle">
12
+ Insights, updates, and deep dives from our geospatial experts.
13
+ </p>
14
+ </div>
15
+ </div>
16
+ </div>
17
+
18
+ <!-- Active Filter Banner -->
19
+ <div v-if="activeTag || activeAuthor" class="active-filter-banner mb-6">
20
+ <span class="filter-label">Active filter:</span>
21
+ <span
22
+ v-if="activeTag"
23
+ :style="getTagStyle(activeTag)"
24
+ class="active-filter-tag shadow-sm inline-flex items-center gap-1.5"
25
+ >
26
+ {{ activeTag }}
27
+ </span>
28
+ <span
29
+ v-if="activeAuthor"
30
+ class="active-filter-author shadow-sm inline-flex items-center gap-1.5"
31
+ >
32
+ Author: {{ getAuthorName(activeAuthor) }}
33
+ </span>
34
+ <button @click="clearFilters" class="clear-filter-btn">
35
+ <i class="mdi mdi-close-circle"></i>
36
+ <span>Clear Filter</span>
37
+ </button>
38
+ </div>
39
+
40
+ <!-- Grid of Blog Posts -->
41
+ <div class="blog-post-layout">
42
+ <!-- Main Content Column -->
43
+ <div class="blog-content-col">
44
+ <div v-if="paginatedPosts.length" class="posts-grid">
45
+ <FeatureCard
46
+ v-for="post in paginatedPosts"
47
+ :key="post.url"
48
+ :title="post.title"
49
+ :content="post.excerpt"
50
+ :image="
51
+ post.coverImage ? getCoverImageUrl(post.coverImage) : undefined
52
+ "
53
+ :date="post.date"
54
+ :authors="post.authors"
55
+ :chips="
56
+ post.tags
57
+ ? post.tags.map((t) => ({
58
+ text: t,
59
+ link: withBase(`/tag/${t}/`),
60
+ }))
61
+ : []
62
+ "
63
+ :link="{
64
+ text: 'Read more',
65
+ href: withBase(post.url),
66
+ target: '_self',
67
+ }"
68
+ variant="cover"
69
+ class="h-full bg-surface-container-low"
70
+ />
71
+ </div>
72
+
73
+ <!-- Empty State -->
74
+ <div
75
+ v-else
76
+ class="text-center py-20 bg-surface-container-low rounded-2xl border border-slate-100/50 max-w-xl mx-auto my-12"
77
+ >
78
+ <i class="mdi mdi-text-search text-5xl text-slate-300 mb-4 block"></i>
79
+ <h3 class="text-xl font-bold text-slate-900 mb-2">No posts found</h3>
80
+ <p class="text-slate-500 mb-6">
81
+ We couldn't find any articles matching your filters.
82
+ </p>
83
+ <button
84
+ @click="clearFilters"
85
+ class="px-5 py-2.5 text-sm font-semibold text-white bg-primary hover:bg-primary/90 rounded-xl transition-colors shadow-sm"
86
+ >
87
+ Clear Filters
88
+ </button>
89
+ </div>
90
+
91
+ <!-- Pagination -->
92
+ <nav
93
+ v-if="totalPages > 1"
94
+ class="mt-16 flex justify-center items-center gap-1 sm:gap-2"
95
+ >
96
+ <button
97
+ @click="changePage(currentPage - 1)"
98
+ :disabled="currentPage === 1"
99
+ class="min-w-[32px] sm:min-w-[40px] h-[32px] sm:h-[40px] flex items-center justify-center rounded-xl border border-slate-200 text-slate-600 hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-transparent disabled:cursor-not-allowed transition-colors"
100
+ >
101
+ <span class="sr-only">Previous</span>
102
+ <i class="mdi mdi-chevron-left text-lg"></i>
103
+ </button>
104
+
105
+ <div class="flex items-center gap-1 sm:gap-1.5">
106
+ <button
107
+ v-for="page in visiblePages"
108
+ :key="page"
109
+ @click="changePage(page)"
110
+ :class="[
111
+ currentPage === page
112
+ ? 'bg-primary text-white border-primary shadow-sm'
113
+ : 'border-slate-200 text-slate-600 hover:bg-slate-50',
114
+ ]"
115
+ class="min-w-[32px] sm:min-w-[40px] h-[32px] sm:h-[40px] px-2 sm:px-3 font-semibold text-sm rounded-xl border flex items-center justify-center transition-all duration-150"
116
+ >
117
+ {{ page }}
118
+ </button>
119
+ </div>
120
+
121
+ <button
122
+ @click="changePage(currentPage + 1)"
123
+ :disabled="currentPage === totalPages"
124
+ class="min-w-[32px] sm:min-w-[40px] h-[32px] sm:h-[40px] flex items-center justify-center rounded-xl border border-slate-200 text-slate-600 hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-transparent disabled:cursor-not-allowed transition-colors"
125
+ >
126
+ <span class="sr-only">Next</span>
127
+ <i class="mdi mdi-chevron-right text-lg"></i>
128
+ </button>
129
+ </nav>
130
+ </div>
131
+
132
+ <!-- Sidebar Column -->
133
+ <aside class="blog-sidebar-col">
134
+ <!-- Search widget -->
135
+ <div class="sidebar-widget">
136
+ <h3 class="sidebar-title">Search</h3>
137
+ <div class="search-container">
138
+ <VPNavBarSearch class="search" />
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Hot Tags -->
143
+ <div class="sidebar-widget">
144
+ <div class="sidebar-title-container">
145
+ <h3 class="sidebar-title-inline">Hot tags</h3>
146
+ <a :href="withBase('/tag')" class="see-all-link">
147
+ <span>see all</span>
148
+ <i class="mdi mdi-arrow-right"></i>
149
+ </a>
150
+ </div>
151
+ <div class="sidebar-tags-container">
152
+ <a
153
+ v-for="tag in calculatedHotTags"
154
+ :key="tag.name"
155
+ :href="withBase(`/tag/${tag.name}/`)"
156
+ :style="getTagStyle(tag.name)"
157
+ class="sidebar-tag"
158
+ >
159
+ {{ tag.name }} ({{ tag.count }})
160
+ </a>
161
+ </div>
162
+ </div>
163
+ </aside>
164
+ </div>
165
+ </div>
166
+ </template>
167
+
168
+ <script setup>
169
+ import { ref, computed, onMounted, watch } from "vue";
170
+ import { useRouter, useData, withBase } from "vitepress";
171
+ import { data as posts } from "../posts.data.js";
172
+ import { data as authorsList } from "../authors.data.js";
173
+ import { getTagColor, getTagStyle } from "../tagUtils.js";
174
+ import { VPNavBarSearch } from "vitepress/theme";
175
+ import FeatureCard from "./FeatureCard/Index.vue";
176
+
177
+ // Dynamic Hot Tags
178
+ const calculatedHotTags = computed(() => {
179
+ const tagCounts = {};
180
+ posts.forEach((p) => {
181
+ if (Array.isArray(p.tags)) {
182
+ p.tags.forEach((t) => {
183
+ const clean = t.trim();
184
+ if (clean) {
185
+ tagCounts[clean] = (tagCounts[clean] || 0) + 1;
186
+ }
187
+ });
188
+ }
189
+ });
190
+ return Object.entries(tagCounts)
191
+ .map(([name, count]) => ({
192
+ name,
193
+ count,
194
+ color: getTagColor(name),
195
+ }))
196
+ .sort((a, b) => b.count - a.count)
197
+ .slice(0, 10);
198
+ });
199
+
200
+ const router = useRouter();
201
+ const { params, frontmatter } = useData();
202
+
203
+ const activeTag = ref("");
204
+ const activeAuthor = ref("");
205
+ const currentPage = ref(1);
206
+ const postsPerPage = 9;
207
+
208
+ // Form cover image URL correctly (prefixing filename with /images/)
209
+ function getCoverImageUrl(coverImage) {
210
+ if (!coverImage) return "";
211
+ if (coverImage.startsWith("http") || coverImage.startsWith("//")) {
212
+ return coverImage;
213
+ }
214
+ const base = withBase("/");
215
+ if (base && base !== "/" && coverImage.startsWith(base)) {
216
+ return coverImage;
217
+ }
218
+ const path = coverImage.startsWith("/")
219
+ ? coverImage
220
+ : `/images/${coverImage}`;
221
+ return withBase(path);
222
+ }
223
+
224
+ const tagFromParam = computed(() => {
225
+ return params.value?.tag || "";
226
+ });
227
+
228
+ const authorFromParam = computed(() => {
229
+ return params.value?.author || "";
230
+ });
231
+
232
+ // Reactively parse query filters on client side
233
+ function updateFilters() {
234
+ if (typeof window === "undefined") return;
235
+ const urlParams = new URLSearchParams(window.location.search);
236
+ activeTag.value = tagFromParam.value || urlParams.get("tag") || "";
237
+ activeAuthor.value = authorFromParam.value || urlParams.get("author") || "";
238
+ currentPage.value = 1; // Reset to page 1 on filter change
239
+ }
240
+
241
+ onMounted(() => {
242
+ updateFilters();
243
+ window.addEventListener("popstate", updateFilters);
244
+ });
245
+
246
+ // Watch router path/search changes
247
+ watch(() => router.route.path, updateFilters);
248
+ watch(tagFromParam, (newVal) => {
249
+ activeTag.value = newVal;
250
+ });
251
+ watch(authorFromParam, (newVal) => {
252
+ activeAuthor.value = newVal;
253
+ });
254
+ if (typeof window !== "undefined") {
255
+ watch(() => window.location.search, updateFilters);
256
+ }
257
+
258
+ // Filter posts
259
+ const filteredPosts = computed(() => {
260
+ return posts.filter((post) => {
261
+ // Filter by tag
262
+ const tagToFilter = activeTag.value || tagFromParam.value;
263
+ if (tagToFilter) {
264
+ const matchTag = post.tags.some(
265
+ (t) => t.toLowerCase().trim() === tagToFilter.toLowerCase().trim(),
266
+ );
267
+ if (!matchTag) return false;
268
+ }
269
+ // Filter by author
270
+ const authorToFilter = activeAuthor.value || authorFromParam.value;
271
+ if (authorToFilter) {
272
+ const matchAuthor = post.authors.some(
273
+ (a) => a.toLowerCase().trim() === authorToFilter.toLowerCase().trim(),
274
+ );
275
+ if (!matchAuthor) return false;
276
+ }
277
+ return true;
278
+ });
279
+ });
280
+
281
+ // Paginated posts
282
+ const paginatedPosts = computed(() => {
283
+ const startIndex = (currentPage.value - 1) * postsPerPage;
284
+ return filteredPosts.value.slice(startIndex, startIndex + postsPerPage);
285
+ });
286
+
287
+ // Total pages
288
+ const totalPages = computed(() => {
289
+ return Math.ceil(filteredPosts.value.length / postsPerPage);
290
+ });
291
+
292
+ // Page navigation helper
293
+ function changePage(page) {
294
+ if (page < 1 || page > totalPages.value) return;
295
+ currentPage.value = page;
296
+ if (typeof window !== "undefined") {
297
+ window.scrollTo({ top: 0, behavior: "smooth" });
298
+ }
299
+ }
300
+
301
+ // Generate list of page numbers to render
302
+ const visiblePages = computed(() => {
303
+ const pages = [];
304
+ const maxPages = 5;
305
+ let start = Math.max(1, currentPage.value - Math.floor(maxPages / 2));
306
+ let end = Math.min(totalPages.value, start + maxPages - 1);
307
+ if (end - start + 1 < maxPages) {
308
+ start = Math.max(1, end - maxPages + 1);
309
+ }
310
+ for (let i = start; i <= end; i++) {
311
+ pages.push(i);
312
+ }
313
+ return pages;
314
+ });
315
+
316
+ // Format publication date
317
+ function formatDate(dateStr) {
318
+ if (!dateStr) return "";
319
+ const date = new Date(dateStr);
320
+ return date.toLocaleDateString("en-US", {
321
+ year: "numeric",
322
+ month: "long",
323
+ day: "numeric",
324
+ });
325
+ }
326
+
327
+ // Get author name
328
+ function getAuthorName(authorId) {
329
+ const auth = authorsList.find((a) => a.id === authorId);
330
+ return auth ? auth.name : authorId;
331
+ }
332
+
333
+ function clearFilters() {
334
+ router.go("/blog");
335
+ }
336
+ </script>
337
+
338
+ <style scoped>
339
+ .blog-landing {
340
+ width: 100%;
341
+ max-width: 1200px !important;
342
+ margin-left: auto !important;
343
+ margin-right: auto !important;
344
+ padding-left: 1rem !important;
345
+ padding-right: 1rem !important;
346
+ box-sizing: border-box !important;
347
+ padding-top: calc(var(--vp-nav-height) + 28px) !important;
348
+ padding-bottom: 48px !important;
349
+ }
350
+
351
+ .blog-landing.has-hero {
352
+ padding-top: 2rem !important;
353
+ }
354
+
355
+ @media (min-width: 640px) {
356
+ .blog-landing {
357
+ padding-left: 1.5rem !important;
358
+ padding-right: 1.5rem !important;
359
+ padding-top: calc(var(--vp-nav-height) + 40px) !important;
360
+ padding-bottom: 64px !important;
361
+ }
362
+ .blog-landing.has-hero {
363
+ padding-top: 4.5rem !important;
364
+ }
365
+ }
366
+
367
+ @media (min-width: 960px) {
368
+ .blog-landing {
369
+ padding-left: 2rem !important;
370
+ padding-right: 2rem !important;
371
+ padding-top: calc(var(--vp-nav-height) + 58px) !important;
372
+ padding-bottom: 96px !important;
373
+ }
374
+ .blog-landing.has-hero {
375
+ padding-top: 8rem !important;
376
+ }
377
+ }
378
+
379
+ .blog-landing-header {
380
+ margin-bottom: 2rem;
381
+ border-bottom: 1px solid #f1f5f9; /* slate-100 */
382
+ padding-bottom: 1.5rem !important;
383
+ padding-top: 0 !important;
384
+ padding-left: 0 !important;
385
+ padding-right: 0 !important;
386
+ background: transparent !important;
387
+ min-height: auto !important;
388
+ height: auto !important;
389
+ }
390
+
391
+ .blog-landing-title {
392
+ font-family: "Inter", sans-serif;
393
+ font-style: normal;
394
+ font-weight: 700;
395
+ font-size: 28px;
396
+ line-height: 36px;
397
+ letter-spacing: 0.5px;
398
+ color: #020617; /* slate-950 */
399
+ margin-top: 0;
400
+ margin-bottom: 0.5rem;
401
+ }
402
+
403
+ @media (min-width: 768px) {
404
+ .blog-landing-title {
405
+ font-size: 32px;
406
+ line-height: 40px;
407
+ }
408
+ }
409
+
410
+ .blog-landing-subtitle {
411
+ font-family: "Inter", sans-serif;
412
+ font-style: normal;
413
+ font-weight: 400;
414
+ font-size: 16px;
415
+ line-height: 24px;
416
+ letter-spacing: 0.5px;
417
+ color: #64748b; /* slate-500 */
418
+ max-width: 620px;
419
+ margin: 0;
420
+ padding: 0;
421
+ }
422
+
423
+ .posts-grid {
424
+ display: flex;
425
+ flex-direction: column;
426
+ gap: 2.5rem;
427
+ }
428
+
429
+ :deep(.card-cover-link) {
430
+ max-height: 280px !important;
431
+ }
432
+
433
+ .blog-post-layout {
434
+ display: grid;
435
+ grid-template-columns: 1fr;
436
+ gap: 2.5rem;
437
+ }
438
+
439
+ .blog-content-col {
440
+ width: 100%;
441
+ min-width: 0;
442
+ }
443
+
444
+ .blog-sidebar-col {
445
+ width: 100%;
446
+ display: flex;
447
+ flex-direction: column;
448
+ gap: 2.5rem;
449
+ }
450
+
451
+ @media (min-width: 1024px) {
452
+ .blog-post-layout {
453
+ grid-template-columns: minmax(0, 1fr) 280px;
454
+ gap: 4rem;
455
+ align-items: start;
456
+ }
457
+ .blog-sidebar-col {
458
+ position: sticky !important;
459
+ align-self: start !important;
460
+ top: calc(var(--vp-nav-height) + 2rem) !important;
461
+ max-height: calc(100vh - var(--vp-nav-height) - 4rem);
462
+ overflow-y: auto;
463
+ scrollbar-width: none; /* Hide scrollbar for cleaner look */
464
+ }
465
+ .blog-sidebar-col::-webkit-scrollbar {
466
+ display: none;
467
+ }
468
+ }
469
+
470
+ .sidebar-widget {
471
+ display: flex;
472
+ flex-direction: column;
473
+ gap: 0.75rem;
474
+ }
475
+
476
+ .sidebar-title {
477
+ font-size: 0.75rem;
478
+ font-weight: 600;
479
+ text-transform: uppercase;
480
+ letter-spacing: 0.05em;
481
+ color: #64748b; /* slate-500 */
482
+ border-bottom: 1px solid #f1f5f9; /* slate-100 */
483
+ padding-bottom: 0.5rem;
484
+ margin: 0;
485
+ }
486
+
487
+ .sidebar-title-container {
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: space-between;
491
+ border-bottom: 1px solid #f1f5f9; /* slate-100 */
492
+ padding-bottom: 0.5rem;
493
+ margin: 0;
494
+ }
495
+
496
+ .sidebar-title-inline {
497
+ font-size: 0.75rem;
498
+ font-weight: 600;
499
+ text-transform: uppercase;
500
+ letter-spacing: 0.05em;
501
+ color: #64748b; /* slate-500 */
502
+ margin: 0;
503
+ padding: 0;
504
+ }
505
+
506
+ .see-all-link {
507
+ font-size: 0.75rem;
508
+ font-weight: 600;
509
+ color: var(--vp-c-brand, #004170);
510
+ text-decoration: none;
511
+ display: inline-flex;
512
+ align-items: center;
513
+ gap: 0.25rem;
514
+ transition: opacity 0.2s;
515
+ }
516
+
517
+ .see-all-link:hover {
518
+ text-decoration: underline;
519
+ opacity: 0.8;
520
+ }
521
+
522
+ .see-all-link i {
523
+ font-size: 0.75rem;
524
+ }
525
+
526
+ .sidebar-tags-container {
527
+ display: flex !important;
528
+ flex-wrap: wrap !important;
529
+ gap: 0.5rem !important;
530
+ margin-top: 0.25rem !important;
531
+ }
532
+
533
+ .sidebar-tag {
534
+ display: inline-flex !important;
535
+ align-items: center !important;
536
+ font-size: 0.75rem !important;
537
+ font-weight: 600 !important;
538
+ padding: 0.25rem 0.75rem !important;
539
+ border-radius: 4px !important;
540
+ text-decoration: none !important;
541
+ line-height: 1.2 !important;
542
+ transition:
543
+ opacity 0.2s ease,
544
+ transform 0.1s ease !important;
545
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
546
+ }
547
+
548
+ /* Active Filter Banner Styles */
549
+ .active-filter-banner {
550
+ display: flex;
551
+ flex-wrap: wrap;
552
+ align-items: center;
553
+ gap: 0.75rem;
554
+ background-color: var(--surface-container-low, #f8fafc);
555
+ padding: 0.75rem 1.25rem !important;
556
+ border-radius: 0.75rem;
557
+ border: 1px solid rgba(226, 232, 240, 0.8); /* slate-200/80 */
558
+ margin-top: 1.5rem;
559
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
560
+ }
561
+
562
+ .filter-label {
563
+ font-size: 0.875rem; /* text-sm */
564
+ color: #64748b; /* slate-500 */
565
+ font-weight: 500;
566
+ }
567
+
568
+ .active-filter-tag {
569
+ display: inline-flex !important;
570
+ align-items: center !important;
571
+ font-size: 0.75rem !important;
572
+ font-weight: 700 !important;
573
+ padding: 0.25rem 0.75rem !important;
574
+ border-radius: 4px !important;
575
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
576
+ line-height: 1.2 !important;
577
+ }
578
+
579
+ .active-filter-author {
580
+ display: inline-flex !important;
581
+ align-items: center !important;
582
+ font-size: 0.75rem !important;
583
+ font-weight: 700 !important;
584
+ padding: 0.25rem 0.75rem !important;
585
+ border-radius: 4px !important;
586
+ background-color: var(--primary-container) !important;
587
+ color: var(--primary) !important;
588
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
589
+ line-height: 1.2 !important;
590
+ }
591
+
592
+ /* Clear Filter Button Reset & Style */
593
+ .clear-filter-btn {
594
+ /* Complete reset of standard button styling */
595
+ background: none !important;
596
+ background-color: transparent !important;
597
+ border: none !important;
598
+ outline: none !important;
599
+ box-shadow: none !important;
600
+ height: auto !important;
601
+ min-height: auto !important;
602
+ width: auto !important;
603
+ padding: 0 !important;
604
+ margin: 0 !important;
605
+ margin-left: 0.5rem !important;
606
+
607
+ display: inline-flex !important;
608
+ align-items: center !important;
609
+ gap: 0.375rem !important;
610
+ font-size: 0.8125rem !important; /* 13px */
611
+ font-weight: 600 !important;
612
+ color: #94a3b8 !important; /* slate-400 */
613
+ cursor: pointer !important;
614
+ transition: color 0.15s ease !important;
615
+ }
616
+
617
+ .clear-filter-btn:hover {
618
+ color: #475569 !important; /* slate-600 */
619
+ background: none !important;
620
+ background-color: transparent !important;
621
+ }
622
+
623
+ .clear-filter-btn i {
624
+ font-size: 1rem !important;
625
+ color: inherit !important;
626
+ vertical-align: middle !important;
627
+ }
628
+
629
+ .sidebar-tag:hover {
630
+ opacity: 0.8 !important;
631
+ transform: translateY(-1px) !important;
632
+ }
633
+
634
+ .search-container {
635
+ margin-top: 0.75rem;
636
+ margin-bottom: 0.5rem;
637
+ }
638
+
639
+ :deep(.VPNavBarSearch) {
640
+ padding-left: 0 !important;
641
+ }
642
+
643
+ .line-clamp-2 {
644
+ display: -webkit-box;
645
+ -webkit-line-clamp: 2;
646
+ -webkit-box-orient: vertical;
647
+ overflow: hidden;
648
+ }
649
+ .line-clamp-3 {
650
+ display: -webkit-box;
651
+ -webkit-line-clamp: 3;
652
+ -webkit-box-orient: vertical;
653
+ overflow: hidden;
654
+ }
655
+ </style>