@fluid-theme/core 0.4.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -29
- package/CHANGELOG.md +543 -2
- package/CLAUDE.md +193 -14
- package/GEMINI.md +6 -45
- package/README.md +17 -8
- package/assets/js/compare.js +77 -0
- package/assets/js/fit-text.js +103 -0
- package/assets/js/lightbox.js +153 -0
- package/assets/js/scroll-reveal.js +72 -0
- package/assets/js/toc-build.mjs +86 -0
- package/assets/js/toc.js +384 -0
- package/dist/fluid-1x.min.css +4 -0
- package/dist/fluid-fonts.min.css +2 -0
- package/dist/fluid-legacy-1x.css +3 -0
- package/dist/fluid-nav.min.css +3 -0
- package/dist/fluid-toc.min.css +3 -0
- package/dist/fluid-zero.min.css +3 -0
- package/dist/fluid.min.css +3 -2
- package/dist/fonts/OFL-newsreader.txt +93 -0
- package/dist/fonts/OFL-schibsted-grotesk.txt +93 -0
- package/dist/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/dist/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/dist/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
- package/fluid-agent-kit/README.md +1 -0
- package/fluid-agent-kit/skills/accessibility.md +3 -1
- package/fluid-agent-kit/skills/css-conventions.md +22 -5
- package/fluid-agent-kit/skills/image-optimization.md +2 -0
- package/fluid-agent-kit/skills/preset-composition.md +108 -0
- package/package.json +29 -3
- package/src/animations/scroll-reveal.css +75 -193
- package/src/components/accordion.css +6 -47
- package/src/components/alert.css +47 -88
- package/src/components/avatar.css +4 -20
- package/src/components/badge.css +25 -48
- package/src/components/button.css +69 -175
- package/src/components/card.css +146 -481
- package/src/components/chip.css +46 -12
- package/src/components/compare.css +234 -0
- package/src/components/dropdown.css +10 -67
- package/src/components/empty-state.css +4 -0
- package/src/components/form.css +113 -81
- package/src/components/gallery.css +252 -0
- package/src/components/hero.css +203 -88
- package/src/components/indicator.css +23 -117
- package/src/components/loading.css +7 -29
- package/src/components/modal.css +12 -65
- package/src/components/nav-modes.css +261 -0
- package/src/components/nav.css +324 -392
- package/src/components/search-input.css +4 -2
- package/src/components/step-indicator.css +26 -27
- package/src/components/table.css +64 -171
- package/src/components/toc.css +254 -0
- package/src/components/tooltip.css +5 -46
- package/src/core/base.css +393 -547
- package/src/core/reset.css +17 -5
- package/src/core/tokens.css +426 -2276
- package/src/fluid.css +258 -17
- package/src/fonts/OFL-newsreader.txt +93 -0
- package/src/fonts/OFL-schibsted-grotesk.txt +93 -0
- package/src/fonts/fluid-fonts.css +55 -0
- package/src/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/src/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/src/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
- package/src/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
- package/src/layouts/positioning.css +6 -3
- package/src/legacy-1x.css +61 -0
- package/src/sections/cta.css +33 -101
- package/src/sections/features.css +42 -219
- package/src/sections/footer.css +53 -134
- package/src/sections/stats.css +40 -153
- package/src/sections/team.css +24 -113
- package/src/sections/testimonials.css +34 -148
- package/src/zero/base.css +166 -0
- package/src/zero/tokens.css +64 -0
- package/src/zero.css +20 -0
- package/src/components/scroll-progress.css +0 -104
- package/src/components/settings-panel.css +0 -603
- package/src/components/themed-images.css +0 -229
- package/src/components/version-badge.css +0 -70
package/src/components/form.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fluid Framework - Form Components
|
|
3
|
-
* @layer components
|
|
3
|
+
* @layer components (the honeypot and conditional fields: @layer utilities)
|
|
4
4
|
*
|
|
5
5
|
* Forms should feel welcoming, not like paperwork.
|
|
6
6
|
* Clear labels, gentle feedback, graceful validation.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - Toggle switches
|
|
12
12
|
* - Input groups and addons
|
|
13
13
|
* - Form validation states
|
|
14
|
+
* - Honeypot and conditional fields
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
@layer components {
|
|
@@ -49,8 +50,8 @@
|
|
|
49
50
|
|
|
50
51
|
/* Label styling */
|
|
51
52
|
.form-label {
|
|
52
|
-
font-size: var(--
|
|
53
|
-
font-weight: var(--weight
|
|
53
|
+
font-size: var(--step--1);
|
|
54
|
+
font-weight: calc(600 + var(--_dark-weight));
|
|
54
55
|
color: var(--color-text);
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -62,14 +63,14 @@
|
|
|
62
63
|
|
|
63
64
|
/* Help text */
|
|
64
65
|
.form-help {
|
|
65
|
-
font-size: var(--
|
|
66
|
+
font-size: var(--step--1);
|
|
66
67
|
color: var(--color-text-muted);
|
|
67
68
|
margin-block-start: var(--space-1);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/* Error message */
|
|
71
72
|
.form-error {
|
|
72
|
-
font-size: var(--
|
|
73
|
+
font-size: var(--step--1);
|
|
73
74
|
color: var(--color-error);
|
|
74
75
|
margin-block-start: var(--space-1);
|
|
75
76
|
}
|
|
@@ -94,14 +95,14 @@
|
|
|
94
95
|
|
|
95
96
|
.input-group > input:first-child,
|
|
96
97
|
.input-group > select:first-child {
|
|
97
|
-
border-start-start-radius: var(--radius-
|
|
98
|
-
border-end-start-radius: var(--radius-
|
|
98
|
+
border-start-start-radius: var(--radius-control);
|
|
99
|
+
border-end-start-radius: var(--radius-control);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
.input-group > input:last-child,
|
|
102
103
|
.input-group > select:last-child {
|
|
103
|
-
border-start-end-radius: var(--radius-
|
|
104
|
-
border-end-end-radius: var(--radius-
|
|
104
|
+
border-start-end-radius: var(--radius-control);
|
|
105
|
+
border-end-end-radius: var(--radius-control);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
.input-group__addon {
|
|
@@ -111,19 +112,19 @@
|
|
|
111
112
|
font-size: var(--font-size-sm);
|
|
112
113
|
color: var(--color-text-muted);
|
|
113
114
|
background-color: var(--color-surface);
|
|
114
|
-
border:
|
|
115
|
+
border: var(--border-width) solid var(--color-border-strong);
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
.input-group__addon:first-child {
|
|
118
119
|
border-inline-end: none;
|
|
119
|
-
border-start-start-radius: var(--radius-
|
|
120
|
-
border-end-start-radius: var(--radius-
|
|
120
|
+
border-start-start-radius: var(--radius-control);
|
|
121
|
+
border-end-start-radius: var(--radius-control);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
.input-group__addon:last-child {
|
|
124
125
|
border-inline-start: none;
|
|
125
|
-
border-start-end-radius: var(--radius-
|
|
126
|
-
border-end-end-radius: var(--radius-
|
|
126
|
+
border-start-end-radius: var(--radius-control);
|
|
127
|
+
border-end-end-radius: var(--radius-control);
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
|
|
@@ -152,16 +153,15 @@
|
|
|
152
153
|
flex-shrink: 0;
|
|
153
154
|
inline-size: 1.25rem;
|
|
154
155
|
block-size: 1.25rem;
|
|
155
|
-
border: var(--border-width-thick) solid var(--color-border);
|
|
156
|
+
border: var(--border-width-thick) solid var(--color-border-strong);
|
|
156
157
|
border-radius: var(--radius-sm);
|
|
157
158
|
background-color: var(--color-background);
|
|
158
159
|
display: flex;
|
|
159
160
|
align-items: center;
|
|
160
161
|
justify-content: center;
|
|
161
162
|
transition:
|
|
162
|
-
background-color var(--duration-
|
|
163
|
-
border-color var(--duration-
|
|
164
|
-
transform var(--duration-fast) var(--ease-settle);
|
|
163
|
+
background-color var(--duration-fast) var(--ease-fluid),
|
|
164
|
+
border-color var(--duration-fast) var(--ease-fluid);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/* Checkmark icon */
|
|
@@ -169,8 +169,8 @@
|
|
|
169
169
|
content: "";
|
|
170
170
|
inline-size: 0.5rem;
|
|
171
171
|
block-size: 0.25rem;
|
|
172
|
-
border-inline-start:
|
|
173
|
-
border-block-end:
|
|
172
|
+
border-inline-start: var(--border-width-thick) solid var(--on-accent);
|
|
173
|
+
border-block-end: var(--border-width-thick) solid var(--on-accent);
|
|
174
174
|
transform: rotate(-45deg) scale(0);
|
|
175
175
|
transition: transform var(--duration-fast) var(--ease-settle);
|
|
176
176
|
}
|
|
@@ -182,7 +182,8 @@
|
|
|
182
182
|
|
|
183
183
|
/* Focus state */
|
|
184
184
|
.checkbox__input:focus-visible + .checkbox__box {
|
|
185
|
-
|
|
185
|
+
outline: var(--focus-ring-width) solid var(--color-focus);
|
|
186
|
+
outline-offset: 2px;
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
/* Checked state */
|
|
@@ -244,7 +245,7 @@
|
|
|
244
245
|
flex-shrink: 0;
|
|
245
246
|
inline-size: 1.25rem;
|
|
246
247
|
block-size: 1.25rem;
|
|
247
|
-
border: var(--border-width-thick) solid var(--color-border);
|
|
248
|
+
border: var(--border-width-thick) solid var(--color-border-strong);
|
|
248
249
|
border-radius: var(--radius-full);
|
|
249
250
|
background-color: var(--color-background);
|
|
250
251
|
display: flex;
|
|
@@ -269,7 +270,8 @@
|
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
.radio__input:focus-visible + .radio__circle {
|
|
272
|
-
|
|
273
|
+
outline: var(--focus-ring-width) solid var(--color-focus);
|
|
274
|
+
outline-offset: 2px;
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
.radio__input:checked + .radio__circle {
|
|
@@ -330,43 +332,45 @@
|
|
|
330
332
|
block-size: 0;
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
/* Square track and thumb (the radius knob); the thumb moves along the
|
|
336
|
+
inline axis with inset-inline-start, so RTL works. */
|
|
333
337
|
.switch__track {
|
|
334
338
|
position: relative;
|
|
339
|
+
flex-shrink: 0;
|
|
335
340
|
inline-size: 2.75rem;
|
|
336
341
|
block-size: 1.5rem;
|
|
337
|
-
background-color: var(--color-
|
|
338
|
-
border
|
|
339
|
-
|
|
342
|
+
background-color: var(--color-background);
|
|
343
|
+
border: var(--border-width) solid var(--color-border-strong);
|
|
344
|
+
border-radius: var(--radius-control);
|
|
345
|
+
transition: background-color var(--duration-fast) var(--ease-fluid);
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
.switch__thumb {
|
|
343
349
|
position: absolute;
|
|
344
350
|
inset-block-start: 0.125rem;
|
|
345
351
|
inset-inline-start: 0.125rem;
|
|
346
|
-
inline-size: 1.25rem;
|
|
347
|
-
block-size: 1.25rem;
|
|
348
|
-
background-color: var(--
|
|
349
|
-
border-radius: var(--radius-
|
|
350
|
-
box-shadow: var(--shadow-sm);
|
|
352
|
+
inline-size: calc(1.25rem - 2 * var(--border-width));
|
|
353
|
+
block-size: calc(1.25rem - 2 * var(--border-width));
|
|
354
|
+
background-color: var(--color-text);
|
|
355
|
+
border-radius: var(--radius-control);
|
|
351
356
|
transition:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
.switch:hover .switch__track {
|
|
357
|
-
background-color: var(--color-text-subtle);
|
|
357
|
+
inset-inline-start var(--duration-normal) var(--ease-settle),
|
|
358
|
+
background-color var(--duration-fast) var(--ease-fluid);
|
|
358
359
|
}
|
|
359
360
|
|
|
360
361
|
.switch__input:focus-visible + .switch__track {
|
|
361
|
-
|
|
362
|
+
outline: var(--focus-ring-width) solid var(--color-focus);
|
|
363
|
+
outline-offset: 2px;
|
|
362
364
|
}
|
|
363
365
|
|
|
364
366
|
.switch__input:checked + .switch__track {
|
|
365
367
|
background-color: var(--accent);
|
|
368
|
+
border-color: var(--accent);
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
.switch__input:checked + .switch__track .switch__thumb {
|
|
369
|
-
|
|
372
|
+
inset-inline-start: calc(100% - 1.25rem + 2 * var(--border-width) - 0.125rem);
|
|
373
|
+
background-color: var(--on-accent);
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
.switch__input:disabled + .switch__track {
|
|
@@ -395,7 +399,7 @@
|
|
|
395
399
|
}
|
|
396
400
|
|
|
397
401
|
.switch[data-size="sm"] .switch__input:checked + .switch__track .switch__thumb {
|
|
398
|
-
|
|
402
|
+
inset-inline-start: calc(100% - 0.875rem - 0.125rem);
|
|
399
403
|
}
|
|
400
404
|
|
|
401
405
|
|
|
@@ -414,12 +418,6 @@
|
|
|
414
418
|
border-color: var(--color-success);
|
|
415
419
|
}
|
|
416
420
|
|
|
417
|
-
input[data-valid]:focus,
|
|
418
|
-
textarea[data-valid]:focus,
|
|
419
|
-
select[data-valid]:focus {
|
|
420
|
-
box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-success) 20%, transparent);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
421
|
/* Invalid/Error state */
|
|
424
422
|
input[data-invalid],
|
|
425
423
|
textarea[data-invalid],
|
|
@@ -427,25 +425,28 @@
|
|
|
427
425
|
input[aria-invalid="true"],
|
|
428
426
|
textarea[aria-invalid="true"],
|
|
429
427
|
select[aria-invalid="true"],
|
|
430
|
-
input:invalid:not(:placeholder-shown),
|
|
431
|
-
textarea:invalid:not(:placeholder-shown),
|
|
432
428
|
.form-group[data-invalid] input,
|
|
433
429
|
.form-group[data-invalid] textarea,
|
|
434
430
|
.form-group[data-invalid] select {
|
|
435
431
|
border-color: var(--color-error);
|
|
432
|
+
border-inline-start-width: calc(var(--border-width) * 4);
|
|
436
433
|
}
|
|
437
434
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
select
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-error) 20%, transparent);
|
|
435
|
+
/* Browser validation shows only after someone has edited or submitted the
|
|
436
|
+
field (:user-invalid). The old :invalid:not(:placeholder-shown) only
|
|
437
|
+
waited on fields with a placeholder, so an empty required field without
|
|
438
|
+
one was red on page load (the Fluid Stack's sign-up form, 2026-10-09).
|
|
439
|
+
Separate rules: a browser without :user-invalid drops only these. */
|
|
440
|
+
:is(input, textarea, select):user-invalid {
|
|
441
|
+
border-color: var(--color-error);
|
|
442
|
+
border-inline-start-width: calc(var(--border-width) * 4);
|
|
447
443
|
}
|
|
448
444
|
|
|
445
|
+
/* An invalid field says so twice: the error colour and a heavy start
|
|
446
|
+
edge, so the state survives where the colour cannot be seen (on an
|
|
447
|
+
accent-strong band --color-error is --on-accent, the resting edge's
|
|
448
|
+
colour too). Focus adds the base outline (base.css) on top. */
|
|
449
|
+
|
|
449
450
|
|
|
450
451
|
/* ========================================
|
|
451
452
|
FORM LAYOUT
|
|
@@ -463,7 +464,7 @@
|
|
|
463
464
|
gap: var(--space-3);
|
|
464
465
|
margin-block-start: var(--space-6);
|
|
465
466
|
padding-block-start: var(--space-6);
|
|
466
|
-
border-block-start:
|
|
467
|
+
border-block-start: var(--border-width) solid var(--color-border);
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
.form-actions[data-align="end"] {
|
|
@@ -475,34 +476,65 @@
|
|
|
475
476
|
}
|
|
476
477
|
|
|
477
478
|
|
|
479
|
+
/* FIELDSET: .form-fieldset is the base fieldset (no box, a top hairline,
|
|
480
|
+
the legend in the label voice; base.css). */
|
|
481
|
+
|
|
482
|
+
|
|
478
483
|
/* ========================================
|
|
479
|
-
|
|
480
|
-
Grouped form sections
|
|
484
|
+
REDUCED MOTION
|
|
481
485
|
======================================== */
|
|
482
486
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
padding: var(--space-5);
|
|
487
|
-
margin-block-end: var(--space-6);
|
|
488
|
-
}
|
|
487
|
+
/* Accessibility: reduced motion */
|
|
488
|
+
/* Reduced motion: the reset shortens every transition. */
|
|
489
|
+
}
|
|
489
490
|
|
|
490
|
-
fieldset.form-fieldset legend {
|
|
491
|
-
font-size: var(--font-size-sm);
|
|
492
|
-
font-weight: var(--weight-semibold);
|
|
493
|
-
padding-inline: var(--space-2);
|
|
494
|
-
color: var(--color-text);
|
|
495
|
-
}
|
|
496
491
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
492
|
+
/* ========================================
|
|
493
|
+
HONEYPOT AND CONDITIONAL FIELDS
|
|
494
|
+
Functional rules the day-one forms depend on (the-fluid-stack
|
|
495
|
+
playbook forms.md D26c, security.md "Forms"). In @layer utilities,
|
|
496
|
+
like .sr-only, so they outrank a child theme in @layer themes (a
|
|
497
|
+
child theme outside a layer, or with !important, still wins).
|
|
498
|
+
======================================== */
|
|
499
|
+
@layer utilities {
|
|
500
|
+
/* <div class="form-group hp" aria-hidden="true"> around
|
|
501
|
+
<input name="website" tabindex="-1" autocomplete="off">, last in the
|
|
502
|
+
form. Clipped to a 1px box where it stands, like .sr-only. A -10000px
|
|
503
|
+
offset scrolled the page sideways: a physical one on any RTL page, a
|
|
504
|
+
logical (inline-start) one whenever the form's direction differed
|
|
505
|
+
from the page's. display: block stops a theme's display: contents
|
|
506
|
+
from undoing it. */
|
|
507
|
+
.hp {
|
|
508
|
+
display: block;
|
|
509
|
+
position: absolute;
|
|
510
|
+
inline-size: 1px;
|
|
511
|
+
block-size: 1px;
|
|
512
|
+
margin: -1px;
|
|
513
|
+
padding: 0;
|
|
514
|
+
overflow: hidden;
|
|
515
|
+
clip-path: inset(50%);
|
|
516
|
+
white-space: nowrap;
|
|
517
|
+
border: 0;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* A field with data-show-if="<reason>" (space-separated for several)
|
|
521
|
+
shows only while its form's select[name="reason"] has that value
|
|
522
|
+
(forms.md pattern 7). No script. revert-layer hands display back to
|
|
523
|
+
the component or child theme. Without :has() every field shows, which
|
|
524
|
+
is safe. A field this rule hides must never block the submit: never
|
|
525
|
+
required, and no constraint a hidden value can fail (no type=number,
|
|
526
|
+
email or url, pattern, min, max, step or minlength; use type="text"
|
|
527
|
+
inputmode="numeric" and check it on the server, forms.md pattern 2).
|
|
528
|
+
Option values must be these exact lowercase words. */
|
|
529
|
+
@supports selector(:has(*)) {
|
|
530
|
+
form:has(select[name="reason"]) [data-show-if] { display: none; }
|
|
531
|
+
form:has(select[name="reason"] option[value="advertise"]:checked) [data-show-if~="advertise"],
|
|
532
|
+
form:has(select[name="reason"] option[value="partner"]:checked) [data-show-if~="partner"],
|
|
533
|
+
form:has(select[name="reason"] option[value="buy-site"]:checked) [data-show-if~="buy-site"],
|
|
534
|
+
form:has(select[name="reason"] option[value="buy-domain"]:checked) [data-show-if~="buy-domain"],
|
|
535
|
+
form:has(select[name="reason"] option[value="hire"]:checked) [data-show-if~="hire"],
|
|
536
|
+
form:has(select[name="reason"] option[value="other"]:checked) [data-show-if~="other"] {
|
|
537
|
+
display: revert-layer;
|
|
506
538
|
}
|
|
507
539
|
}
|
|
508
540
|
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluid Framework - Gallery + Lightbox
|
|
3
|
+
* @layer components
|
|
4
|
+
*
|
|
5
|
+
* A grid of photos for portfolios (architects, interior designers, landscape
|
|
6
|
+
* architects, builders, photographers) and a full-screen viewer.
|
|
7
|
+
*
|
|
8
|
+
* Progressive enhancement: without JavaScript every thumbnail is a plain link
|
|
9
|
+
* to the full-size image. With assets/js/lightbox.js the links open in a
|
|
10
|
+
* native <dialog> with previous / next, arrow keys, Escape, swipe, a counter
|
|
11
|
+
* and the caption, and focus returns to the thumbnail on close.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* <ul class="gallery" role="list" data-lightbox>
|
|
15
|
+
* <li>
|
|
16
|
+
* <figure class="gallery__figure">
|
|
17
|
+
* <a class="gallery__link" href="photos/kitchen-2400.jpg">
|
|
18
|
+
* <img class="gallery__image" src="photos/kitchen-800.jpg" alt="Kitchen with walnut island"
|
|
19
|
+
* width="800" height="600" loading="lazy">
|
|
20
|
+
* </a>
|
|
21
|
+
* <figcaption class="gallery__caption">Kitchen · Photo: A. Smith</figcaption>
|
|
22
|
+
* </figure>
|
|
23
|
+
* </li>
|
|
24
|
+
* </ul>
|
|
25
|
+
* <script src="assets/js/lightbox.js" defer></script>
|
|
26
|
+
*
|
|
27
|
+
* Options on .gallery:
|
|
28
|
+
* data-aspect="square|landscape|portrait|video" crop of the thumbnails (default: natural ratio)
|
|
29
|
+
* data-size="sm|lg" thumbnail width (default 24rem: three across an
|
|
30
|
+
* 80rem column, a body of work, not a catalogue;
|
|
31
|
+
* --gallery-min on :root sets it, sm 11rem, lg 32rem)
|
|
32
|
+
* data-captions="overlay" captions over the image instead of below
|
|
33
|
+
* Per image: style="--focus-x: 30%; --focus-y: 60%" keeps the subject in the crop.
|
|
34
|
+
* Captions: write dimensions with no-break spaces (40 × 50 cm), so
|
|
35
|
+
* they never break inside.
|
|
36
|
+
*
|
|
37
|
+
* The hang line: each item spans two rows of a subgrid with its image at
|
|
38
|
+
* the bottom of the first, so image bottoms and caption tops share one
|
|
39
|
+
* line across a row, whatever each image's ratio.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
@layer components {
|
|
43
|
+
/* ========================================
|
|
44
|
+
GRID
|
|
45
|
+
======================================== */
|
|
46
|
+
|
|
47
|
+
.gallery {
|
|
48
|
+
--_gallery-min: var(--gallery-min, 24rem);
|
|
49
|
+
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: repeat(auto-fill, minmax(min(var(--_gallery-min), 100%), 1fr));
|
|
52
|
+
gap: var(--space-m);
|
|
53
|
+
list-style: none;
|
|
54
|
+
padding: 0;
|
|
55
|
+
margin: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.gallery[data-size="sm"] { --_gallery-min: 11rem; }
|
|
59
|
+
.gallery[data-size="lg"] { --_gallery-min: 32rem; }
|
|
60
|
+
|
|
61
|
+
.gallery[data-aspect="square"] { --_gallery-aspect: 1; }
|
|
62
|
+
.gallery[data-aspect="landscape"] { --_gallery-aspect: 4 / 3; }
|
|
63
|
+
.gallery[data-aspect="portrait"] { --_gallery-aspect: 3 / 4; }
|
|
64
|
+
.gallery[data-aspect="video"] { --_gallery-aspect: 16 / 9; }
|
|
65
|
+
|
|
66
|
+
.gallery > *,
|
|
67
|
+
.gallery > * > .gallery__figure {
|
|
68
|
+
display: grid;
|
|
69
|
+
grid-row: span 2;
|
|
70
|
+
grid-template-rows: subgrid;
|
|
71
|
+
row-gap: var(--space-xs);
|
|
72
|
+
min-inline-size: 0;
|
|
73
|
+
margin: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* The hang line: the image (or its link) sits at the bottom of row one. */
|
|
77
|
+
.gallery__figure > :is(.gallery__image, picture) { align-self: end; }
|
|
78
|
+
|
|
79
|
+
/* Firefox sizes the grid too tall when a bare <img> is a subgrid item
|
|
80
|
+
(each row +142px at 390px once the images load; Firefox 157). Those
|
|
81
|
+
figures leave the subgrid there; a link or <picture> wrapper keeps it. */
|
|
82
|
+
@supports (-moz-appearance: none) {
|
|
83
|
+
.gallery > :has(> .gallery__figure > img) { display: block; }
|
|
84
|
+
.gallery__figure:has(> img) { block-size: 100%; grid-template-rows: 1fr auto; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.gallery__link {
|
|
88
|
+
display: block;
|
|
89
|
+
align-self: end;
|
|
90
|
+
background: var(--color-surface);
|
|
91
|
+
transition: opacity var(--duration-fast) var(--ease-fluid);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.gallery__link:hover { opacity: 0.9; }
|
|
95
|
+
|
|
96
|
+
.gallery__link:focus-visible {
|
|
97
|
+
outline: 3px solid var(--color-focus);
|
|
98
|
+
outline-offset: 2px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.gallery__image {
|
|
102
|
+
display: block;
|
|
103
|
+
inline-size: 100%;
|
|
104
|
+
block-size: auto;
|
|
105
|
+
object-fit: cover;
|
|
106
|
+
object-position: var(--focus-x, 50%) var(--focus-y, 50%);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Only a crop sets the ratio: a bare `auto` here replaced the width and
|
|
110
|
+
height attributes' ratio, so a lazy image was only as tall as its alt
|
|
111
|
+
text until it loaded, and the page grew under the reader. */
|
|
112
|
+
.gallery[data-aspect] .gallery__image { aspect-ratio: var(--_gallery-aspect); }
|
|
113
|
+
|
|
114
|
+
.gallery__caption {
|
|
115
|
+
margin: 0;
|
|
116
|
+
overflow-wrap: anywhere;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Captions over the image */
|
|
120
|
+
.gallery[data-captions="overlay"] .gallery__figure { position: relative; }
|
|
121
|
+
|
|
122
|
+
.gallery[data-captions="overlay"] .gallery__caption {
|
|
123
|
+
position: absolute;
|
|
124
|
+
inset-inline: 0;
|
|
125
|
+
inset-block-end: 0;
|
|
126
|
+
padding: var(--space-6) var(--space-3) var(--space-3);
|
|
127
|
+
background: linear-gradient(to top, oklch(0% 0 0 / 0.75), transparent);
|
|
128
|
+
color: oklch(100% 0 0);
|
|
129
|
+
max-inline-size: none;
|
|
130
|
+
pointer-events: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
/* ========================================
|
|
135
|
+
LIGHTBOX (built by assets/js/lightbox.js)
|
|
136
|
+
======================================== */
|
|
137
|
+
|
|
138
|
+
.lightbox {
|
|
139
|
+
border: none;
|
|
140
|
+
padding: 0;
|
|
141
|
+
margin: 0;
|
|
142
|
+
max-inline-size: none;
|
|
143
|
+
max-block-size: none;
|
|
144
|
+
inline-size: 100%;
|
|
145
|
+
block-size: 100%;
|
|
146
|
+
background: oklch(12% 0 0 / 0.96);
|
|
147
|
+
color: oklch(96% 0 0);
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.lightbox::backdrop {
|
|
152
|
+
background: oklch(0% 0 0 / 0.6);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.lightbox[open] {
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-rows: auto 1fr auto;
|
|
158
|
+
grid-template-areas: "bar" "stage" "caption";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.lightbox__bar {
|
|
162
|
+
grid-area: bar;
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: space-between;
|
|
166
|
+
gap: var(--space-3);
|
|
167
|
+
padding: var(--space-3) var(--space-4);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.lightbox__counter {
|
|
171
|
+
font-size: var(--font-size-sm);
|
|
172
|
+
font-variant-numeric: tabular-nums;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.lightbox__stage {
|
|
176
|
+
grid-area: stage;
|
|
177
|
+
position: relative;
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
justify-content: center;
|
|
181
|
+
min-block-size: 0;
|
|
182
|
+
padding-inline: var(--space-3);
|
|
183
|
+
touch-action: pan-y;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.lightbox__image {
|
|
187
|
+
display: block;
|
|
188
|
+
max-inline-size: 100%;
|
|
189
|
+
max-block-size: 100%;
|
|
190
|
+
object-fit: contain;
|
|
191
|
+
border-radius: var(--radius-sm);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.lightbox__caption {
|
|
195
|
+
grid-area: caption;
|
|
196
|
+
padding: var(--space-3) var(--space-4) var(--space-5);
|
|
197
|
+
text-align: center;
|
|
198
|
+
font-size: var(--font-size-sm);
|
|
199
|
+
line-height: var(--leading-snug);
|
|
200
|
+
max-inline-size: 60ch;
|
|
201
|
+
margin-inline: auto;
|
|
202
|
+
overflow-wrap: anywhere;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.lightbox .lightbox__button {
|
|
206
|
+
display: inline-flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
min-inline-size: 2.75rem;
|
|
210
|
+
min-block-size: 2.75rem;
|
|
211
|
+
padding: var(--space-2);
|
|
212
|
+
border: var(--border-width) solid oklch(100% 0 0 / 0.35);
|
|
213
|
+
border-radius: var(--radius-full);
|
|
214
|
+
background: oklch(0% 0 0 / 0.55);
|
|
215
|
+
color: oklch(100% 0 0);
|
|
216
|
+
font-size: var(--font-size-lg);
|
|
217
|
+
line-height: 1;
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
box-shadow: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.lightbox .lightbox__button:hover {
|
|
223
|
+
background: oklch(0% 0 0 / 0.8);
|
|
224
|
+
border-color: oklch(100% 0 0 / 0.7);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.lightbox .lightbox__button:focus-visible {
|
|
228
|
+
outline: 3px solid oklch(100% 0 0);
|
|
229
|
+
outline-offset: 2px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.lightbox__prev,
|
|
233
|
+
.lightbox__next {
|
|
234
|
+
position: absolute;
|
|
235
|
+
inset-block-start: 50%;
|
|
236
|
+
translate: 0 -50%;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.lightbox__prev { inset-inline-start: var(--space-3); }
|
|
240
|
+
.lightbox__next { inset-inline-end: var(--space-3); }
|
|
241
|
+
|
|
242
|
+
.lightbox__prev[hidden],
|
|
243
|
+
.lightbox__next[hidden] {
|
|
244
|
+
display: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Keep the page still behind an open lightbox */
|
|
248
|
+
:root:has(.lightbox[open]) {
|
|
249
|
+
overflow: hidden;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
}
|