@forgebuild/sidon-booking-widget 0.4.17
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.
- package/README.md +242 -0
- package/dist/custom-element-berth-map.d.ts +1 -0
- package/dist/custom-element.d.ts +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/sidon-berth-map.js +5207 -0
- package/dist/sidon-booking-widget.js +13467 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# @forgebuild/sidon-booking-widget
|
|
2
|
+
|
|
3
|
+
A drop-in berth booking widget for sidon-ai marinas. Ships as `<sidon-booking-widget>` — a
|
|
4
|
+
self-contained [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)
|
|
5
|
+
compiled from Svelte, so it works on any site (Astro, plain HTML, React, WordPress — anything)
|
|
6
|
+
with one script include and one HTML tag. No framework integration required in the consumer.
|
|
7
|
+
|
|
8
|
+
It ports sidon-ai's `Book*.astro` wizard (document upload with AI-extracted autofill → sailor/boat
|
|
9
|
+
details + contract + Stripe payment hold → confirmation) into a portable package, talking directly
|
|
10
|
+
to sidon-ai's public marina API (the same cross-origin pattern `portals` already uses via
|
|
11
|
+
`SIDON_ORIGIN`).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<script type="module" src="/path/to/sidon-booking-widget.js"></script>
|
|
17
|
+
|
|
18
|
+
<sidon-booking-widget
|
|
19
|
+
marina="org_3D8ZaTo2ZvmMVRjcHgNeMBC7bIq"
|
|
20
|
+
api-origin="https://sidonmarine.uk"
|
|
21
|
+
from="2026-08-02"
|
|
22
|
+
to="2026-08-03"
|
|
23
|
+
length="25"
|
|
24
|
+
berth="B32"
|
|
25
|
+
locale="it"
|
|
26
|
+
></sidon-booking-widget>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or as an npm dependency (once published to npm — see below):
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import '@forgebuild/sidon-booking-widget';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
then use `<sidon-booking-widget>` directly in markup — in Astro this needs no
|
|
36
|
+
`@astrojs/svelte` integration, since a custom element is just an HTML tag.
|
|
37
|
+
|
|
38
|
+
### Attributes
|
|
39
|
+
|
|
40
|
+
| Attribute | Required | Description |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `marina` | yes | The marina's `ownerId` (e.g. `org_...`) — **not** the URL slug. Read it off the live booking page's network requests if you don't already have it (content markdown copies of this value have gone stale before — verify against a live request, don't trust it blindly). |
|
|
43
|
+
| `api-origin` | no | Defaults to `https://sidonmarine.uk`. Override for a staging/dev sidon-ai instance. |
|
|
44
|
+
| `from` / `to` | no | Initial `YYYY-MM-DD` date range. Falls back to today → +3 nights. |
|
|
45
|
+
| `length` | no | Initial boat length in metres. Defaults to `15`. |
|
|
46
|
+
| `berth` | no | Pre-selected berth id. If omitted (or no longer fits/available), the widget auto-picks the smallest available berth that fits the current length. |
|
|
47
|
+
| `locale` | no | `it` / `en` / `fr` / `es`. Defaults to `en`. |
|
|
48
|
+
| `currency-symbol` | no | Defaults to `€`. |
|
|
49
|
+
| `variant` | no | `navy` / `dark`. Defaults to `navy`. See [Theming](#theming). |
|
|
50
|
+
|
|
51
|
+
The widget does **not** rewrite the host page's URL to stay in sync with its internal state
|
|
52
|
+
(unlike sidon-ai's own book.astro) — it can't assume it owns the page it's embedded in. Listen for
|
|
53
|
+
the events below if the host page wants to reflect state itself.
|
|
54
|
+
|
|
55
|
+
### Events
|
|
56
|
+
|
|
57
|
+
Dispatched on the `<sidon-booking-widget>` element (`bubbles: true, composed: true`, so they cross
|
|
58
|
+
the shadow boundary and can be caught anywhere on the page):
|
|
59
|
+
|
|
60
|
+
- `sidon-step-change` — `{ step: 1 | 2 | 3 }`, fired whenever the active wizard step changes.
|
|
61
|
+
- `sidon-loa-change` — `{ loa: number }`, fired on mount and whenever the sailor adjusts boat length in
|
|
62
|
+
Summary's +/- control. Lets a host page (e.g. a map) grey out berths that no longer fit.
|
|
63
|
+
- `sidon-booked` — `{ bookingId, marina }`, fired once a booking is successfully submitted.
|
|
64
|
+
- `sidon-booking-status` — `{ bookingId, status: 'confirmed' | 'cancelled' }`, fired on step 3 if the
|
|
65
|
+
marina confirms/declines the booking while the sailor still has the widget open (live, via Ably —
|
|
66
|
+
see the caveat below). Never fires if the tab is closed before the marina acts.
|
|
67
|
+
|
|
68
|
+
### Theming
|
|
69
|
+
|
|
70
|
+
The widget renders inside a Shadow DOM with its own compiled CSS, so it never depends on or
|
|
71
|
+
collides with the host page's stylesheet. Brand colours cross the shadow boundary via inherited
|
|
72
|
+
CSS custom properties — the same convention sidon-ai already uses to re-skin Pirates Bight:
|
|
73
|
+
|
|
74
|
+
```css
|
|
75
|
+
sidon-booking-widget {
|
|
76
|
+
--brand-navy: #0e3a3e;
|
|
77
|
+
--brand-gold: #d49a3c;
|
|
78
|
+
--brand-ink: #0e3a3e; /* dark card surface */
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Falls back to sidon-ai's defaults (`#16365b` navy, `#c9a86b` gold) when unset.
|
|
83
|
+
|
|
84
|
+
Colour custom properties re-skin the palette; the `variant` attribute switches the Next button and
|
|
85
|
+
document-upload rows between sidon-ai's two button *conventions* (same mechanism as
|
|
86
|
+
`BookStepNav`/`BookStep1Documents`'s own `variant` prop — kept in sync with those on purpose):
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<sidon-booking-widget variant="dark" ...></sidon-booking-widget>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `navy` (default, Positano) — gold Next button, navy-on-hover, rounded corners.
|
|
93
|
+
- `dark` (Fox/Pevero) — solid `--brand-ink` Next button, gold-on-hover, flush corners.
|
|
94
|
+
|
|
95
|
+
### Live booking status (Ably)
|
|
96
|
+
|
|
97
|
+
Step 3 subscribes to sidon-ai's `booking:<id>` Ably channel (same mechanism as
|
|
98
|
+
`book-success-live.ts`) and repaints the panel in place — gold "pending" → green "confirmed" / red
|
|
99
|
+
"cancelled" — the moment the marina acts, without a page reload. `ably` is dynamically imported only
|
|
100
|
+
when step 3 is reached, but note the current build inlines all dynamic imports into the single
|
|
101
|
+
output file (see `vite.config.ts`), so it does add to every consumer's download regardless of
|
|
102
|
+
whether they ever reach step 3. Fails open on any error — the sailor just keeps the static "pending"
|
|
103
|
+
state, exactly like before this existed.
|
|
104
|
+
|
|
105
|
+
**Known gap:** sidon-ai's `/api/marina/public/[marinaId]/*` endpoints have CORS enabled
|
|
106
|
+
(`src/lib/cors.ts`) so this widget can call them from any embedding origin, but
|
|
107
|
+
`/api/ably/token` currently does not — a cross-origin token request 404s on preflight. Live status
|
|
108
|
+
updates work today only when the widget is embedded on a sidon-ai-served page; on a marina's own
|
|
109
|
+
domain (the actual npm/script-tag use case) the token fetch fails, is swallowed by the fail-open
|
|
110
|
+
error handling, and the panel silently stays in the static "pending" state. Needs a fix on the
|
|
111
|
+
sidon-ai side (add `preflight`/`corsJson` from `lib/cors.ts` to `src/pages/api/ably/token.ts`,
|
|
112
|
+
matching the other public endpoints) before this is live for real embeds.
|
|
113
|
+
|
|
114
|
+
## What's deliberately different from sidon-ai's own wizard (v1 scope)
|
|
115
|
+
|
|
116
|
+
- **No resend-confirmation-email** on the confirmation step.
|
|
117
|
+
- **No season-gated date picker / berth map integration** — this widget doesn't ship a map. Berth
|
|
118
|
+
auto-pick is length + availability only, same math as sidon-ai's `recalc()`, minus the
|
|
119
|
+
map-driven manual-pick override.
|
|
120
|
+
- **Plain CSS, not Tailwind** — kept the build dependency-free; visuals are a close but not
|
|
121
|
+
pixel-exact port (no Teko/Hind webfonts bundled — system font stack instead).
|
|
122
|
+
- **UI scope is the full-page wizard only** (`Book*.astro`, matches the `/services/book` flow) —
|
|
123
|
+
not `portals`' map-embedded `BerthsSizeBar` popover, which was a different, more tightly-coupled
|
|
124
|
+
UI left for a future package version. That version now exists as `<sidon-berth-map>` — see below.
|
|
125
|
+
|
|
126
|
+
## `<sidon-berth-map>`
|
|
127
|
+
|
|
128
|
+
A second custom element in this package: a self-contained berth map (buoy circles or SVG pontoon
|
|
129
|
+
overlays) with a floating quick-book card (dates, boat length, selected berth, submit). Ports
|
|
130
|
+
sea-company's `BerthsGoogleMap.astro`/`berth-map-google.ts` + `BerthsBookingCard.astro`/
|
|
131
|
+
`berths-booking-card.ts` — previously coordinated via a `window.sidonBerthMap` global — into one
|
|
132
|
+
globals-free widget, same "one drop-in tag" model as `<sidon-booking-widget>`.
|
|
133
|
+
|
|
134
|
+
### Usage
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import '@forgebuild/sidon-booking-widget/berth-map';
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<sidon-berth-map
|
|
142
|
+
marina="org_3D8ZaTo2ZvmMVRjcHgNeMBC7bIq"
|
|
143
|
+
maps-api-key="YOUR_GOOGLE_MAPS_JS_API_KEY"
|
|
144
|
+
api-origin="https://sidonmarine.uk"
|
|
145
|
+
map-style="chart"
|
|
146
|
+
locale="en"
|
|
147
|
+
book-path="/services/book"
|
|
148
|
+
></sidon-berth-map>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Attributes
|
|
152
|
+
|
|
153
|
+
| Attribute | Required | Description |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `marina` | yes | Same `ownerId` convention as `<sidon-booking-widget>` |
|
|
156
|
+
| `maps-api-key` | yes | Google Maps JS API key — this element's only extra credential |
|
|
157
|
+
| `api-origin` | no | Defaults to `https://sidonmarine.uk` |
|
|
158
|
+
| `map-style` | no | `chart` (buoy circles, default) or `coastal` (SVG pontoon overlays) |
|
|
159
|
+
| `locale` / `currency-symbol` | no | Same as `<sidon-booking-widget>` |
|
|
160
|
+
| `from` / `to` / `length` / `berth` | no | Same initial-state convention as `<sidon-booking-widget>` |
|
|
161
|
+
| `book-path` | no | If set, auto-navigates here with `from`/`to`/`length`/`berth` query params on submit — unless a `sidon-quickbook-submit` listener calls `preventDefault()`. Omit it and the element only fires the event; the host handles routing itself. |
|
|
162
|
+
| `logo-mark` / `marina-name` | no | Centre-pin logo for this widget's own marina (symmetric with `otherMarinas` below) |
|
|
163
|
+
| `eyebrow` / `card-title` / `intro` | no | Optional heading copy rendered above the quick-book card's form fields (eyebrow line, title, intro paragraph). Omit any of the three to skip that line — there's no default text, unlike the labels below. `card-title` (not `title`) to avoid colliding with the native HTML `title` attribute's browser tooltip. |
|
|
164
|
+
| `range-label` / `range-placeholder` / `length-label` / `selected-label` / `selected-placeholder` / `cta-label` | no | Override the quick-book card's copy (defaults: "Arrival – Departure", "", "Boat length (m)", "Selected buoy", "Tap a buoy", "Book a mooring") |
|
|
165
|
+
|
|
166
|
+
### `otherMarinas` (JS property)
|
|
167
|
+
|
|
168
|
+
Peer-marina logo pins — set as a JS property, not a flat HTML attribute, since it's structured data:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
document.querySelector('sidon-berth-map').otherMarinas = [
|
|
172
|
+
{ name: 'Pevero Mooring', lat: 41.117, lng: 9.394, logoMark: 'https://.../pevero-mark.svg', href: '/m/pevero-mooring/berths' },
|
|
173
|
+
];
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`href` is always host-supplied — the package owns no site's routing convention, so it never builds
|
|
177
|
+
peer-marina links itself.
|
|
178
|
+
|
|
179
|
+
### Events
|
|
180
|
+
|
|
181
|
+
Same `sidon-*`, `{ bubbles: true, composed: true }` convention as `<sidon-booking-widget>`:
|
|
182
|
+
|
|
183
|
+
- `sidon-berth-selected` — `{ berthId: string | null }`
|
|
184
|
+
- `sidon-loa-change` — `{ loa: number }` (same name/shape `<sidon-booking-widget>` already uses, so
|
|
185
|
+
one listener on a host page covers either element)
|
|
186
|
+
- `sidon-quickbook-submit` — `{ from, to, length, berth }`, dispatched `cancelable: true`
|
|
187
|
+
|
|
188
|
+
### Theming
|
|
189
|
+
|
|
190
|
+
Same `--brand-navy` / `--brand-gold` / `--brand-ink` CSS custom properties as `<sidon-booking-widget>`
|
|
191
|
+
— one theme block styles both elements consistently.
|
|
192
|
+
|
|
193
|
+
### What's different from sea-company's original map + card
|
|
194
|
+
|
|
195
|
+
- **No shared globals** — doesn't read or write `window.sidonBerthMap`; all state lives in the
|
|
196
|
+
element itself, exposed only via attributes/properties/events.
|
|
197
|
+
- **No `#book-stepper` selection lock** — that assumed a shared page-level stepper from
|
|
198
|
+
sea-company's `/book` page layout, which has no equivalent in a self-contained widget.
|
|
199
|
+
- **Plain CSS, not Tailwind**, system font stack — not Teko/Hind — same dependency-free trade-off
|
|
200
|
+
as `<sidon-booking-widget>`.
|
|
201
|
+
- **Submit navigates via URL params, not an in-place wizard hand-off** — clicking "Book a mooring"
|
|
202
|
+
does not mount `<sidon-booking-widget>` itself; it fires an event and/or navigates to `book-path`.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm install
|
|
208
|
+
npm run dev # demo/ harness — hits sidon-ai's live public API directly
|
|
209
|
+
npm run build # library build → dist/sidon-booking-widget.js + dist/sidon-berth-map.js
|
|
210
|
+
# (each a single self-contained ES module, Svelte runtime bundled in —
|
|
211
|
+
# zero peer deps for consumers) + matching dist/*.d.ts for each
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`demo/index.html` renders three `<sidon-booking-widget>` instances (default theme, re-themed, and
|
|
215
|
+
`variant="dark"`) plus one `<sidon-berth-map>` instance, all against Fox Mooring's real marina
|
|
216
|
+
data — useful for a quick visual/functional check without touching any consumer repo. The
|
|
217
|
+
berth-map instance needs a real Google Maps JS API key substituted in for local testing (don't
|
|
218
|
+
commit one).
|
|
219
|
+
|
|
220
|
+
**Verified live** (2026-08-02) against sidon-ai's production API: document upload, contract preview
|
|
221
|
+
with live placeholder substitution, and Stripe's Payment Element mounting and initializing inside
|
|
222
|
+
the widget's Shadow DOM all work — confirmed with Fox Mooring (real marina, test-mode Stripe key).
|
|
223
|
+
Did not complete an actual booking submission, to avoid leaving a test record in a live dashboard.
|
|
224
|
+
|
|
225
|
+
## Publishing (public npm)
|
|
226
|
+
|
|
227
|
+
Published to `github.com/robooko/sidon-booking-widget` (private repo) and the public npm registry as
|
|
228
|
+
`@forgebuild/sidon-booking-widget`. To publish a new version:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npm version <patch|minor|major>
|
|
232
|
+
npm publish # requires npm login as an account with publish rights on the @forgebuild scope
|
|
233
|
+
git push && git push --tags
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`npm publish` always rebuilds `dist/` first via a `prepublishOnly` script — no need to run
|
|
237
|
+
`npm run build` separately, and no way to accidentally publish a stale build from an earlier
|
|
238
|
+
version (0.4.8 shipped this way before `prepublishOnly` existed: identical `dist/` output to
|
|
239
|
+
0.4.7, silently missing that release's actual changes).
|
|
240
|
+
|
|
241
|
+
To install it as a dependency elsewhere: `npm install @forgebuild/sidon-booking-widget`. No registry
|
|
242
|
+
config or auth token needed — it's a public package.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { }
|
package/dist/favicon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|