@playpilot/tpi 5.6.0 → 5.7.0-beta.display-2

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/events.md CHANGED
@@ -66,3 +66,9 @@ Event | Action | Info | Payload
66
66
  --- | --- | --- | ---
67
67
  `ali_split_test_view` | _Should be fired for any active split test_ | | `key`, `variant` (a whole number starting at 0)
68
68
  `ali_split_test_action` | _Should be fired for any assertion in split tests_ | | `key` (matches the key of `ali_split_test_view`), `variant` (a whole number starting at 0), `action`
69
+
70
+ ### Ads
71
+ Event | Action | Info | Payload
72
+ --- | --- | --- | ---
73
+ `ali_top_scroll_click` | _Fired any time the top scroll ad is called_ | | `campaign_name`
74
+ `ali_display_ad_click` | _Fired any time the top scroll ad is clicked_ | | `campaign_name`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.6.0",
3
+ "version": "5.7.0-beta.display-2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
package/src/lib/ads.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { getApiToken } from "./api"
2
+ import { apiBaseUrl } from "./constants"
3
+ import type { Campaign, CampaignFormat } from "./types/campaign"
4
+ import type { PlaylinkData } from "./types/playlink"
5
+
6
+ export async function fetchAds() {
7
+ const headers = new Headers({ 'Content-Type': 'application/json' })
8
+ const apiToken = getApiToken()
9
+
10
+ if (!apiToken) throw new Error('No token was provided')
11
+
12
+ const response = await fetch(apiBaseUrl + `/ads/browse/?region=nl&api-token=${apiToken}`, { headers })
13
+
14
+ if (!response.ok) throw response
15
+
16
+ const parsed = await response.json()
17
+
18
+ return parsed
19
+ }
20
+
21
+ export function getFirstAdOfType(format: CampaignFormat): Campaign | null {
22
+ return (window.PlayPilotLinkInjections?.ads || []).find(i => i.campaign_format === format) || null
23
+ }
24
+
25
+ export function campaignToPlaylink(campaign: Campaign): PlaylinkData {
26
+ return {
27
+ sid: '',
28
+ name: campaign.content.header || '',
29
+ url: campaign.cta.url || '',
30
+ logo_url: campaign.cta.image || '',
31
+ highlighted: true,
32
+ cta_text: campaign.content.subheader,
33
+ action_text: campaign.cta.header,
34
+ extra_info: {
35
+ category: 'SVOD'
36
+ }
37
+ }
38
+ }
@@ -1,4 +1,5 @@
1
1
  export const playPilotBaseUrl = 'https://www.playpilot.com'
2
- export const apiBaseUrl = 'https://partner-api.playpilot.tech/1.0.0'
2
+ export const apiBaseUrl = 'https://partner-api-staging.playpilot.tech/1.0.0'
3
+ export const imageBaseUrl = 'https://img-staging.playpilot.tech'
3
4
 
4
5
  export const imagePlaceholderDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFQAAAB+CAMAAACNgsajAAAAb1BMVEU1Q2fI1N5YZoNVYoFndI9qd5JBT3FFU3S8yNS3xNC4xNFBTnDG0t2lscJJV3e9ytaptcWToLOOm6/BzdiZprh3hJ1aaIWJlatte5VPXHx7iJ9zgZlfbYk3RWmNmq5jcY1XZIO2wtCjsMB+i6I9S25IprTeAAABqUlEQVRo3u3W2ZKCMBCF4T5ExRmRVXBfZnn/Z5wKiU5pz1AJ7ZXV/x03HxVCB0jTNE3TNE3TNO0l2rbPNxcFdk8334AINTUD5eSaWbPoQs0qw0CVN98BzNNgE4NVv+ZHsJliuNVt7UgotATAafp/5mZiG4waAB0N5k0kUeg0wMykKDfLvRTl5pxyCFFupjQVo9ykiRTlphzl5nNQNu8C9Hv2lylDT0W2NMyUoeXdLC68KUNLuM7O9HskQ0uLLAEUR2aOQjfORE5LzHP2PMehxpl2k6otM8eh2aQGkBlieyRBYbs3y5Rk6IPZn8mT65XJR6LcROGErwaoxqLm4XvkiTVsy1FoYe5n06zcjazp1Wk0umHz3k9BT6+bXjXR6PnRtKpr755PfRjx4WPz7tXW/26gGYnOvOmrM7xtiK4qYtDOrpGZtnR7JFcSi+Z1XZt7k5d4NCZmcrWxqMzkbRgqN+nAULHpx1RiylFvftJwlxjUz2bWdpPB7NnSxZih5RFrD20Vai4izGOgeenPukMSUE6hte5denI7NMyU1xrSNE3TNE3TNE17hX4ADHsS0j0OCOoAAAAASUVORK5CYII='
@@ -0,0 +1,7 @@
1
+ // Image dimensions to be used to generate images from the supplied image UUIDs.
2
+ // The number given is a multiplier of the base 32 pixels.
3
+ // https://docs.google.com/spreadsheets/d/1Nx73ux1fgGKJfuaUeQRwSXUJdSQonPG5DxjW2zxnyz0
4
+ export const ImageDimensions = {
5
+ TopScrollBackground: 'topscrollBackgroundMobile', // 600x140
6
+ TopScrollContent: '8by1x23',
7
+ } as const
@@ -6,5 +6,18 @@ export const SplitTest = {
6
6
  MultipleVariants: {
7
7
  key: 'multiple_variants',
8
8
  numberOfVariants: 4,
9
- }
9
+ },
10
+ TopScrollFormat: {
11
+ key: 'top_scroll_format',
12
+ numberOfVariants: 2,
13
+ // Variant 0 is separate bubble
14
+ // Variant 1 is inline bubble
15
+ },
16
+ DisplayAdPosition: {
17
+ key: 'display_ad_position',
18
+ numberOfVariants: 3,
19
+ // Variant 0 is display ad in playlinks
20
+ // Variant 1 is display ad above card with highlighted playlink
21
+ // Variant 2 is display ad above card without highlighted playlink
22
+ },
10
23
  } as const
@@ -26,6 +26,9 @@ export const TrackingEvent = Object.freeze({
26
26
  EditorError: 'ali_editor_error',
27
27
  InjectionError: 'ali_injection_error',
28
28
 
29
+ TopScrollClick: 'ali_top_scroll_click',
30
+ DisplayAdClick: 'ali_display_ad_click',
31
+
29
32
  SplitTestView: 'ali_split_test_view',
30
33
  SplitTestAction: 'ali_split_test_action'
31
34
  })
package/src/lib/image.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { imageBaseUrl } from "./constants"
2
+ import type { ImageDimensions } from "./enums/ImageDimensions"
3
+
1
4
  /**
2
5
  * NOTE: This is a temporary measure. Images url from the API use a previous format which is to be replaced,
3
6
  * but that isn't live yet and requires some extra work. In the meantime we remove part of the URL ourselves.
@@ -6,3 +9,12 @@ export function removeImageUrlPrefix(url: string | null): string | null {
6
9
  if (!url) return null
7
10
  return url.replace('/src/img', '')
8
11
  }
12
+
13
+ /**
14
+ * Creates a CDN image URL from a UUID.
15
+ */
16
+ export function imageFromUUID(uuid: string | null, dimensions: (typeof ImageDimensions)[keyof typeof ImageDimensions] | null = null): string {
17
+ if (!uuid?.length) return ''
18
+
19
+ return `${imageBaseUrl}/${uuid}?${dimensions ? `class=${dimensions}&` : ''}optimizer=image`
20
+ }
@@ -0,0 +1,45 @@
1
+ export type Campaign = {
2
+ campaign_format: CampaignFormat
3
+ campaign_type: 'playlist' | 'video' | 'image'
4
+ campaign_platforms: CampaignPlatform[]
5
+ campaign_name: string
6
+ campaign_start: string
7
+ campaign_end: string
8
+ campaign_region: string
9
+ content: CampaignContent
10
+ cta: CampaignCTA
11
+ content_playlist: object
12
+ provider: object | null
13
+ impression_trackers: string[]
14
+ target_title_uuid: string
15
+ target_title_sid: string
16
+ backfill_providers: string[]
17
+ autogenerated: boolean
18
+ enabled: boolean
19
+ hide_imdb_score: boolean
20
+ hide_sponsored_message: boolean
21
+ disclaimer: string | null
22
+ }
23
+
24
+ export type CampaignContent = {
25
+ header: string | null
26
+ header_logo: string | null
27
+ header_logo_uuid: string | null
28
+ subheader: string | null
29
+ image: string | null
30
+ image_uuid: string | null
31
+ video: string | null
32
+ format: string | null
33
+ }
34
+
35
+ export type CampaignCTA = {
36
+ header: string | null
37
+ subheader: string | null
38
+ image: string | null
39
+ image_uuid: string | null
40
+ url: string | null
41
+ }
42
+
43
+ export type CampaignPlatform = 'web' | 'android_app' | 'ios_app'
44
+
45
+ export type CampaignFormat = 'takeover' | 'horizontal' | 'hero' | 'top_scroll' | 'card'
@@ -5,6 +5,8 @@ export type PlaylinkData = {
5
5
  logo_url: string
6
6
  highlighted?: boolean
7
7
  cta_text?: string | null
8
+ // This doesn't actually exists on the API objects, it's only present from ads
9
+ action_text?: string | null,
8
10
  extra_info: {
9
11
  category: PlaylinkCategory
10
12
  }
@@ -1,3 +1,4 @@
1
+ import type { Campaign } from "./campaign"
1
2
  import type { LinkInjection } from "./injection"
2
3
 
3
4
  export type ScriptConfig = {
@@ -13,4 +14,5 @@ export type ScriptConfig = {
13
14
  tracked_events?: { event: string, payload: Record<string, any> }[]
14
15
  split_test_identifiers?: Record<string, number>
15
16
  evaluated_link_injections?: LinkInjection[]
17
+ ads?: Campaign[]
16
18
  }
package/src/main.ts CHANGED
@@ -16,6 +16,7 @@ window.PlayPilotLinkInjections = {
16
16
  tracked_events: [],
17
17
  split_test_identifiers: {},
18
18
  evaluated_link_injections: [],
19
+ ads: [],
19
20
  app: null,
20
21
 
21
22
  initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }): void {
@@ -14,6 +14,7 @@
14
14
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
15
15
  import Alert from './components/Editorial/Alert.svelte'
16
16
  import TrackingPixels from './components/TrackingPixels.svelte'
17
+ import { fetchAds } from '$lib/ads'
17
18
 
18
19
  let parentElement: HTMLElement | null = $state(null)
19
20
  let elements: HTMLElement[] = $state([])
@@ -43,6 +44,8 @@
43
44
  await initialize()
44
45
  track(TrackingEvent.ArticlePageView)
45
46
 
47
+ if (aiInjections.length || manualInjections.length) window.PlayPilotLinkInjections.ads = await fetchAds()
48
+
46
49
  trackSplitTestView(SplitTest.MultipleVariants)
47
50
  })()
48
51
 
@@ -0,0 +1,140 @@
1
+ <script lang="ts">
2
+ import { SplitTest } from '$lib/enums/SplitTest'
3
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
4
+ import { trackSplitTestAction, trackSplitTestView } from '$lib/splitTest'
5
+ import { track } from '$lib/tracking'
6
+ import type { Campaign } from '$lib/types/campaign'
7
+
8
+ interface Props {
9
+ campaign: Campaign
10
+ }
11
+
12
+ const { campaign }: Props = $props()
13
+
14
+ const { disclaimer, content, cta } = $derived(campaign)
15
+ const { header, subheader, header_logo: logo, image } = $derived(content)
16
+ const { header: buttonLabel, url: href } = $derived(cta)
17
+
18
+ trackSplitTestView(SplitTest.DisplayAdPosition)
19
+
20
+ function onclick(): void {
21
+ track(TrackingEvent.DisplayAdClick, null, { campaign_name: campaign.campaign_name })
22
+ trackSplitTestAction(SplitTest.DisplayAdPosition, 'click')
23
+ }
24
+ </script>
25
+
26
+ <a {href} target="_blank" class="display" rel="sponsored" {onclick}>
27
+ {#if disclaimer}
28
+ <div class="disclaimer">
29
+ {disclaimer}
30
+ </div>
31
+ {/if}
32
+
33
+ {#if logo || header || subheader || buttonLabel}
34
+ <div class="cta">
35
+ {#if logo}
36
+ <img class="logo" src={logo} alt="" height="40" width="40" />
37
+ {/if}
38
+
39
+ <div>
40
+ {#if header}
41
+ <span class="name">{header}</span>
42
+ {/if}
43
+
44
+ {#if subheader}
45
+ <div class="subheader">{subheader}</div>
46
+ {/if}
47
+ </div>
48
+
49
+ {#if buttonLabel}
50
+ <div class="action">{buttonLabel}</div>
51
+ {/if}
52
+ </div>
53
+ {/if}
54
+
55
+ {#if image}
56
+ <img class="background" src={image} alt="" />
57
+ {/if}
58
+ </a>
59
+
60
+ <style lang="scss">
61
+ .display {
62
+ display: block;
63
+ position: relative;
64
+ border-radius: var(--playpilot-display-ad-border-radius, margin(0.5));
65
+ overflow: hidden;
66
+ background: black;
67
+ aspect-ratio: 16 / 9;
68
+ color: white !important;
69
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.35);
70
+ font-family: var(--playpilot-detail-font-family, var(--playpilot-font-family));
71
+ line-height: 1.2;
72
+ font-style: normal !important;
73
+ }
74
+
75
+ .disclaimer {
76
+ position: absolute;
77
+ top: 0;
78
+ left: 0;
79
+ padding: margin(0.5);
80
+ font-size: margin(0.75);
81
+ text-decoration: underline;
82
+
83
+ @media (min-width: 600px) {
84
+ padding: margin(1);
85
+ }
86
+ }
87
+
88
+ .cta {
89
+ z-index: 1;
90
+ display: flex;
91
+ align-items: center;
92
+ gap: margin(0.5);
93
+ position: absolute;
94
+ right: 0;
95
+ bottom: 0;
96
+ left: 0;
97
+ padding: margin(0.5);
98
+ background: linear-gradient(to right, rgba(110, 110, 110, 1), rgba(85, 85, 85, 0.3));
99
+ font-size: margin(0.875);
100
+ text-decoration: none;
101
+
102
+ @media (min-width: 600px) {
103
+ padding: margin(1);
104
+ }
105
+ }
106
+
107
+ .logo {
108
+ border-radius: var(--playpilot-display-ad-border-radius, margin(0.5));
109
+ }
110
+
111
+ .name {
112
+ font-weight: 500;
113
+ }
114
+
115
+ .subheader {
116
+ font-size: margin(0.75);
117
+ }
118
+
119
+ .action {
120
+ margin-left: auto;
121
+ padding: margin(0.5) margin(0.75);
122
+ background: var(--playpilot-display-ad-action-background, white);
123
+ border-radius: var(--playpilot-display-action-border-radius, margin(2));
124
+ transition: transform 50ms;
125
+ text-shadow: none;
126
+ font-weight: var(--playpilot-display-action-font-weight, 500);
127
+ color: var(--playpilot-display-action-text-color, black);
128
+ white-space: nowrap;
129
+
130
+ &:hover {
131
+ transform: scale(1.05);
132
+ }
133
+ }
134
+
135
+ .background {
136
+ display: block;
137
+ width: 100%;
138
+ height: auto;
139
+ }
140
+ </style>
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ import { ImageDimensions } from '$lib/enums/ImageDimensions'
3
+ import { SplitTest } from '$lib/enums/SplitTest'
4
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
5
+ import { imageFromUUID } from '$lib/image'
6
+ import { isInSplitTestVariant, trackSplitTestView } from '$lib/splitTest'
7
+ import { track } from '$lib/tracking'
8
+ import type { Campaign } from '$lib/types/campaign'
9
+ import { fade } from 'svelte/transition'
10
+
11
+ interface Props {
12
+ campaign: Campaign
13
+ }
14
+
15
+ const { campaign }: Props = $props()
16
+
17
+ trackSplitTestView(SplitTest.TopScrollFormat)
18
+
19
+ const { disclaimer, content, cta } = $derived(campaign)
20
+ const { format, header, header_logo: logo, image_uuid: backgroundImageUUID } = $derived(content)
21
+ const { header: buttonLabel, url: href } = $derived(cta)
22
+
23
+ const inline = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
24
+ const simple = $derived(format === 'large')
25
+
26
+ const backgroundImage = $derived(imageFromUUID(backgroundImageUUID, ImageDimensions.TopScrollBackground))
27
+ const contentImage = $derived(imageFromUUID(backgroundImageUUID, ImageDimensions.TopScrollContent))
28
+
29
+ let clientWidth = $state(0)
30
+ let tooltipVisible = $state(false)
31
+
32
+ function trackClick(): void {
33
+ track(TrackingEvent.TopScrollClick, null, { campaign_name: campaign.campaign_name })
34
+ }
35
+
36
+ function toggleTooltip(event: MouseEvent): void {
37
+ event.preventDefault()
38
+ event.stopPropagation()
39
+
40
+ tooltipVisible = !tooltipVisible
41
+ }
42
+ </script>
43
+
44
+ <a
45
+ {href}
46
+ target="_blank"
47
+ class="top-scroll"
48
+ class:simple
49
+ class:inline
50
+ tabindex="-1"
51
+ onclick={trackClick}
52
+ rel="sponsored"
53
+ style:--width="{clientWidth}px"
54
+ bind:clientWidth>
55
+ <div class="content">
56
+ {#if simple}
57
+ <img class="content-image" src={contentImage} alt={header} width="728" height="90" />
58
+ {:else}
59
+ {#if logo}
60
+ <img class="logo" src={logo} alt="" width="50" height="50" />
61
+ {/if}
62
+
63
+ <p class="tagline">{header}</p>
64
+
65
+ {#if buttonLabel}
66
+ <button class="button">{buttonLabel}</button>
67
+ {/if}
68
+ {/if}
69
+ </div>
70
+
71
+ {#if !simple}
72
+ {#if backgroundImageUUID}
73
+ <div class="background" style:--background="url('{backgroundImage}')"></div>
74
+ {/if}
75
+
76
+ {#if disclaimer}
77
+ <button class="tooltip" onclick={toggleTooltip}>
78
+ <div class="disclaimer">i</div>
79
+
80
+ {#if tooltipVisible}
81
+ <div class="tooltip-content" transition:fade={{ duration: 50 }}>
82
+ {disclaimer}
83
+ </div>
84
+ {/if}
85
+ </button>
86
+ {/if}
87
+ {/if}
88
+ </a>
89
+
90
+ <style lang="scss">
91
+ $border-radius-size: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
92
+
93
+ .top-scroll {
94
+ position: relative;
95
+ display: block;
96
+ width: 100%;
97
+ border-radius: $border-radius-size;
98
+ background: black;
99
+ color: var(--playpilot-top-scroll-text-color, white);
100
+ font-family: var(--playpilot-top-scroll-font-family, var(--playpilot-detail-font-family, var(--playpilot-font-family)));
101
+ font-size: var(--playpilot-top-scroll-font-size, var(--playpilot-detail-font-size, 14px));
102
+ text-decoration: none;
103
+ line-height: 1.35;
104
+
105
+ &.inline {
106
+ border-radius: $border-radius-size $border-radius-size 0 0;
107
+ }
108
+ }
109
+
110
+ .content {
111
+ display: flex;
112
+ z-index: 1;
113
+ position: relative;
114
+ align-items: center;
115
+ height: 100%;
116
+ min-height: margin(4.5);
117
+ padding: margin(0.25) margin(0.5);
118
+ gap: margin(0.5);
119
+ color: var(--playpilot-top-scroll-text-color, white);
120
+
121
+ .simple & {
122
+ justify-content: center;
123
+ min-height: 0;
124
+ padding: 0;
125
+ }
126
+ }
127
+
128
+ .logo {
129
+ border-radius: var(--playpilot-top-scroll-logo-border-radius, margin(0.5));
130
+ background: black;
131
+ }
132
+
133
+ .tagline {
134
+ margin: 0;
135
+ text-shadow: var(--playpilot-shadow);
136
+ }
137
+
138
+ .button {
139
+ margin-left: auto;
140
+ margin-right: margin(1);
141
+ padding: margin(0.25) margin(0.5);
142
+ background: var(--playpilot-top-scroll-button-background, white);
143
+ border: 0;
144
+ border-radius: var(--playpilot-top-scroll-button-border-radius, margin(0.25));
145
+ box-shadow: var(--playpilot-shadow);
146
+ color: var(--playpilot-top-scroll-button-text-color, black);
147
+ font-family: inherit;
148
+ font-size: inherit;
149
+ line-height: inherit;
150
+
151
+ &:hover {
152
+ outline: margin(0.25) solid var(--playpilot-top-scroll-button-background, rgba(white, 0.35));
153
+ background: var(--playpilot-top-scroll-button-background, white);
154
+ }
155
+
156
+ &:active {
157
+ background: var(--playpilot-top-scroll-button-background, white);
158
+ }
159
+ }
160
+
161
+ .background {
162
+ z-index: 0;
163
+ position: absolute;
164
+ top: 0;
165
+ right: 0;
166
+ bottom: 0;
167
+ left: 0;
168
+ border-radius: $border-radius-size;
169
+ background-image: var(--background);
170
+ background-position: center;
171
+ background-size: cover;
172
+ opacity: 0.4;
173
+
174
+ .top-scroll:hover & {
175
+ filter: brightness(1.15);
176
+ }
177
+
178
+ .inline & {
179
+ border-radius: $border-radius-size $border-radius-size 0 0;
180
+ }
181
+ }
182
+
183
+ .content-image {
184
+ display: block;
185
+ max-width: 100%;
186
+ height: auto;
187
+ background: black;
188
+ border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
189
+
190
+ .top-scroll:hover & {
191
+ filter: brightness(1.15);
192
+ }
193
+ }
194
+
195
+ .disclaimer {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ border: var(--playpilot-top-scroll-disclaimer-border, 2px solid rgba(255, 255, 255, 0.5));
200
+ padding: margin(0.25);
201
+ height: margin(1.25);
202
+ width: margin(1.25);
203
+ border-radius: 99px;
204
+ color: var(--playpilot-top-scroll-disclaimer-text-color, rgba(255, 255, 255, 0.75));
205
+ font-weight: bold;
206
+ line-height: 1;
207
+
208
+ &:hover {
209
+ border-color: var(--playpilot-top-scroll-disclaimer-hover-border-color, white);
210
+ color: var(--playpilot-top-scroll-disclaimer-hover-text-color, white);
211
+ }
212
+ }
213
+
214
+ .tooltip {
215
+ z-index: 2;
216
+ position: absolute;
217
+ right: 0;
218
+ bottom: 0;
219
+ padding: margin(0.25);
220
+ background: transparent;
221
+ border: 0;
222
+ color: var(--playpilot-top-scroll-text-color, white);
223
+ cursor: pointer;
224
+ }
225
+
226
+ .tooltip-content {
227
+ display: block;
228
+ z-index: 20;
229
+ position: absolute;
230
+ right: 100%;
231
+ bottom: margin(0.25);
232
+ width: calc(var(--width) * 0.8);
233
+ padding: margin(0.5);
234
+ border-radius: margin(1);
235
+ background: var(--playpilot-content);
236
+ box-shadow: var(--playpilot-shadow);
237
+ line-height: 1.1;
238
+ text-align: right;
239
+ }
240
+ </style>