@deriv-com/dds-marketing 1.0.1 → 1.0.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.
Files changed (62) hide show
  1. package/.llm/components/background.md +371 -0
  2. package/.llm/components/ctabanner.md +25 -0
  3. package/.llm/components/ctabannerslider.md +21 -0
  4. package/.llm/components/illustrationcard.md +236 -0
  5. package/.llm/components/illustrationscene.md +233 -0
  6. package/.llm/components/landinghero.md +24 -0
  7. package/.llm/components/pageheader.md +21 -0
  8. package/.llm/digest/background.md +52 -0
  9. package/.llm/digest/ctabanner.md +4 -0
  10. package/.llm/digest/ctabannerslider.md +3 -0
  11. package/.llm/digest/illustrationcard.md +46 -0
  12. package/.llm/digest/illustrationscene.md +51 -0
  13. package/.llm/digest/landinghero.md +7 -0
  14. package/.llm/digest/pageheader.md +3 -0
  15. package/.llm/index.md +7 -0
  16. package/.llm/tokens.md +11 -0
  17. package/AGENTS.md +3 -0
  18. package/dist/components/blocks/cta-banner/CtaBanner.d.ts +1 -1
  19. package/dist/components/blocks/cta-banner/CtaBanner.js +3 -2
  20. package/dist/components/blocks/cta-banner/CtaBanner.types.d.ts +5 -0
  21. package/dist/components/blocks/cta-banner/cta-banner-impl.css +8 -0
  22. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.d.ts +1 -1
  23. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.js +5 -4
  24. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.types.d.ts +4 -0
  25. package/dist/components/blocks/cta-banner-slider/cta-banner-slider-impl.css +8 -0
  26. package/dist/components/blocks/landing-hero/LandingHero.d.ts +1 -1
  27. package/dist/components/blocks/landing-hero/LandingHero.js +4 -3
  28. package/dist/components/blocks/landing-hero/LandingHero.types.d.ts +7 -3
  29. package/dist/components/blocks/page-header/PageHeader.d.ts +1 -1
  30. package/dist/components/blocks/page-header/PageHeader.js +3 -2
  31. package/dist/components/blocks/page-header/PageHeader.types.d.ts +4 -0
  32. package/dist/components/blocks/page-header/page-header-impl.css +8 -0
  33. package/dist/components/core/background/Background.css +69 -0
  34. package/dist/components/core/background/Background.d.ts +21 -0
  35. package/dist/components/core/background/Background.js +36 -0
  36. package/dist/components/core/background/Background.types.d.ts +36 -0
  37. package/dist/components/core/background/Background.types.js +9 -0
  38. package/dist/components/core/background/background-impl.css +427 -0
  39. package/dist/components/core/background/halftone.d.ts +123 -0
  40. package/dist/components/core/background/halftone.js +682 -0
  41. package/dist/components/core/background/use-halftone.d.ts +15 -0
  42. package/dist/components/core/background/use-halftone.js +27 -0
  43. package/dist/components/core/background/use-offscreen.d.ts +10 -0
  44. package/dist/components/core/background/use-offscreen.js +30 -0
  45. package/dist/components/core/background/use-spotlight.d.ts +13 -0
  46. package/dist/components/core/background/use-spotlight.js +65 -0
  47. package/dist/components/core/illustration-card/IllustrationCard.css +40 -0
  48. package/dist/components/core/illustration-card/IllustrationCard.d.ts +22 -0
  49. package/dist/components/core/illustration-card/IllustrationCard.js +82 -0
  50. package/dist/components/core/illustration-card/IllustrationCard.types.d.ts +23 -0
  51. package/dist/components/core/illustration-card/IllustrationCard.types.js +8 -0
  52. package/dist/components/core/illustration-card/illustration-card-impl.css +27 -0
  53. package/dist/components/core/illustration-scene/IllustrationScene.css +44 -0
  54. package/dist/components/core/illustration-scene/IllustrationScene.d.ts +14 -0
  55. package/dist/components/core/illustration-scene/IllustrationScene.js +16 -0
  56. package/dist/components/core/illustration-scene/IllustrationScene.types.d.ts +33 -0
  57. package/dist/components/core/illustration-scene/IllustrationScene.types.js +8 -0
  58. package/dist/components/core/illustration-scene/illustration-scene-impl.css +40 -0
  59. package/dist/components/index.d.ts +6 -0
  60. package/dist/components/index.js +6 -0
  61. package/dist/marketing.css +24 -0
  62. package/package.json +1 -1
@@ -0,0 +1,21 @@
1
+ import './Background.css';
2
+ import './background-impl.css';
3
+ import { type BackgroundProps } from './Background.types.js';
4
+ export type { BackgroundProps };
5
+ /**
6
+ * A ground a section or a block sits on, painted with a pattern.
7
+ *
8
+ * The band paints the tone's flat colour, the pattern is laid over it, and the content over that.
9
+ * The flat colour is what shows before anything has drawn — on a server-rendered page, until
10
+ * hydration — so the band is finished from the first paint and the pattern only ever adds to it.
11
+ *
12
+ * Four patterns. The halftone is drawn by script on a canvas, through `useHalftone`, which owns
13
+ * every question of its cost: it draws only while something moves, pauses out of view and holds
14
+ * the bitmap to twice the pixel density. The aurora, the grid and the rings are painted by the
15
+ * stylesheet, with no canvas; `useSpotlight` only tells them where the pointer is.
16
+ *
17
+ * With nothing to hold it is a layer: it fills its positioned parent behind that parent's own
18
+ * content, and listens for the pointer on the parent, because the content covers it. That is how a
19
+ * block draws a pattern under itself without its own layout changing.
20
+ */
21
+ export declare function Background({ tone, pattern, animated, interactive, children, ...rest }: BackgroundProps): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import './Background.css';
4
+ import './background-impl.css';
5
+ import { backgroundDefaults, backgroundParts } from './Background.types.js';
6
+ import { useHalftone } from './use-halftone.js';
7
+ import { useOffscreen } from './use-offscreen.js';
8
+ import { useSpotlight } from './use-spotlight.js';
9
+ /**
10
+ * A ground a section or a block sits on, painted with a pattern.
11
+ *
12
+ * The band paints the tone's flat colour, the pattern is laid over it, and the content over that.
13
+ * The flat colour is what shows before anything has drawn — on a server-rendered page, until
14
+ * hydration — so the band is finished from the first paint and the pattern only ever adds to it.
15
+ *
16
+ * Four patterns. The halftone is drawn by script on a canvas, through `useHalftone`, which owns
17
+ * every question of its cost: it draws only while something moves, pauses out of view and holds
18
+ * the bitmap to twice the pixel density. The aurora, the grid and the rings are painted by the
19
+ * stylesheet, with no canvas; `useSpotlight` only tells them where the pointer is.
20
+ *
21
+ * With nothing to hold it is a layer: it fills its positioned parent behind that parent's own
22
+ * content, and listens for the pointer on the parent, because the content covers it. That is how a
23
+ * block draws a pattern under itself without its own layout changing.
24
+ */
25
+ export function Background({ tone = backgroundDefaults.tone, pattern = backgroundDefaults.pattern, animated = backgroundDefaults.animated, interactive = backgroundDefaults.interactive, children, ...rest }) {
26
+ // Nothing to hold: a layer under the parent's own content. See above.
27
+ const layer = children === undefined || children === null || children === false;
28
+ const [root, canvas] = useHalftone(pattern === 'halftone', interactive, animated, tone, layer);
29
+ // The patterns the stylesheet paints answer the pointer from where it is: the grid's lines and
30
+ // the rings brighten around it, and the aurora carries a pool of its light after it.
31
+ useSpotlight(root, pattern !== 'halftone' && interactive, layer);
32
+ // The patterns the stylesheet moves at rest wait while the band is off screen; the halftone's
33
+ // painter has its own observer.
34
+ useOffscreen(root, pattern !== 'halftone' && animated);
35
+ return (_jsxs("div", { ...rest, ref: root, className: backgroundParts.root, "data-tone": tone, "data-pattern": pattern, "data-animated": animated ? undefined : 'false', "data-layer": layer || undefined, "aria-hidden": layer || undefined, children: [pattern === 'halftone' ? (_jsx("canvas", { ref: canvas, className: backgroundParts.canvas, "aria-hidden": "true" })) : null, layer ? null : _jsx("div", { className: backgroundParts.content, children: children })] }));
36
+ }
@@ -0,0 +1,36 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ export interface BackgroundOwnProps {
3
+ /**
4
+ * `dark` — The default, and the band the pattern was drawn for: coral dots glowing on color.bg.brand-strong, which is dark in both modes. Reach for it for the band a page opens or closes on, where the glow gives the band a centre
5
+ * `light` — Coral dots on the page's own ground, for a light page that wants one moment of brand colour without a solid coral band. The glow reads as warmth rather than light, so keep it for a section that is not competing with a dark one nearby
6
+ * `brand` — White dots on the coral brand fill, for a call to action standing on it. The heading is white too, and the glow rises from the foot of the band towards it, so check the words read on the coral alone — the dots can only add to what is behind them
7
+ */
8
+ tone?: 'dark' | 'light' | 'brand';
9
+ /**
10
+ * `halftone` — A grid of dots across the whole band that grow towards a soft glow and settle to a fine screen away from it. The default, and the one that moves most
11
+ * `aurora` — Broad soft pools of the brand's colour rising from the band's foot and drifting slowly into one another — light as colour rather than as points. Drawn by the stylesheet: no canvas. At rest the pools wander, there and back, over motion.duration.drift; on an interactive band a smaller pool trails the pointer. Reach for it where the halftone would be too busy — an opening band with a picture or a product on it, where dots behind the picture fight it and a wash of colour does not
12
+ * `grid` — A fine rule grid of hairlines, fading in towards the band's foot — the grid PageHeader paints, offered for any band. Drawn by the stylesheet: no canvas. On an interactive band the lines brighten in a soft circle around the pointer and fade back when it leaves; at rest the rules rise slowly, a pitch over motion.duration.drift. Reach for it where the band should read as deliberate and quiet rather than lit
13
+ * `rings` — Hairline circles spreading out from the middle of the band's foot, fading in towards it, at size.rings.pitch. Drawn by the stylesheet: no canvas. On an interactive band the rings brighten in a soft circle around the pointer and fade back when it leaves; at rest the rings spread slowly outward, a pitch over motion.duration.drift. As quiet as the grid, but round: reach for it where the band should feel open and centred rather than ruled — a call to action, a closing band with one thing to say
14
+ */
15
+ pattern?: 'halftone' | 'aurora' | 'grid' | 'rings';
16
+ /** On by default: every pattern moves slowly at rest, whether or not anybody is pointing at it — the halftone's glow and the aurora's pools drift along the band's foot, the grid's rules rise and the rings spread outward, each over motion.duration.drift. Turn it off for a band that should be still until somebody reaches for it — a band beside a video or an animation of its own, or one a page repeats further down — and the pattern rests where it is. What the pattern does under the pointer is `interactive`, which is set apart from this. */
17
+ animated?: boolean;
18
+ /** On by default: the pattern answers a pointer over the band. The halftone's glow follows it, gathers into a smaller pool and parts the dots it crosses, and a click sends a small ripple through them; the aurora sends a pool of its light after it; the grid's rules and the rings brighten around it. Turn it off for a band whose content is itself interactive enough that a moving ground would compete with it — a band of controls, a form — and the pattern ignores the pointer. Touch has no hover, so on a touch screen nothing follows a finger. Whether it moves at rest is `animated`, which is set apart from this. */
19
+ interactive?: boolean;
20
+ /** Allowed: any content */
21
+ children: ReactNode;
22
+ }
23
+ export type BackgroundProps = BackgroundOwnProps & Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'tone' | 'pattern' | 'animated' | 'interactive'>;
24
+ /** The class each part carries. Apply these — do not retype them. */
25
+ export declare const backgroundParts: {
26
+ readonly root: "dds-background";
27
+ readonly canvas: "dds-background__canvas";
28
+ readonly content: "dds-background__content";
29
+ };
30
+ /** Generated so defaults cannot drift from the contract. */
31
+ export declare const backgroundDefaults: {
32
+ readonly tone: "dark";
33
+ readonly pattern: "halftone";
34
+ readonly animated: true;
35
+ readonly interactive: true;
36
+ };
@@ -0,0 +1,9 @@
1
+ // GENERATED from background.contract.json. Do not edit.
2
+ /** The class each part carries. Apply these — do not retype them. */
3
+ export const backgroundParts = {
4
+ root: 'dds-background',
5
+ canvas: 'dds-background__canvas',
6
+ content: 'dds-background__content',
7
+ };
8
+ /** Generated so defaults cannot drift from the contract. */
9
+ export const backgroundDefaults = { tone: 'dark', pattern: 'halftone', animated: true, interactive: true };
@@ -0,0 +1,427 @@
1
+ /*
2
+ * Scoped to this package, like the generated sheet beside it.
3
+ *
4
+ * `:where([data-dds-context="marketing"])` confines these rules without contributing
5
+ * specificity, so every override below still ties or beats exactly what it did before.
6
+ */
7
+
8
+ /*
9
+ * The canvas fills the band and sits under what the band holds.
10
+ *
11
+ * Absolutely placed at all four edges rather than sized by width and height, so it follows the
12
+ * band through every resize without the script having to set its CSS size — the script sets only
13
+ * the bitmap, from the box this gives it. No pointer events: the pattern is paint, and every press
14
+ * on the band belongs to the content.
15
+ *
16
+ * These are positions and a pointer rule, not design decisions, and a contract has no way to state
17
+ * an inset of zero on a part — which is why they are here rather than bound.
18
+ */
19
+ :where([data-dds-context="marketing"]) .dds-background .dds-background__canvas {
20
+ position: absolute;
21
+ inset: 0;
22
+ display: block;
23
+ inline-size: 100%;
24
+ block-size: 100%;
25
+ pointer-events: none;
26
+ }
27
+
28
+ /*
29
+ * The content stacks above the canvas by being positioned after it. The root isolates, so this
30
+ * order is settled inside the band and nothing outside it has to know a canvas is there.
31
+ */
32
+ :where([data-dds-context="marketing"]) .dds-background .dds-background__content {
33
+ position: relative;
34
+ }
35
+
36
+ /*
37
+ * A Background with nothing to hold is a layer: it fills the band it is placed in, behind that
38
+ * band's own content, which is how a block draws the pattern under itself without its layout
39
+ * changing. The band has to be positioned, and every block that offers the pattern is. It takes no
40
+ * pointer events — the content is on top and gets every press — and the pointer is listened for on
41
+ * the band instead, in `useHalftone`.
42
+ */
43
+ :where([data-dds-context="marketing"]) .dds-background[data-layer='true'] {
44
+ position: absolute;
45
+ inset: 0;
46
+ pointer-events: none;
47
+ }
48
+
49
+ /*
50
+ * The two hairline patterns: the rule grid, for `pattern="grid"` — the grid PageHeader paints, for
51
+ * any band — and the rings, for `pattern="rings"`.
52
+ *
53
+ * Both are repeating gradients of hairlines painted onto the root under everything it holds, with
54
+ * the band's own ground laid over them as a fade: opaque at the top, clear at the foot, so the
55
+ * lines rise from the bottom of the band the way the halftone does. A background-image rather than
56
+ * an element, because it is a texture on the ground and not content, and rather than a canvas
57
+ * because the only thing about it that moves is where the lines start — see *The idle motion*.
58
+ *
59
+ * The line takes the pattern role for the ground it is on: the inverse on the dark ground, a
60
+ * brighter white on the coral, and the brand's coral on the light — each a step above the subtle
61
+ * border, which a hairline drawn across a whole band thinned out to almost nothing. The fade is each tone's own ground, so
62
+ * the lines disappear into the band rather than into a colour the band is not.
63
+ */
64
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings']) {
65
+ --dds-background-rule: var(--color-border-pattern-inverse);
66
+ --dds-background-fade: var(--color-bg-brand-strong);
67
+ /*
68
+ * How thick a rule is: border.width.hairline, which is half a CSS pixel — one device pixel on a
69
+ * screen of twice the density, and the line this grid was drawn with. A standard-density screen
70
+ * has no half pixel to draw it on, and there the rules came out faint or not at all depending on
71
+ * where each one fell: on a tall band every horizontal rule vanished. So below twice the density
72
+ * the rule is border.width.default, one pixel — the thinnest line that screen can draw.
73
+ */
74
+ --dds-background-hairline: var(--border-width-hairline);
75
+ }
76
+
77
+ @media (max-resolution: 1.99dppx) {
78
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings']) {
79
+ --dds-background-hairline: var(--border-width-default);
80
+ }
81
+ }
82
+
83
+ /*
84
+ * On the light ground the lines are the brand's coral rather than a grey, as the halftone's dots
85
+ * and the aurora's pools are there: a third of color.bg.brand, about as strong as the grey it
86
+ * replaces, so the grid stays quiet but reads as the brand's. It lights up to the full coral.
87
+ */
88
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings'])[data-tone='light'] {
89
+ --dds-background-rule: color-mix(in srgb, var(--color-bg-brand) 35%, transparent);
90
+ --dds-background-fade: var(--color-bg-page);
91
+ }
92
+
93
+ /*
94
+ * On the coral ground the inverse pattern line, white at a quarter, all but vanished into the
95
+ * coral, so the lines rest at the muted inverse there and light up to full white.
96
+ */
97
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings'])[data-tone='brand'] {
98
+ --dds-background-rule: var(--color-fg-muted-inverse);
99
+ --dds-background-fade: var(--color-bg-brand);
100
+ }
101
+
102
+ /*
103
+ * The grid: two sets of rules at size.rule-grid.pitch, one across and one down.
104
+ *
105
+ * `to right` is physical: a CSS gradient has no logical direction. A grid is symmetric, so it reads
106
+ * the same in either direction, and it is centred across the band — see the positions below.
107
+ */
108
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='grid'] {
109
+ --dds-background-pitch: var(--size-rule-grid-pitch);
110
+ background-image:
111
+ linear-gradient(to bottom, var(--dds-background-fade), transparent),
112
+ linear-gradient(to right, var(--dds-background-rule) 0 var(--dds-background-hairline), transparent 0),
113
+ linear-gradient(to bottom, var(--dds-background-rule) 0 var(--dds-background-hairline), transparent 0);
114
+ /*
115
+ * Each set of rules is one rule on a tile one pitch long, repeated: a tile the size of the band
116
+ * left a seam wherever the band was not a whole number of pitches, and a rising grid carried
117
+ * that seam up through it. Across, a rule falls on the band's middle — the tile is placed half a
118
+ * pitch right of centre, and its rule is its left edge — so the grid is symmetric about the
119
+ * middle, where the rings are centred and the words sit, and never ends on a sliver at one side
120
+ * only. Down, it counts from the top less the idle shift, so the rules rise a pitch a cycle.
121
+ */
122
+ background-size:
123
+ 100% 100%,
124
+ var(--dds-background-pitch) 100%,
125
+ 100% var(--dds-background-pitch);
126
+ background-repeat: no-repeat, repeat-x, repeat-y;
127
+ background-position:
128
+ left top,
129
+ calc(50% + var(--dds-background-pitch) / 2) top,
130
+ left calc(var(--dds-background-shift, 0%) * -1);
131
+ }
132
+
133
+ /*
134
+ * The rings: circles at size.rings.pitch, centred on the middle of the band's foot, so they spread
135
+ * up and out from where the halftone's glow would rest.
136
+ *
137
+ * Each ring is a hairline at full ink — as much line as a grid rule, so the two read at the same
138
+ * strength — with half a hairline of fade either side. A curve drawn with hard stops steps along
139
+ * every pixel it crosses; the fade anti-aliases it. A ridge that only touched the ink at its peak
140
+ * came out a good deal fainter than the grid in the same colour.
141
+ */
142
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='rings'] {
143
+ --dds-background-pitch: var(--size-rings-pitch);
144
+ background-image:
145
+ linear-gradient(to bottom, var(--dds-background-fade), transparent),
146
+ var(--dds-background-rings);
147
+ }
148
+
149
+ /*
150
+ * The rings themselves, declared on the root and on its lit copy alike, so each resolves them in
151
+ * its own ink. They start at the idle shift rather than at the centre, which spreads every ring a
152
+ * pitch outward each cycle: a repeating gradient repeats over its first stop to its last, and both
153
+ * move together, so the spacing never changes.
154
+ */
155
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='rings'],
156
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='rings']::before {
157
+ --dds-background-rings: repeating-radial-gradient(
158
+ circle at 50% 100%,
159
+ transparent var(--dds-background-shift, 0%)
160
+ calc(var(--dds-background-shift, 0%) + var(--dds-background-pitch) - var(--dds-background-hairline) * 2),
161
+ var(--dds-background-rule)
162
+ calc(var(--dds-background-shift, 0%) + var(--dds-background-pitch) - var(--dds-background-hairline) * 1.5)
163
+ calc(var(--dds-background-shift, 0%) + var(--dds-background-pitch) - var(--dds-background-hairline) * 0.5),
164
+ transparent calc(var(--dds-background-shift, 0%) + var(--dds-background-pitch))
165
+ );
166
+ }
167
+
168
+ /*
169
+ * The pointer's light on the grid and the rings, on an interactive band.
170
+ *
171
+ * A second, brighter copy of the lines, drawn in a pseudo-element behind the band's content and
172
+ * shown only inside a soft circle around the pointer — size.rule-grid.spotlight on the grid,
173
+ * size.rings.spotlight on the rings — so the lines under the pointer brighten and fade back into
174
+ * the resting ones, and nothing else changes. The pointer's position arrives as two custom
175
+ * properties written once a frame by `useSpotlight`, and `data-spotlit` while it is over the band;
176
+ * the circle fades in and out on the element's opacity, which the compositor runs.
177
+ *
178
+ * `::before`, so it comes before the content in the band's painting order and the content stays on
179
+ * top of it. The lit line is a step up from the resting one on each ground: muted-inverse on the
180
+ * dark ground, full white on the coral, the full brand coral on the light one. Same hairline, same pitch, same
181
+ * position, so a lit line is the resting line brighter rather than a line beside it.
182
+ */
183
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings'])::before {
184
+ --dds-background-lit: var(--color-fg-muted-inverse);
185
+ /* The shift is registered as not inherited — see *The idle motion* — so the lit copy asks for it. */
186
+ --dds-background-shift: inherit;
187
+ content: '';
188
+ position: absolute;
189
+ inset: 0;
190
+ pointer-events: none;
191
+ mask-image: radial-gradient(
192
+ circle var(--dds-background-reach) at var(--dds-background-spot-x, 50%) var(--dds-background-spot-y, 50%),
193
+ #000,
194
+ transparent
195
+ );
196
+ opacity: 0;
197
+ transition-property: opacity;
198
+ transition-duration: var(--motion-duration-normal);
199
+ transition-timing-function: var(--motion-easing-enter);
200
+ }
201
+
202
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='grid']::before {
203
+ --dds-background-reach: var(--size-rule-grid-spotlight);
204
+ background-image:
205
+ linear-gradient(to right, var(--dds-background-lit) 0 var(--dds-background-hairline), transparent 0),
206
+ linear-gradient(to bottom, var(--dds-background-lit) 0 var(--dds-background-hairline), transparent 0);
207
+ background-size:
208
+ var(--dds-background-pitch) 100%,
209
+ 100% var(--dds-background-pitch);
210
+ background-repeat: repeat-x, repeat-y;
211
+ background-position:
212
+ calc(50% + var(--dds-background-pitch) / 2) top,
213
+ left calc(var(--dds-background-shift, 0%) * -1);
214
+ }
215
+
216
+ /* The rings' lit copy: the same rings, with the rule redeclared as the lit ink. */
217
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='rings']::before {
218
+ --dds-background-reach: var(--size-rings-spotlight);
219
+ --dds-background-rule: var(--dds-background-lit);
220
+ background-image: var(--dds-background-rings);
221
+ }
222
+
223
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings'])[data-tone='light']::before {
224
+ --dds-background-lit: var(--color-bg-brand);
225
+ }
226
+
227
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings'])[data-tone='brand']::before {
228
+ --dds-background-lit: var(--color-fg-on-brand);
229
+ }
230
+
231
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings'])[data-spotlit='true']::before {
232
+ opacity: 1;
233
+ }
234
+
235
+ /*
236
+ * The aurora, for `pattern="aurora"`: broad soft pools of the brand's colour rising from the
237
+ * band's foot and drifting into one another — the band lit by colour rather than by points.
238
+ *
239
+ * Three ellipses fading to nothing, in two inks, painted on a pseudo-element behind the content
240
+ * and larger than the band, so it can sway either way without its edge showing. The sway is a
241
+ * transform about the middle of the foot, there and back over motion.duration.drift: a small
242
+ * slide, a small turn and a slight swell, and because the turn lifts one side as it lowers the
243
+ * other, the pools pass across each other rather than moving as one. A transform, so the
244
+ * compositor runs it and the gradients are painted once rather than every frame.
245
+ *
246
+ * The inks are the tone's: coral deepening to the brand's darkest on the dark ground; coral and
247
+ * its hover step, washed thin, on the light ground; and on the coral ground white and the brand's
248
+ * darkest, as the halftone's dots turn white there — darker inks alone were lost on the coral, and
249
+ * near-black came out as a muddy stain rather than light. The white stays at the foot, under the
250
+ * words rather than behind them, and the words are checked against the coral alone. How strongly
251
+ * it is painted is a share of the pool's own opacity, as the halftone's is — under half on the
252
+ * dark ground, where coral at seven tenths burned rather than glowed, and faintest on the light,
253
+ * where coral at strength would be a coral band.
254
+ *
255
+ * The band's top stays plain: the pools fade out under a mask before they reach the upper third.
256
+ */
257
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora'] {
258
+ --dds-background-pool: var(--color-bg-brand);
259
+ --dds-background-pool-deep: var(--color-bg-brand-active);
260
+ --dds-background-strength: 0.4;
261
+ /*
262
+ * How strongly the pool under the pointer is painted, per ground. Its own number rather than a
263
+ * share of the pools': half of the light ground's faint wash was invisible, and half of the
264
+ * coral ground's white washed out the white words the pointer was over.
265
+ */
266
+ --dds-background-hover: 0.25;
267
+ }
268
+
269
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora'][data-tone='light'] {
270
+ --dds-background-pool-deep: var(--color-bg-brand-hover);
271
+ --dds-background-strength: 0.22;
272
+ --dds-background-hover: 0.3;
273
+ }
274
+
275
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora'][data-tone='brand'] {
276
+ --dds-background-pool: var(--color-fg-on-brand);
277
+ --dds-background-pool-deep: var(--color-bg-brand-active);
278
+ --dds-background-strength: 0.6;
279
+ --dds-background-hover: 0.2;
280
+ }
281
+
282
+ /*
283
+ * Behind the content without being before it: the root isolates, so a negative stacking order
284
+ * puts both pseudo-elements over the band's ground and under everything it holds.
285
+ */
286
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora']::before,
287
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora']::after {
288
+ content: '';
289
+ position: absolute;
290
+ z-index: -1;
291
+ pointer-events: none;
292
+ }
293
+
294
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora']::before {
295
+ /*
296
+ * Half a band deeper than the band, as well as wider: the turn lowers one end of the layer, and
297
+ * a layer that stopped at the foot showed its own edge there as a hard diagonal. So the foot is
298
+ * two thirds of the way down the layer, and every position and reach below is measured to it.
299
+ */
300
+ inset-block: 0 -50%;
301
+ inset-inline: -25%;
302
+ background-image:
303
+ radial-gradient(ellipse 30% 53% at 32% calc(100% / 1.5), var(--dds-background-pool), transparent),
304
+ radial-gradient(ellipse 26% 43% at 66% calc(100% / 1.5), var(--dds-background-pool-deep), transparent),
305
+ radial-gradient(ellipse 18% 31% at 50% calc(100% / 1.5), var(--dds-background-pool), transparent);
306
+ mask-image: linear-gradient(to bottom, transparent 20%, #000 calc(100% / 1.5));
307
+ opacity: var(--dds-background-strength);
308
+ transform-origin: 50% calc(100% / 1.5);
309
+ animation: dds-background-aurora var(--motion-duration-drift) var(--motion-easing-shift) infinite alternate;
310
+ }
311
+
312
+ @keyframes dds-background-aurora {
313
+ from {
314
+ transform: translateX(-6%) rotate(-3deg);
315
+ }
316
+ to {
317
+ transform: translateX(6%) rotate(3deg) scale(1.08, 1.14);
318
+ }
319
+ }
320
+
321
+ /*
322
+ * The pool that trails the pointer: a disc of the pool's ink, size.aurora.spotlight in radius,
323
+ * moved under the pointer by a transform that eases after it over motion.duration.trail — the
324
+ * halftone's glow and this one keep the same distance behind the hand. Placed from the band's
325
+ * physical top left because the pointer's position is measured from there. It fades in while the
326
+ * pointer is over the band and out when it leaves, at the ground's own hover strength: at the
327
+ * pools' full strength, and twice the size, it washed over a narrow band and drowned its words.
328
+ *
329
+ * It trails the pointer only while lit. Unlit it moves at once, so the pointer arriving puts the
330
+ * pool where the pointer is before it fades in — `useSpotlight` writes the position first —
331
+ * rather than the pool sliding in from wherever it last was, or from the band's corner.
332
+ */
333
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora']::after {
334
+ left: 0;
335
+ top: 0;
336
+ inline-size: calc(var(--size-aurora-spotlight) * 2);
337
+ block-size: calc(var(--size-aurora-spotlight) * 2);
338
+ /*
339
+ * Eased to nothing rather than straight to it: a straight fade stops abruptly at its rim, and the
340
+ * eye read the stop as the edge of a disc. The steps follow a smooth curve down from the centre.
341
+ */
342
+ background-image: radial-gradient(
343
+ circle closest-side,
344
+ var(--dds-background-pool),
345
+ color-mix(in srgb, var(--dds-background-pool) 62%, transparent) 30%,
346
+ color-mix(in srgb, var(--dds-background-pool) 26%, transparent) 58%,
347
+ color-mix(in srgb, var(--dds-background-pool) 7%, transparent) 82%,
348
+ transparent
349
+ );
350
+ transform: translate(
351
+ calc(var(--dds-background-spot-x, 50%) - var(--size-aurora-spotlight)),
352
+ calc(var(--dds-background-spot-y, 50%) - var(--size-aurora-spotlight))
353
+ );
354
+ opacity: 0;
355
+ transition-property: opacity, transform;
356
+ transition-duration: var(--motion-duration-normal), var(--motion-duration-instant);
357
+ transition-timing-function: var(--motion-easing-enter);
358
+ }
359
+
360
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora'][data-spotlit='true']::after {
361
+ opacity: var(--dds-background-hover);
362
+ transition-duration: var(--motion-duration-normal), var(--motion-duration-trail);
363
+ }
364
+
365
+ /*
366
+ * The idle motion of the grid and the rings: the grid's rules rise a pitch and the rings spread a
367
+ * pitch outward, over motion.duration.drift at an even pace, and again — a pitch on, the pattern
368
+ * is where it began, so the loop has no seam. The halftone's glow and the
369
+ * aurora's pools drift in the same time, so every pattern is moving at a band's pace at rest.
370
+ *
371
+ * The shift is registered so it can be animated: an unregistered custom property only flips from
372
+ * one value to the next. It is not inherited, so the frames reach the root and the lit copy that
373
+ * asks for it, and never restyle the band's content. What moves is a background's start, which
374
+ * repaints the band whenever it changes — so it changes in two hundred even steps a cycle, about
375
+ * twenty a second, rather than at the display's rate: well under a pixel a step at this pace,
376
+ * so it still reads as smooth, and as cheap as the halftone's drift, which draws at thirty. It
377
+ * stops while the band is off screen (`data-offscreen`, written by Background), and when the band
378
+ * is asked to hold still.
379
+ */
380
+ @property --dds-background-shift {
381
+ syntax: '<length>';
382
+ inherits: false;
383
+ initial-value: 0;
384
+ }
385
+ /*
386
+ * Every use names a fallback of nothing, for an engine without `@property`: there the shift is an
387
+ * unregistered property nobody sets while the band is still, and a bare reference to it would make
388
+ * the whole background invalid — the grid and the rings would vanish exactly when motion is off.
389
+ */
390
+
391
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings']) {
392
+ animation: dds-background-shift var(--motion-duration-drift) steps(200) infinite;
393
+ }
394
+
395
+ @keyframes dds-background-shift {
396
+ to {
397
+ --dds-background-shift: var(--dds-background-pitch);
398
+ }
399
+ }
400
+
401
+ /* Off screen, nobody sees it move: every pattern's idle motion waits where it is. */
402
+ :where([data-dds-context="marketing"]) .dds-background[data-offscreen='true'],
403
+ :where([data-dds-context="marketing"]) .dds-background[data-offscreen='true']::before {
404
+ animation-play-state: paused;
405
+ }
406
+
407
+ /* `animated={false}`: every pattern holds still at rest. What it does under the pointer is `interactive`. */
408
+ :where([data-dds-context="marketing"]) .dds-background[data-animated='false'],
409
+ :where([data-dds-context="marketing"]) .dds-background[data-animated='false'][data-pattern='aurora']::before {
410
+ animation: none;
411
+ }
412
+
413
+ /*
414
+ * For a reader who asked for less motion, nothing moves at rest, and the pointer's light on every
415
+ * pattern comes and goes at once rather than fading or trailing.
416
+ */
417
+ @media (prefers-reduced-motion: reduce) {
418
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings']),
419
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora']::before {
420
+ animation: none;
421
+ }
422
+
423
+ :where([data-dds-context="marketing"]) .dds-background:is([data-pattern='grid'], [data-pattern='rings'], [data-pattern='aurora'])::before,
424
+ :where([data-dds-context="marketing"]) .dds-background[data-pattern='aurora']::after {
425
+ transition: none;
426
+ }
427
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The halftone, drawn on a canvas and only when it has to be.
3
+ *
4
+ * A grid of dots, each as large and as bright as the glow is where it sits: full at the glow's
5
+ * centre, falling away along a soft ellipse, and gone at the edges. The glow rests at the middle of
6
+ * the band's bottom edge and — where the band allows it — follows the pointer. The dots themselves
7
+ * answer the pointer too: a moving pointer shoves the ones near it aside and a click sends a ripple
8
+ * out from where it landed, and each dot springs back to its place in the grid.
9
+ *
10
+ * What it costs is the reason it is written this way rather than as CSS or as an animation loop:
11
+ *
12
+ * - It draws on change and nowhere else. A resize draws one frame; a pointer draws frames only
13
+ * while the glow is catching up or a dot is still moving. The resting drift is the one thing that
14
+ * keeps it drawing, at half the display's rate, and only while the band is on screen and motion
15
+ * is allowed — with the drift set to nothing, a band nobody is touching runs no code at all.
16
+ * - A band out of view draws nothing, and catches up with one frame when it returns.
17
+ * - The bitmap is held to twice the CSS pixel density, which is all a dot this small can use.
18
+ * - The dots are batched by brightness into a few dozen paths, so a frame is a few dozen fills
19
+ * rather than one per dot. Drawing visits only the rows below the pattern's edge, rejects a dot
20
+ * too small to be seen before working out its light — every dot, while the size floor keeps the
21
+ * screen across the whole band — and draws a dot under a pixel and a half as a square, which
22
+ * nobody can tell from a circle at that size.
23
+ * - Pointer events only record where the pointer is. The frame reads the band's position once and
24
+ * acts on it, so a fast mouse costs one layout read a frame rather than one per event; a shove or
25
+ * a ripple visits the grid cells it can reach, and the spring visits only the dots still moving.
26
+ *
27
+ * Every default comes from a token: the inks are the canvas's computed `color` (the glow's edge) and
28
+ * `outline-color` (its centre), the grid is
29
+ * size.halftone.pitch and size.halftone.dot, and the glow's movement is the canvas's transition
30
+ * duration and timing function. The rest are settings this file names, and `retune` overrides any
31
+ * of them while a pattern is being decided — the preview's Tune panel, and nothing a page uses.
32
+ */
33
+ export interface HalftoneOptions {
34
+ /** Whether the pattern answers the pointer. Ignored for reduced motion. */
35
+ interactive: boolean;
36
+ /** Whether the resting glow drifts on its own. Ignored for reduced motion. */
37
+ animated: boolean;
38
+ /**
39
+ * Where the pointer is listened for, when that is not the band itself — a Background used as a
40
+ * layer behind a block's content, where the content covers the layer and the pointer never
41
+ * reaches it. Positions are still measured against the band.
42
+ */
43
+ host?: HTMLElement;
44
+ }
45
+ export interface Halftone {
46
+ /** Stop listening, stop drawing, and let go of every observer. */
47
+ destroy: () => void;
48
+ }
49
+ /** Every setting the pattern is drawn with. Colours are any CSS colour; lengths are CSS pixels. */
50
+ export interface HalftoneTuning {
51
+ /** The dots' colour at the edge of the glow. Defaults to the canvas's `color`. */
52
+ base: string;
53
+ /** The dots' colour at the glow's centre. Defaults to the canvas's outline colour. */
54
+ active: string;
55
+ /** How opaque the faintest dots are, from 0 to 1 — what makes the edge a fade, not a line. */
56
+ floor: number;
57
+ /**
58
+ * How strong the whole pattern is, from 0 to 1: every dot's opacity, scaled. The pattern's shape
59
+ * is untouched — the glow, the fade, the floor — only how loudly it is drawn.
60
+ */
61
+ opacity: number;
62
+ /** The widest a dot grows, at the glow's centre. size.halftone.dot. */
63
+ dot: number;
64
+ /** Centre to centre, between neighbouring dots. size.halftone.pitch. */
65
+ pitch: number;
66
+ /** How wide the glow reaches, as a share of the band's width. */
67
+ reachX: number;
68
+ /** How tall the glow reaches, as a share of the band's height. */
69
+ reachY: number;
70
+ /**
71
+ * How large the glow is while a pointer is over the band, as a share of its resting size. Below 1
72
+ * the light gathers under the pointer instead of washing across the band it rested across.
73
+ */
74
+ hoverScale: number;
75
+ /**
76
+ * Where the pattern begins, as a share of the band's height from the top. Above it no dot is
77
+ * drawn, whatever the glow is doing — 0.3 would keep the pattern to the band's lower seven tenths.
78
+ */
79
+ top: number;
80
+ /** How soft the pattern's upper edge is, as a share of the band's height. */
81
+ topFade: number;
82
+ /**
83
+ * The smallest a dot gets, however far it is from the glow, as a share of its widest — what
84
+ * keeps the screen of dots across the whole band rather than only around the glow. 0 lets the
85
+ * dots far from the glow shrink to nothing.
86
+ */
87
+ sizeFloor: number;
88
+ /** The highest the glow may rise towards the pointer, as a share of the band's height from the top. */
89
+ ceiling: number;
90
+ /** How sharply the glow falls off — higher is a tighter, harder-edged glow. */
91
+ falloff: number;
92
+ /** Where the glow rests, as a share of the band's width. */
93
+ restX: number;
94
+ /** Where the glow rests, as a share of the band's height. */
95
+ restY: number;
96
+ /** How long the glow takes to catch up with the pointer and to glide back to rest, in ms. */
97
+ follow: number;
98
+ /** How far the resting glow drifts either side of its rest, as a share of the band's width. 0 holds it still. */
99
+ drift: number;
100
+ /** How far it rises and falls as it drifts, as a share of the band's height. */
101
+ driftRise: number;
102
+ /** How long one sweep of the drift takes, in seconds — longer is slower. */
103
+ driftPeriod: number;
104
+ /** How far from a moving pointer the dots are shoved aside, in px. 0 turns the shove off. */
105
+ proximity: number;
106
+ /** How hard the dots resist being shoved — higher moves them less. */
107
+ resistance: number;
108
+ /** How long a shoved dot takes to settle back into the grid, in seconds. */
109
+ returnDuration: number;
110
+ /** How far a click's ripple reaches, in px. 0 turns the ripple off. */
111
+ shockRadius: number;
112
+ /** How hard a click's ripple pushes the dots it reaches. */
113
+ shockStrength: number;
114
+ }
115
+ /**
116
+ * Override settings on every halftone on the page, live. For deciding a pattern — the preview's
117
+ * Tune panel calls it — and never for a page: what a page draws is what the tokens and the
118
+ * settings above say.
119
+ */
120
+ export declare function retune(next: Partial<HalftoneTuning>): void;
121
+ /** The settings the last halftone drew with, overrides included — for a panel to show. */
122
+ export declare function currentTuning(): HalftoneTuning | null;
123
+ export declare function halftone(root: HTMLElement, canvas: HTMLCanvasElement, options: HalftoneOptions): Halftone;