@struq/skill 0.2.2 → 0.4.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 (122) hide show
  1. package/README.md +23 -35
  2. package/package.json +3 -2
  3. package/skills/struq/SKILL.md +148 -212
  4. package/skills/struq/blocks/CATALOG.md +426 -0
  5. package/skills/struq/blocks/backgrounds/background-aurora.tsx +818 -0
  6. package/skills/struq/blocks/backgrounds/background-facets.tsx +1283 -0
  7. package/skills/struq/blocks/backgrounds/background-silk.tsx +906 -0
  8. package/skills/struq/blocks/catalog.json +1302 -0
  9. package/skills/struq/blocks/footers/footer-contact.tsx +789 -0
  10. package/skills/struq/blocks/footers/footer-reveal.tsx +313 -0
  11. package/skills/struq/blocks/headers/header-bar.tsx +652 -0
  12. package/skills/struq/blocks/headers/menu-curtain.tsx +790 -0
  13. package/skills/struq/blocks/heroes/hero-framed.tsx +627 -0
  14. package/skills/struq/blocks/heroes/hero-index.tsx +855 -0
  15. package/skills/struq/blocks/heroes/hero-poster.tsx +1086 -0
  16. package/skills/struq/blocks/heroes/hero-split.tsx +685 -0
  17. package/skills/struq/blocks/heroes/hero-still.tsx +862 -0
  18. package/skills/struq/blocks/kit/Cursor.tsx +117 -0
  19. package/skills/struq/blocks/kit/HeroCanvas.tsx +278 -0
  20. package/skills/struq/blocks/kit/HorizontalGallery.tsx +106 -0
  21. package/skills/struq/blocks/kit/MagneticButton.tsx +92 -0
  22. package/skills/struq/blocks/kit/MediaFrame.tsx +96 -0
  23. package/skills/struq/blocks/kit/PinnedZoom.tsx +89 -0
  24. package/skills/struq/blocks/kit/Reveal.tsx +64 -0
  25. package/skills/struq/blocks/kit/SmoothScroll.tsx +57 -0
  26. package/skills/struq/blocks/kit/SplitHeadline.tsx +68 -0
  27. package/skills/struq/blocks/kit/blocks.css +2102 -0
  28. package/skills/struq/blocks/kit/gsap.ts +21 -0
  29. package/skills/struq/blocks/kit/images.ts +17 -0
  30. package/skills/struq/blocks/kit/photo-read.ts +102 -0
  31. package/skills/struq/blocks/kit/scenes/contours.ts +161 -0
  32. package/skills/struq/blocks/kit/scenes/dots.ts +110 -0
  33. package/skills/struq/blocks/kit/scenes/ripple.ts +99 -0
  34. package/skills/struq/blocks/kit/scenes/types.ts +114 -0
  35. package/skills/struq/blocks/page-transitions/page-transition-gate.tsx +706 -0
  36. package/skills/struq/blocks/page-transitions/page-transition-slices.tsx +469 -0
  37. package/skills/struq/blocks/page-transitions/page-transition-stack.tsx +583 -0
  38. package/skills/struq/blocks/preloaders/preloader-gate.tsx +358 -0
  39. package/skills/struq/blocks/preloaders/preloader-slices-uncovered.ts +50 -0
  40. package/skills/struq/blocks/preloaders/preloader-slices.tsx +289 -0
  41. package/skills/struq/blocks/preloaders/preloader-stack.tsx +367 -0
  42. package/skills/struq/blocks/preloaders/preloader-wordmark.tsx +568 -0
  43. package/skills/struq/blocks/sections/about-studio.tsx +436 -0
  44. package/skills/struq/blocks/sections/approach.tsx +661 -0
  45. package/skills/struq/blocks/sections/contact-form.tsx +664 -0
  46. package/skills/struq/blocks/sections/cta-closing.tsx +763 -0
  47. package/skills/struq/blocks/sections/fanned-cards.tsx +547 -0
  48. package/skills/struq/blocks/sections/film-strip.tsx +385 -0
  49. package/skills/struq/blocks/sections/grid-assembly.tsx +616 -0
  50. package/skills/struq/blocks/sections/hover-list.tsx +467 -0
  51. package/skills/struq/blocks/sections/image-trail.tsx +432 -0
  52. package/skills/struq/blocks/sections/manifesto.tsx +691 -0
  53. package/skills/struq/blocks/sections/panel-accordion.tsx +380 -0
  54. package/skills/struq/blocks/sections/reveals.tsx +416 -0
  55. package/skills/struq/blocks/sections/split-pair.tsx +670 -0
  56. package/skills/struq/blocks/sections/sticky-chapters.tsx +256 -0
  57. package/skills/struq/blocks/sections/velocity-type.tsx +269 -0
  58. package/skills/struq/blocks/showpieces/fill-rows.tsx +74 -0
  59. package/skills/struq/blocks/showpieces/hero-opener.tsx +128 -0
  60. package/skills/struq/blocks/showpieces/image-to-fullbleed.tsx +89 -0
  61. package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +48 -0
  62. package/skills/struq/blocks/showpieces/panel-wipe.tsx +89 -0
  63. package/skills/struq/blocks/showpieces/pixel-resolve.tsx +107 -0
  64. package/skills/struq/blocks/showpieces/project-stack.tsx +86 -0
  65. package/skills/struq/blocks/showpieces/scrub-connector.tsx +307 -0
  66. package/skills/struq/blocks/showpieces/wordmark-finale.tsx +105 -0
  67. package/skills/struq/blocks/transitions/zoom-through.tsx +613 -0
  68. package/skills/struq/canon/canon.json +627 -0
  69. package/skills/struq/cinematic/README.md +7 -18
  70. package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +1 -1
  71. package/skills/struq/cinematic/examples/code/shared/styles.css +1 -1
  72. package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +1 -1
  73. package/skills/struq/cinematic/references/01-core-mental-model.md +1 -1
  74. package/skills/struq/cinematic/references/02-vocabulary.md +1 -1
  75. package/skills/struq/cinematic/references/07-stack-integration.md +2 -2
  76. package/skills/struq/recipes/app/fonts.ts +12 -0
  77. package/skills/struq/recipes/app/globals.css +42 -0
  78. package/skills/struq/recipes/app/layout.tsx +30 -0
  79. package/skills/struq/references/craft.md +77 -0
  80. package/skills/struq/references/iconography.md +28 -230
  81. package/skills/struq/references/stack-targets.md +23 -74
  82. package/skills/struq/references/tells.md +52 -0
  83. package/skills/struq/references/triage.md +6 -7
  84. package/skills/struq/scripts/blocks.mjs +156 -0
  85. package/skills/struq/scripts/canon.mjs +134 -0
  86. package/skills/struq/scripts/check.mjs +513 -0
  87. package/skills/struq/scripts/images.mjs +360 -0
  88. package/skills/struq/scripts/log.mjs +75 -0
  89. package/skills/struq/scripts/packs.mjs +156 -0
  90. package/skills/struq/scripts/tokens.mjs +222 -0
  91. package/skills/struq/references/anti-patterns.md +0 -265
  92. package/skills/struq/references/baseline-prompt.md +0 -118
  93. package/skills/struq/references/brand.md +0 -143
  94. package/skills/struq/references/brief-router.md +0 -219
  95. package/skills/struq/references/composition.md +0 -191
  96. package/skills/struq/references/depth-parallax.md +0 -235
  97. package/skills/struq/references/direction.md +0 -188
  98. package/skills/struq/references/feel-layer.md +0 -107
  99. package/skills/struq/references/gsap-api.md +0 -293
  100. package/skills/struq/references/imagery.md +0 -144
  101. package/skills/struq/references/loaders.md +0 -287
  102. package/skills/struq/references/micro-interactions.md +0 -270
  103. package/skills/struq/references/mobile-composition.md +0 -70
  104. package/skills/struq/references/nav-header.md +0 -296
  105. package/skills/struq/references/page-architecture.md +0 -134
  106. package/skills/struq/references/page-entrances.md +0 -198
  107. package/skills/struq/references/performance.md +0 -235
  108. package/skills/struq/references/pinning-scrub.md +0 -341
  109. package/skills/struq/references/pitfalls.md +0 -290
  110. package/skills/struq/references/route-transitions.md +0 -281
  111. package/skills/struq/references/scaling.md +0 -151
  112. package/skills/struq/references/scroll-reveals.md +0 -280
  113. package/skills/struq/references/seo.md +0 -219
  114. package/skills/struq/references/setup.md +0 -397
  115. package/skills/struq/references/showcase.md +0 -87
  116. package/skills/struq/references/struq-imagery.md +0 -107
  117. package/skills/struq/references/svg-motion.md +0 -390
  118. package/skills/struq/references/transitions.md +0 -160
  119. package/skills/struq/references/typography.md +0 -720
  120. package/skills/struq/references/vibe-presets.md +0 -287
  121. package/skills/struq/references/webgl-r3f.md +0 -274
  122. package/skills/struq/references/webgl-scenes.md +0 -593
@@ -0,0 +1,2102 @@
1
+ /* ==========================================================================
2
+ The block kit's CSS, as a client project gets it from the skill (recipes/app/globals.css), with one change:
3
+ everything that the skill sets on :root and html is scoped to [data-blocks] here, so the blocks can render
4
+ inside struq.nl's own page without touching it. Imported once from app/globals.css.
5
+
6
+ Two names exist in Struq's own theme too: --color-accent and --font-display. Inside [data-blocks] they are
7
+ pointed at the block tokens, so bg-accent and font-display mean the block's palette and face there and
8
+ Struq's everywhere else.
9
+ ========================================================================== */
10
+
11
+ @theme inline {
12
+ --color-surface-0: var(--surface-0);
13
+ --color-surface-1: var(--surface-1);
14
+ --color-surface-2: var(--surface-2);
15
+ --color-surface-3: var(--surface-3);
16
+ --color-ink: var(--ink);
17
+ --color-ink-muted: var(--ink-muted);
18
+ --color-accent-ink: var(--accent-ink);
19
+ --color-line: var(--line);
20
+ --color-on-photo: var(--on-photo);
21
+ --color-inverse: var(--inverse);
22
+ --color-on-inverse: var(--on-inverse);
23
+ --color-field: var(--field);
24
+ --color-on-field: var(--on-field);
25
+ --font-body: var(--face-body), ui-sans-serif, system-ui, sans-serif;
26
+ --font-accent: var(--face-accent, var(--face-display)), ui-serif, Georgia, serif;
27
+ }
28
+
29
+ @theme {
30
+ /* Type roles: text-display, text-h2, text-h3, text-lead, text-body, text-small. */
31
+ --text-display: clamp(2.875rem, 1.2rem + 6.86vw, 8rem);
32
+ --text-display--line-height: 0.95;
33
+ --text-display--letter-spacing: -0.025em;
34
+ --text-h2: clamp(2.25rem, 1.3rem + 3.4vw, 4.75rem);
35
+ --text-h2--line-height: 1.04;
36
+ --text-h2--letter-spacing: -0.015em;
37
+ --text-h3: clamp(1.375rem, 1.15rem + 0.8vw, 1.875rem);
38
+ --text-h3--line-height: 1.2;
39
+ --text-lead: clamp(1.125rem, 1.05rem + 0.3vw, 1.3125rem);
40
+ --text-lead--line-height: 1.5;
41
+ --text-body: 1.0625rem;
42
+ --text-body--line-height: 1.65;
43
+ --text-small: 0.875rem;
44
+ --text-small--line-height: 1.5;
45
+ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
46
+ --ease-in-out-quint: cubic-bezier(0.83, 0, 0.17, 1);
47
+ }
48
+
49
+ /* The default tokens (the skill's offline "paper" preset). The lab overwrites them per palette. */
50
+ [data-blocks] {
51
+ --surface-0: #f5efe6;
52
+ --surface-1: #ede4d6;
53
+ --surface-2: #e1d4bf;
54
+ --surface-3: #cdbb9f;
55
+ --ink: #1e1a15;
56
+ --ink-muted: #5d5446;
57
+ --accent: #8a3b22;
58
+ --accent-ink: #fbf6ee;
59
+ --line: rgb(30 26 21 / 0.12);
60
+ --on-photo: #f7f3ec;
61
+ --inverse: #1e1a15;
62
+ --on-inverse: #f5efe6;
63
+ --field: #8a3b22;
64
+ --on-field: #fbf6ee;
65
+ --display-weight: 300;
66
+
67
+ --color-accent: var(--accent);
68
+ --font-display: var(--face-display), ui-serif, Georgia, serif;
69
+
70
+ font-size: clamp(1rem, 0.92rem + 0.2vw, 1.125rem);
71
+ background: var(--surface-0);
72
+ color: var(--ink);
73
+ font-family: var(--font-body);
74
+ line-height: 1.65;
75
+ -webkit-font-smoothing: antialiased;
76
+ text-rendering: optimizeLegibility;
77
+ }
78
+
79
+ [data-blocks] :is(h1, h2, h3, .font-display) {
80
+ font-family: var(--font-display);
81
+ font-weight: var(--display-weight);
82
+ text-wrap: balance;
83
+ }
84
+ [data-blocks] p {
85
+ text-wrap: pretty;
86
+ }
87
+ [data-blocks] ::selection {
88
+ background: var(--accent);
89
+ color: var(--accent-ink);
90
+ }
91
+ [data-blocks] :focus-visible {
92
+ outline: 2px solid var(--accent);
93
+ outline-offset: 3px;
94
+ }
95
+
96
+ /* The contrast scene: wrap a section in scene-inverse or scene-field; the tokens turn inside it. */
97
+ [data-blocks] .scene-inverse {
98
+ --surface-0: var(--inverse);
99
+ --surface-1: color-mix(in oklab, var(--inverse), var(--on-inverse) 6%);
100
+ --surface-2: color-mix(in oklab, var(--inverse), var(--on-inverse) 12%);
101
+ --surface-3: color-mix(in oklab, var(--inverse), var(--on-inverse) 22%);
102
+ --ink: var(--on-inverse);
103
+ --ink-muted: color-mix(in oklab, var(--on-inverse) 72%, var(--inverse));
104
+ --line: color-mix(in oklab, var(--on-inverse) 18%, transparent);
105
+ background-color: var(--surface-0);
106
+ color: var(--ink);
107
+ }
108
+ [data-blocks] .scene-field {
109
+ --surface-0: var(--field);
110
+ --surface-1: color-mix(in oklab, var(--field), var(--on-field) 8%);
111
+ --surface-2: color-mix(in oklab, var(--field), var(--on-field) 14%);
112
+ --surface-3: color-mix(in oklab, var(--field), var(--on-field) 24%);
113
+ --ink: var(--on-field);
114
+ --ink-muted: color-mix(in oklab, var(--on-field) 78%, var(--field));
115
+ --line: color-mix(in oklab, var(--on-field) 22%, transparent);
116
+ --accent: var(--on-field);
117
+ --accent-ink: var(--field);
118
+ background-color: var(--surface-0);
119
+ color: var(--ink);
120
+ }
121
+
122
+ /* The hero opener's handover: the section after it slides up over the pinned opener. */
123
+ [data-blocks] [data-showpiece="hero-opener"] + *,
124
+ [data-blocks] .pin-spacer:has(> [data-showpiece="hero-opener"]) + * {
125
+ position: relative;
126
+ z-index: 2;
127
+ background-color: var(--surface-0);
128
+ }
129
+
130
+ /* The motion gate. In a client project the skill's layout puts `js` on <html> before paint; on struq.nl the root
131
+ layout's pre-paint script puts `sq-js` there (components/site/loaders/use-first-visit.ts), so a visitor without
132
+ JavaScript gets every element in its final state and nothing waits for a script that will not run. */
133
+ .sq-js [data-blocks] [data-split]:not([data-ready]) {
134
+ visibility: hidden;
135
+ animation: struq-show 0s 3s forwards;
136
+ }
137
+ .sq-js [data-blocks] [data-reveal]:not([data-ready]) > * {
138
+ opacity: 0;
139
+ animation: struq-fade 0s 3s forwards;
140
+ }
141
+ @media (prefers-reduced-motion: no-preference) {
142
+ .sq-js [data-blocks] [data-frame="mask"]:not([data-ready]) {
143
+ clip-path: inset(100% 0 0 0);
144
+ animation: struq-open 0s 3s forwards;
145
+ }
146
+ .sq-js [data-blocks] [data-zoom-frame] {
147
+ clip-path: inset(22% 30% 22% 30%);
148
+ }
149
+ }
150
+ @media (min-width: 768px) and (prefers-reduced-motion: no-preference) {
151
+ .sq-js [data-blocks] [data-showpiece="image-to-fullbleed"] [data-frame] {
152
+ clip-path: inset(31% 40% 31% 40%);
153
+ }
154
+ /* The connector's start is computed from the layout, so the joined still waits unseen until it is set. */
155
+ .sq-js [data-blocks] [data-showpiece="scrub-connector"]:not([data-motion]) > [data-row] {
156
+ visibility: hidden;
157
+ animation: struq-show 0s 3s forwards;
158
+ }
159
+ .sq-js [data-blocks] [data-showpiece="pixel-resolve"] canvas {
160
+ opacity: 1;
161
+ background: var(--surface-2);
162
+ animation: struq-unveil 0s 4s forwards;
163
+ }
164
+ }
165
+ @keyframes struq-unveil { to { opacity: 0; visibility: hidden; } }
166
+ @keyframes struq-show { to { visibility: visible; } }
167
+ @keyframes struq-fade { to { opacity: 1; } }
168
+ @keyframes struq-open { to { clip-path: inset(0 0 0 0); } }
169
+
170
+ /* SplitText masks: room for descenders inside each line's clip. */
171
+ [data-blocks] .struq-line-mask {
172
+ padding-block: 0.06em 0.16em;
173
+ margin-block: -0.06em -0.16em;
174
+ }
175
+
176
+ /* --- preloader-gate and page-transition-gate --- */
177
+ /* The gate's ground. "deep" (the default) is a step darker than the page in every palette: the depth of an ink on
178
+ a light ground, a deeper night on a dark one, so the gate reads as the room the page is shown in. Browsers without
179
+ relative colour take the inverse. --on-gate is the palette's light colour, which reads on either. */
180
+ [data-blocks] [data-gate] {
181
+ --gate: var(--inverse);
182
+ --on-gate: var(--on-inverse);
183
+ }
184
+ @supports (color: oklch(from red l c h)) {
185
+ [data-blocks] [data-gate] {
186
+ --gate: oklch(from var(--surface-0) min(calc(l * 0.55), 0.2) c h);
187
+ --on-gate: var(--on-photo);
188
+ }
189
+ }
190
+ [data-blocks] [data-gate][data-tone="inverse"] {
191
+ --gate: var(--inverse);
192
+ --on-gate: var(--on-inverse);
193
+ }
194
+ [data-blocks] [data-gate][data-tone="field"] {
195
+ --gate: var(--field);
196
+ --on-gate: var(--on-field);
197
+ }
198
+ /* The preloader is never painted unless its own inline script, which runs in the HTML before the gate is parsed,
199
+ shows it: so without JS, under reduced motion and on a session revisit it covers nothing. From its first paint
200
+ the leader runs on CSS alone (the count is one number, --gate-p, everything else is derived from it); the block's
201
+ script takes the same number over when React hydrates. If the script never arrives the gate still opens at 1.6s
202
+ and is gone at 2.4s, so a failed bundle can never leave a cover on the page. */
203
+ @property --gate-p {
204
+ syntax: "<number>";
205
+ inherits: true;
206
+ initial-value: 0;
207
+ }
208
+ [data-blocks] [data-preloader-gate] {
209
+ display: none;
210
+ --gate-b: min(calc(var(--gate-p) * 3), 2.99);
211
+ animation:
212
+ struq-gate-count 1.56s linear forwards,
213
+ struq-gate-gone 0s linear 2.4s forwards;
214
+ }
215
+ /* One beat per figure: --on is 1 during beat --i and 0 outside it, a hard cut as on a film leader. */
216
+ [data-blocks] [data-preloader-gate] [data-beat] {
217
+ --on: calc(
218
+ clamp(0, (var(--gate-b) - var(--i) + 0.01) * 1000, 1) * clamp(0, (var(--i) + 1 - var(--gate-b)) * 1000, 1)
219
+ );
220
+ opacity: var(--on);
221
+ }
222
+ [data-blocks] [data-preloader-gate] [data-sweep] {
223
+ stroke-dashoffset: clamp(0, calc(1 - var(--gate-b) + var(--i)), 1);
224
+ }
225
+ [data-blocks] [data-preloader-gate] [data-hand] {
226
+ transform-box: view-box;
227
+ transform-origin: 50% 50%;
228
+ rotate: calc(var(--gate-b) * 360deg);
229
+ }
230
+ [data-blocks] [data-preloader-gate] [data-bar="top"] {
231
+ animation: struq-gate-top 0.78s cubic-bezier(0.76, 0, 0.24, 1) 1.6s forwards;
232
+ }
233
+ [data-blocks] [data-preloader-gate] [data-bar="bottom"] {
234
+ animation: struq-gate-bottom 0.78s cubic-bezier(0.76, 0, 0.24, 1) 1.6s forwards;
235
+ }
236
+ [data-blocks] [data-preloader-gate] :is([data-leader], [data-name]) {
237
+ animation: struq-gate-leader 0.3s cubic-bezier(0.55, 0, 1, 0.45) 1.42s forwards;
238
+ }
239
+ [data-blocks] [data-preloader-gate] [data-light] {
240
+ scale: 0 1;
241
+ animation: struq-gate-light 0.4s cubic-bezier(0.16, 1, 0.3, 1) 1.5s forwards;
242
+ }
243
+ /* Once the block's script drives the gate, the CSS clock stops everywhere in it. */
244
+ [data-blocks] [data-preloader-gate][data-driven],
245
+ [data-blocks] [data-preloader-gate][data-driven] * {
246
+ animation: none;
247
+ }
248
+ @keyframes struq-gate-count {
249
+ 0% {
250
+ --gate-p: 0;
251
+ animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
252
+ }
253
+ 73.7% {
254
+ --gate-p: 0.86;
255
+ }
256
+ 83.3% {
257
+ --gate-p: 0.86;
258
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
259
+ }
260
+ 100% {
261
+ --gate-p: 1;
262
+ }
263
+ }
264
+ @keyframes struq-gate-gone { to { visibility: hidden; } }
265
+ @keyframes struq-gate-top { to { translate: 0 -100%; } }
266
+ @keyframes struq-gate-bottom { to { translate: 0 100%; } }
267
+ @keyframes struq-gate-leader { to { opacity: 0; scale: 0.92; } }
268
+ @keyframes struq-gate-light { to { scale: 1 1; } }
269
+
270
+ /* --- preloader-stack --- */
271
+ /* The stack is never painted unless its own inline script, which runs while the cover is parsed, armed it: so without
272
+ JS, under reduced motion and on a session revisit it covers nothing. From its first paint it is alive on CSS alone:
273
+ the first print shows once it is decoded (data-first), the caption once its own faces are in (data-fonts), both
274
+ set by tiny scripts parsed inside the cover, and the count runs on one number, --stack-n, shown three digits wide.
275
+ The block's script takes that number over where it stands when it hydrates (data-live), which also stops this
276
+ clock and its failsafe. If the script never arrives the cover is gone at 2.3s; everything in it inherits that
277
+ visibility, so a failed bundle can never leave any of it on the page. */
278
+ @property --stack-n {
279
+ syntax: "<integer>";
280
+ inherits: true;
281
+ initial-value: 0;
282
+ }
283
+ @counter-style struq-stack-count {
284
+ system: extends decimal;
285
+ pad: 3 "0";
286
+ }
287
+ [data-blocks] [data-preloader="stack"] {
288
+ display: none;
289
+ }
290
+ [data-blocks] [data-preloader="stack"][data-armed] {
291
+ display: block;
292
+ animation:
293
+ struq-stack-count 1.7s cubic-bezier(0.33, 1, 0.68, 1) forwards,
294
+ struq-stack-gone 0s linear 2.3s forwards;
295
+ }
296
+ [data-blocks] [data-preloader="stack"][data-armed][data-first] [data-cut]:first-child {
297
+ visibility: inherit;
298
+ }
299
+ [data-blocks] [data-preloader="stack"][data-armed]:not([data-fonts]) [data-caption] {
300
+ visibility: hidden;
301
+ }
302
+ [data-blocks] [data-preloader="stack"] [data-count] {
303
+ counter-reset: struq-stack-n var(--stack-n);
304
+ }
305
+ [data-blocks] [data-preloader="stack"] [data-count]::after {
306
+ content: counter(struq-stack-n, struq-stack-count);
307
+ }
308
+ [data-blocks] [data-preloader="stack"][data-live] {
309
+ animation: none;
310
+ }
311
+ @media (prefers-reduced-motion: reduce) {
312
+ [data-blocks] [data-preloader="stack"] {
313
+ display: none !important;
314
+ }
315
+ }
316
+ @keyframes struq-stack-count { from { --stack-n: 0; } to { --stack-n: 90; } }
317
+ @keyframes struq-stack-gone { to { visibility: hidden; } }
318
+
319
+ /* --- grid-assembly --- */
320
+ /* The scatter is the first paint on wide screens with motion, and only once the page runs scripts: each tile is
321
+ moved from its cell to its place in the pile by its own custom properties, and the block's script then drives
322
+ one number per tile (--p into the cell; the push out of the way is written inline, in whole pixels). If the
323
+ script never arrives, the tiles drop into the grid after 3s. Phones open each tile from its foot; without
324
+ scripts nothing is hidden. */
325
+ @media (min-width: 768px) and (prefers-reduced-motion: no-preference) {
326
+ :is(.js, .sq-js) [data-blocks] [data-grid-assembly] [data-tile] {
327
+ transform: translate3d(calc((1 - var(--p, 0)) * var(--sx, 0px)), calc((1 - var(--p, 0)) * var(--sy, 0px)), 0)
328
+ rotate(calc((1 - var(--p, 0)) * var(--sr, 0deg)))
329
+ scale(calc(1 - (1 - var(--p, 0)) * var(--sd, 0)));
330
+ }
331
+ :is(.js, .sq-js) [data-blocks] [data-grid-assembly] [data-ga-line] {
332
+ transform: translate3d(0, calc((1 - var(--p, 0)) * 112%), 0);
333
+ }
334
+ :is(.js, .sq-js) [data-blocks] [data-grid-assembly]:not([data-motion]) [data-tile] {
335
+ animation: struq-ga-rest 0s 3s forwards;
336
+ }
337
+ }
338
+ @media (max-width: 767px) and (prefers-reduced-motion: no-preference) {
339
+ :is(.js, .sq-js) [data-blocks] [data-grid-assembly]:not([data-shown]) [data-tile] {
340
+ clip-path: inset(100% 0 0 0);
341
+ animation: struq-open 0s 3s forwards;
342
+ }
343
+ }
344
+ @keyframes struq-ga-rest { to { --p: 1; } }
345
+
346
+ /* --- panel-accordion --- */
347
+ /* The panels arrive from their bottom edge, so with motion and scripts they wait below it from the first paint; the
348
+ block's script takes over (data-motion) and draws every window itself. If the script never arrives they open after
349
+ 3s. Without scripts or under reduced motion nothing is hidden. */
350
+ @media (prefers-reduced-motion: no-preference) {
351
+ :is(.js, .sq-js) [data-blocks] [data-panel-accordion]:not([data-motion]) [data-panel] {
352
+ clip-path: inset(100% 0 0 0);
353
+ animation: struq-open 0s 3s forwards;
354
+ }
355
+ }
356
+
357
+ /* --- hero-poster --- */
358
+ /* With scripts and motion the poster's first paint is the moment before its entrance: the lines wait below their
359
+ masks and the window is a hairline. The block's script takes over (data-live) and plays the entrance; if it never
360
+ arrives everything opens after 3s. Without scripts or under reduced motion the composed poster shows at once. */
361
+ @media (prefers-reduced-motion: no-preference) {
362
+ :is(.js, .sq-js) [data-blocks] [data-hero-poster]:not([data-live]) [data-hp-line] {
363
+ translate: 0 112%;
364
+ animation: struq-hp-rise 0s 3s forwards;
365
+ }
366
+ :is(.js, .sq-js) [data-blocks] [data-hero-poster]:not([data-live]) [data-hp-slot] {
367
+ clip-path: inset(50% 0 50% 0);
368
+ animation: struq-open 0s 3s forwards;
369
+ }
370
+ }
371
+ @keyframes struq-hp-rise { to { translate: 0 0; } }
372
+
373
+ /* --- hero-poster variants --- */
374
+ /* column: the slit's place as shares of the screen (its left edge, its right margin) and the band of the foot line
375
+ below it; the script writes the band in pixels once it has measured the foot. The slit's box is what a stack
376
+ preloader lands on; the cut layer inside it is the whole screen at the screen's origin, clipped to the slit, and
377
+ holds the photo, its light veil and the copy of the type. With scripts and motion the slit waits closed at the top
378
+ edge and draws down with the entrance, or after 3s. The stack needs nothing here: its windows use the gate above. */
379
+ [data-blocks] [data-hero-poster][data-hp-variant="column"] {
380
+ --hp-cl: 0.6;
381
+ --hp-cr: 0.24;
382
+ --hp-cb: calc(5svh + 1.4rem);
383
+ }
384
+ @media (max-width: 767px) {
385
+ [data-blocks] [data-hero-poster][data-hp-variant="column"] {
386
+ --hp-cl: 0.56;
387
+ --hp-cr: 0.18;
388
+ --hp-cb: 6.5rem;
389
+ }
390
+ }
391
+ [data-blocks] [data-hero-poster] [data-hp-cutslot] {
392
+ position: absolute;
393
+ top: 0;
394
+ left: calc(var(--hp-cl) * 100%);
395
+ right: calc(var(--hp-cr) * 100%);
396
+ bottom: var(--hp-cb);
397
+ z-index: 10;
398
+ }
399
+ [data-blocks] [data-hero-poster] [data-hp-cut] {
400
+ position: absolute;
401
+ top: 0;
402
+ left: calc(-100% * var(--hp-cl) / (1 - var(--hp-cl) - var(--hp-cr)));
403
+ width: calc(100% / (1 - var(--hp-cl) - var(--hp-cr)));
404
+ height: calc(100% + var(--hp-cb));
405
+ clip-path: inset(0 calc(var(--hp-cr) * 100%) var(--hp-cb) calc(var(--hp-cl) * 100%));
406
+ }
407
+ [data-blocks] [data-hero-poster] [data-hp-veil] {
408
+ background: var(--inverse);
409
+ background: oklch(from var(--surface-0) min(l, 0.2) c h);
410
+ opacity: 0.4;
411
+ }
412
+ [data-blocks] [data-hero-poster] [data-hp-veil][data-tone="light"] {
413
+ background: var(--on-photo);
414
+ }
415
+ @media (prefers-reduced-motion: no-preference) {
416
+ :is(.js, .sq-js) [data-blocks] [data-hero-poster]:not([data-live]) [data-hp-cut] {
417
+ clip-path: inset(0 calc(var(--hp-cr) * 100%) 100% calc(var(--hp-cl) * 100%));
418
+ animation: struq-hp-cut 0s 3s forwards;
419
+ }
420
+ }
421
+ @keyframes struq-hp-cut { to { clip-path: inset(0 calc(var(--hp-cr) * 100%) var(--hp-cb) calc(var(--hp-cl) * 100%)); } }
422
+
423
+ /* --- hero-still --- */
424
+ /* The veil over the photograph is the page ground held dark (its own hue, never black); the block's script tunes its
425
+ strength to what the photo needs (3:1 for the display-size headline, never past 0.45; the caption sits on the
426
+ ground in the ink), and the safe strength here stays when the photo cannot be read or scripts never run: the cap
427
+ on the left, a lighter one when the headline is set on a calm right side. The photograph is laid out six percent larger than its box and shown scaled
428
+ back to fit, so at rest it is exactly an object-cover of the box; the push-in is set up from the first paint, paused
429
+ (so the layer is rastered at its final scale before anything is seen), and runs once the entrance has played. With
430
+ motion and scripts the headline and the caption wait below their masks from the first paint (the composed still is
431
+ the photo and its veil) and arrive after 3s if the script never runs; without scripts nothing waits. */
432
+ [data-blocks] [data-hero-still] [data-veil] {
433
+ background: var(--inverse);
434
+ background: oklch(from var(--surface-0) min(l, 0.2) c h);
435
+ opacity: 0.45;
436
+ transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1);
437
+ }
438
+ [data-blocks] [data-hero-still][data-side="right"] [data-veil] {
439
+ opacity: 0.36;
440
+ }
441
+ [data-blocks] [data-hero-still] [data-push] {
442
+ transform: scale(0.9434);
443
+ will-change: transform;
444
+ }
445
+ @media (prefers-reduced-motion: no-preference) {
446
+ [data-blocks] [data-hero-still] [data-push] {
447
+ animation: struq-hs-push 20s cubic-bezier(0.45, 0, 0.55, 1) 1.2s forwards paused;
448
+ }
449
+ [data-blocks] [data-hero-still][data-pushing] [data-push] {
450
+ animation-play-state: running;
451
+ }
452
+ :is(.js, .sq-js) [data-blocks] [data-hero-still]:not([data-ready]) [data-rise] {
453
+ transform: translateY(115%);
454
+ animation: struq-hs-rise 0s 3s forwards;
455
+ }
456
+ }
457
+ @keyframes struq-hs-push { to { transform: scale(1); } }
458
+ @keyframes struq-hs-rise { to { transform: none; } }
459
+
460
+ /* --- hero-still variants --- */
461
+ /* frame: the photograph inside margins of page ground; the frame's box is what a stack preloader lands on, and the
462
+ photo inside it is the whole still at the still's origin (placed here from the viewport units, measured by the
463
+ script), so the frame shows its part of the still and opening it is a clip. Phones keep the photo's own crop in a
464
+ smaller frame above the headline. horizon: a veil band that a dark ink turns pale, and the lower half of the
465
+ headline waiting above its masks (it drops to the horizon as the upper half rises to it). */
466
+ [data-blocks] [data-hero-still] [data-hs-slot] {
467
+ position: absolute;
468
+ top: 4svh;
469
+ left: 1.5rem;
470
+ right: 1.5rem;
471
+ bottom: 42%;
472
+ overflow: hidden;
473
+ }
474
+ [data-blocks] [data-hero-still] [data-hs-photo] {
475
+ position: absolute;
476
+ inset: 0;
477
+ }
478
+ @media (min-width: 768px) {
479
+ [data-blocks] [data-hero-still] [data-hs-slot] {
480
+ top: 7svh;
481
+ left: 42vw;
482
+ right: 4vw;
483
+ bottom: 3svh;
484
+ }
485
+ [data-blocks] [data-hero-still] [data-hs-photo] {
486
+ inset: auto;
487
+ left: -42vw;
488
+ top: -7svh;
489
+ width: 100vw;
490
+ height: calc(100% + 10svh);
491
+ }
492
+ [data-blocks] [data-hero-still][data-live] [data-hs-slot] {
493
+ overflow: visible;
494
+ }
495
+ }
496
+ [data-blocks] [data-hero-still] [data-veil][data-tone="light"] {
497
+ background: var(--on-photo);
498
+ }
499
+ @media (prefers-reduced-motion: no-preference) {
500
+ :is(.js, .sq-js) [data-blocks] [data-hero-still]:not([data-ready]) [data-below] [data-rise] {
501
+ transform: translateY(-115%);
502
+ }
503
+ }
504
+
505
+ /* --- hero-index --- */
506
+ /* Each photograph layer carries its own light veil: the page ground held dark (its own hue, never black), tuned by
507
+ the block's script per photograph (none under the dark ink, at most a quarter under the light ink); the safe
508
+ strength here stays when a photograph cannot be read. One ink for the whole list, chosen per photograph and
509
+ switched once the incoming photograph has passed the list; each place line takes its own ink, or stands at the
510
+ foot of the list in the list's ink when neither holds (no box, no scrim, no glow). With scripts on a fine pointer only the
511
+ current project's place shows (at rest, the first one's). With motion and scripts the names wait below their masks
512
+ from the first paint and arrive after 3s if the script never runs; without scripts nothing waits. */
513
+ /* Without scripts nothing can read the photo, so the light ink stands over a veil strong enough for any of them; with
514
+ scripts the veil starts clear and the script writes each photograph's own value before the names rise. */
515
+ [data-blocks] [data-hero-index] [data-veil] {
516
+ background: var(--inverse);
517
+ background: oklch(from var(--surface-0) min(l, 0.2) c h);
518
+ opacity: 0.45;
519
+ transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1);
520
+ }
521
+ :is(.js, .sq-js) [data-blocks] [data-hero-index] [data-veil] {
522
+ opacity: 0;
523
+ }
524
+ [data-blocks] [data-hero-index] [data-list] {
525
+ color: var(--on-photo);
526
+ }
527
+ [data-blocks] [data-hero-index][data-tone="dark"] [data-list] {
528
+ color: var(--inverse);
529
+ color: oklch(from var(--surface-0) min(l, 0.2) c h);
530
+ }
531
+ [data-blocks] [data-hero-index] [data-meta] {
532
+ transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1);
533
+ }
534
+ [data-blocks] [data-hero-index] [data-row][data-place-ink="light"] [data-meta] {
535
+ color: var(--on-photo);
536
+ }
537
+ [data-blocks] [data-hero-index] [data-row][data-place-ink="dark"] [data-meta] {
538
+ color: var(--inverse);
539
+ color: oklch(from var(--surface-0) min(l, 0.2) c h);
540
+ }
541
+ /* A place line that cannot hold its own ink stays out of the row; the script sets it at the foot of the list, in
542
+ the list's ink, while its row is shown. Each place line stands on its own line under its name. */
543
+ [data-blocks] [data-hero-index] [data-row][data-place-ink="foot"] [data-meta] {
544
+ visibility: hidden;
545
+ }
546
+ [data-blocks] [data-hero-index] [data-foot]:not([data-on]) {
547
+ visibility: hidden;
548
+ }
549
+ @media (hover: hover) and (pointer: fine) and (min-width: 1024px) {
550
+ [data-blocks] [data-hero-index][data-ready] [data-meta] {
551
+ opacity: 0;
552
+ }
553
+ [data-blocks] [data-hero-index][data-ready] [data-row][data-current] [data-meta],
554
+ [data-blocks] [data-hero-index][data-ready] [data-list]:not([data-active]) li:first-child [data-meta] {
555
+ opacity: 1;
556
+ }
557
+ }
558
+ @media (prefers-reduced-motion: no-preference) {
559
+ :is(.js, .sq-js) [data-blocks] [data-hero-index]:not([data-ready]) [data-rise] {
560
+ transform: translateY(115%);
561
+ animation: struq-hi-rise 0s 3s forwards;
562
+ }
563
+ }
564
+ @keyframes struq-hi-rise { to { transform: none; } }
565
+
566
+ /* --- hero-split --- */
567
+ /* With motion and scripts the type waits below its masks from the first paint (the composed still is the photograph
568
+ and the ground) and arrives with the entrance; if the script never runs it arrives after 3s, and the block's script
569
+ reads this clock to leave type it has already shown in place. Only the first word and the first photograph show
570
+ without scripts; the others wait below their mask and behind their clip. Each photograph carries a light veil
571
+ (never past 0.3), of the ground held dark under the photo ink or of the ground under the ground's ink, whichever
572
+ the script finds weaker for that photo; the safe one here stays when a photo cannot be read or scripts never run,
573
+ and a re-grade eases. Phones carry no veil: the headline is not on the photo there. */
574
+ [data-blocks] [data-hero-split] [data-veil] {
575
+ background: var(--inverse);
576
+ background: oklch(from var(--surface-0) min(l, 0.2) c h);
577
+ opacity: 0.3;
578
+ transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1);
579
+ }
580
+ @media (max-width: 767px) {
581
+ [data-blocks] [data-hero-split] [data-veil] {
582
+ opacity: 0 !important;
583
+ }
584
+ }
585
+ @media (prefers-reduced-motion: no-preference) {
586
+ :is(.js, .sq-js) [data-blocks] [data-hero-split]:not([data-ready]) [data-rise] {
587
+ transform: translateY(115%);
588
+ animation: struq-hsp-rise 0s 3s forwards;
589
+ }
590
+ }
591
+ @keyframes struq-hsp-rise { to { transform: none; } }
592
+
593
+ /* --- hero-framed --- */
594
+ /* With scripts and motion the wall's first paint is the moment before its entrance: the marks undrawn, the
595
+ photograph a hairline in its frame, the label below its masks. The block's script takes over (data-live); if it
596
+ never arrives the wall is whole after 3s. The copy that stands on the open photograph waits below its masks for the
597
+ scroll. Without scripts or under reduced motion the composed wall shows at once. */
598
+ @media (prefers-reduced-motion: no-preference) {
599
+ :is(.js, .sq-js) [data-blocks] [data-hero-framed]:not([data-live]) [data-hf-rise] {
600
+ translate: 0 140%;
601
+ animation: struq-hf-rise 0s 3s forwards;
602
+ }
603
+ :is(.js, .sq-js) [data-blocks] [data-hero-framed]:not([data-live]) [data-hf-mark] > [data-arm="h"] {
604
+ transform: scaleX(0);
605
+ animation: struq-hf-arm 0s 3s forwards;
606
+ }
607
+ :is(.js, .sq-js) [data-blocks] [data-hero-framed]:not([data-live]) [data-hf-mark] > [data-arm="v"] {
608
+ transform: scaleY(0);
609
+ animation: struq-hf-arm 0s 3s forwards;
610
+ }
611
+ :is(.js, .sq-js) [data-blocks] [data-hero-framed]:not([data-live]) [data-hf-frame] > div:first-child {
612
+ clip-path: inset(50% 0 50% 0);
613
+ animation: struq-open 0s 3s forwards;
614
+ }
615
+ :is(.js, .sq-js) [data-blocks] [data-hero-framed]:not([data-live]) [data-hf-copy-rise] {
616
+ translate: 0 140%;
617
+ }
618
+ }
619
+ @keyframes struq-hf-rise { to { translate: 0 0; } }
620
+ @keyframes struq-hf-arm { to { transform: none; } }
621
+
622
+ /* --- heroes: light display weights --- */
623
+ /* Roy, 2026-11-07: "make sure we use light font weights, we sometimes use quite thick font weights in the hero
624
+ section". A hero's display type never goes heavier than 300, whatever the pairing asks for (display-drama asks
625
+ 700, condensed-poster 800). A face without a 300 falls to its lightest loaded weight. A new hero adds its root
626
+ attribute here. */
627
+ [data-blocks] :is([data-hero-still], [data-hero-poster], [data-hero-index], [data-hero-split], [data-hero-framed], [data-bg-aurora], [data-bg-silk], [data-bg-facets], [data-showpiece="hero-opener"]) :is(h1, h2, h3, .font-display) {
628
+ font-weight: min(var(--display-weight, 300), 300);
629
+ }
630
+
631
+ /* --- background-aurora --- */
632
+ /* The field's canvas stays hidden until the block has drawn its first frame (data-lit), then fades in while the
633
+ light dawns; without scripts it never shows and the hero stands on the plain ground. With scripts and motion the
634
+ type waits below its masks and is released after 3s if the script never runs. */
635
+ [data-blocks] [data-bg-aurora] [data-field] {
636
+ opacity: 0;
637
+ }
638
+ [data-blocks] [data-bg-aurora][data-lit] [data-field] {
639
+ opacity: 1;
640
+ }
641
+ /* With reduced motion the held still simply appears. */
642
+ @media (prefers-reduced-motion: no-preference) {
643
+ [data-blocks] [data-bg-aurora] [data-field] {
644
+ transition: opacity 1.4s cubic-bezier(0.16, 1, 0.3, 1);
645
+ }
646
+ }
647
+ @media (prefers-reduced-motion: no-preference) {
648
+ :is(.js, .sq-js) [data-blocks] [data-bg-aurora]:not([data-ready]) [data-rise] {
649
+ transform: translateY(115%);
650
+ animation: struq-bga-rise 0s 3s forwards;
651
+ }
652
+ }
653
+ @keyframes struq-bga-rise { to { transform: none; } }
654
+
655
+ /* --- background-facets --- */
656
+ /* The surface's canvas waits invisible until its first frame (the flat ground) is drawn, then fades in
657
+ (data-drawn); without scripts or WebGL the hero is the plain ground with its type. The copy stands on the calm side
658
+ of each variant: left for shard, bottom left for relief, bottom right for turn, and at the bottom on phones. With
659
+ motion and scripts the lines wait below their masks from the first paint and arrive after 3s if the script never
660
+ runs; without scripts nothing waits. */
661
+ [data-blocks] [data-bg-facets] [data-bf-canvas] {
662
+ display: block;
663
+ opacity: 0;
664
+ transition: opacity 1.2s cubic-bezier(0.22, 1, 0.36, 1);
665
+ }
666
+ [data-blocks] [data-bg-facets][data-drawn] [data-bf-canvas] {
667
+ opacity: 1;
668
+ }
669
+ [data-blocks] [data-bg-facets] [data-copy] {
670
+ left: 24px;
671
+ right: 24px;
672
+ bottom: 9svh;
673
+ }
674
+ [data-blocks] [data-bg-facets] h1 {
675
+ font-size: clamp(2.75rem, 12.5vw, 4.25rem);
676
+ }
677
+ @media (min-width: 768px) {
678
+ [data-blocks] [data-bg-facets] [data-copy] {
679
+ left: 6vw;
680
+ right: auto;
681
+ width: min(46vw, 44rem);
682
+ }
683
+ [data-blocks] [data-bg-facets][data-variant="shard"] [data-copy] {
684
+ bottom: 15svh;
685
+ }
686
+ [data-blocks] [data-bg-facets][data-variant="turn"] [data-copy] {
687
+ left: 58vw;
688
+ width: 36vw;
689
+ bottom: 11svh;
690
+ }
691
+ [data-blocks] [data-bg-facets] h1 {
692
+ font-size: clamp(3.5rem, 6.6vw, 8rem);
693
+ }
694
+ [data-blocks] [data-bg-facets][data-variant="turn"] h1 {
695
+ font-size: clamp(3.25rem, 5.4vw, 6.5rem);
696
+ }
697
+ }
698
+ @media (prefers-reduced-motion: reduce) {
699
+ [data-blocks] [data-bg-facets] [data-bf-canvas] {
700
+ transition: none;
701
+ }
702
+ }
703
+ @media (prefers-reduced-motion: no-preference) {
704
+ :is(.js, .sq-js) [data-blocks] [data-bg-facets]:not([data-ready]) [data-rise] {
705
+ transform: translateY(115%);
706
+ animation: struq-bf-rise 0s 3s forwards;
707
+ }
708
+ }
709
+ @keyframes struq-bf-rise { to { transform: none; } }
710
+
711
+ /* --- background-silk --- */
712
+ /* The cloth's canvas is made by the script, so without scripts or WebGL the hero is the plain ground with its type.
713
+ The canvas waits invisible until its first frame is drawn, then fades in (data-live). With motion and scripts the
714
+ type waits below its masks from the first paint and arrives after 3s if the script never runs; without scripts
715
+ nothing waits. */
716
+ [data-blocks] [data-bg-silk] [data-field] > canvas {
717
+ position: absolute;
718
+ inset: 0;
719
+ display: block;
720
+ width: 100%;
721
+ height: 100%;
722
+ opacity: 0;
723
+ transition: opacity 1.6s cubic-bezier(0.22, 1, 0.36, 1);
724
+ }
725
+ [data-blocks] [data-bg-silk][data-live] [data-field] > canvas {
726
+ opacity: 1;
727
+ }
728
+ @media (prefers-reduced-motion: reduce) {
729
+ [data-blocks] [data-bg-silk] [data-field] > canvas {
730
+ transition: none;
731
+ }
732
+ }
733
+ @media (prefers-reduced-motion: no-preference) {
734
+ :is(.js, .sq-js) [data-blocks] [data-bg-silk]:not([data-ready]) [data-rise] {
735
+ transform: translateY(115%);
736
+ animation: struq-silk-rise 0s 3s forwards;
737
+ }
738
+ }
739
+ @keyframes struq-silk-rise { to { transform: none; } }
740
+
741
+ /* --- reveals --- */
742
+ /* The card variant's top edge: a hairline and a short soft shadow, both from the ground held dark (its own hue, never
743
+ black); browsers without relative colour keep the hairline. The shade above the card is the dark that falls over the
744
+ section it covers; it starts clear and the block's scroll writes its strength. With motion and scripts every start
745
+ state waits from the first paint (the statement unseen until it is cut into lines, the paragraph and the link and
746
+ the card's content clear, the photograph's clip shut) and arrives after 3s if the script never runs; without scripts
747
+ nothing waits. */
748
+ [data-blocks] [data-reveals="card"] {
749
+ box-shadow: 0 -1px 0 var(--line);
750
+ box-shadow: 0 -1px 0 var(--line), 0 -22px 44px -22px oklch(from var(--surface-0) calc(l * 0.3) c h / 0.3);
751
+ }
752
+ [data-blocks] [data-reveals] [data-rv-shade] {
753
+ background: var(--inverse);
754
+ background: oklch(from var(--surface-0) calc(l * 0.3) c h);
755
+ opacity: 0;
756
+ }
757
+ @media (prefers-reduced-motion: no-preference) {
758
+ :is(.js, .sq-js) [data-blocks] [data-reveals]:not([data-ready]) [data-rv-type] {
759
+ visibility: hidden;
760
+ animation: struq-show 0s 3s forwards;
761
+ }
762
+ :is(.js, .sq-js) [data-blocks] [data-reveals]:not([data-ready]) :is([data-rv-soft], [data-rv-settle]) {
763
+ opacity: 0;
764
+ animation: struq-fade 0s 3s forwards;
765
+ }
766
+ :is(.js, .sq-js) [data-blocks] [data-reveals="lines"]:not([data-ready]) [data-rv-frame] {
767
+ clip-path: inset(100% 0% 0% 0%);
768
+ animation: struq-open 0s 3s forwards;
769
+ }
770
+ :is(.js, .sq-js) [data-blocks] [data-reveals="clip"]:not([data-ready]) [data-rv-frame] {
771
+ clip-path: inset(49.6% 7% 49.6% 7%);
772
+ animation: struq-open 0s 3s forwards;
773
+ }
774
+ }
775
+
776
+ /* --- preloader-wordmark --- */
777
+ /* The cover is never painted unless its own inline script, which runs while the cover is parsed, armed it: so without
778
+ JS and on a session revisit it covers nothing. The name stays unseen until a second inline script has loaded its
779
+ face and fitted it to the width (data-fit), so it is never drawn in a fallback. From the first paint the fill runs
780
+ on one number, --wm-p, on a CSS clock; the block's script takes the number over where it stands (data-live), which
781
+ stops this clock and its failsafe. If the script never arrives, the cover fades from 1.95s and is hidden at 2.3s,
782
+ and everything in it inherits that, so a failed bundle never leaves it over the hero. Reduced motion: the name is
783
+ filled from the start and the cover fades sooner. */
784
+ @property --wm-p {
785
+ syntax: "<number>";
786
+ inherits: true;
787
+ initial-value: 0;
788
+ }
789
+ [data-blocks] [data-preloader="wordmark"] {
790
+ display: none;
791
+ }
792
+ /* Nothing in the cover transitions: the fit measures in the same frame it sets a size, and a site-wide reduced-motion
793
+ rule that gives every element a tiny transition would otherwise hand it the old size. */
794
+ [data-blocks] :is([data-preloader="wordmark"], [data-preloader="wordmark"] *) {
795
+ transition-property: none;
796
+ }
797
+ [data-blocks] [data-preloader="wordmark"][data-armed] {
798
+ display: block;
799
+ animation:
800
+ struq-wm-fill 1.5s linear forwards,
801
+ struq-wm-fade 0.35s cubic-bezier(0.33, 1, 0.68, 1) 1.95s forwards,
802
+ struq-wm-gone 0s linear 2.3s forwards;
803
+ }
804
+ [data-blocks] [data-preloader="wordmark"][data-armed]:not([data-fit]) [data-name] {
805
+ visibility: hidden;
806
+ }
807
+ [data-blocks] [data-preloader="wordmark"][data-live] {
808
+ animation: none;
809
+ }
810
+ @media (prefers-reduced-motion: reduce) {
811
+ [data-blocks] [data-preloader="wordmark"][data-armed] {
812
+ --wm-p: 1;
813
+ animation:
814
+ struq-wm-fade 0.3s ease-out 1.1s forwards,
815
+ struq-wm-gone 0s linear 1.4s forwards;
816
+ }
817
+ [data-blocks] [data-preloader="wordmark"][data-live] {
818
+ animation: none;
819
+ }
820
+ }
821
+ /* The name and the header's wordmark it settles into share one face, weight and tracking, so the hand-over is exact:
822
+ the display face at a light weight (300, or the pairing's own if lighter), no kerning or ligatures, since the
823
+ preloader sets each letter in a cell of its own. */
824
+ [data-blocks] :is([data-preloader="wordmark"] [data-name], [data-wordmark-target]) {
825
+ font-family: var(--font-display);
826
+ font-weight: min(var(--display-weight, 300), 300);
827
+ letter-spacing: -0.03em;
828
+ font-kerning: none;
829
+ font-variant-ligatures: none;
830
+ }
831
+ [data-blocks] [data-preloader="wordmark"] [data-name] {
832
+ display: flex;
833
+ flex: none;
834
+ white-space: nowrap;
835
+ font-size: var(--wm-size, 22vw);
836
+ line-height: 1;
837
+ /* The last letter's tracking is taken back, so the box holds its whole glyph and centres it. */
838
+ padding-inline-end: 0.03em;
839
+ color: var(--wm-ink, var(--ink));
840
+ }
841
+ /* Letters sit above every column of the cover, so a glyph's overhang is never cut by a neighbour's column. */
842
+ [data-blocks] [data-preloader="wordmark"] [data-fitbox] {
843
+ z-index: 1;
844
+ }
845
+ [data-blocks] [data-preloader="wordmark"] [data-l] {
846
+ display: grid;
847
+ /* Each letter fills a little after the one before: the whole name spans 1.6 of --wm-p. */
848
+ --f: clamp(0, var(--wm-p) * 1.6 - var(--wm-lag, 0) * var(--i, 0), 1);
849
+ }
850
+ /* Both copies of a letter in one cell, trimmed to the cap height and the baseline where the browser can, so the name
851
+ centres on its cap band. Where it cannot (Firefox), the cell is the whole line box and the script's measures say
852
+ where the ink and the cap band sit in it instead. */
853
+ [data-blocks] [data-preloader="wordmark"] [data-l] > * {
854
+ grid-area: 1 / 1;
855
+ }
856
+ @supports (text-box: trim-both cap alphabetic) {
857
+ [data-blocks] [data-preloader="wordmark"] [data-l] > * {
858
+ text-box: trim-both cap alphabetic;
859
+ }
860
+ }
861
+ [data-blocks] [data-preloader="wordmark"] [data-faint] {
862
+ color: color-mix(in oklch, var(--wm-ink, var(--ink)) 15%, transparent);
863
+ }
864
+ /* The ink rises over the letter's own measured ink, from its lowest point (--wm-b, in the cell) to its highest
865
+ (--wm-a), with 0.02em of air at each end: shut at --f 0, the whole glyph at --f 1 and not before, so the last letter
866
+ completes as the fill does. */
867
+ [data-blocks] [data-preloader="wordmark"] [data-ink] {
868
+ clip-path: inset(
869
+ calc(var(--wm-b) + 0.02em + (var(--wm-a) - var(--wm-b) - 0.04em) * var(--f)) -0.12em calc(100% - var(--wm-b) - 0.02em) -0.12em
870
+ );
871
+ }
872
+ @keyframes struq-wm-fill {
873
+ 0% {
874
+ --wm-p: 0;
875
+ animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
876
+ }
877
+ 80% {
878
+ --wm-p: 0.85;
879
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
880
+ }
881
+ 100% {
882
+ --wm-p: 1;
883
+ }
884
+ }
885
+ @keyframes struq-wm-fade { to { opacity: 0; } }
886
+ @keyframes struq-wm-gone { to { visibility: hidden; } }
887
+
888
+ /* --- menu-curtain --- */
889
+ /* The menu's ground is the gate's deep one (a step darker than the page in every palette, so the menu reads as the
890
+ room the page is shown in); the slices take their own, as in page-transition-slices. Browsers without relative
891
+ colour take the inverse. The bar's height is shared with the page, which keeps its hero type below it. */
892
+ [data-blocks] [data-menu-curtain] {
893
+ --menu-bar-h: 4rem;
894
+ --menu: var(--inverse);
895
+ --on-menu: var(--on-inverse);
896
+ --on-menu-muted: color-mix(in oklch, var(--on-menu) 70%, var(--menu));
897
+ }
898
+ @media (min-width: 768px) {
899
+ [data-blocks] [data-menu-curtain] {
900
+ --menu-bar-h: 4.5rem;
901
+ }
902
+ }
903
+ @supports (color: oklch(from red l c h)) {
904
+ [data-blocks] [data-menu-curtain] {
905
+ --menu: oklch(from var(--surface-0) min(calc(l * 0.55), 0.2) c h);
906
+ --on-menu: var(--on-photo);
907
+ }
908
+ [data-blocks] [data-menu-curtain][data-variant="slices"] {
909
+ --menu: oklch(from var(--surface-0) min(l + 0.07, 0.27) c h);
910
+ }
911
+ }
912
+ /* Large type in the first screen stays light, as in the heroes. */
913
+ [data-blocks] [data-menu-curtain] .font-display {
914
+ font-weight: min(var(--display-weight, 300), 300);
915
+ }
916
+ [data-blocks] [data-menu-curtain] [data-menu-panel] :focus-visible {
917
+ outline-color: var(--on-menu);
918
+ }
919
+ /* The hovered or focused link stands; the others step back. */
920
+ [data-blocks] [data-menu-curtain] [data-menu-link] {
921
+ transition:
922
+ opacity 0.3s var(--ease-out-expo),
923
+ transform 0.45s var(--ease-out-expo);
924
+ }
925
+ [data-blocks] [data-menu-curtain] [data-menu-list]:is(:hover, :focus-within) [data-menu-link]:not([data-on]) {
926
+ opacity: 0.42;
927
+ }
928
+ [data-blocks] [data-menu-curtain] [data-menu-list]:is(:hover, :focus-within) [data-menu-link][data-on] {
929
+ transform: translateX(0.06em);
930
+ }
931
+ /* The button works only with the script. Without it the bar shows its links in a row; with it but before it runs, the
932
+ button shows for 3 seconds and then gives way to the row if the script never arrived. */
933
+ [data-blocks] [data-menu-curtain] [data-menu-toggle] {
934
+ visibility: hidden;
935
+ }
936
+ .sq-js [data-blocks] [data-menu-curtain]:not([data-ready]) [data-menu-toggle] {
937
+ visibility: inherit;
938
+ animation: struq-mc-gone 0s 3s forwards;
939
+ }
940
+ .sq-js [data-blocks] [data-menu-curtain]:not([data-ready]) [data-menu-fallback] {
941
+ visibility: hidden;
942
+ animation: struq-show 0s 3s forwards;
943
+ }
944
+ [data-blocks] [data-menu-curtain][data-ready] [data-menu-toggle] {
945
+ visibility: inherit;
946
+ }
947
+ [data-blocks] [data-menu-curtain][data-ready] [data-menu-fallback] {
948
+ visibility: hidden;
949
+ }
950
+ @keyframes struq-mc-gone { to { visibility: hidden; } }
951
+
952
+ /* --- footer-reveal --- */
953
+ /* The last section sits above the footer in one wrapper; the footer goes sticky beneath it only once the script is
954
+ live (it sets data-live, and only with motion allowed), so without scripts and under reduced motion the footer simply
955
+ follows the page and nothing waits. --fr-bottom is 0 for a footer that fits the screen and the screen height minus
956
+ the footer's for a taller one, which then sticks by its top. The wrapper clips sideways only (clip, not hidden, so
957
+ the sticky footer still sticks) for the fold's tilt. The edge is a transparent strip in the sheet's foot whose
958
+ hairline and short shadow fall on the footer, from the ground held dark; the shade is the dark the fold lifts off
959
+ the footer. The footer's name never goes heavier than 300. */
960
+ [data-blocks] [data-footer-reveal] {
961
+ position: relative;
962
+ isolation: isolate;
963
+ overflow-x: clip;
964
+ background-color: var(--surface-0);
965
+ }
966
+ [data-blocks] [data-footer-reveal] > [data-fr-sheet] {
967
+ position: relative;
968
+ z-index: 1;
969
+ background-color: var(--surface-0);
970
+ }
971
+ [data-blocks] [data-footer-reveal] > [data-fr-foot] {
972
+ position: relative;
973
+ z-index: 0;
974
+ }
975
+ [data-blocks] [data-footer-reveal][data-live] > [data-fr-foot] {
976
+ position: sticky;
977
+ bottom: var(--fr-bottom, 0px);
978
+ }
979
+ [data-blocks] [data-footer-reveal] [data-fr-edge] {
980
+ position: absolute;
981
+ inset-inline: 0;
982
+ bottom: 0;
983
+ height: 3rem;
984
+ pointer-events: none;
985
+ opacity: 0;
986
+ box-shadow: 0 1px 0 var(--line);
987
+ box-shadow: 0 1px 0 var(--line), 0 26px 44px -20px oklch(from var(--surface-1) calc(l * 0.3) c h / 0.32);
988
+ }
989
+ [data-blocks] [data-footer-reveal] [data-fr-shade] {
990
+ position: absolute;
991
+ inset: 0;
992
+ z-index: 1;
993
+ pointer-events: none;
994
+ opacity: 0;
995
+ }
996
+ /* Only where relative colour works: a fallback colour would brighten a dark footer instead of shading it. */
997
+ @supports (color: oklch(from red l c h)) {
998
+ [data-blocks] [data-footer-reveal] [data-fr-shade] {
999
+ background: oklch(from var(--surface-1) calc(l * 0.3) c h);
1000
+ }
1001
+ }
1002
+ [data-blocks] [data-footer-reveal] [data-fr-name] {
1003
+ font-weight: min(var(--display-weight, 300), 300);
1004
+ }
1005
+
1006
+ /* --- cta-closing --- */
1007
+ /* The line and the link stay light, whatever the pairing asks, as in the heroes. The line's size is fitted by the
1008
+ block's script to its column with the real face (--cc-size); until then, and without scripts, a clamp that wraps. */
1009
+ [data-blocks] [data-cta-closing] .font-display {
1010
+ font-weight: min(var(--display-weight, 300), 300);
1011
+ }
1012
+ [data-blocks] [data-cta-closing] [data-cc-head] {
1013
+ font-size: var(--cc-size, clamp(2.75rem, 1rem + 6vw, 8.5rem));
1014
+ line-height: 0.98;
1015
+ letter-spacing: -0.03em;
1016
+ }
1017
+ [data-blocks] [data-cta-closing][data-fit] [data-cc-text] {
1018
+ white-space: nowrap;
1019
+ }
1020
+ /* photo: the ink and the veil come in two pairs (light ink over the ground held dark, dark ink over the photo's own
1021
+ light), chosen and graded by the block's script from the photograph; the safe pair stays when it cannot be read.
1022
+ Browsers without relative colour take the inverse for the dark. */
1023
+ [data-blocks] [data-cta-closing="photo"] {
1024
+ --cc-dark: var(--inverse);
1025
+ --cc-light: var(--on-photo);
1026
+ }
1027
+ @supports (color: oklch(from red l c h)) {
1028
+ [data-blocks] [data-cta-closing="photo"] {
1029
+ --cc-dark: oklch(from var(--surface-0) min(l, 0.2) c h);
1030
+ }
1031
+ }
1032
+ [data-blocks] [data-cta-closing="photo"] [data-cc-veil] {
1033
+ background: var(--cc-dark);
1034
+ opacity: 0.4;
1035
+ }
1036
+ [data-blocks] [data-cta-closing="photo"] [data-cc-ink] {
1037
+ color: var(--cc-light);
1038
+ }
1039
+ [data-blocks] [data-cta-closing="photo"][data-ink="dark"] [data-cc-veil] {
1040
+ background: var(--cc-light);
1041
+ }
1042
+ [data-blocks] [data-cta-closing="photo"][data-ink="dark"] [data-cc-ink] {
1043
+ color: var(--cc-dark);
1044
+ }
1045
+ [data-blocks] [data-cta-closing="photo"] [data-cc-link]:focus-visible {
1046
+ outline-color: currentColor;
1047
+ }
1048
+ @media (min-width: 768px) {
1049
+ [data-blocks] [data-cta-closing="photo"][data-side="right"] [data-cc-copy] {
1050
+ margin-left: auto;
1051
+ }
1052
+ }
1053
+ /* The copy stands at the bottom, or at the top when the read of the photograph finds the top calmer. */
1054
+ [data-blocks] [data-cta-closing="photo"][data-v="top"] [data-cc-stage] {
1055
+ justify-content: flex-start;
1056
+ }
1057
+ /* marquee: the band is a backdrop, not a second voice: the muted ink drawn far toward the ground (about 1.7:1; it is
1058
+ decoration and hidden from screen readers), so the link in the full ink reads in front of it without any halo. */
1059
+ [data-blocks] [data-cta-closing="marquee"] [data-cc-band] {
1060
+ color: var(--ink-muted);
1061
+ color: color-mix(in oklch, var(--ink-muted) 34%, var(--surface-0));
1062
+ }
1063
+ /* pointer: the project frame is shut until the pointer or the focus opens it. */
1064
+ [data-blocks] [data-cta-closing="pointer"] [data-cc-float] {
1065
+ clip-path: inset(0 0 100% 0);
1066
+ }
1067
+ /* Every photograph waits unseen until it is decoded, and shows after 3 s whatever happens. */
1068
+ [data-blocks] [data-cta-closing] [data-cc-pic] {
1069
+ transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1);
1070
+ }
1071
+ :is(.js, .sq-js) [data-blocks] [data-cta-closing] [data-cc-pic]:not([data-shown]) {
1072
+ opacity: 0;
1073
+ animation: struq-fade 0s 3s forwards;
1074
+ }
1075
+ /* The photo variant's copy waits for the first read of the photograph (it sets the side and the ink). */
1076
+ :is(.js, .sq-js) [data-blocks] [data-cta-closing="photo"]:not([data-graded]) [data-cc-copy] {
1077
+ opacity: 0;
1078
+ animation: struq-fade 0s 3s forwards;
1079
+ }
1080
+ /* With motion and scripts every start state waits from the first paint (the lines and the band unseen, the link clear,
1081
+ the photograph's clip shut) and arrives after 3 s if the script never runs; without scripts nothing waits. */
1082
+ @media (prefers-reduced-motion: no-preference) {
1083
+ :is(.js, .sq-js) [data-blocks] [data-cta-closing]:not([data-ready]) [data-cc-rise] {
1084
+ visibility: hidden;
1085
+ animation: struq-show 0s 3s forwards;
1086
+ }
1087
+ :is(.js, .sq-js) [data-blocks] [data-cta-closing]:not([data-ready]) [data-cc-soft] {
1088
+ opacity: 0;
1089
+ animation: struq-fade 0s 3s forwards;
1090
+ }
1091
+ :is(.js, .sq-js) [data-blocks] [data-cta-closing="photo"]:not([data-ready]) [data-cc-frame] {
1092
+ clip-path: inset(50% 50% 50% 50%);
1093
+ animation: struq-open 0s 3s forwards;
1094
+ }
1095
+ }
1096
+
1097
+ /* --- footer-contact --- */
1098
+ /* Display type stays light (300, or the pairing's own if lighter); the column headings are in the body face. The mail
1099
+ address rolls per character: each one sits in its own mask with a copy below it, and hover or keyboard focus lifts
1100
+ the pair by one line, each after its own delay (--d, written by the script from the pointer's side). Plain links
1101
+ draw an underline from the side in --fc-o. With scripts the time and the open marker are unseen until the time is
1102
+ known; without them they are left out and the clock's slot shows the opening hours (a noscript line), never a
1103
+ made-up time. The fitted address and clock are unseen until measured with their face, and show after 3s if the
1104
+ script never runs. With motion and scripts every entrance start state waits from the first paint, with the same 3s
1105
+ way out. */
1106
+ /* The footer clips what hangs below it: the waiting copies of the rolling characters would otherwise lengthen the
1107
+ page by a line. */
1108
+ [data-blocks] [data-footer-contact] {
1109
+ overflow: clip;
1110
+ }
1111
+ [data-blocks] [data-footer-contact] :is(h2, .font-display) {
1112
+ font-weight: min(var(--display-weight, 300), 300);
1113
+ }
1114
+ [data-blocks] [data-footer-contact] h3 {
1115
+ font-family: inherit;
1116
+ font-weight: inherit;
1117
+ }
1118
+ [data-blocks] [data-footer-contact] a {
1119
+ color: inherit;
1120
+ text-decoration: none;
1121
+ }
1122
+ [data-blocks] [data-footer-contact] [data-fc-rule] {
1123
+ transform-origin: left center;
1124
+ }
1125
+ [data-blocks] [data-footer-contact="address"] [data-fc-address] {
1126
+ display: block;
1127
+ width: max-content;
1128
+ max-width: none;
1129
+ font-size: min(7vw, 30svh);
1130
+ line-height: 1.2;
1131
+ letter-spacing: -0.03em;
1132
+ white-space: nowrap;
1133
+ }
1134
+ @media (max-width: 639px) {
1135
+ [data-blocks] [data-footer-contact="address"] [data-fc-address]:not([data-lines]) {
1136
+ font-size: 10.5vw;
1137
+ }
1138
+ [data-blocks] [data-footer-contact="address"] [data-fc-address]:not([data-lines]) [data-fc-part] {
1139
+ display: block;
1140
+ width: max-content;
1141
+ }
1142
+ }
1143
+ [data-blocks] [data-footer-contact="address"] [data-fc-address][data-lines="2"] [data-fc-part] {
1144
+ display: block;
1145
+ width: max-content;
1146
+ }
1147
+ [data-blocks] [data-footer-contact] :is([data-fc-address], [data-fc-clock]) {
1148
+ font-kerning: none;
1149
+ font-variant-ligatures: none;
1150
+ }
1151
+ [data-blocks] [data-footer-contact] [data-fc-part] {
1152
+ display: inline-block;
1153
+ white-space: nowrap;
1154
+ clip-path: inset(0 -0.3em);
1155
+ }
1156
+ [data-blocks] [data-footer-contact] [data-fc-ch] {
1157
+ display: inline-block;
1158
+ position: relative;
1159
+ white-space: pre;
1160
+ clip-path: inset(0 -0.2em);
1161
+ }
1162
+ [data-blocks] [data-footer-contact] [data-fc-enter] {
1163
+ display: block;
1164
+ }
1165
+ [data-blocks] [data-footer-contact] [data-fc-roll] {
1166
+ display: block;
1167
+ position: relative;
1168
+ transition: transform 0.75s var(--ease-out-expo) var(--d, 0s);
1169
+ }
1170
+ [data-blocks] [data-footer-contact] [data-fc-roll] > span {
1171
+ display: block;
1172
+ }
1173
+ [data-blocks] [data-footer-contact] [data-fc-roll] > [data-fc-copy] {
1174
+ position: absolute;
1175
+ left: 0;
1176
+ top: 100%;
1177
+ }
1178
+ [data-blocks] [data-footer-contact] [data-fc-roll-host]:is(:hover, :focus-visible) [data-fc-roll] {
1179
+ transform: translateY(-100%);
1180
+ }
1181
+ [data-blocks] [data-footer-contact] [data-fc-draw]::after {
1182
+ content: "";
1183
+ position: absolute;
1184
+ left: 0;
1185
+ right: 0;
1186
+ bottom: -0.08em;
1187
+ height: 1px;
1188
+ background: currentColor;
1189
+ transform: scaleX(0);
1190
+ transform-origin: var(--fc-o, left) center;
1191
+ transition: transform 0.55s var(--ease-out-expo);
1192
+ }
1193
+ [data-blocks] [data-footer-contact] [data-fc-draw]:is(:hover, :focus-visible)::after {
1194
+ transform: scaleX(1);
1195
+ }
1196
+ [data-blocks] [data-footer-contact] [data-fc-dot] {
1197
+ display: inline-block;
1198
+ flex: none;
1199
+ width: 0.5em;
1200
+ height: 0.5em;
1201
+ border: 1px solid currentColor;
1202
+ border-radius: 50%;
1203
+ }
1204
+ [data-blocks] [data-footer-contact] [data-fc-dot][data-open] {
1205
+ background: currentColor;
1206
+ }
1207
+ [data-blocks] [data-footer-contact] :is([data-fc-fade], [data-fc-dot], [data-fc-clock]) {
1208
+ transition: opacity 0.45s cubic-bezier(0.33, 1, 0.68, 1);
1209
+ }
1210
+ .sq-js [data-blocks] [data-footer-contact] [data-pending] {
1211
+ opacity: 0;
1212
+ }
1213
+ html:not(.sq-js) [data-blocks] [data-footer-contact] [data-pending] {
1214
+ display: none;
1215
+ }
1216
+ /* The clock: hours and minutes each in a mask that clips sideways (they slide out from the colon), every digit in a
1217
+ mask that clips up and down (a changed digit rolls over), the colon breathing once a second. */
1218
+ [data-blocks] [data-footer-contact="clock"] [data-fc-clock] {
1219
+ display: inline-flex;
1220
+ align-items: baseline;
1221
+ justify-content: center;
1222
+ font-size: min(24vw, 40svh);
1223
+ line-height: 1;
1224
+ letter-spacing: -0.04em;
1225
+ font-variant-numeric: tabular-nums lining-nums;
1226
+ white-space: nowrap;
1227
+ }
1228
+ [data-blocks] [data-footer-contact] :is([data-fc-hmask], [data-fc-mmask]) {
1229
+ display: inline-block;
1230
+ clip-path: inset(-0.3em 0);
1231
+ }
1232
+ [data-blocks] [data-footer-contact] [data-fc-colon-wrap] {
1233
+ display: inline-block;
1234
+ padding-inline: 0.04em;
1235
+ }
1236
+ [data-blocks] [data-footer-contact] [data-fc-colon] {
1237
+ display: inline-block;
1238
+ transform: translateY(-0.06em);
1239
+ animation: struq-fc-breathe 1s ease-in-out infinite;
1240
+ }
1241
+ /* Every digit stands in a slot as wide as the face's widest (--fc-dw, measured by the script), centred, so a narrow
1242
+ 1 never shifts the colon on the minute; many display faces keep proportional figures whatever tabular-nums asks. */
1243
+ [data-blocks] [data-footer-contact] [data-fc-digit] {
1244
+ display: inline-block;
1245
+ position: relative;
1246
+ width: var(--fc-dw, auto);
1247
+ text-align: center;
1248
+ clip-path: inset(0 -0.12em);
1249
+ }
1250
+ [data-blocks] [data-footer-contact] [data-fc-out] {
1251
+ position: absolute;
1252
+ inset: 0;
1253
+ animation: struq-fc-out 0.8s var(--ease-out-expo) forwards;
1254
+ }
1255
+ [data-blocks] [data-footer-contact] [data-fc-in] {
1256
+ animation: struq-fc-in 0.8s var(--ease-out-expo) both;
1257
+ }
1258
+ @keyframes struq-fc-breathe {
1259
+ 0%,
1260
+ 100% {
1261
+ opacity: 1;
1262
+ }
1263
+ 50% {
1264
+ opacity: 0.22;
1265
+ }
1266
+ }
1267
+ @keyframes struq-fc-out { to { transform: translateY(-100%); } }
1268
+ @keyframes struq-fc-in { from { transform: translateY(100%); } }
1269
+ @keyframes struq-fc-rule { to { transform: none; } }
1270
+ .sq-js [data-blocks] [data-footer-contact]:not([data-fit]) :is([data-fc-address], [data-fc-clock]) {
1271
+ visibility: hidden;
1272
+ animation: struq-show 0s 3s forwards;
1273
+ }
1274
+ @media (prefers-reduced-motion: no-preference) {
1275
+ .sq-js [data-blocks] [data-footer-contact]:not([data-ready]) :is([data-fc-enter], [data-fc-split], [data-fc-h], [data-fc-m], [data-fc-colon-wrap]) {
1276
+ visibility: hidden;
1277
+ animation: struq-show 0s 3s forwards;
1278
+ }
1279
+ .sq-js [data-blocks] [data-footer-contact]:not([data-ready]) [data-fc-soft] {
1280
+ opacity: 0;
1281
+ animation: struq-fade 0s 3s forwards;
1282
+ }
1283
+ .sq-js [data-blocks] [data-footer-contact]:not([data-ready]) [data-fc-rule] {
1284
+ transform: scaleX(0);
1285
+ animation: struq-fc-rule 0s 3s forwards;
1286
+ }
1287
+ }
1288
+ /* Reduced motion: no roll and no draw, the hover shows as a cut (the address steps back to the muted ink, a link's
1289
+ underline is simply there); the colon holds and a changed digit cuts. */
1290
+ @media (prefers-reduced-motion: reduce) {
1291
+ [data-blocks] [data-footer-contact] [data-fc-roll],
1292
+ [data-blocks] [data-footer-contact] [data-fc-draw]::after {
1293
+ transition: none;
1294
+ }
1295
+ [data-blocks] [data-footer-contact] [data-fc-roll-host]:is(:hover, :focus-visible) [data-fc-roll] {
1296
+ transform: none;
1297
+ }
1298
+ [data-blocks] [data-footer-contact] [data-fc-roll-host]:is(:hover, :focus-visible) {
1299
+ color: var(--ink-muted);
1300
+ }
1301
+ [data-blocks] [data-footer-contact] [data-fc-colon],
1302
+ [data-blocks] [data-footer-contact] [data-fc-in] {
1303
+ animation: none;
1304
+ }
1305
+ [data-blocks] [data-footer-contact] [data-fc-out] {
1306
+ display: none;
1307
+ }
1308
+ }
1309
+
1310
+ /* --- manifesto --- */
1311
+ /* The statement is display type at a light weight, whatever the pairing asks. The quote's veil is the page ground held
1312
+ dark (its own hue, never black), and the frame under the photograph is that same dark, so the light quote reads
1313
+ before the photo is decoded; the block's script tunes the veil's strength to the photo (never past 0.45) and this
1314
+ safe strength stays when the photo cannot be read. The caret is unseen until the script writes it. With motion and
1315
+ scripts every start state waits from the first paint (the statement unseen until it is cut into lines and every
1316
+ clip or mask is set, the signature clear, the photograph clear until decoded) and arrives after 3s if the script
1317
+ never runs; without scripts nothing waits. */
1318
+ [data-blocks] [data-manifesto] .font-display {
1319
+ font-weight: min(var(--display-weight, 300), 300);
1320
+ }
1321
+ [data-blocks] [data-manifesto] :is([data-mf-frame], [data-mf-veil]) {
1322
+ background: var(--inverse);
1323
+ background: oklch(from var(--surface-0) min(l, 0.2) c h);
1324
+ }
1325
+ [data-blocks] [data-manifesto] [data-mf-veil] {
1326
+ opacity: 0.4;
1327
+ transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1);
1328
+ }
1329
+ [data-blocks] [data-manifesto] [data-mf-caret] {
1330
+ opacity: 0;
1331
+ will-change: transform;
1332
+ }
1333
+ @media (prefers-reduced-motion: no-preference) {
1334
+ :is(.js, .sq-js) [data-blocks] [data-manifesto]:not([data-ready]) [data-mf-text] {
1335
+ visibility: hidden;
1336
+ animation: struq-show 0s 3s forwards;
1337
+ }
1338
+ :is(.js, .sq-js) [data-blocks] [data-manifesto]:not([data-ready]) [data-mf-soft] {
1339
+ opacity: 0;
1340
+ animation: struq-fade 0s 3s forwards;
1341
+ }
1342
+ :is(.js, .sq-js) [data-blocks] [data-manifesto="quote"]:not([data-ready]) [data-mf-layer] {
1343
+ opacity: 0;
1344
+ animation: struq-fade 0s 3s forwards;
1345
+ }
1346
+ [data-blocks] [data-manifesto="quote"] [data-mf-layer] {
1347
+ will-change: transform;
1348
+ }
1349
+ }
1350
+
1351
+ /* --- header-bar --- */
1352
+ /* The bar's height is shared with the page through the nearest [data-blocks]; the block sets --hb-top there once it has
1353
+ measured (the bar's height, or for settle the bottom of the large wordmark), so a hero keeps its type below it. The
1354
+ copies (one per ink) show only once the script has cut them; until then, and without scripts, the real bar stands in
1355
+ the start tone's ink as the plain bar (settle's set large, below). */
1356
+ [data-blocks]:has([data-header-bar]),
1357
+ [data-blocks] [data-header-bar] {
1358
+ --hb-h: 4rem;
1359
+ }
1360
+ [data-blocks]:has([data-header-bar]) {
1361
+ --hb-top: var(--hb-h);
1362
+ }
1363
+ /* Without scripts the plain bar stays on the hero it was inked for, instead of riding over light sections in light ink. */
1364
+ html:not(.sq-js) [data-blocks] [data-header-bar] [data-hb-move] {
1365
+ position: absolute;
1366
+ }
1367
+ @media (min-width: 768px) {
1368
+ [data-blocks]:has([data-header-bar]),
1369
+ [data-blocks] [data-header-bar] {
1370
+ --hb-h: 4.5rem;
1371
+ }
1372
+ }
1373
+ [data-blocks] [data-header-bar] .font-display {
1374
+ font-weight: min(var(--display-weight, 300), 300);
1375
+ }
1376
+ [data-blocks] [data-header-bar] :is(a, button) {
1377
+ color: inherit;
1378
+ text-decoration: none;
1379
+ }
1380
+ [data-blocks] [data-header-bar] [data-hb-real] :is(a, button) {
1381
+ pointer-events: auto;
1382
+ }
1383
+ [data-blocks] [data-header-bar][data-links-off] [data-hb-real] [data-hb-fade] :is(a, button) {
1384
+ pointer-events: none;
1385
+ }
1386
+ [data-blocks] [data-header-bar]:not([data-ready]) [data-hb-copy] {
1387
+ visibility: hidden;
1388
+ }
1389
+ [data-blocks] [data-header-bar][data-ready] [data-hb-real] {
1390
+ color: transparent;
1391
+ }
1392
+ /* settle from the first paint: the wordmark already stands large across the hero, its scale the gutter span over its
1393
+ estimated advance (--hb-adv, set by the block, read against the face on screen as half em and half ch), capped at
1394
+ 55svh over its line, at the top the script will use;
1395
+ the links wait for the landing. The script takes over with the measured face and eases out the difference. If the
1396
+ script never runs the plain bar returns after 3s. The wordmark rises out of its mask from 0s, script or not. */
1397
+ [data-blocks] [data-header-bar] {
1398
+ --hb-gx: 1.5rem;
1399
+ }
1400
+ @media (min-width: 768px) {
1401
+ [data-blocks] [data-header-bar] {
1402
+ --hb-gx: 3vw;
1403
+ }
1404
+ }
1405
+ @media (prefers-reduced-motion: no-preference) {
1406
+ .sq-js [data-blocks] [data-header-bar="settle"]:not([data-ready]) [data-hb-word] {
1407
+ translate: 0 calc(var(--hb-h) * 0.5 - (var(--hb-h) - 1em) / 2);
1408
+ scale: max(1, min(tan(atan2(100vw - 2 * var(--hb-gx), var(--hb-adv, 3) * (0.5em + 0.76ch))), tan(atan2(55svh, 1em))));
1409
+ animation: hb-plain 0s 3s forwards;
1410
+ }
1411
+ .sq-js [data-blocks] [data-header-bar="settle"]:not([data-ready]) [data-hb-fade] {
1412
+ visibility: hidden;
1413
+ animation: struq-show 0s 3s forwards;
1414
+ }
1415
+ .sq-js [data-blocks] [data-header-bar="settle"] [data-hb-word] [data-hb-wroll] {
1416
+ animation: hb-rise 1.2s var(--ease-out-expo) both;
1417
+ }
1418
+ }
1419
+ @keyframes hb-plain {
1420
+ to {
1421
+ translate: none;
1422
+ scale: none;
1423
+ }
1424
+ }
1425
+ @keyframes hb-rise {
1426
+ from {
1427
+ translate: 0 calc(100% + 0.22em);
1428
+ }
1429
+ }
1430
+ /* Keyboard focus: drawn on the copies in their own ink (the real links' text is transparent once they show). */
1431
+ [data-blocks] [data-header-bar] [data-hb-real] :focus-visible {
1432
+ outline: 1.5px solid currentColor;
1433
+ outline-offset: 6px;
1434
+ }
1435
+ [data-blocks] [data-header-bar][data-ready] [data-hb-real] :focus-visible {
1436
+ outline: none;
1437
+ }
1438
+ [data-blocks] [data-header-bar] [data-hb-copy] [data-hb-link][data-focus] {
1439
+ outline: 1.5px solid currentColor;
1440
+ outline-offset: 6px;
1441
+ border-radius: 1px;
1442
+ }
1443
+ /* Once the page has scrolled the bar takes the ground under it (over a photo the palette's dark over a blur) and quiet
1444
+ its hairline, all scrubbed by --hb-k over the bar's own height of scroll. */
1445
+ [data-blocks] [data-header-bar] :is([data-hb-ground], [data-hb-blur]) {
1446
+ opacity: var(--hb-k, 0);
1447
+ }
1448
+ [data-blocks] [data-header-bar] [data-hb-line] {
1449
+ opacity: calc(var(--hb-k, 0) * 0.2);
1450
+ }
1451
+ [data-blocks] [data-header-bar] [data-hb-track] {
1452
+ opacity: 0.2;
1453
+ }
1454
+ /* The wordmark rolls whole (its letters keep their kerning, even set large): the name slides up out of its mask and its
1455
+ copy rises in from below. The mask has room for descenders; the copy waits a little lower than the line. */
1456
+ [data-blocks] [data-header-bar] [data-hb-wmask] {
1457
+ overflow: hidden;
1458
+ padding-block: 0.08em 0.14em;
1459
+ margin-block: -0.08em -0.14em;
1460
+ }
1461
+ [data-blocks] [data-header-bar] [data-hb-wroll] > span:last-child {
1462
+ top: calc(100% + 0.22em);
1463
+ }
1464
+ [data-blocks] [data-header-bar] [data-hb-wroll] {
1465
+ transition: transform 0.6s var(--ease-out-expo);
1466
+ }
1467
+ [data-blocks] [data-header-bar] [data-hb-word][data-hot] [data-hb-wroll] {
1468
+ transform: translateY(calc(-100% - 0.22em));
1469
+ }
1470
+ /* quiet: each letter in its own mask, rolling after the one before it. */
1471
+ [data-blocks] [data-header-bar] [data-hb-ch] {
1472
+ position: relative;
1473
+ display: inline-block;
1474
+ overflow: hidden;
1475
+ white-space: pre;
1476
+ padding-block: 0.15em;
1477
+ margin-block: -0.15em;
1478
+ }
1479
+ [data-blocks] [data-header-bar] [data-hb-ch] > span {
1480
+ display: block;
1481
+ transition: transform 0.55s var(--ease-out-expo) calc(var(--i, 0) * 24ms);
1482
+ }
1483
+ [data-blocks] [data-header-bar] [data-hb-ch] > span:last-child {
1484
+ position: absolute;
1485
+ left: 0;
1486
+ top: calc(100% + 0.15em);
1487
+ }
1488
+ [data-blocks] [data-header-bar] [data-hb-link][data-hot] [data-hb-ch] > span {
1489
+ transform: translateY(calc(-100% - 0.3em));
1490
+ }
1491
+ /* The action: a text link with a faint rule under it at rest; quiet's rule comes up to the full ink on hover. */
1492
+ [data-blocks] [data-header-bar] [data-hb-action]::before {
1493
+ content: "";
1494
+ position: absolute;
1495
+ left: 0;
1496
+ right: 0;
1497
+ bottom: 0.2rem;
1498
+ height: 1px;
1499
+ background: currentColor;
1500
+ opacity: 0.35;
1501
+ transition: opacity 0.3s var(--ease-out-expo);
1502
+ }
1503
+ [data-blocks] [data-header-bar="quiet"] [data-hb-action][data-hot]::before {
1504
+ opacity: 1;
1505
+ }
1506
+ /* settle (every link but the wordmark) and split's action: a line draws from the side the pointer came in and
1507
+ retracts toward the side it leaves (--hb-o); settle's other links step back. */
1508
+ [data-blocks] [data-header-bar="settle"] [data-hb-link]:not([data-hb-link="w"])::after,
1509
+ [data-blocks] [data-header-bar="split"] [data-hb-action]::after {
1510
+ content: "";
1511
+ position: absolute;
1512
+ left: 0;
1513
+ right: 0;
1514
+ bottom: 0.2rem;
1515
+ height: 1px;
1516
+ background: currentColor;
1517
+ transform: scaleX(0);
1518
+ transform-origin: var(--hb-o, left) center;
1519
+ transition: transform 0.5s var(--ease-out-expo);
1520
+ }
1521
+ [data-blocks] [data-header-bar="settle"] [data-hb-link][data-hot]::after,
1522
+ [data-blocks] [data-header-bar="split"] [data-hb-action][data-hot]::after {
1523
+ transform: scaleX(1);
1524
+ }
1525
+ [data-blocks] [data-header-bar="settle"] [data-hb-fade] [data-hb-link] {
1526
+ transition: opacity 0.3s var(--ease-out-expo);
1527
+ }
1528
+ [data-blocks] [data-header-bar="settle"][data-hot-any] [data-hb-fade] [data-hb-link]:not([data-hot]) {
1529
+ opacity: 0.45;
1530
+ }
1531
+ /* split: one line glides under the hovered link and takes its width; it appears in place the first time. */
1532
+ [data-blocks] [data-header-bar="split"] [data-hb-mark] {
1533
+ bottom: 0.2rem;
1534
+ width: var(--hb-mw, 0px);
1535
+ transform: translate3d(var(--hb-mx, 0px), 0, 0);
1536
+ opacity: 0;
1537
+ transition:
1538
+ transform 0.45s var(--ease-out-expo),
1539
+ width 0.45s var(--ease-out-expo),
1540
+ opacity 0.25s linear;
1541
+ }
1542
+ [data-blocks] [data-header-bar="split"][data-mark] [data-hb-mark] {
1543
+ opacity: 1;
1544
+ }
1545
+ [data-blocks] [data-header-bar="split"][data-mark-snap] [data-hb-mark] {
1546
+ transition: opacity 0.25s linear;
1547
+ }
1548
+ /* The local time: unseen until it is known; without scripts it is left out, never a made-up time. */
1549
+ .sq-js [data-blocks] [data-header-bar] [data-hb-time][data-pending] {
1550
+ visibility: hidden;
1551
+ }
1552
+ html:not(.sq-js) [data-blocks] [data-header-bar] [data-hb-time] {
1553
+ display: none;
1554
+ }
1555
+ /* Reduced motion: nothing rolls or glides; a hover underlines (quiet and the wordmark), draws its line as a cut
1556
+ (settle) or puts the line under the link at once (split); the scrolled ground cuts in with a short fade. */
1557
+ @media (prefers-reduced-motion: reduce) {
1558
+ [data-blocks] [data-header-bar] :is([data-hb-ch] > span, [data-hb-wroll], [data-hb-mark]),
1559
+ [data-blocks] [data-header-bar] [data-hb-link]::after {
1560
+ transition: none;
1561
+ }
1562
+ [data-blocks] [data-header-bar] [data-hb-link][data-hot] :is([data-hb-ch] > span, [data-hb-wroll]) {
1563
+ transform: none;
1564
+ }
1565
+ [data-blocks] [data-header-bar="quiet"] [data-hb-link][data-hot] [data-hb-label] {
1566
+ box-shadow: 0 1px 0 currentColor;
1567
+ }
1568
+ [data-blocks] [data-header-bar] [data-hb-word][data-hot] [data-hb-wroll] > span:first-child {
1569
+ text-decoration: underline;
1570
+ text-decoration-thickness: 1px;
1571
+ text-underline-offset: 0.2em;
1572
+ }
1573
+ [data-blocks] [data-header-bar] :is([data-hb-ground], [data-hb-blur], [data-hb-line]) {
1574
+ transition: opacity 0.2s linear;
1575
+ }
1576
+ }
1577
+
1578
+ /* --- contact-form --- */
1579
+ /* Display type stays light, whatever the pairing asks, as in the heroes. */
1580
+ [data-blocks] [data-contact-form] .font-display,
1581
+ [data-blocks] [data-contact-form] [data-cf-float] :is([data-cf-grow], label) {
1582
+ font-weight: min(var(--display-weight, 300), 300);
1583
+ }
1584
+ /* The fields carry no box and no browser chrome: the rule under them is the field. */
1585
+ [data-blocks] [data-contact-form] :is(input[type="text"], input[type="email"], textarea) {
1586
+ appearance: none;
1587
+ min-width: 0;
1588
+ margin: 0;
1589
+ padding: 0;
1590
+ border: 0;
1591
+ border-radius: 0;
1592
+ background: transparent;
1593
+ box-shadow: none;
1594
+ color: inherit;
1595
+ font: inherit;
1596
+ letter-spacing: inherit;
1597
+ outline: none;
1598
+ caret-color: var(--accent);
1599
+ resize: none;
1600
+ overflow: hidden;
1601
+ overflow-wrap: anywhere;
1602
+ /* struq.nl's own base layer paints text inputs with these two (as !important, which a layer wins); pointed at the
1603
+ block's ground and ink here, so the field stays a rule. */
1604
+ --color-panel: transparent;
1605
+ --color-primary-text: var(--ink);
1606
+ }
1607
+ [data-blocks] [data-contact-form] :is(input, textarea)::placeholder {
1608
+ color: color-mix(in oklch, var(--ink-muted) 72%, var(--surface-0));
1609
+ opacity: 1;
1610
+ }
1611
+ /* Autofill keeps its own ground away from a field that is only a rule. */
1612
+ [data-blocks] [data-contact-form] :is(input, textarea):-webkit-autofill {
1613
+ -webkit-text-fill-color: var(--ink);
1614
+ transition: background-color 0s 86400s, color 0s 86400s;
1615
+ }
1616
+ /* A field that grows with its text: the hidden copy of the value (data-mirror) sizes the cell the field fills, in the
1617
+ same face, so nothing is measured by script. */
1618
+ [data-blocks] [data-contact-form] [data-cf-grow] {
1619
+ position: relative;
1620
+ display: inline-grid;
1621
+ max-width: 100%;
1622
+ padding-bottom: 0.14em;
1623
+ vertical-align: baseline;
1624
+ }
1625
+ [data-blocks] [data-contact-form] [data-cf-grow][data-block] {
1626
+ display: grid;
1627
+ width: 100%;
1628
+ }
1629
+ [data-blocks] [data-contact-form] [data-cf-grow] > :is(input, textarea) {
1630
+ grid-area: 1 / 1;
1631
+ width: 100%;
1632
+ }
1633
+ [data-blocks] [data-contact-form] [data-cf-grow]::after {
1634
+ content: attr(data-mirror);
1635
+ grid-area: 1 / 1;
1636
+ padding-right: 0.08em;
1637
+ visibility: hidden;
1638
+ white-space: pre;
1639
+ overflow: hidden;
1640
+ pointer-events: none;
1641
+ }
1642
+ /* A text area keeps a trailing space in its copy, so a closing new line still takes its height. */
1643
+ [data-blocks] [data-contact-form] [data-cf-grow][data-multiline]::after {
1644
+ content: attr(data-mirror) " ";
1645
+ }
1646
+ [data-blocks] [data-contact-form] [data-cf-grow][data-multiline]::after {
1647
+ white-space: pre-wrap;
1648
+ overflow-wrap: anywhere;
1649
+ }
1650
+ /* The rule: a quiet line at rest; on focus the accent draws over it from the left, and draws back the way it came on
1651
+ leaving. An invalid field's rule turns to the full ink. */
1652
+ [data-blocks] [data-contact-form] [data-cf-rule] {
1653
+ position: absolute;
1654
+ inset: auto 0 0 0;
1655
+ height: 1px;
1656
+ background: color-mix(in oklch, var(--ink-muted) 55%, transparent);
1657
+ pointer-events: none;
1658
+ }
1659
+ [data-blocks] [data-contact-form] [data-cf-rule]::after {
1660
+ content: "";
1661
+ position: absolute;
1662
+ inset: auto 0 0 0;
1663
+ height: 2px;
1664
+ background: var(--accent);
1665
+ transform: scaleX(0);
1666
+ transform-origin: left;
1667
+ transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
1668
+ }
1669
+ [data-blocks] [data-contact-form] [data-cf-grow]:focus-within [data-cf-rule]::after {
1670
+ transform: scaleX(1);
1671
+ }
1672
+ [data-blocks] [data-contact-form] [data-invalid] [data-cf-rule] {
1673
+ height: 2px;
1674
+ background: var(--ink);
1675
+ }
1676
+ /* Each message has its slot from the start, one line high, so saying it moves nothing. */
1677
+ [data-blocks] [data-contact-form] [data-cf-msg] {
1678
+ display: block;
1679
+ min-height: 1.95rem;
1680
+ padding-top: 0.6rem;
1681
+ font-family: var(--face-body), ui-sans-serif, system-ui, sans-serif;
1682
+ font-size: 0.875rem;
1683
+ font-weight: 400;
1684
+ line-height: 1.5;
1685
+ letter-spacing: 0;
1686
+ color: var(--ink);
1687
+ }
1688
+
1689
+ /* lines: the label rests inside the field and lifts to a small label above it when the field is focused or filled. */
1690
+ [data-blocks] [data-contact-form="lines"] [data-cf-float] {
1691
+ --cf-size: clamp(1.25rem, 1.05rem + 0.7vw, 1.75rem);
1692
+ position: relative;
1693
+ padding-top: calc(var(--cf-size) * 0.9);
1694
+ }
1695
+ [data-blocks] [data-contact-form="lines"] [data-cf-float] :is([data-cf-grow], label) {
1696
+ font-family: var(--font-display);
1697
+ font-size: var(--cf-size);
1698
+ line-height: 1.35;
1699
+ letter-spacing: -0.01em;
1700
+ }
1701
+ [data-blocks] [data-contact-form="lines"] [data-cf-float] > label {
1702
+ position: absolute;
1703
+ left: 0;
1704
+ top: calc(var(--cf-size) * 0.9);
1705
+ color: var(--ink-muted);
1706
+ white-space: nowrap;
1707
+ transform-origin: 0 0;
1708
+ pointer-events: none;
1709
+ transition:
1710
+ transform 0.5s cubic-bezier(0.16, 1, 0.3, 1),
1711
+ color 0.5s cubic-bezier(0.16, 1, 0.3, 1);
1712
+ }
1713
+ [data-blocks] [data-contact-form="lines"] [data-cf-float]:focus-within > label,
1714
+ [data-blocks] [data-contact-form="lines"] [data-cf-float]:has(:is(input, textarea):not(:placeholder-shown)) > label {
1715
+ transform: translateY(calc(var(--cf-size) * -0.9)) scale(0.58);
1716
+ color: var(--ink);
1717
+ }
1718
+
1719
+ /* sentence: the blanks inline at display size; each message hangs under its blank. Below 768 px the clauses stack,
1720
+ each fragment the label above its own field. */
1721
+ [data-blocks] [data-contact-form="sentence"] [data-cf-sentence] {
1722
+ font-size: clamp(2rem, 0.9rem + 3.6vw, 4.25rem);
1723
+ line-height: 1.75;
1724
+ letter-spacing: -0.02em;
1725
+ }
1726
+ [data-blocks] [data-contact-form="sentence"] [data-cf-blank] {
1727
+ position: relative;
1728
+ display: inline-block;
1729
+ max-width: 100%;
1730
+ line-height: 1.1;
1731
+ vertical-align: baseline;
1732
+ }
1733
+ /* The text area's first line sits on the sentence's own line: top-aligned, down by half the difference in leading. */
1734
+ [data-blocks] [data-contact-form="sentence"] [data-cf-blank][data-multiline] {
1735
+ vertical-align: top;
1736
+ margin-top: 0.25em;
1737
+ }
1738
+ @media (min-width: 768px) {
1739
+ [data-blocks] [data-contact-form="sentence"] [data-cf-msg] {
1740
+ position: absolute;
1741
+ left: 0;
1742
+ top: calc(100% + 0.05em);
1743
+ min-height: 0;
1744
+ padding-top: 0.15rem;
1745
+ line-height: 1.2;
1746
+ white-space: nowrap;
1747
+ }
1748
+ }
1749
+ @media (max-width: 767px) {
1750
+ [data-blocks] [data-contact-form="sentence"] [data-cf-sentence] {
1751
+ font-size: clamp(1.625rem, 1rem + 2.6vw, 2rem);
1752
+ line-height: 1.2;
1753
+ }
1754
+ [data-blocks] [data-contact-form="sentence"] [data-cf-clause] {
1755
+ display: block;
1756
+ }
1757
+ [data-blocks] [data-contact-form="sentence"] [data-cf-clause] + [data-cf-clause] {
1758
+ margin-top: 1rem;
1759
+ }
1760
+ [data-blocks] [data-contact-form="sentence"] [data-cf-clause] > label {
1761
+ display: block;
1762
+ margin-bottom: 0.3em;
1763
+ }
1764
+ [data-blocks] [data-contact-form="sentence"] [data-cf-blank],
1765
+ [data-blocks] [data-contact-form="sentence"] [data-cf-blank][data-multiline] {
1766
+ display: block;
1767
+ margin-top: 0;
1768
+ }
1769
+ [data-blocks] [data-contact-form="sentence"] [data-cf-blank] [data-cf-grow] {
1770
+ display: grid;
1771
+ width: 100%;
1772
+ }
1773
+ [data-blocks] [data-contact-form="sentence"] [data-cf-comma] {
1774
+ display: none;
1775
+ }
1776
+ }
1777
+
1778
+ /* The choice: real radio inputs as quiet rings; the checked one takes a small dot in the accent. */
1779
+ [data-blocks] [data-contact-form] [data-cf-radio] {
1780
+ appearance: none;
1781
+ display: inline-grid;
1782
+ place-content: center;
1783
+ flex-shrink: 0;
1784
+ width: 0.9em;
1785
+ height: 0.9em;
1786
+ margin: 0;
1787
+ border: 1px solid var(--ink-muted);
1788
+ border-radius: 50%;
1789
+ background: transparent;
1790
+ cursor: pointer;
1791
+ transition: border-color 0.25s ease;
1792
+ }
1793
+ [data-blocks] [data-contact-form] [data-cf-radio]::before {
1794
+ content: "";
1795
+ width: 0.46em;
1796
+ height: 0.46em;
1797
+ border-radius: 50%;
1798
+ background: var(--accent);
1799
+ transform: scale(0);
1800
+ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
1801
+ }
1802
+ [data-blocks] [data-contact-form] [data-cf-radio]:checked,
1803
+ [data-blocks] [data-contact-form] [data-cf-choice]:hover [data-cf-radio] {
1804
+ border-color: var(--ink);
1805
+ }
1806
+ [data-blocks] [data-contact-form] [data-cf-radio]:checked::before {
1807
+ transform: scale(1);
1808
+ }
1809
+
1810
+ /* The send link: its arrow nudges on hover and focus, and travels while the message is on its way. */
1811
+ [data-blocks] [data-contact-form] [data-cf-arrow] {
1812
+ padding-right: 0.25em;
1813
+ margin-right: -0.25em;
1814
+ }
1815
+ [data-blocks] [data-contact-form] [data-cf-arrow] svg {
1816
+ transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
1817
+ }
1818
+ [data-blocks] [data-contact-form] button:is(:hover, :focus-visible) [data-cf-arrow] svg {
1819
+ transform: translateX(0.22em);
1820
+ }
1821
+ [data-blocks] [data-contact-form][data-state="sending"] [data-cf-send] {
1822
+ cursor: progress;
1823
+ }
1824
+ [data-blocks] [data-contact-form][data-state="sending"] [data-cf-send] [data-cf-arrow] svg {
1825
+ animation: cf-travel 1.1s cubic-bezier(0.65, 0, 0.35, 1) infinite;
1826
+ }
1827
+ /* The arrow leaves on the right and comes back in from the left; the arrow's own overflow clip does the hiding. */
1828
+ @keyframes cf-travel {
1829
+ 0% { transform: translateX(0); }
1830
+ 50% { transform: translateX(110%); }
1831
+ 50.01% { transform: translateX(-110%); }
1832
+ 100% { transform: translateX(0); }
1833
+ }
1834
+ /* The link buttons say focus with their own full underline, never the kit's boxed outline. */
1835
+ [data-blocks] [data-contact-form] [data-cf-link]:focus-visible {
1836
+ outline: none;
1837
+ }
1838
+ [data-blocks] [data-contact-form] [data-cf-fields] {
1839
+ transition: opacity 0.3s ease;
1840
+ }
1841
+ [data-blocks] [data-contact-form][data-state="sending"] [data-cf-fields] {
1842
+ opacity: 0.5;
1843
+ pointer-events: none;
1844
+ }
1845
+
1846
+ /* The form and the thank-you share one cell: the stage keeps the form's height, so the exchange moves nothing. */
1847
+ [data-blocks] [data-contact-form] [data-cf-stage] {
1848
+ display: grid;
1849
+ }
1850
+ [data-blocks] [data-contact-form] [data-cf-stage] > * {
1851
+ grid-area: 1 / 1;
1852
+ min-width: 0;
1853
+ }
1854
+ [data-blocks] [data-contact-form] [data-cf-form] {
1855
+ transition:
1856
+ opacity 0.35s ease,
1857
+ transform 0.6s cubic-bezier(0.16, 1, 0.3, 1),
1858
+ visibility 0s;
1859
+ }
1860
+ [data-blocks] [data-contact-form][data-state="sent"] [data-cf-form] {
1861
+ opacity: 0;
1862
+ transform: translateY(-0.75rem);
1863
+ visibility: hidden;
1864
+ transition:
1865
+ opacity 0.35s ease,
1866
+ transform 0.6s cubic-bezier(0.16, 1, 0.3, 1),
1867
+ visibility 0s 0.35s;
1868
+ }
1869
+ [data-blocks] [data-contact-form] [data-cf-thanks] {
1870
+ visibility: hidden;
1871
+ opacity: 0;
1872
+ transition:
1873
+ opacity 0.3s ease,
1874
+ visibility 0s 0.3s;
1875
+ }
1876
+ [data-blocks] [data-contact-form][data-state="sent"] [data-cf-thanks] {
1877
+ visibility: visible;
1878
+ opacity: 1;
1879
+ transition:
1880
+ opacity 0.2s ease 0.2s,
1881
+ visibility 0s 0.2s;
1882
+ }
1883
+ [data-blocks] [data-contact-form] [data-cf-thanks] [data-cf-rise] {
1884
+ transform: translateY(105%);
1885
+ transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1) 0.25s;
1886
+ }
1887
+ [data-blocks] [data-contact-form] [data-cf-thanks] [data-cf-soft] {
1888
+ opacity: 0;
1889
+ transform: translateY(0.75rem);
1890
+ transition:
1891
+ opacity 0.6s ease 0.55s,
1892
+ transform 0.9s cubic-bezier(0.16, 1, 0.3, 1) 0.55s;
1893
+ }
1894
+ [data-blocks] [data-contact-form][data-state="sent"] [data-cf-thanks] :is([data-cf-rise], [data-cf-soft]) {
1895
+ opacity: 1;
1896
+ transform: none;
1897
+ }
1898
+
1899
+ /* steps: every question shares one cell, so the stage is as tall as the tallest from the first paint. With scripts the
1900
+ first question shows and the rest wait; if the script never runs they stand stacked after 3 s. Without scripts they
1901
+ stand stacked from the start. */
1902
+ [data-blocks] [data-contact-form="steps"] [data-cf-fill] {
1903
+ transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
1904
+ }
1905
+ .sq-js [data-blocks] [data-contact-form="steps"] [data-cf-steps],
1906
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-steps] {
1907
+ display: grid;
1908
+ }
1909
+ .sq-js [data-blocks] [data-contact-form="steps"] [data-cf-step],
1910
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step] {
1911
+ grid-area: 1 / 1;
1912
+ }
1913
+ .sq-js [data-blocks] [data-contact-form="steps"]:not([data-ready]) [data-cf-step]:not(:first-child) {
1914
+ visibility: hidden;
1915
+ animation: cf-unstack 0s 3s forwards;
1916
+ }
1917
+ @keyframes cf-unstack {
1918
+ to { visibility: visible; grid-area: auto; }
1919
+ }
1920
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step] {
1921
+ transition:
1922
+ opacity 0.45s ease 0.1s,
1923
+ transform 0.8s cubic-bezier(0.16, 1, 0.3, 1),
1924
+ visibility 0s;
1925
+ }
1926
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step]:not([data-pos="current"]) {
1927
+ opacity: 0;
1928
+ visibility: hidden;
1929
+ transition:
1930
+ opacity 0.25s ease,
1931
+ transform 0.6s cubic-bezier(0.16, 1, 0.3, 1),
1932
+ visibility 0s 0.6s;
1933
+ }
1934
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step][data-pos="before"] {
1935
+ transform: translateY(-2.5rem);
1936
+ }
1937
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step][data-pos="after"] {
1938
+ transform: translateY(2.5rem);
1939
+ }
1940
+ [data-blocks] [data-contact-form="steps"] [data-cf-grow] {
1941
+ line-height: 1.15;
1942
+ }
1943
+
1944
+ /* Reduced motion: the same states as cuts and short fades, nothing travels. */
1945
+ @media (prefers-reduced-motion: reduce) {
1946
+ [data-blocks] [data-contact-form] [data-cf-rule]::after {
1947
+ transform: none;
1948
+ opacity: 0;
1949
+ transition: opacity 0.15s ease;
1950
+ }
1951
+ [data-blocks] [data-contact-form] [data-cf-grow]:focus-within [data-cf-rule]::after {
1952
+ transform: none;
1953
+ opacity: 1;
1954
+ }
1955
+ [data-blocks] [data-contact-form="lines"] [data-cf-float] > label,
1956
+ [data-blocks] [data-contact-form] [data-cf-radio]::before,
1957
+ [data-blocks] [data-contact-form] [data-cf-arrow] svg,
1958
+ [data-blocks] [data-contact-form="steps"] [data-cf-fill] {
1959
+ transition: none;
1960
+ }
1961
+ [data-blocks] [data-contact-form] button:is(:hover, :focus-visible) [data-cf-arrow] svg {
1962
+ transform: none;
1963
+ }
1964
+ [data-blocks] [data-contact-form][data-state="sending"] [data-cf-send] [data-cf-arrow] svg {
1965
+ animation: none;
1966
+ opacity: 0.45;
1967
+ }
1968
+ [data-blocks] [data-contact-form][data-state="sent"] [data-cf-form],
1969
+ [data-blocks] [data-contact-form] [data-cf-thanks] :is([data-cf-rise], [data-cf-soft]),
1970
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step][data-pos] {
1971
+ transform: none;
1972
+ }
1973
+ [data-blocks] [data-contact-form] [data-cf-thanks] :is([data-cf-rise], [data-cf-soft]) {
1974
+ transition: none;
1975
+ }
1976
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step] {
1977
+ transition:
1978
+ opacity 0.15s ease,
1979
+ visibility 0s;
1980
+ }
1981
+ [data-blocks] [data-contact-form="steps"][data-ready] [data-cf-step]:not([data-pos="current"]) {
1982
+ transition:
1983
+ opacity 0.15s ease,
1984
+ visibility 0s 0.15s;
1985
+ }
1986
+ }
1987
+
1988
+ /* --- hero-index variants --- */
1989
+ /* venster: the names stand on the page ground in the ink, their place lines in the muted ink; the photograph in its
1990
+ window carries no veil, since no name stands on it. */
1991
+ [data-blocks] [data-hero-index][data-variant="venster"] [data-veil] {
1992
+ display: none;
1993
+ }
1994
+ [data-blocks] [data-hero-index][data-variant="venster"] :is([data-list], [data-foot]) {
1995
+ color: var(--ink);
1996
+ }
1997
+ [data-blocks] [data-hero-index][data-variant="venster"] [data-meta] {
1998
+ color: var(--ink-muted);
1999
+ }
2000
+
2001
+ /* --- hero-split variants --- */
2002
+ /* opening: on wide screens with motion and scripts, from the first paint until the script writes the window, the
2003
+ photograph waits as a strip at the outer edge (16% of the screen out of its 58%, the photo behind it at the cell's
2004
+ full width) and the headline's copies are clipped to that seam. Without scripts it is the split; if the script
2005
+ never runs the strip stays, a whole composition with nothing hidden. */
2006
+ @media (min-width: 768px) and (prefers-reduced-motion: no-preference) {
2007
+ :is(.js, .sq-js) [data-blocks] [data-hero-split][data-variant="opening"]:not([data-ready]) [data-photo-box] {
2008
+ width: 27.5862%;
2009
+ }
2010
+ :is(.js, .sq-js) [data-blocks] [data-hero-split][data-variant="opening"]:not([data-ready]) [data-drift] {
2011
+ width: 362.5%;
2012
+ }
2013
+ :is(.js, .sq-js) [data-blocks] [data-hero-split][data-variant="opening"][data-photo="right"]:not([data-ready]) [data-copy="ground"] {
2014
+ clip-path: inset(0 16% 0 0) !important;
2015
+ }
2016
+ :is(.js, .sq-js) [data-blocks] [data-hero-split][data-variant="opening"][data-photo="right"]:not([data-ready]) [data-copy="photo"] {
2017
+ clip-path: inset(0 0 0 84%) !important;
2018
+ }
2019
+ :is(.js, .sq-js) [data-blocks] [data-hero-split][data-variant="opening"][data-photo="left"]:not([data-ready]) [data-copy="ground"] {
2020
+ clip-path: inset(0 0 0 16%) !important;
2021
+ }
2022
+ :is(.js, .sq-js) [data-blocks] [data-hero-split][data-variant="opening"][data-photo="left"]:not([data-ready]) [data-copy="photo"] {
2023
+ clip-path: inset(0 84% 0 0) !important;
2024
+ }
2025
+ }
2026
+
2027
+ /* --- about-studio --- */
2028
+ /* Display type at a light weight, whatever the pairing asks. Each fact's rule is drawn by the script through
2029
+ --as-draw (0 to 1, plain numbers); without it the rule stands whole. doorkijk's window width is shared by the frame
2030
+ and the grid track it sits in. With motion and scripts the statement waits unseen until it is cut into lines, the
2031
+ copy and the photographs wait clear (a photograph until it is decoded), and all of it arrives after 3s if the script
2032
+ never runs. */
2033
+ [data-blocks] [data-about-studio] .font-display {
2034
+ font-weight: min(var(--display-weight, 300), 300);
2035
+ }
2036
+ [data-blocks] [data-about-studio] [data-as-rule]::before {
2037
+ content: "";
2038
+ position: absolute;
2039
+ inset-inline: 0;
2040
+ top: 0;
2041
+ height: 1px;
2042
+ background: var(--line);
2043
+ clip-path: inset(0 calc((1 - var(--as-draw, 1)) * 100%) 0 0);
2044
+ }
2045
+ [data-blocks] [data-about-studio="doorkijk"] {
2046
+ --dk-w: min(62vw, 34svh);
2047
+ }
2048
+ @media (min-width: 768px) {
2049
+ [data-blocks] [data-about-studio="doorkijk"] {
2050
+ --dk-w: min(34vw, 54svh);
2051
+ }
2052
+ }
2053
+ @media (prefers-reduced-motion: no-preference) {
2054
+ [data-blocks] [data-about-studio] :is([data-as-layer], [data-as-col="1"]) {
2055
+ will-change: transform;
2056
+ }
2057
+ :is(.js, .sq-js) [data-blocks] [data-about-studio]:not([data-ready]) [data-as-head] {
2058
+ visibility: hidden;
2059
+ animation: struq-show 0s 3s forwards;
2060
+ }
2061
+ :is(.js, .sq-js) [data-blocks] [data-about-studio]:not([data-ready]) :is([data-as-soft], [data-as-photo]) {
2062
+ opacity: 0;
2063
+ animation: struq-fade 0s 3s forwards;
2064
+ }
2065
+ }
2066
+
2067
+ /* --- approach --- */
2068
+ /* Display type at a light weight. Once the script runs (data-stacked) the notes of zin and schuif share one cell, so
2069
+ only the current one stands; without it they read as a plain list. schuif's later photographs wait shut, so without
2070
+ scripts the first one stands alone. With motion and scripts the titles and names wait unseen until they are cut into
2071
+ lines, draad's line, ticks and copy wait clear, zin's notes and schuif's photographs wait unseen, and all of it
2072
+ arrives after 3s if the script never runs. */
2073
+ [data-blocks] [data-approach] .font-display {
2074
+ font-weight: min(var(--display-weight, 300), 300);
2075
+ }
2076
+ [data-blocks] [data-approach][data-stacked] [data-ap-notes] > * {
2077
+ grid-area: 1 / 1;
2078
+ }
2079
+ [data-blocks] [data-approach="schuif"] [data-ap-layer] + [data-ap-layer] {
2080
+ clip-path: inset(0 0 0 100%);
2081
+ }
2082
+ @media (prefers-reduced-motion: no-preference) {
2083
+ [data-blocks] [data-approach="schuif"] [data-ap-handle] {
2084
+ will-change: transform;
2085
+ }
2086
+ :is(.js, .sq-js) [data-blocks] [data-approach]:not([data-ready]) :is([data-ap-title], [data-ap-head]) {
2087
+ visibility: hidden;
2088
+ animation: struq-show 0s 3s forwards;
2089
+ }
2090
+ :is(.js, .sq-js) [data-blocks] [data-approach="draad"]:not([data-ready]) :is([data-ap-soft], [data-ap-ink], [data-ap-tick]) {
2091
+ opacity: 0;
2092
+ animation: struq-fade 0s 3s forwards;
2093
+ }
2094
+ :is(.js, .sq-js) [data-blocks] [data-approach="zin"]:not([data-ready]) [data-ap-notes] {
2095
+ visibility: hidden;
2096
+ animation: struq-show 0s 3s forwards;
2097
+ }
2098
+ :is(.js, .sq-js) [data-blocks] [data-approach="schuif"]:not([data-ready]) [data-ap-layer] {
2099
+ opacity: 0;
2100
+ animation: struq-fade 0s 3s forwards;
2101
+ }
2102
+ }