@easy-web/content-blocks 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 (54) hide show
  1. package/README.md +332 -0
  2. package/dist/components/__tests__/UniversalMedia.test.d.ts +2 -0
  3. package/dist/components/__tests__/UniversalMedia.test.d.ts.map +1 -0
  4. package/dist/components/__tests__/UniversalMedia.test.js +46 -0
  5. package/dist/components/__tests__/UniversalMedia.test.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/schemas/__tests__/notFound.test.d.ts +2 -0
  11. package/dist/schemas/__tests__/notFound.test.d.ts.map +1 -0
  12. package/dist/schemas/__tests__/notFound.test.js +29 -0
  13. package/dist/schemas/__tests__/notFound.test.js.map +1 -0
  14. package/dist/schemas/galleries.d.ts +319 -0
  15. package/dist/schemas/galleries.d.ts.map +1 -0
  16. package/dist/schemas/galleries.js +136 -0
  17. package/dist/schemas/galleries.js.map +1 -0
  18. package/dist/schemas/notFound.d.ts +35 -0
  19. package/dist/schemas/notFound.d.ts.map +1 -0
  20. package/dist/schemas/notFound.js +30 -0
  21. package/dist/schemas/notFound.js.map +1 -0
  22. package/package.json +51 -0
  23. package/src/components/.gitkeep +0 -0
  24. package/src/components/BlogPostCard.astro +79 -0
  25. package/src/components/Card.astro +70 -0
  26. package/src/components/CardGrid.astro +29 -0
  27. package/src/components/ContactSection.astro +73 -0
  28. package/src/components/CtaSection.astro +82 -0
  29. package/src/components/DraftBanner.astro +33 -0
  30. package/src/components/Footer.astro +81 -0
  31. package/src/components/GalleryCarousel.astro +296 -0
  32. package/src/components/GalleryFeatureHighlight.astro +153 -0
  33. package/src/components/GalleryHeroSlider.astro +362 -0
  34. package/src/components/GalleryImageGrid.astro +143 -0
  35. package/src/components/GalleryLightboxGrid.astro +353 -0
  36. package/src/components/GalleryMasonryGrid.astro +127 -0
  37. package/src/components/GallerySection.astro +69 -0
  38. package/src/components/Header.astro +210 -0
  39. package/src/components/HeaderCentered.astro +231 -0
  40. package/src/components/HeaderFlyout.astro +373 -0
  41. package/src/components/HeaderHideOnScroll.astro +237 -0
  42. package/src/components/Hero.astro +78 -0
  43. package/src/components/LanguageNotice.astro +32 -0
  44. package/src/components/LanguageSwitch.astro +67 -0
  45. package/src/components/LegalLayout.astro +53 -0
  46. package/src/components/NotFound.astro +124 -0
  47. package/src/components/Prose.astro +156 -0
  48. package/src/components/Section.astro +37 -0
  49. package/src/components/ThemeToggle.astro +82 -0
  50. package/src/components/UniversalMedia.astro +102 -0
  51. package/src/components/__tests__/UniversalMedia.test.ts +65 -0
  52. package/src/schemas/__tests__/notFound.test.ts +32 -0
  53. package/src/schemas/galleries.ts +152 -0
  54. package/src/schemas/notFound.ts +33 -0
@@ -0,0 +1,70 @@
1
+ ---
2
+ /**
3
+ * Card – content card with optional image, description, and link.
4
+ * Renders as clickable anchor when href is provided, plain container otherwise.
5
+ * Pure Astro component using --ew-* design tokens.
6
+ */
7
+ interface Props {
8
+ title: string;
9
+ description?: string;
10
+ href?: string;
11
+ image?: string;
12
+ imageAlt?: string;
13
+ }
14
+
15
+ const { title, description, href, image, imageAlt } = Astro.props;
16
+ const alt = imageAlt || title;
17
+ const Tag = href ? 'a' : 'div';
18
+ ---
19
+
20
+ <Tag class:list={['ew-card', { 'ew-card--link': !!href }]} {...(href ? { href } : {})}>
21
+ {image && (
22
+ <img src={image} alt={alt} class="ew-card__image" loading="lazy" />
23
+ )}
24
+ <div class="ew-card__body">
25
+ <h3 class="ew-card__title">{title}</h3>
26
+ {description && <p class="ew-card__description">{description}</p>}
27
+ </div>
28
+ </Tag>
29
+
30
+ <style>
31
+ .ew-card {
32
+ display: flex;
33
+ flex-direction: column;
34
+ background: var(--ew-surface);
35
+ border: 1px solid var(--ew-border);
36
+ border-radius: var(--ew-radius-lg);
37
+ overflow: hidden;
38
+ }
39
+ .ew-card--link {
40
+ text-decoration: none;
41
+ color: inherit;
42
+ }
43
+ .ew-card--link:hover {
44
+ border-color: var(--ew-primary);
45
+ }
46
+ .ew-card__image {
47
+ width: 100%;
48
+ aspect-ratio: 16 / 9;
49
+ object-fit: cover;
50
+ display: block;
51
+ }
52
+ .ew-card__body {
53
+ padding: var(--ew-space-4);
54
+ }
55
+ .ew-card__title {
56
+ font-family: var(--ew-font-sans);
57
+ font-size: var(--ew-text-lg);
58
+ font-weight: 600;
59
+ color: var(--ew-on-surface);
60
+ margin: 0;
61
+ line-height: var(--ew-leading-tight);
62
+ }
63
+ .ew-card__description {
64
+ font-family: var(--ew-font-sans);
65
+ font-size: var(--ew-text-sm);
66
+ color: var(--ew-muted);
67
+ margin: var(--ew-space-2) 0 0;
68
+ line-height: var(--ew-leading-normal);
69
+ }
70
+ </style>
@@ -0,0 +1,29 @@
1
+ ---
2
+ /**
3
+ * CardGrid – responsive CSS grid container for Card children.
4
+ * 1 column mobile, 2 tablet, 3 desktop.
5
+ * Pure Astro component using --ew-* design tokens.
6
+ */
7
+ ---
8
+
9
+ <div class="ew-card-grid">
10
+ <slot />
11
+ </div>
12
+
13
+ <style>
14
+ .ew-card-grid {
15
+ display: grid;
16
+ grid-template-columns: 1fr;
17
+ gap: var(--ew-space-5);
18
+ }
19
+ @media (min-width: 640px) {
20
+ .ew-card-grid {
21
+ grid-template-columns: repeat(2, 1fr);
22
+ }
23
+ }
24
+ @media (min-width: 1024px) {
25
+ .ew-card-grid {
26
+ grid-template-columns: repeat(3, 1fr);
27
+ }
28
+ }
29
+ </style>
@@ -0,0 +1,73 @@
1
+ ---
2
+ /**
3
+ * ContactSection – centered mailto-only contact call to action.
4
+ * Pure Astro component using --ew-* design tokens.
5
+ */
6
+ interface Props {
7
+ heading: string;
8
+ body?: string;
9
+ email: string;
10
+ buttonLabel: string;
11
+ }
12
+
13
+ const { heading, body, email, buttonLabel } = Astro.props;
14
+ ---
15
+
16
+ <section class="ew-contact">
17
+ <div class="ew-contact__inner">
18
+ <h2 class="ew-contact__heading">{heading}</h2>
19
+ {body && <p class="ew-contact__body">{body}</p>}
20
+ <a href={`mailto:${email}`} class="ew-contact__button">{buttonLabel}</a>
21
+ </div>
22
+ </section>
23
+
24
+ <style>
25
+ .ew-contact {
26
+ background: var(--ew-surface-muted);
27
+ padding: var(--ew-space-8) var(--ew-space-5);
28
+ text-align: center;
29
+ }
30
+
31
+ .ew-contact__inner {
32
+ max-width: 72rem;
33
+ margin: 0 auto;
34
+ display: flex;
35
+ flex-direction: column;
36
+ align-items: center;
37
+ }
38
+
39
+ .ew-contact__heading {
40
+ font-family: var(--ew-font-sans);
41
+ font-size: var(--ew-text-3xl);
42
+ font-weight: 800;
43
+ line-height: var(--ew-leading-tight);
44
+ color: var(--ew-on-surface);
45
+ margin: 0;
46
+ }
47
+
48
+ .ew-contact__body {
49
+ font-family: var(--ew-font-sans);
50
+ font-size: var(--ew-text-xl);
51
+ line-height: var(--ew-leading-normal);
52
+ color: var(--ew-muted);
53
+ margin: var(--ew-space-4) 0 0;
54
+ max-width: 40rem;
55
+ }
56
+
57
+ .ew-contact__button {
58
+ display: inline-block;
59
+ margin-top: var(--ew-space-6);
60
+ padding: var(--ew-space-3) var(--ew-space-6);
61
+ background: var(--ew-primary);
62
+ color: var(--ew-on-primary);
63
+ font-family: var(--ew-font-sans);
64
+ font-size: var(--ew-text-base);
65
+ font-weight: 600;
66
+ text-decoration: none;
67
+ border-radius: var(--ew-radius-md);
68
+ }
69
+
70
+ .ew-contact__button:hover {
71
+ opacity: 0.9;
72
+ }
73
+ </style>
@@ -0,0 +1,82 @@
1
+ ---
2
+ /**
3
+ * CtaSection – call-to-action banner with heading, optional body, and button.
4
+ * Pure Astro component using --ew-* design tokens.
5
+ */
6
+ interface Props {
7
+ heading: string;
8
+ body?: string;
9
+ buttonLabel: string;
10
+ buttonHref: string;
11
+ variant?: 'default' | 'muted' | 'primary';
12
+ }
13
+
14
+ const { heading, body, buttonLabel, buttonHref, variant = 'default' } = Astro.props;
15
+ ---
16
+
17
+ <section class:list={['ew-cta', `ew-cta--${variant}`]}>
18
+ <div class="ew-cta__inner">
19
+ <h2 class="ew-cta__heading">{heading}</h2>
20
+ {body && <p class="ew-cta__body">{body}</p>}
21
+ <a class="ew-cta__button" href={buttonHref}>{buttonLabel}</a>
22
+ </div>
23
+ </section>
24
+
25
+ <style>
26
+ .ew-cta {
27
+ padding: var(--ew-space-9) var(--ew-space-5);
28
+ text-align: center;
29
+ }
30
+ .ew-cta__inner {
31
+ max-width: 72rem;
32
+ margin: 0 auto;
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ }
37
+ .ew-cta--default {
38
+ background: var(--ew-surface);
39
+ color: var(--ew-on-surface);
40
+ }
41
+ .ew-cta--muted {
42
+ background: var(--ew-surface-muted);
43
+ color: var(--ew-on-surface);
44
+ }
45
+ .ew-cta--primary {
46
+ background: var(--ew-primary);
47
+ color: var(--ew-on-primary);
48
+ }
49
+ .ew-cta__heading {
50
+ font-family: var(--ew-font-sans);
51
+ font-size: var(--ew-text-3xl);
52
+ font-weight: 800;
53
+ line-height: var(--ew-leading-tight);
54
+ margin: 0;
55
+ }
56
+ .ew-cta__body {
57
+ font-family: var(--ew-font-sans);
58
+ font-size: var(--ew-text-xl);
59
+ line-height: var(--ew-leading-normal);
60
+ margin: var(--ew-space-4) 0 0;
61
+ max-width: 40rem;
62
+ }
63
+ .ew-cta__button {
64
+ display: inline-block;
65
+ margin-top: var(--ew-space-6);
66
+ padding: var(--ew-space-3) var(--ew-space-6);
67
+ background: var(--ew-primary);
68
+ color: var(--ew-on-primary);
69
+ font-family: var(--ew-font-sans);
70
+ font-size: var(--ew-text-base);
71
+ font-weight: 600;
72
+ text-decoration: none;
73
+ border-radius: var(--ew-radius-md);
74
+ }
75
+ .ew-cta--primary .ew-cta__button {
76
+ background: var(--ew-on-primary);
77
+ color: var(--ew-primary);
78
+ }
79
+ .ew-cta__button:hover {
80
+ opacity: 0.9;
81
+ }
82
+ </style>
@@ -0,0 +1,33 @@
1
+ ---
2
+ /**
3
+ * DraftBanner – warning banner for draft/unfinished legal content.
4
+ * Pure Astro component using --ew-* design tokens.
5
+ */
6
+ interface Props {
7
+ message: string;
8
+ }
9
+
10
+ const { message } = Astro.props;
11
+ ---
12
+
13
+ <div class="ew-draft-banner" role="status">
14
+ <p class="ew-draft-banner__text">{message}</p>
15
+ </div>
16
+
17
+ <style>
18
+ .ew-draft-banner {
19
+ background: var(--ew-surface-muted);
20
+ border: 2px solid var(--ew-border);
21
+ border-radius: var(--ew-radius-md);
22
+ padding: var(--ew-space-3) var(--ew-space-4);
23
+ margin: 0 0 var(--ew-space-5);
24
+ }
25
+ .ew-draft-banner__text {
26
+ font-family: var(--ew-font-sans);
27
+ font-size: var(--ew-text-sm);
28
+ font-weight: 600;
29
+ color: var(--ew-on-surface);
30
+ margin: 0;
31
+ line-height: var(--ew-leading-normal);
32
+ }
33
+ </style>
@@ -0,0 +1,81 @@
1
+ ---
2
+ /**
3
+ * Footer – site footer with copyright line and legal links.
4
+ * Pure Astro component using theme tokens only.
5
+ */
6
+ interface Props {
7
+ siteName: string;
8
+ legalLinks: Array<{ label: string; href: string }>;
9
+ currentYear?: number;
10
+ }
11
+
12
+ const { siteName, legalLinks, currentYear = new Date().getFullYear() } = Astro.props;
13
+ ---
14
+
15
+ <footer class="ew-footer">
16
+ <div class="ew-footer__inner">
17
+ <p class="ew-footer__copyright">&copy; {currentYear} {siteName}</p>
18
+ {legalLinks.length > 0 && (
19
+ <nav class="ew-footer__legal" aria-label="Legal">
20
+ <ul class="ew-footer__links">
21
+ {legalLinks.map((link) => (
22
+ <li>
23
+ <a href={link.href} class="ew-footer__link">{link.label}</a>
24
+ </li>
25
+ ))}
26
+ </ul>
27
+ </nav>
28
+ )}
29
+ </div>
30
+ </footer>
31
+
32
+ <style>
33
+ .ew-footer {
34
+ background: var(--ew-surface-muted);
35
+ border-top: 1px solid var(--ew-border);
36
+ color: var(--ew-on-surface);
37
+ font-family: var(--ew-font-sans);
38
+ font-size: var(--ew-text-sm);
39
+ }
40
+ .ew-footer__inner {
41
+ display: flex;
42
+ flex-direction: column;
43
+ align-items: center;
44
+ gap: var(--ew-space-3);
45
+ max-width: 72rem;
46
+ margin: 0 auto;
47
+ padding: var(--ew-space-6) var(--ew-space-5);
48
+ text-align: center;
49
+ }
50
+ .ew-footer__copyright {
51
+ margin: 0;
52
+ color: var(--ew-muted);
53
+ }
54
+ .ew-footer__links {
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ justify-content: center;
58
+ list-style: none;
59
+ margin: 0;
60
+ padding: 0;
61
+ gap: var(--ew-space-4);
62
+ }
63
+ .ew-footer__link {
64
+ color: var(--ew-muted);
65
+ text-decoration: none;
66
+ }
67
+ .ew-footer__link:hover {
68
+ color: var(--ew-on-surface);
69
+ }
70
+ .ew-footer__link:focus-visible {
71
+ outline: 2px solid var(--ew-primary);
72
+ outline-offset: 2px;
73
+ }
74
+
75
+ @media (min-width: 769px) {
76
+ .ew-footer__inner {
77
+ flex-direction: row;
78
+ justify-content: space-between;
79
+ }
80
+ }
81
+ </style>
@@ -0,0 +1,296 @@
1
+ ---
2
+ import type { GalleryCarouselData } from '../schemas/galleries';
3
+
4
+ type Props = Omit<GalleryCarouselData, 'kind' | 'locale' | 'translationKey'>;
5
+
6
+ const {
7
+ title,
8
+ autoplay = false,
9
+ interval = 4000,
10
+ showDots = true,
11
+ showArrows = true,
12
+ slides,
13
+ } = Astro.props;
14
+ ---
15
+
16
+ {slides.length > 0 && (
17
+ <section class="ew-carousel-wrap">
18
+ <div class="ew-carousel-wrap__inner">
19
+ {title && <h2 class="ew-carousel-wrap__title">{title}</h2>}
20
+ <div
21
+ class="ew-carousel"
22
+ data-autoplay={String(autoplay)}
23
+ data-interval={String(interval)}
24
+ aria-roledescription="carousel"
25
+ aria-label={title ?? 'Image carousel'}
26
+ tabindex="0"
27
+ >
28
+ <div class="ew-carousel__viewport">
29
+ {slides.map((slide, i) => (
30
+ <figure
31
+ class="ew-carousel__slide"
32
+ data-active={i === 0 ? 'true' : 'false'}
33
+ aria-hidden={i !== 0}
34
+ aria-roledescription="slide"
35
+ aria-label={`${i + 1} / ${slides.length}`}
36
+ >
37
+ <img
38
+ src={slide.src}
39
+ alt={slide.alt}
40
+ class="ew-carousel__img"
41
+ loading={i === 0 ? 'eager' : 'lazy'}
42
+ />
43
+ {slide.caption && (
44
+ <figcaption class="ew-carousel__caption">{slide.caption}</figcaption>
45
+ )}
46
+ </figure>
47
+ ))}
48
+ </div>
49
+
50
+ {slides.length > 1 && showArrows && (
51
+ <>
52
+ <button
53
+ type="button"
54
+ class="ew-carousel__arrow ew-carousel__arrow--prev"
55
+ aria-label="Previous slide"
56
+ >
57
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
58
+ <polyline points="15 18 9 12 15 6"></polyline>
59
+ </svg>
60
+ </button>
61
+ <button
62
+ type="button"
63
+ class="ew-carousel__arrow ew-carousel__arrow--next"
64
+ aria-label="Next slide"
65
+ >
66
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
67
+ <polyline points="9 18 15 12 9 6"></polyline>
68
+ </svg>
69
+ </button>
70
+ </>
71
+ )}
72
+
73
+ {slides.length > 1 && showDots && (
74
+ <div class="ew-carousel__dots" role="tablist" aria-label="Slide navigation">
75
+ {slides.map((_, i) => (
76
+ <button
77
+ type="button"
78
+ class="ew-carousel__dot"
79
+ data-active={i === 0 ? 'true' : 'false'}
80
+ data-index={i}
81
+ role="tab"
82
+ aria-label={`Go to slide ${i + 1}`}
83
+ aria-selected={i === 0 ? 'true' : 'false'}
84
+ ></button>
85
+ ))}
86
+ </div>
87
+ )}
88
+ </div>
89
+ </div>
90
+ </section>
91
+ )}
92
+
93
+ <script>
94
+ type Carousel = HTMLElement;
95
+
96
+ const initCarousel = (el: Carousel) => {
97
+ const slides = Array.from(el.querySelectorAll<HTMLElement>('.ew-carousel__slide'));
98
+ const dots = Array.from(el.querySelectorAll<HTMLElement>('.ew-carousel__dot'));
99
+ if (slides.length <= 1) return;
100
+
101
+ let current = 0;
102
+ let timer: ReturnType<typeof setInterval> | null = null;
103
+ const intervalMs = Number(el.dataset.interval) || 4000;
104
+ const autoplay = el.dataset.autoplay === 'true';
105
+ const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
106
+
107
+ const go = (idx: number) => {
108
+ const next = ((idx % slides.length) + slides.length) % slides.length;
109
+ if (next === current) return;
110
+ slides[current].dataset.active = 'false';
111
+ slides[current].setAttribute('aria-hidden', 'true');
112
+ slides[next].dataset.active = 'true';
113
+ slides[next].setAttribute('aria-hidden', 'false');
114
+ if (dots[current]) {
115
+ dots[current].dataset.active = 'false';
116
+ dots[current].setAttribute('aria-selected', 'false');
117
+ }
118
+ if (dots[next]) {
119
+ dots[next].dataset.active = 'true';
120
+ dots[next].setAttribute('aria-selected', 'true');
121
+ }
122
+ current = next;
123
+ };
124
+
125
+ const stop = () => {
126
+ if (timer) {
127
+ clearInterval(timer);
128
+ timer = null;
129
+ }
130
+ };
131
+ const start = () => {
132
+ stop();
133
+ if (autoplay && !reduced) {
134
+ timer = setInterval(() => go(current + 1), intervalMs);
135
+ }
136
+ };
137
+
138
+ el.querySelector<HTMLElement>('.ew-carousel__arrow--prev')
139
+ ?.addEventListener('click', () => { go(current - 1); start(); });
140
+ el.querySelector<HTMLElement>('.ew-carousel__arrow--next')
141
+ ?.addEventListener('click', () => { go(current + 1); start(); });
142
+ dots.forEach((dot, i) => {
143
+ dot.addEventListener('click', () => { go(i); start(); });
144
+ });
145
+
146
+ el.addEventListener('mouseenter', stop);
147
+ el.addEventListener('mouseleave', start);
148
+ el.addEventListener('focusin', stop);
149
+ el.addEventListener('focusout', start);
150
+
151
+ el.addEventListener('keydown', (e: KeyboardEvent) => {
152
+ if (e.key === 'ArrowLeft') { e.preventDefault(); go(current - 1); start(); }
153
+ else if (e.key === 'ArrowRight') { e.preventDefault(); go(current + 1); start(); }
154
+ });
155
+
156
+ start();
157
+ };
158
+
159
+ document.querySelectorAll<HTMLElement>('.ew-carousel').forEach(initCarousel);
160
+ </script>
161
+
162
+ <style>
163
+ .ew-carousel-wrap {
164
+ padding: var(--ew-space-8) var(--ew-space-5);
165
+ }
166
+ .ew-carousel-wrap__inner {
167
+ max-width: 72rem;
168
+ margin: 0 auto;
169
+ }
170
+ .ew-carousel-wrap__title {
171
+ font-family: var(--ew-font-sans);
172
+ font-size: var(--ew-text-xl);
173
+ font-weight: 700;
174
+ color: var(--ew-on-surface);
175
+ margin: 0 0 var(--ew-space-5);
176
+ line-height: var(--ew-leading-tight);
177
+ }
178
+
179
+ .ew-carousel {
180
+ position: relative;
181
+ background: var(--ew-surface-muted);
182
+ border-radius: var(--ew-radius-md);
183
+ overflow: hidden;
184
+ outline: none;
185
+ }
186
+ .ew-carousel:focus-visible {
187
+ outline: 3px solid var(--ew-primary);
188
+ outline-offset: -3px;
189
+ }
190
+
191
+ .ew-carousel__viewport {
192
+ position: relative;
193
+ width: 100%;
194
+ aspect-ratio: 16 / 9;
195
+ }
196
+
197
+ .ew-carousel__slide {
198
+ position: absolute;
199
+ inset: 0;
200
+ margin: 0;
201
+ opacity: 0;
202
+ transition: opacity 0.4s ease;
203
+ pointer-events: none;
204
+ }
205
+ .ew-carousel__slide[data-active='true'] {
206
+ opacity: 1;
207
+ pointer-events: auto;
208
+ }
209
+ @media (prefers-reduced-motion: reduce) {
210
+ .ew-carousel__slide {
211
+ transition: none;
212
+ }
213
+ }
214
+
215
+ .ew-carousel__img {
216
+ width: 100%;
217
+ height: 100%;
218
+ object-fit: cover;
219
+ display: block;
220
+ }
221
+
222
+ .ew-carousel__caption {
223
+ position: absolute;
224
+ left: 0;
225
+ right: 0;
226
+ bottom: 0;
227
+ padding: var(--ew-space-3) var(--ew-space-5);
228
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0));
229
+ color: #fff;
230
+ font-family: var(--ew-font-sans);
231
+ font-size: var(--ew-text-sm);
232
+ line-height: var(--ew-leading-normal);
233
+ margin: 0;
234
+ }
235
+
236
+ .ew-carousel__arrow {
237
+ position: absolute;
238
+ top: 50%;
239
+ transform: translateY(-50%);
240
+ width: 2.5rem;
241
+ height: 2.5rem;
242
+ border-radius: var(--ew-radius-full);
243
+ border: none;
244
+ background: rgba(0, 0, 0, 0.45);
245
+ color: #fff;
246
+ cursor: pointer;
247
+ display: inline-flex;
248
+ align-items: center;
249
+ justify-content: center;
250
+ transition: background 0.15s ease;
251
+ z-index: 2;
252
+ }
253
+ .ew-carousel__arrow:hover,
254
+ .ew-carousel__arrow:focus-visible {
255
+ background: rgba(0, 0, 0, 0.7);
256
+ }
257
+ .ew-carousel__arrow:focus-visible {
258
+ outline: 2px solid #fff;
259
+ outline-offset: 2px;
260
+ }
261
+ .ew-carousel__arrow--prev { left: var(--ew-space-3); }
262
+ .ew-carousel__arrow--next { right: var(--ew-space-3); }
263
+
264
+ .ew-carousel__dots {
265
+ position: absolute;
266
+ bottom: var(--ew-space-3);
267
+ left: 0;
268
+ right: 0;
269
+ display: flex;
270
+ justify-content: center;
271
+ gap: var(--ew-space-2);
272
+ z-index: 2;
273
+ }
274
+ .ew-carousel__dot {
275
+ width: 0.5rem;
276
+ height: 0.5rem;
277
+ border-radius: var(--ew-radius-full);
278
+ border: none;
279
+ background: rgba(255, 255, 255, 0.5);
280
+ cursor: pointer;
281
+ padding: 0;
282
+ transition: background 0.15s ease, transform 0.15s ease;
283
+ }
284
+ .ew-carousel__dot[data-active='true'] {
285
+ background: #fff;
286
+ transform: scale(1.3);
287
+ }
288
+ .ew-carousel__dot:hover,
289
+ .ew-carousel__dot:focus-visible {
290
+ background: rgba(255, 255, 255, 0.85);
291
+ }
292
+ .ew-carousel__dot:focus-visible {
293
+ outline: 2px solid #fff;
294
+ outline-offset: 2px;
295
+ }
296
+ </style>