@delightstack/components 0.1.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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. package/package.json +139 -0
@@ -0,0 +1,2881 @@
1
+ <script lang="ts" module>
2
+ import type { Component } from 'svelte';
3
+ import type { CarouselItem } from './carousel';
4
+
5
+ /** A single action button that can be shown on a gallery item or in the carousel header. */
6
+ export interface GalleryItemAction {
7
+ /** The icon component to show for the action */
8
+ icon?: Component<Record<string, unknown>>;
9
+
10
+ /** The main action text - e.g. 'Download' or 'Pay now' */
11
+ name?: string;
12
+
13
+ /** A short descriptor of the action - e.g. file size or filename */
14
+ tooltip?: string;
15
+
16
+ /** The link that the button should go to */
17
+ href?: string;
18
+
19
+ /** Called when the button is clicked */
20
+ click?: (event: Event) => unknown;
21
+
22
+ /** Anchor target (only used if href is provided) */
23
+ target?: '_blank' | '_self';
24
+
25
+ /** The list of subactions (shown in a context menu) */
26
+ actions?: GalleryItemAction[];
27
+ }
28
+
29
+ export type GalleryDisplay =
30
+ | 'grid'
31
+ | 'masonry'
32
+ | 'masonry-row'
33
+ | 'list'
34
+ | 'slider'
35
+ | 'slideshow'
36
+ | 'lightbox';
37
+
38
+ /**
39
+ * Thumbnail size on the delightstack numeric scale: `'1'` is the standard,
40
+ * lower (`'0'`, `'00'`) is smaller, higher (`'2'`, `'3'`) is larger.
41
+ */
42
+ export type GallerySize = '00' | '0' | '1' | '2' | '3';
43
+
44
+ /**
45
+ * Gap between gallery items on the delightstack numeric scale: `'0'` removes
46
+ * the gap, `'1'` is the standard, `'2'`/`'3'` are progressively larger.
47
+ */
48
+ export type GallerySpacing = '0' | '1' | '2' | '3';
49
+
50
+ /**
51
+ * Corner radius of gallery items on the delightstack numeric scale: `'0'` is
52
+ * square, `'1'` is the standard, `'2'`/`'3'` are progressively rounder.
53
+ */
54
+ export type GalleryRadius = '0' | '1' | '2' | '3';
55
+
56
+ export type GalleryItem = string | (Partial<CarouselItem> & { favorite?: boolean });
57
+ </script>
58
+
59
+ <script lang="ts">
60
+ import { type TransitionConfig, fade } from 'svelte/transition';
61
+ import { circInOut } from 'svelte/easing';
62
+ import { onDestroy, onMount, untrack, type Snippet } from 'svelte';
63
+ import { SvelteSet } from 'svelte/reactivity';
64
+ import { focusTrap, intersectionObserver, ripple } from '@delightstack/utilities';
65
+
66
+ // Minimal subset of the focus-trap instance we use, declared locally so the
67
+ // 'focus-trap' package doesn't need to be a direct dep of this package.
68
+ interface FocusTrapInstance {
69
+ active: boolean;
70
+ deactivate: () => void;
71
+ }
72
+ import Button from '../actions/Button.svelte';
73
+ import List from '../display/List.svelte';
74
+ import ListItem from '../display/ListItem.svelte';
75
+ import Portal from '../actions/Portal.svelte';
76
+ import { contextMenu } from '../actions/ContextMenu.svelte';
77
+ import Carousel from './Carousel.svelte';
78
+ import {
79
+ decodeThumbHash,
80
+ getItemThumbnailSrc,
81
+ isResponsiveSrcset,
82
+ normalizeCarouselItem,
83
+ pickLargestSrc,
84
+ } from './carousel';
85
+
86
+ let {
87
+ /**
88
+ * How the gallery should be displayed - whether a grid, slideshow, etc.
89
+ *
90
+ * Use `'lightbox'` for a headless mode: Gallery renders no thumbnails of its
91
+ * own, and you provide your own trigger elements (buttons, images, cards) that
92
+ * open the carousel by setting `slide` to the desired index (`-1` keeps it
93
+ * closed). For a nice open animation from your trigger element, call the
94
+ * exported `open(index, fromElement)` method instead of setting `slide`
95
+ * directly.
96
+ */
97
+ display = 'masonry' as GalleryDisplay,
98
+
99
+ /** The size of the thumbnails in the gallery (`'00'`–`'3'`, default `'1'`) */
100
+ size = '1' as GallerySize,
101
+
102
+ /** The size of the spacing between thumbnails in the gallery (`'0'`–`'3'`, default `'1'`) */
103
+ spacing = '1' as GallerySpacing,
104
+
105
+ /** The border radius of the gallery items (`'0'`–`'3'`, default `'1'`) */
106
+ radius = '1' as GalleryRadius,
107
+
108
+ /** The currently displayed item index. -1 closes the modal/slider. */
109
+ slide = $bindable(display === 'slider' || display === 'slideshow' ? 0 : -1) as number,
110
+
111
+ /** The object-fit attribute for all items in the gallery */
112
+ fit = 'contain' as 'cover' | 'contain',
113
+
114
+ /** The list of items to display. Strings are treated as image URLs. */
115
+ items = [] as GalleryItem[],
116
+
117
+ /** The duration (in ms) between slide auto-transitions */
118
+ duration = 8000,
119
+
120
+ /** Whether the gallery should auto transition between slides */
121
+ autoplay = false,
122
+
123
+ /**
124
+ * Whether a video should start playing automatically when the modal/lightbox
125
+ * is launched onto it (e.g. clicking a video thumbnail, or `open()`). Only
126
+ * the slide the lightbox opens to auto-plays, and only when it's a video.
127
+ * Navigating/swiping between slides does NOT auto-play. Because the launch is
128
+ * a user gesture, the browser permits playback, with sound.
129
+ */
130
+ autoplay_video = false,
131
+
132
+ /** The css aspect ratio the gallery should be forced into (only when not a modal) */
133
+ aspect_ratio = undefined as string | undefined,
134
+
135
+ /** Whether the full screen button should be disabled */
136
+ disable_fullscreen = false,
137
+
138
+ /**
139
+ * Whether the gallery is 'inline' in the page - not a modal or fullscreen.
140
+ * If 'inline', vertical gestures & mouse wheel are disabled.
141
+ * @default true when display is 'slider' and not in fullscreen
142
+ */
143
+ inline = undefined as boolean | undefined,
144
+
145
+ /**
146
+ * How the slider controls should be displayed.
147
+ * - inline: the controls sit below the slideshow element
148
+ * - overlay: the controls overlay on top of the slideshow element
149
+ * - disable: the controls are not shown at all
150
+ * - default: 'inline' when the carousel is inline, 'overlay' when modal
151
+ */
152
+ controls = 'default' as 'default' | 'inline' | 'overlay' | 'disable',
153
+
154
+ /** The currently displayed page (a vertical carousel within the current slide - used for pdf pages) */
155
+ page = $bindable(0) as number,
156
+
157
+ /** The amount of pages available in the current slide (applies to PDFs) */
158
+ num_pages = $bindable(1) as number,
159
+
160
+ /** The display style of the metadata (name, description, etc) for each item */
161
+ meta_display = 'hover' as 'none' | 'always' | 'hover',
162
+
163
+ /** How file names should be displayed in the fullscreen/carousel view */
164
+ meta_display_fullscreen = 'none' as 'none' | 'always',
165
+
166
+ /** The display style of the actions (download buttons, etc) for each item */
167
+ action_display = 'hover' as 'none' | 'always' | 'hover',
168
+
169
+ /**
170
+ * The list of potential actions a user can take on each gallery item.
171
+ * Each gallery item can have multiple actions.
172
+ */
173
+ actions = [] as GalleryItemAction[][],
174
+
175
+ /**
176
+ * Snippet used to render `type: 'custom'` items in the carousel.
177
+ * Pass-through to Carousel. See Carousel's `custom` prop for the
178
+ * full signature.
179
+ */
180
+ custom = undefined as
181
+ | Snippet<
182
+ [
183
+ {
184
+ item: CarouselItem;
185
+ onload: () => void;
186
+ onerror: (err: unknown) => void;
187
+ active: boolean;
188
+ gesture_disabled: boolean;
189
+ },
190
+ ]
191
+ >
192
+ | undefined,
193
+
194
+ /**
195
+ * Called when an item's thumbnail/text is clicked.
196
+ * If the function returns false, the default behavior (opening the modal/slider) is prevented.
197
+ */
198
+ onclick = undefined as
199
+ | undefined
200
+ | ((event: MouseEvent | KeyboardEvent, index: number) => void | false),
201
+
202
+ /** The css style string added to the component from the parent */
203
+ style = '',
204
+ } = $props();
205
+
206
+ /** The percent (0-1) of how 'closed' the gallery is - while swiping/dismissing the gallery away */
207
+ let dismissing = $state(0);
208
+
209
+ /**
210
+ * Item IDs whose `<img>` was *not* loaded at mount time and therefore needs
211
+ * the fade-in transition when its `onload` eventually fires. On SSR this
212
+ * stays empty so the rendered HTML has the main image at its default
213
+ * (visible) opacity — cached images then paint instantly without waiting
214
+ * for hydration.
215
+ */
216
+ const fadingKeys = new SvelteSet<string>();
217
+ function thumbnailKey(item: { id?: string; src?: string }) {
218
+ return item.id || item.src || '';
219
+ }
220
+
221
+ /** The instance of the focus trap class - used to programmatically deactivate the focus trap */
222
+ let focusTrapInstance: FocusTrapInstance | undefined;
223
+
224
+ /** The element that the carousel item will be animated from */
225
+ let animationTarget = $state<HTMLElement | undefined>(undefined);
226
+
227
+ /** The instance of the carousel (can be used to control it) */
228
+ let carousel = $state<ReturnType<typeof Carousel> | undefined>(undefined);
229
+
230
+ /** Whether or not the gallery is being viewed in fullscreen */
231
+ let fullscreenActive = $state(false);
232
+
233
+ /** Whether the slider element is currently visible on screen */
234
+ let intersected = $state(false);
235
+
236
+ const list = $derived(
237
+ items
238
+ .map((v) => normalizeCarouselItem(v as string | Partial<CarouselItem>))
239
+ .filter((v): v is CarouselItem => !!v)
240
+ .map((item, i) => {
241
+ const original = items[i] as Partial<CarouselItem> & { favorite?: boolean };
242
+ return {
243
+ ...item,
244
+ favorite: typeof original === 'object' ? !!original?.favorite : false,
245
+ };
246
+ }),
247
+ );
248
+
249
+ const sliderActive = $derived(
250
+ display === 'slider' || display === 'slideshow' || slide >= 0,
251
+ );
252
+ const isModal = $derived(
253
+ fullscreenActive || (display !== 'slider' && display !== 'slideshow' && slide >= 0),
254
+ );
255
+ $effect(() => {
256
+ if (typeof window !== 'undefined') {
257
+ if (isModal) {
258
+ window.document.body.style.overflow = 'hidden';
259
+ } else {
260
+ window.document.body.style.overflow = '';
261
+ }
262
+ }
263
+ });
264
+
265
+ // Prevent the modal from automatically being active when switching between display modes
266
+ let previousDisplay = undefined as typeof display | undefined;
267
+ $effect.pre(() => {
268
+ const wasSliderLike = previousDisplay === 'slider' || previousDisplay === 'slideshow';
269
+ const isSliderLike = display === 'slider' || display === 'slideshow';
270
+ if (wasSliderLike) {
271
+ if (!isSliderLike) slide = -1;
272
+ } else {
273
+ if (isSliderLike) slide = 0;
274
+ }
275
+ if (previousDisplay) untrack(() => pause());
276
+ previousDisplay = display;
277
+ });
278
+
279
+ /** Autoplay state */
280
+ let autoplayPaused = $state(false);
281
+ const autoplayTransitionInterval = 300; // ms between progress ticks
282
+ let autoplayTransitionStart = $state<number | undefined>(undefined);
283
+ let autoplayTransitionProgress = $state<number | undefined>(undefined);
284
+ let autoplayTransitionTimer = $state<ReturnType<typeof setInterval> | undefined>(
285
+ undefined,
286
+ );
287
+ $effect.pre(() => {
288
+ if (autoplay && intersected && !autoplayTransitionTimer && !autoplayPaused) play();
289
+ });
290
+ onDestroy(() => pause());
291
+
292
+ /**
293
+ * Opens the gallery modal at the given item index.
294
+ *
295
+ * Primarily intended for `display="lightbox"`, where the developer renders
296
+ * their own thumbnails: pass `event.currentTarget` (or another element) as
297
+ * `from` to anchor the open animation to that element. Equivalent to setting
298
+ * `slide = index` directly, except it also captures the animation origin.
299
+ */
300
+ export function open(index: number, from?: HTMLElement) {
301
+ if (!list[index]) return;
302
+ dismissing = 0;
303
+ animationTarget = from;
304
+ slide = index;
305
+ }
306
+
307
+ /** Closes the gallery modal */
308
+ export function close() {
309
+ if (fullscreenActive) return closeFullscreen();
310
+ if (!sliderActive) return;
311
+ if ((display === 'slider' || display === 'slideshow') && isModal)
312
+ return closeFullscreen();
313
+ if (focusTrapInstance?.active) {
314
+ focusTrapInstance.deactivate();
315
+ } else {
316
+ slide = -1;
317
+ }
318
+ }
319
+
320
+ /** Navigates to the item at the given index */
321
+ export function goto(i: number) {
322
+ if (!sliderActive || !list[i]) return;
323
+ pause();
324
+ slide = i;
325
+ }
326
+
327
+ /** Navigates to the next item */
328
+ export function next(amount = 1) {
329
+ if (!sliderActive) return;
330
+ pause();
331
+ const target = Math.floor(slide + amount) % list.length;
332
+ slide = target;
333
+ }
334
+
335
+ /** Navigates to the previous item */
336
+ export function prev(amount = 1) {
337
+ if (!sliderActive) return;
338
+ pause();
339
+ const target = Math.floor(slide - amount + list.length) % list.length;
340
+ slide = target;
341
+ }
342
+
343
+ /** Starts the slideshow */
344
+ export function play() {
345
+ if (!sliderActive || autoplayTransitionTimer) return;
346
+ autoplayPaused = false;
347
+ autoplayTransitionStart = Date.now();
348
+ autoplayTransitionTimer = setInterval(() => {
349
+ if (!autoplayTransitionStart) return clearInterval(autoplayTransitionTimer);
350
+ const now = Date.now();
351
+ if (!intersected) {
352
+ autoplayTransitionStart = Math.min(
353
+ now,
354
+ Math.floor(
355
+ now -
356
+ duration * (autoplayTransitionProgress || 0) +
357
+ autoplayTransitionInterval,
358
+ ),
359
+ );
360
+ return;
361
+ }
362
+ autoplayTransitionProgress = (now - autoplayTransitionStart) / duration;
363
+ if (autoplayTransitionProgress >= 1) {
364
+ autoplayTransitionStart = now;
365
+ setTimeout(() => (autoplayTransitionProgress = 0), 10);
366
+ const target = Math.floor(slide + 1) % list.length;
367
+ slide = target;
368
+ }
369
+ }, autoplayTransitionInterval);
370
+ }
371
+
372
+ /** Pauses the slideshow */
373
+ export function pause() {
374
+ if (!autoplayTransitionTimer) return;
375
+ clearInterval(autoplayTransitionTimer);
376
+ autoplayTransitionTimer = undefined;
377
+ autoplayTransitionStart = undefined;
378
+ autoplayTransitionProgress = undefined;
379
+ autoplayPaused = true;
380
+ }
381
+
382
+ /** Handles when a grid item is clicked (opens the modal) */
383
+ function onItemClick(i: number, evt: MouseEvent | KeyboardEvent) {
384
+ if (onclick) {
385
+ const result = onclick(evt, i);
386
+ if (result === false) {
387
+ evt.preventDefault();
388
+ return;
389
+ }
390
+ }
391
+ let target = evt.target as HTMLElement;
392
+ let isActionButton = false;
393
+ while (target && !isActionButton && !target.classList.contains('gallery-item')) {
394
+ isActionButton =
395
+ target.classList.contains('actions') || target.classList.contains('button');
396
+ target = target.parentElement as HTMLElement;
397
+ }
398
+ if (isActionButton) return;
399
+ dismissing = 0;
400
+ animationTarget = (evt.target as HTMLElement) || undefined;
401
+ slide = i;
402
+ }
403
+
404
+ /** Returns the gallery item element that triggered this carousel open so focus can be returned to it */
405
+ function focusTrapSetReturnFocus(
406
+ elFocusedBeforeActivation: HTMLElement | SVGElement,
407
+ ): HTMLElement | false {
408
+ const items = Array.from(document.querySelectorAll('.gallery.grid .gallery-item'));
409
+ const target = items[slide] as HTMLElement;
410
+ return target || elFocusedBeforeActivation;
411
+ }
412
+
413
+ /** Opens the media player in full screen mode */
414
+ async function openFullscreen() {
415
+ const promise =
416
+ document?.documentElement?.requestFullscreen() ||
417
+ (document?.documentElement as any)?.mozRequestFullScreen() ||
418
+ (document?.documentElement as any)?.webkitRequestFullscreen() ||
419
+ (document?.documentElement as any)?.msRequestFullscreen();
420
+ if (!promise) return;
421
+ fullscreenActive = true;
422
+ await promise.catch(() => {
423
+ fullscreenActive = false;
424
+ });
425
+ if (document.fullscreenElement === null && fullscreenActive) fullscreenActive = false;
426
+ }
427
+
428
+ /** Closes the fullscreen mode */
429
+ function closeFullscreen() {
430
+ document?.exitFullscreen() ||
431
+ (document as any)?.mozCancelFullScreen() ||
432
+ (document as any)?.webkitExitFullscreen() ||
433
+ (document as any)?.msExitFullscreen();
434
+ if (carousel) carousel.reset();
435
+ }
436
+
437
+ /** Toggles fullscreen mode */
438
+ export function toggleFullscreen() {
439
+ if (fullscreenActive) {
440
+ closeFullscreen();
441
+ } else {
442
+ openFullscreen();
443
+ }
444
+ }
445
+
446
+ onMount(() => {
447
+ const listener = () => {
448
+ if (document.fullscreenElement === null) fullscreenActive = false;
449
+ };
450
+ document.addEventListener('fullscreenchange', listener);
451
+ document.addEventListener('webkitfullscreenchange', listener);
452
+ document.addEventListener('mozfullscreenchange', listener);
453
+ document.addEventListener('msfullscreenchange', listener);
454
+ return () => {
455
+ document.removeEventListener('fullscreenchange', listener);
456
+ document.removeEventListener('webkitfullscreenchange', listener);
457
+ document.removeEventListener('mozfullscreenchange', listener);
458
+ document.removeEventListener('msfullscreenchange', listener);
459
+ };
460
+ });
461
+
462
+ /** Animates the scale of the gallery item on dismiss */
463
+ function carouselCloseTransition(node: HTMLElement): () => TransitionConfig {
464
+ return () => {
465
+ const duration = 300;
466
+ const start = 0.5;
467
+ const el = node.querySelector<HTMLElement>('.carousel .item.active');
468
+ if (!el) {
469
+ const parentOpacity = +getComputedStyle(node).opacity;
470
+ return { duration: 150, css: (_t, u) => `opacity: ${parentOpacity - u}` };
471
+ }
472
+ const activePageEl = node.querySelector<HTMLElement>(
473
+ '.carousel .item.active > *.active:not(.preview)',
474
+ );
475
+ let inactivePageEls: HTMLElement[] = [];
476
+ if (activePageEl) {
477
+ inactivePageEls = Array.from(
478
+ node.querySelectorAll<HTMLElement>('.carousel .item.active > *:not(.active)'),
479
+ );
480
+ } else {
481
+ inactivePageEls = Array.from(
482
+ node.querySelectorAll<HTMLElement>(
483
+ '.carousel .item.active > *:not(:first-child)',
484
+ ),
485
+ );
486
+ }
487
+ inactivePageEls.forEach((el) => {
488
+ el.style.opacity = '0';
489
+ });
490
+ const style = getComputedStyle(el);
491
+ const targetOpacity = +style.opacity;
492
+ const transform = style.transform === 'none' ? '' : style.transform;
493
+ return {
494
+ duration,
495
+ easing: circInOut,
496
+ tick: (_t, u) => {
497
+ const sd = 1 - start;
498
+ el.style.transformOrigin = 'center center';
499
+ el.style.opacity = `${targetOpacity - u}`;
500
+ el.style.transform = `${transform} perspective(100px) translate3d(0px, ${
501
+ 500 * u
502
+ }px, ${sd * u * -300}px)`;
503
+ },
504
+ };
505
+ };
506
+ }
507
+
508
+ /** The full set of context-menu actions for the slide at the given index */
509
+ function flattenActions(itemActions: GalleryItemAction[] | undefined) {
510
+ return (
511
+ itemActions?.flatMap((parentAction) =>
512
+ [parentAction, ...(parentAction.actions || [])]
513
+ .filter((action) => !!action?.name)
514
+ .map((action) => ({
515
+ label: action.name,
516
+ icon: action.icon || parentAction.icon,
517
+ href: action.href,
518
+ target: action.target,
519
+ onclick: action.click
520
+ ? (event: PointerEvent) => {
521
+ action.click?.(event);
522
+ }
523
+ : undefined,
524
+ })),
525
+ ) || []
526
+ );
527
+ }
528
+ </script>
529
+
530
+ <!-- Inline icons (kept terse to avoid pulling in an icon dep) -->
531
+ {#snippet iconPlay()}
532
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
533
+ <path d="M8 5v14l11-7L8 5z" />
534
+ </svg>
535
+ {/snippet}
536
+ {#snippet iconPause()}
537
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
538
+ <path d="M6 5h4v14H6zM14 5h4v14h-4z" />
539
+ </svg>
540
+ {/snippet}
541
+ {#snippet iconDocument()}
542
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
543
+ <path
544
+ d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm0 7V3.5L19.5 9H14z" />
545
+ </svg>
546
+ {/snippet}
547
+ {#snippet iconEmbed()}
548
+ <svg
549
+ viewBox="0 0 24 24"
550
+ fill="none"
551
+ stroke="currentColor"
552
+ stroke-width="2"
553
+ stroke-linecap="round"
554
+ stroke-linejoin="round"
555
+ aria-hidden="true">
556
+ <polyline points="16 18 22 12 16 6" />
557
+ <polyline points="8 6 2 12 8 18" />
558
+ </svg>
559
+ {/snippet}
560
+ {#snippet iconPanorama()}
561
+ <!-- Panoramic photo: a wide frame with curved (barrel) top/bottom edges
562
+ and a mountain scene — reads more clearly as "360 panorama" than the
563
+ previous globe/grid ellipse. -->
564
+ <svg
565
+ viewBox="0 0 24 24"
566
+ fill="none"
567
+ stroke="currentColor"
568
+ stroke-width="2"
569
+ stroke-linecap="round"
570
+ stroke-linejoin="round"
571
+ aria-hidden="true">
572
+ <path d="M2 6c6.5 1.6 13.5 1.6 20 0v12c-6.5-1.6-13.5-1.6-20 0V6z" />
573
+ <path d="M6 15l3.5-4 2.5 3 3-4 3 5" />
574
+ </svg>
575
+ {/snippet}
576
+ {#snippet iconChevronLeft()}
577
+ <svg
578
+ viewBox="0 0 24 24"
579
+ fill="none"
580
+ stroke="currentColor"
581
+ stroke-width="2"
582
+ stroke-linecap="round"
583
+ stroke-linejoin="round"
584
+ aria-hidden="true">
585
+ <polyline points="15 18 9 12 15 6" />
586
+ </svg>
587
+ {/snippet}
588
+ {#snippet iconChevronRight()}
589
+ <svg
590
+ viewBox="0 0 24 24"
591
+ fill="none"
592
+ stroke="currentColor"
593
+ stroke-width="2"
594
+ stroke-linecap="round"
595
+ stroke-linejoin="round"
596
+ aria-hidden="true">
597
+ <polyline points="9 18 15 12 9 6" />
598
+ </svg>
599
+ {/snippet}
600
+ {#snippet iconFullscreen()}
601
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
602
+ <path
603
+ d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
604
+ </svg>
605
+ {/snippet}
606
+ {#snippet iconFullscreenExit()}
607
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
608
+ <path
609
+ d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" />
610
+ </svg>
611
+ {/snippet}
612
+ {#snippet iconClose()}
613
+ <svg
614
+ viewBox="0 0 24 24"
615
+ fill="none"
616
+ stroke="currentColor"
617
+ stroke-width="2"
618
+ stroke-linecap="round"
619
+ stroke-linejoin="round"
620
+ aria-hidden="true">
621
+ <line x1="18" y1="6" x2="6" y2="18" />
622
+ <line x1="6" y1="6" x2="18" y2="18" />
623
+ </svg>
624
+ {/snippet}
625
+ {#snippet iconDownload()}
626
+ <svg
627
+ viewBox="0 0 24 24"
628
+ fill="none"
629
+ stroke="currentColor"
630
+ stroke-width="2"
631
+ stroke-linecap="round"
632
+ stroke-linejoin="round"
633
+ aria-hidden="true">
634
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
635
+ <polyline points="7 10 12 15 17 10" />
636
+ <line x1="12" y1="15" x2="12" y2="3" />
637
+ </svg>
638
+ {/snippet}
639
+
640
+ {#snippet itemThumbnail(item: (typeof list)[number], sizesFallback: string)}
641
+ {@const key = thumbnailKey(item)}
642
+ {@const eager = !!item.priority}
643
+ {@const isImage = !item.type || item.type === 'image'}
644
+ {@const thumbSrc = getItemThumbnailSrc(item)}
645
+ {#if item.thumbhash}
646
+ <img
647
+ class="thumbnail-blur"
648
+ src={decodeThumbHash(item.thumbhash)}
649
+ alt=""
650
+ aria-hidden="true"
651
+ draggable="false" />
652
+ {/if}
653
+ {#if thumbSrc}
654
+ {@const responsive = isImage && isResponsiveSrcset(item.src)}
655
+ <!--
656
+ Match the Carousel's image attributes for single-URL sources so the
657
+ lightbox <img> hits this thumbnail's memory-cached pixels instead of
658
+ re-fetching. Mismatched srcset/sizes attributes between the two <img>
659
+ elements make Chrome treat them as separate "responsive selections"
660
+ and bypass the memory cache when devtools "Disable cache" is on.
661
+ -->
662
+ <img
663
+ class="thumbnail-img"
664
+ class:fading={fadingKeys.has(key)}
665
+ class:no-blur={!item.thumbhash}
666
+ src={thumbSrc}
667
+ srcset={responsive ? item.src : undefined}
668
+ sizes={responsive ? (eager ? sizesFallback : `auto, ${sizesFallback}`) : undefined}
669
+ alt={item.alt ?? item.name ?? ''}
670
+ loading={eager ? 'eager' : 'lazy'}
671
+ fetchpriority={eager ? 'high' : undefined}
672
+ draggable="false"
673
+ onload={() => fadingKeys.delete(key)}
674
+ onerror={() => fadingKeys.delete(key)}
675
+ {@attach (el: HTMLImageElement) => {
676
+ if (!el.complete || el.naturalWidth === 0) {
677
+ fadingKeys.add(key);
678
+ }
679
+ }} />
680
+ {:else}
681
+ <!-- No thumbnail available — render a styled placeholder so the type-icon
682
+ overlay has a background and the layout slot still has its expected
683
+ aspect ratio. -->
684
+ <div class="thumbnail-placeholder" aria-hidden="true"></div>
685
+ {/if}
686
+ {/snippet}
687
+
688
+ {#snippet galleryItemAction(
689
+ index: number,
690
+ style: 'overlay' | 'transparent' = 'overlay',
691
+ button_size: '00' | '0' | '1' = '00',
692
+ )}
693
+ {@const itemActions = actions?.[index]}
694
+ {#if itemActions?.length}
695
+ <div class="actions" class:hover-only={action_display === 'hover'}>
696
+ {#each itemActions as action (action)}
697
+ {#if action?.actions?.length}
698
+ <Button
699
+ icon
700
+ overlay={style === 'overlay'}
701
+ transparent={style === 'transparent'}
702
+ dense
703
+ size={button_size}
704
+ tooltip={action.tooltip || action.name}>
705
+ {#if action.icon}
706
+ <action.icon></action.icon>
707
+ {:else}
708
+ {@render iconDownload()}
709
+ {/if}
710
+ {#snippet menu()}
711
+ <List>
712
+ {#each action?.actions || [] as subAction (subAction)}
713
+ <ListItem
714
+ onclick={(e) => {
715
+ if (subAction.click) subAction.click(e);
716
+ }}
717
+ href={subAction.href}
718
+ target={subAction.target}>
719
+ <span class="list-item-icon">
720
+ {#if subAction.icon}
721
+ <subAction.icon></subAction.icon>
722
+ {:else if action.icon}
723
+ <action.icon></action.icon>
724
+ {:else}
725
+ {@render iconDownload()}
726
+ {/if}
727
+ </span>
728
+ {subAction.name}
729
+ </ListItem>
730
+ {/each}
731
+ </List>
732
+ {/snippet}
733
+ </Button>
734
+ {:else}
735
+ <Button
736
+ icon
737
+ overlay={style === 'overlay'}
738
+ transparent={style === 'transparent'}
739
+ dense
740
+ size={button_size}
741
+ tooltip={action.tooltip || action.name}
742
+ href={action.href}
743
+ target={action.target}
744
+ onclick={(e) => {
745
+ if (action.click) action.click(e);
746
+ }}>
747
+ {#if action.icon}
748
+ <action.icon></action.icon>
749
+ {:else}
750
+ {@render iconDownload()}
751
+ {/if}
752
+ </Button>
753
+ {/if}
754
+ {/each}
755
+ </div>
756
+ {/if}
757
+ {/snippet}
758
+
759
+ {#snippet galleryItem(item: (typeof list)[number], index: number)}
760
+ <div
761
+ class="gallery-item"
762
+ role="button"
763
+ tabindex="0"
764
+ {@attach ripple({ zIndex: 1, opacity: 0.2, color: 'white' })}
765
+ class:favorite={item.favorite}
766
+ style:--ratio={(display === 'masonry-row' || display === 'masonry') &&
767
+ item.width &&
768
+ item.height
769
+ ? item.width / item.height
770
+ : undefined}
771
+ {@attach contextMenu({ actions: flattenActions(actions?.[index]) })}
772
+ onclick={(e) => onItemClick(index, e)}
773
+ onkeydown={(e) => e.key !== 'Enter' || onItemClick(index, e)}>
774
+ <div class="image">
775
+ {@render itemThumbnail(item, '100vw')}
776
+ </div>
777
+ {#if item.type !== 'image' || item.panorama}
778
+ <div class="icon">
779
+ {#if item.type === 'video'}
780
+ {@render iconPlay()}
781
+ {:else if item.type === 'pdf'}
782
+ {@render iconDocument()}
783
+ {:else if item.type === 'embed'}
784
+ {@render iconEmbed()}
785
+ {:else if item.panorama}
786
+ {@render iconPanorama()}
787
+ {/if}
788
+ </div>
789
+ {/if}
790
+ {#if meta_display === 'always' || meta_display === 'hover'}
791
+ {#if item.name}
792
+ <div class="name" class:hover-only={meta_display === 'hover'}>{item.name}</div>
793
+ {/if}
794
+ {/if}
795
+ {#if action_display === 'always' || action_display === 'hover'}
796
+ {@render galleryItemAction(index, 'overlay')}
797
+ {/if}
798
+ </div>
799
+ {/snippet}
800
+
801
+ {#if display === 'grid' || display === 'masonry' || display === 'masonry-row'}
802
+ <div
803
+ class="gallery display-{display} size-{size} spacing-{spacing} radius-{radius}"
804
+ role="group"
805
+ {style}>
806
+ {#each list as item, i (i)}
807
+ {@render galleryItem(item, i)}
808
+ {/each}
809
+ </div>
810
+ {/if}
811
+
812
+ {#if display === 'list'}
813
+ <div
814
+ class="gallery display-list size-{size} spacing-{spacing} radius-{radius}"
815
+ role="group"
816
+ {style}>
817
+ {#each list as item, index (index)}
818
+ <div class="list-item">
819
+ <div
820
+ class="info"
821
+ role="button"
822
+ tabindex="0"
823
+ {@attach ripple({
824
+ zIndex: 1,
825
+ opacity: 0.2,
826
+ color: 'var(--color-text, currentColor)',
827
+ })}
828
+ onclick={(e) => onItemClick(index, e)}
829
+ onkeydown={(e) => e.key !== 'Enter' || onItemClick(index, e)}
830
+ {@attach contextMenu({ actions: flattenActions(actions?.[index]) })}>
831
+ <div class="thumbnail">
832
+ {@render itemThumbnail(item, '64px')}
833
+ {#if item.type !== 'image' || item.panorama}
834
+ <div class="icon">
835
+ {#if item.type === 'video'}
836
+ {@render iconPlay()}
837
+ {:else if item.type === 'pdf'}
838
+ {@render iconDocument()}
839
+ {:else if item.type === 'embed'}
840
+ {@render iconEmbed()}
841
+ {:else if item.panorama}
842
+ {@render iconPanorama()}
843
+ {/if}
844
+ </div>
845
+ {/if}
846
+ </div>
847
+ <div class="name">{item.name || ''}</div>
848
+ </div>
849
+ {#if action_display === 'always' || action_display === 'hover'}
850
+ {@render galleryItemAction(index, 'transparent')}
851
+ {/if}
852
+ </div>
853
+ {/each}
854
+ </div>
855
+ {/if}
856
+
857
+ {#snippet sliderControls()}
858
+ <div
859
+ class="controls"
860
+ in:fade={{ duration: 150 }}
861
+ out:fade={{ duration: 150 }}
862
+ style:opacity={1 - dismissing}>
863
+ <!-- Always rendered (and positioned absolutely) so it never re-mounts as
864
+ `num_pages` settles while a PDF loads its pages — that remounting was
865
+ replaying the scale-in transition repeatedly. Visibility is toggled
866
+ purely with CSS instead. -->
867
+ <nav class="pages" class:shown={num_pages > 1} aria-hidden={num_pages <= 1}>
868
+ <Button
869
+ icon
870
+ transparent
871
+ size="0"
872
+ disabled={page <= 0}
873
+ onclick={() => (page = Math.max(0, page - 1))}
874
+ tooltip="Previous page">
875
+ <span class="visuallyhidden">Previous page</span>
876
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">
877
+ <path
878
+ d="M10 3L5 8L10 13"
879
+ stroke="currentColor"
880
+ stroke-width="1.8"
881
+ stroke-linecap="round"
882
+ stroke-linejoin="round" />
883
+ </svg>
884
+ </Button>
885
+ <span class="page-counter" aria-live="polite">
886
+ <span class="page-current">{page + 1}</span>
887
+ <span class="page-separator">/</span>
888
+ <span class="page-total">{num_pages}</span>
889
+ </span>
890
+ <Button
891
+ icon
892
+ transparent
893
+ size="0"
894
+ disabled={page >= num_pages - 1}
895
+ onclick={() => (page = Math.min(num_pages - 1, page + 1))}
896
+ tooltip="Next page">
897
+ <span class="visuallyhidden">Next page</span>
898
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">
899
+ <path
900
+ d="M6 3L11 8L6 13"
901
+ stroke="currentColor"
902
+ stroke-width="1.8"
903
+ stroke-linecap="round"
904
+ stroke-linejoin="round" />
905
+ </svg>
906
+ </Button>
907
+ </nav>
908
+ {#if isModal}
909
+ <Button icon transparent dense size="1" class="close" onclick={() => close()}>
910
+ <span class="visuallyhidden">Close</span>
911
+ {@render iconClose()}
912
+ </Button>
913
+ {#if actions?.length}
914
+ {@render galleryItemAction(slide, 'transparent', '1')}
915
+ {/if}
916
+ {:else}
917
+ {#if disable_fullscreen === false}
918
+ <Button
919
+ icon
920
+ transparent
921
+ dense
922
+ size="0"
923
+ class="fullscreen"
924
+ tooltip="Toggle Fullscreen"
925
+ onclick={() => toggleFullscreen()}>
926
+ <span class="visuallyhidden">Fullscreen</span>
927
+ {#if fullscreenActive}
928
+ {@render iconFullscreenExit()}
929
+ {:else}
930
+ {@render iconFullscreen()}
931
+ {/if}
932
+ </Button>
933
+ {/if}
934
+ {#if list.length > 1}
935
+ <Button
936
+ icon
937
+ transparent
938
+ dense
939
+ size="0"
940
+ class="play"
941
+ tooltip={autoplayTransitionTimer ? 'Pause Slideshow' : 'Start Slideshow'}
942
+ onclick={() => (autoplayTransitionTimer ? pause() : play())}>
943
+ <span class="visuallyhidden">Start Slideshow</span>
944
+ {#if autoplayTransitionTimer}
945
+ {@render iconPause()}
946
+ {:else}
947
+ {@render iconPlay()}
948
+ {/if}
949
+ {#if autoplayTransitionTimer}
950
+ {@const progress = autoplayTransitionProgress || 0}
951
+ <svg
952
+ class="progress"
953
+ viewBox="0 0 56 56"
954
+ style:--progress={progress}
955
+ style:--speed="{autoplayTransitionInterval}ms"
956
+ style:transition={progress >= 0.99 || progress < 0.01 ? 'none' : null}>
957
+ <circle cx="28" cy="28" r="26" />
958
+ </svg>
959
+ {/if}
960
+ </Button>
961
+ {/if}
962
+ {/if}
963
+ <div class="spacer"></div>
964
+ {#if list.length > 1}
965
+ <div class="pagination">{Math.max(0, slide) + 1} / {list.length}</div>
966
+ {/if}
967
+ {#if list.length > 1}
968
+ <Button
969
+ icon
970
+ transparent
971
+ dense
972
+ size={isModal ? '1' : '0'}
973
+ class="prev"
974
+ onclick={() => prev()}
975
+ tooltip="Previous Item">
976
+ <span class="visuallyhidden">Previous Item</span>
977
+ {@render iconChevronLeft()}
978
+ </Button>
979
+ <Button
980
+ icon
981
+ transparent
982
+ dense
983
+ size={isModal ? '1' : '0'}
984
+ class="next"
985
+ onclick={() => next()}
986
+ tooltip="Next Item">
987
+ <span class="visuallyhidden">Next Item</span>
988
+ {@render iconChevronRight()}
989
+ </Button>
990
+ {/if}
991
+ </div>
992
+ {/snippet}
993
+
994
+ {#snippet slider()}
995
+ {#if sliderActive}
996
+ <div
997
+ class="gallery slider size-{size} radius-{radius}"
998
+ class:modal={isModal}
999
+ class:controls-inline={controls === 'inline' ||
1000
+ (controls === 'default' && !isModal)}
1001
+ class:controls-overlay={controls === 'overlay' ||
1002
+ (controls === 'default' && isModal)}
1003
+ class:fullscreen={fullscreenActive}
1004
+ style={!isModal && (display === 'slider' || display === 'slideshow') ? style : null}
1005
+ style:--aspect-ratio={isModal || !aspect_ratio ? null : aspect_ratio}
1006
+ aria-label="Media Gallery Carousel"
1007
+ out:carouselCloseTransition
1008
+ {@attach intersectionObserver({
1009
+ enabled: true,
1010
+ onintersectchange: (event) => (intersected = event.isIntersecting),
1011
+ })}
1012
+ {@attach focusTrap({
1013
+ preventScroll: true,
1014
+ onPostDeactivate: () => (slide = -1),
1015
+ allowOutsideClick: true,
1016
+ enabled: isModal,
1017
+ escapeDeactivates: (e) => {
1018
+ e.stopPropagation();
1019
+ return true;
1020
+ },
1021
+ setReturnFocus: focusTrapSetReturnFocus,
1022
+ oninit: (instance) => (focusTrapInstance = instance),
1023
+ initialFocus: false,
1024
+ })}
1025
+ {@attach contextMenu({ actions: flattenActions(actions?.[slide]) })}>
1026
+ <div
1027
+ class="bg"
1028
+ in:fade={{ duration: 350 }}
1029
+ out:fade={{ duration: 350 }}
1030
+ style:opacity={1 - dismissing}>
1031
+ </div>
1032
+ <Carousel
1033
+ items={list}
1034
+ bind:dismissing
1035
+ bind:this={carousel}
1036
+ bind:slide
1037
+ bind:page
1038
+ bind:num_pages
1039
+ animation={(display === 'slider' || display === 'slideshow') &&
1040
+ autoplayTransitionTimer &&
1041
+ list.length > 1
1042
+ ? 'zoom'
1043
+ : 'none'}
1044
+ transition={(display === 'slider' || display === 'slideshow') &&
1045
+ autoplayTransitionTimer
1046
+ ? 'fade'
1047
+ : 'none'}
1048
+ inline={inline ??
1049
+ ((display === 'slider' || display === 'slideshow') && !fullscreenActive)}
1050
+ {autoplay_video}
1051
+ dismissable={isModal}
1052
+ disable_entry_exit_animation={display === 'slider' || display === 'slideshow'}
1053
+ animation_target={animationTarget}
1054
+ {fit}
1055
+ {custom}
1056
+ oninteraction={() => pause()}
1057
+ onclose={() => {
1058
+ if (fullscreenActive) return closeFullscreen();
1059
+ slide = -1;
1060
+ }} />
1061
+ {#if meta_display_fullscreen === 'always' && isModal && (list[slide]?.caption || list[slide]?.name)}
1062
+ <div class="fullscreen-name" style:opacity={1 - dismissing}>
1063
+ {list[slide]?.caption || list[slide]?.name}
1064
+ </div>
1065
+ {/if}
1066
+ {#if controls !== 'disable'}
1067
+ {@render sliderControls()}
1068
+ {/if}
1069
+ <div class="visuallyhidden" aria-live="polite" aria-atomic="true" inert>
1070
+ Media Item {slide + 1} of {list.length}
1071
+ </div>
1072
+ </div>
1073
+ {/if}
1074
+ {/snippet}
1075
+
1076
+ {#if (display !== 'slider' && display !== 'slideshow') || isModal}
1077
+ <Portal>
1078
+ {@render slider()}
1079
+ </Portal>
1080
+ {:else}
1081
+ {@render slider()}
1082
+ {/if}
1083
+
1084
+ <style>
1085
+ .visuallyhidden {
1086
+ border: 0;
1087
+ clip: rect(0 0 0 0);
1088
+ clip-path: inset(50%);
1089
+ height: 1px;
1090
+ margin: -1px;
1091
+ overflow: hidden;
1092
+ padding: 0;
1093
+ position: absolute;
1094
+ width: 1px;
1095
+ white-space: nowrap;
1096
+ }
1097
+
1098
+ .pagination {
1099
+ white-space: nowrap;
1100
+ }
1101
+
1102
+ .list-item-icon {
1103
+ display: inline-flex;
1104
+ align-items: center;
1105
+ padding-right: 0.5rem;
1106
+ font-size: 1.1rem;
1107
+ }
1108
+
1109
+ .list-item-icon :global(svg),
1110
+ .icon :global(svg) {
1111
+ width: 1em;
1112
+ height: 1em;
1113
+ }
1114
+
1115
+ .gallery {
1116
+ /* Galleries are the largest surface and reach --radius-3xl at their
1117
+ biggest size by design, so they clamp each radius tier to that ceiling
1118
+ rather than the smaller shared --radius-cap: an over-rounded radius
1119
+ token can't blob a gallery, while the shipped looks (incl. the 3xl
1120
+ tier) are never clipped. Private (--_cap) so the raised ceiling doesn't
1121
+ leak to nested components. Both radius systems funnel through these:
1122
+ the slider .bg/.carousel/.controls use them directly, and the grid/
1123
+ masonry size remaps assign them to --radius-lg. */
1124
+ --_cap: var(--radius-3xl, 60px);
1125
+ --_rxl: min(var(--radius-xl, 0.75rem), var(--_cap));
1126
+ --_r2xl: min(var(--radius-2xl, 1rem), var(--_cap));
1127
+ --_r3xl: min(var(--radius-3xl, 1.5rem), var(--_cap));
1128
+ }
1129
+
1130
+ .gallery-item {
1131
+ position: relative;
1132
+ display: grid;
1133
+ grid-template-rows: 1fr;
1134
+ grid-template-columns: 1fr;
1135
+ cursor: pointer;
1136
+ border-radius: var(--radius-lg);
1137
+ @supports (corner-shape: squircle) {
1138
+ corner-shape: squircle;
1139
+ border-radius: calc(var(--radius-lg) * var(--squircle-ratio, 2));
1140
+ }
1141
+ isolation: isolate;
1142
+ overflow: hidden;
1143
+ transition:
1144
+ box-shadow 150ms ease,
1145
+ scale 150ms ease;
1146
+ box-shadow: var(--shadow-sm);
1147
+ background-color: var(--color-bg-muted, var(--bg-high));
1148
+
1149
+ .image {
1150
+ position: absolute;
1151
+ inset: 0;
1152
+ transition: transform 150ms ease;
1153
+ transform: scale(1);
1154
+ will-change: transform;
1155
+ overflow: hidden;
1156
+ }
1157
+ .thumbnail-blur,
1158
+ .thumbnail-img,
1159
+ .thumbnail-placeholder {
1160
+ position: absolute;
1161
+ inset: 0;
1162
+ width: 100%;
1163
+ height: 100%;
1164
+ object-fit: cover;
1165
+ display: block;
1166
+ }
1167
+ .thumbnail-blur {
1168
+ z-index: 0;
1169
+ filter: blur(24px) saturate(1.2) contrast(1.05);
1170
+ transform: scale(1.2);
1171
+ pointer-events: none;
1172
+ user-select: none;
1173
+ }
1174
+ .thumbnail-placeholder {
1175
+ z-index: 0;
1176
+ background: linear-gradient(
1177
+ 135deg,
1178
+ light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.04)),
1179
+ light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1))
1180
+ );
1181
+ }
1182
+ .thumbnail-img {
1183
+ z-index: 1;
1184
+ opacity: 1;
1185
+ transform: scale(1);
1186
+ transition:
1187
+ opacity 400ms ease,
1188
+ transform 700ms cubic-bezier(0.22, 1, 0.36, 1);
1189
+ &.no-blur {
1190
+ transition:
1191
+ opacity 250ms ease,
1192
+ transform 400ms ease;
1193
+ }
1194
+ /* JS adds .fading after mount only when the image isn't already loaded.
1195
+ When .fading is removed (on `onload`), the default transition above
1196
+ smoothly fades the image in over the blur. */
1197
+ &.fading {
1198
+ opacity: 0;
1199
+ transform: scale(1.04);
1200
+ transition: none;
1201
+ }
1202
+ }
1203
+
1204
+ &:active {
1205
+ scale: 0.98;
1206
+ }
1207
+ &:hover {
1208
+ box-shadow: var(--shadow-md);
1209
+ .image {
1210
+ opacity: 0.97;
1211
+ transform: scale(1.018);
1212
+ }
1213
+ }
1214
+ &:focus {
1215
+ outline: solid 4px var(--color-text, var(--text));
1216
+ }
1217
+ &:focus:not(:focus-visible) {
1218
+ outline: none;
1219
+ }
1220
+ > :global(*) {
1221
+ grid-row: 1 / 1;
1222
+ grid-column: 1 / 1;
1223
+ position: relative;
1224
+ }
1225
+ .icon {
1226
+ display: flex;
1227
+ align-items: center;
1228
+ justify-content: center;
1229
+ justify-self: center;
1230
+ align-self: center;
1231
+ color: white;
1232
+ background-color: rgba(0, 0, 0, 0.6);
1233
+ border-radius: 100%;
1234
+ width: min(max(20%, 3rem), 7rem);
1235
+ aspect-ratio: 1 / 1;
1236
+ padding: 0.5rem;
1237
+ backdrop-filter: blur(10px);
1238
+ :global(svg) {
1239
+ width: max(2rem, 70%);
1240
+ height: max(2rem, 70%);
1241
+ }
1242
+ }
1243
+ .name {
1244
+ position: absolute;
1245
+ bottom: 0;
1246
+ left: 0;
1247
+ right: 0;
1248
+ width: 100%;
1249
+ color: white;
1250
+ background-color: rgba(0, 0, 0, 0.6);
1251
+ padding: max(0.5rem, calc(var(--radius-lg, 0px) / 2))
1252
+ max(1rem, var(--radius-lg, 0px));
1253
+ text-overflow: ellipsis;
1254
+ overflow: hidden;
1255
+ white-space: nowrap;
1256
+ backdrop-filter: blur(5px);
1257
+ &.hover-only {
1258
+ transition: transform 250ms ease;
1259
+ backdrop-filter: none;
1260
+ transform: translate3d(0, 100%, 0);
1261
+ }
1262
+ }
1263
+ .actions {
1264
+ position: absolute;
1265
+ top: max(4px, min(16px, var(--radius-lg, 0px)));
1266
+ right: max(4px, min(16px, var(--radius-lg, 0px)));
1267
+ z-index: 2;
1268
+ display: flex;
1269
+ gap: 0.25rem;
1270
+ &.hover-only {
1271
+ opacity: 0;
1272
+ transition: opacity 250ms ease;
1273
+ }
1274
+ }
1275
+ &:focus-visible,
1276
+ &:has(.actions:focus-within) {
1277
+ .actions {
1278
+ opacity: 1;
1279
+ }
1280
+ .name {
1281
+ transform: translate3d(0px, 0px, 0px);
1282
+ }
1283
+ }
1284
+
1285
+ @media (hover: hover) and (pointer: fine) {
1286
+ &:hover {
1287
+ .name.hover-only {
1288
+ transform: translate3d(0px, 0px, 0px);
1289
+ }
1290
+ .actions.hover-only {
1291
+ opacity: 1;
1292
+ }
1293
+ }
1294
+ }
1295
+ @media not ((hover: hover) and (pointer: fine)) {
1296
+ .actions.hover-only {
1297
+ display: none;
1298
+ }
1299
+ }
1300
+ }
1301
+
1302
+ .gallery.slider {
1303
+ z-index: 1;
1304
+ perspective: 100px;
1305
+ perspective-origin: center center;
1306
+ user-select: none;
1307
+ -webkit-user-select: none;
1308
+ -webkit-tap-highlight-color: transparent;
1309
+ transform: translateZ(0px);
1310
+ position: relative;
1311
+ margin: 0 auto;
1312
+
1313
+ :global(.carousel) {
1314
+ position: relative;
1315
+ z-index: 1;
1316
+ height: 100%;
1317
+ }
1318
+ .bg {
1319
+ position: absolute;
1320
+ top: 0;
1321
+ left: 0;
1322
+ right: 0;
1323
+ bottom: 0;
1324
+ z-index: -1;
1325
+ }
1326
+
1327
+ .controls {
1328
+ display: flex;
1329
+ position: absolute;
1330
+ align-items: center;
1331
+ bottom: 0;
1332
+ left: 0;
1333
+ right: 0;
1334
+ width: 100%;
1335
+ height: 3.5rem;
1336
+ gap: 1rem;
1337
+ padding: 0 1rem;
1338
+ pointer-events: none;
1339
+ :global(.button) {
1340
+ pointer-events: all;
1341
+ }
1342
+ .spacer {
1343
+ flex: 1;
1344
+ }
1345
+ }
1346
+
1347
+ nav.pages {
1348
+ position: absolute;
1349
+ z-index: 2;
1350
+ display: flex;
1351
+ align-items: center;
1352
+ gap: 0.125rem;
1353
+ padding-inline: 0.25rem;
1354
+ /* Show/hide via CSS (no remount) so the entry animation can't replay
1355
+ as num_pages settles during PDF load. */
1356
+ transform-origin: center center;
1357
+ opacity: 0;
1358
+ scale: 0.6;
1359
+ visibility: hidden;
1360
+ transition:
1361
+ opacity 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
1362
+ scale 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
1363
+ visibility 0s linear 300ms;
1364
+ &.shown {
1365
+ opacity: 1;
1366
+ scale: 1;
1367
+ visibility: visible;
1368
+ transition:
1369
+ opacity 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
1370
+ scale 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
1371
+ visibility 0s linear 0s;
1372
+ }
1373
+ /* Force a high-contrast white pill so the page numbers are
1374
+ readable on top of any media (dark images, videos, PDFs,
1375
+ panoramas) inside a modal. The inherited button colors
1376
+ pick this up via --color-text. */
1377
+ color: #1e293b;
1378
+ --color-text: #1e293b;
1379
+ --color-action: #3b82f6;
1380
+ background-color: #ffffff;
1381
+ border-radius: 9999px;
1382
+ box-shadow:
1383
+ 0 4px 12px rgb(0 0 0 / 0.18),
1384
+ 0 1px 3px rgb(0 0 0 / 0.12);
1385
+ border: none;
1386
+ outline: none;
1387
+ font-weight: 500;
1388
+
1389
+ .page-counter {
1390
+ display: inline-flex;
1391
+ align-items: baseline;
1392
+ gap: 0.25rem;
1393
+ min-width: 4rem;
1394
+ justify-content: center;
1395
+ padding-inline: 0.5rem;
1396
+ font-variant-numeric: tabular-nums;
1397
+ font-size: 1.35rem;
1398
+ line-height: 1;
1399
+ user-select: none;
1400
+ }
1401
+
1402
+ .page-separator {
1403
+ opacity: 0.4;
1404
+ font-weight: 400;
1405
+ }
1406
+
1407
+ /* The chevron Buttons inherit `--color-text` from the pill. The
1408
+ `disabled` attribute lives on the inner <button> rendered by
1409
+ `Button.svelte`, so dim the wrapper via :has() — the transparent
1410
+ variant has no background of its own to dim otherwise. */
1411
+ :global(.button:has(> button[disabled])) {
1412
+ opacity: 0.3;
1413
+ cursor: default;
1414
+ }
1415
+ }
1416
+
1417
+ .pagination {
1418
+ z-index: 1;
1419
+ }
1420
+
1421
+ :global(.play svg.progress) {
1422
+ stroke-width: 3;
1423
+ stroke-dasharray: 163.41;
1424
+ stroke-dashoffset: calc(163.41 - (163.41 * var(--progress, 0)));
1425
+ transition: stroke-dashoffset var(--speed, 300ms) linear;
1426
+ width: 70%;
1427
+ height: 70%;
1428
+ stroke: white;
1429
+ fill: none;
1430
+ transform: rotate(-90deg) !important;
1431
+ transform-origin: center center;
1432
+ opacity: 0.25;
1433
+ position: absolute;
1434
+ top: 15%;
1435
+ left: 15%;
1436
+ }
1437
+ }
1438
+
1439
+ .gallery.slider:not(.modal) {
1440
+ container: gallery-slider / inline-size;
1441
+ :global(.carousel) {
1442
+ aspect-ratio: var(--aspect-ratio);
1443
+ }
1444
+ .bg {
1445
+ background-color: var(--color-bg-muted, var(--bg-high));
1446
+ }
1447
+ &.size-0 {
1448
+ --aspect-ratio: 1 / 1;
1449
+ height: auto;
1450
+ &.radius-1 {
1451
+ .bg,
1452
+ :global(.carousel) {
1453
+ @container (min-width: 80ch) {
1454
+ border-radius: var(--radius-lg, 0.5rem);
1455
+ @supports (corner-shape: squircle) {
1456
+ corner-shape: squircle;
1457
+ border-radius: calc(var(--radius-lg, 0.5rem) * var(--squircle-ratio, 2));
1458
+ }
1459
+ }
1460
+ }
1461
+ }
1462
+ &.radius-2 {
1463
+ .bg,
1464
+ :global(.carousel) {
1465
+ @container (min-width: 80ch) {
1466
+ border-radius: var(--_rxl);
1467
+ @supports (corner-shape: squircle) {
1468
+ corner-shape: squircle;
1469
+ border-radius: calc(var(--_rxl) * var(--squircle-ratio, 2));
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+ &.radius-3 {
1475
+ .bg,
1476
+ :global(.carousel) {
1477
+ @container (min-width: 80ch) {
1478
+ border-radius: var(--_r2xl);
1479
+ @supports (corner-shape: squircle) {
1480
+ corner-shape: squircle;
1481
+ border-radius: calc(var(--_r2xl) * var(--squircle-ratio, 2));
1482
+ }
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1487
+ &.size-1 {
1488
+ &.radius-1 {
1489
+ .bg,
1490
+ :global(.carousel) {
1491
+ @container (min-width: 1200px) {
1492
+ border-radius: var(--_rxl);
1493
+ @supports (corner-shape: squircle) {
1494
+ corner-shape: squircle;
1495
+ border-radius: calc(var(--_rxl) * var(--squircle-ratio, 2));
1496
+ }
1497
+ }
1498
+ }
1499
+ }
1500
+ &.radius-2 {
1501
+ .bg,
1502
+ :global(.carousel) {
1503
+ @container (min-width: 1200px) {
1504
+ border-radius: var(--_r2xl);
1505
+ @supports (corner-shape: squircle) {
1506
+ corner-shape: squircle;
1507
+ border-radius: calc(var(--_r2xl) * var(--squircle-ratio, 2));
1508
+ }
1509
+ }
1510
+ }
1511
+ }
1512
+ &.radius-3 {
1513
+ .bg,
1514
+ :global(.carousel) {
1515
+ @container (min-width: 1200px) {
1516
+ border-radius: var(--_r3xl);
1517
+ @supports (corner-shape: squircle) {
1518
+ corner-shape: squircle;
1519
+ border-radius: calc(var(--_r3xl) * var(--squircle-ratio, 2));
1520
+ }
1521
+ }
1522
+ }
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ .gallery.slider:not(.modal).controls-overlay {
1528
+ &.radius-1 {
1529
+ .controls {
1530
+ border-top-left-radius: var(--_rxl);
1531
+ border-top-right-radius: var(--_rxl);
1532
+ @supports (corner-shape: squircle) {
1533
+ corner-shape: squircle;
1534
+ border-top-left-radius: calc(var(--_rxl) * var(--squircle-ratio, 2));
1535
+ border-top-right-radius: calc(var(--_rxl) * var(--squircle-ratio, 2));
1536
+ }
1537
+ }
1538
+ }
1539
+ &.radius-2 {
1540
+ .controls {
1541
+ border-top-left-radius: var(--_r2xl);
1542
+ border-top-right-radius: var(--_r2xl);
1543
+ border-bottom-left-radius: var(--_r2xl);
1544
+ border-bottom-right-radius: var(--_r2xl);
1545
+ @supports (corner-shape: squircle) {
1546
+ corner-shape: squircle;
1547
+ border-top-left-radius: calc(var(--_r2xl) * var(--squircle-ratio, 2));
1548
+ border-top-right-radius: calc(var(--_r2xl) * var(--squircle-ratio, 2));
1549
+ border-bottom-left-radius: calc(var(--_r2xl) * var(--squircle-ratio, 2));
1550
+ border-bottom-right-radius: calc(var(--_r2xl) * var(--squircle-ratio, 2));
1551
+ }
1552
+ }
1553
+ }
1554
+ &.radius-3 {
1555
+ .controls {
1556
+ border-top-left-radius: var(--_r3xl);
1557
+ border-top-right-radius: var(--_r3xl);
1558
+ border-bottom-left-radius: var(--_r3xl);
1559
+ border-bottom-right-radius: var(--_r3xl);
1560
+ @supports (corner-shape: squircle) {
1561
+ corner-shape: squircle;
1562
+ border-top-left-radius: calc(var(--_r3xl) * var(--squircle-ratio, 2));
1563
+ border-top-right-radius: calc(var(--_r3xl) * var(--squircle-ratio, 2));
1564
+ border-bottom-left-radius: calc(var(--_r3xl) * var(--squircle-ratio, 2));
1565
+ border-bottom-right-radius: calc(var(--_r3xl) * var(--squircle-ratio, 2));
1566
+ }
1567
+ }
1568
+ }
1569
+ .controls {
1570
+ z-index: 2;
1571
+ justify-content: center;
1572
+ > .spacer {
1573
+ display: none;
1574
+ }
1575
+ background-color: color-mix(
1576
+ in oklch,
1577
+ var(--color-bg-muted, var(--bg-high)),
1578
+ transparent 30%
1579
+ );
1580
+ backdrop-filter: blur(10px);
1581
+ width: fit-content;
1582
+ left: 50%;
1583
+ transform: translateX(-50%);
1584
+ padding: 0.25rem;
1585
+ gap: 0.5rem;
1586
+ }
1587
+ }
1588
+ .gallery.slider.modal.controls-overlay {
1589
+ .controls {
1590
+ z-index: 3;
1591
+ bottom: 0.5rem;
1592
+ gap: 0.5rem;
1593
+ nav.pages {
1594
+ left: 50%;
1595
+ transform: translate3d(-50%, 0, 0);
1596
+ bottom: 3.5rem;
1597
+ z-index: 2;
1598
+ }
1599
+ /* The lightbox backdrop is always dark regardless of light/dark mode,
1600
+ so don't let the transparent Button variant's light-dark() tokens
1601
+ leak in (its light-mode --color-text-active is near-black). Pin a
1602
+ fixed dark-surface palette: white icons on a translucent white pill
1603
+ that gets *brighter* on hover, never darker. */
1604
+ :global(> .button),
1605
+ .actions :global(> .button) {
1606
+ --color-text: rgb(255 255 255 / 0.92);
1607
+ --color-text-active: #ffffff;
1608
+ --color-text-disabled: rgb(255 255 255 / 0.4);
1609
+ --color-bg: rgb(255 255 255 / 0.12);
1610
+ --color-bg-active: rgb(255 255 255 / 0.28);
1611
+ }
1612
+ :global(> .button button),
1613
+ .actions :global(> .button button) {
1614
+ backdrop-filter: blur(8px);
1615
+ }
1616
+ .pagination {
1617
+ margin: 0 0.5rem;
1618
+ }
1619
+ .actions {
1620
+ position: absolute;
1621
+ bottom: 0;
1622
+ left: 5rem;
1623
+ z-index: 2;
1624
+ display: flex;
1625
+ :global(> .button button svg) {
1626
+ filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.95))
1627
+ drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.25))
1628
+ drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.5));
1629
+ }
1630
+ }
1631
+
1632
+ @media (min-width: 768px) {
1633
+ nav.pages {
1634
+ bottom: 1rem;
1635
+ }
1636
+ display: block;
1637
+ position: static;
1638
+ height: unset;
1639
+ width: unset;
1640
+ bottom: unset;
1641
+ left: unset;
1642
+ right: unset;
1643
+ :global(> .button) {
1644
+ bottom: 1rem;
1645
+ position: absolute;
1646
+ }
1647
+ :global(> .button button svg) {
1648
+ filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.95))
1649
+ drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.25))
1650
+ drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.5));
1651
+ }
1652
+ .actions {
1653
+ position: absolute;
1654
+ top: 4.75rem;
1655
+ right: 0.875rem;
1656
+ left: unset;
1657
+ bottom: unset;
1658
+ display: flex;
1659
+ flex-direction: column;
1660
+ justify-content: center;
1661
+ align-items: center;
1662
+ }
1663
+ .pagination {
1664
+ position: absolute;
1665
+ font-size: 1.5rem;
1666
+ top: 0.875rem;
1667
+ right: 4.25rem;
1668
+ height: 3rem;
1669
+ margin: 0;
1670
+ display: flex;
1671
+ align-items: center;
1672
+ text-align: right;
1673
+ z-index: 2;
1674
+ backdrop-filter: blur(5px);
1675
+ padding: 0 1rem;
1676
+ border-radius: 9999px;
1677
+ }
1678
+ :global(.play) {
1679
+ z-index: 2;
1680
+ }
1681
+ :global(.close) {
1682
+ right: 0.875rem;
1683
+ top: 0.875rem;
1684
+ z-index: 2;
1685
+ /* Nudge the dismiss control up a touch. The icon button sizes
1686
+ off its own font-size (× --control-height-ratio), so bumping
1687
+ the font scales the pill AND the icon together, keeping the
1688
+ translucent-white-pill language intact. */
1689
+ font-size: 1.15rem;
1690
+ }
1691
+ :global(.prev),
1692
+ :global(.next) {
1693
+ top: 50%;
1694
+ transform: translateY(-50%);
1695
+ bottom: unset;
1696
+ width: 4.5rem;
1697
+ height: min(20rem, 50%);
1698
+ aspect-ratio: auto;
1699
+ box-shadow: none;
1700
+ cursor: pointer;
1701
+ z-index: 2;
1702
+ /* These stretch into full-height edge strips on desktop — hit
1703
+ areas, not pills. Keep them invisible at rest (no white slab,
1704
+ no blur) and brighten with only a soft white wash on hover. */
1705
+ --color-bg: transparent;
1706
+ --color-bg-active: rgb(255 255 255 / 0.08);
1707
+ }
1708
+ :global(.prev button),
1709
+ :global(.next button) {
1710
+ backdrop-filter: none;
1711
+ }
1712
+ :global(.prev button svg),
1713
+ :global(.next button svg) {
1714
+ height: 80%;
1715
+ width: 80%;
1716
+ }
1717
+ :global(.prev) {
1718
+ left: 0;
1719
+ padding-left: 0.5rem;
1720
+ }
1721
+ :global(.next) {
1722
+ right: 0;
1723
+ padding-right: 0.5rem;
1724
+ }
1725
+ }
1726
+ }
1727
+ }
1728
+ .gallery.slider.controls-inline {
1729
+ nav.pages {
1730
+ top: -4.5rem;
1731
+ bottom: unset;
1732
+ }
1733
+ .controls {
1734
+ justify-content: center;
1735
+ gap: 0;
1736
+ top: 100%;
1737
+ bottom: unset;
1738
+ @container (max-width: 500px) {
1739
+ gap: 0.5rem;
1740
+ padding: 0;
1741
+ .pagination {
1742
+ padding: 0 0.5rem;
1743
+ font-size: 1rem;
1744
+ }
1745
+ }
1746
+ .spacer {
1747
+ display: none;
1748
+ flex: 0;
1749
+ }
1750
+ }
1751
+ .controls > .pagination {
1752
+ color: var(--color-text, var(--text));
1753
+ margin: 0 1rem;
1754
+ font-weight: normal;
1755
+ font-size: 1.5rem;
1756
+ }
1757
+ .controls > :global(.play) {
1758
+ svg.progress {
1759
+ stroke: var(--color-text-muted);
1760
+ opacity: 1;
1761
+ }
1762
+ }
1763
+ }
1764
+
1765
+ .gallery.slider.modal {
1766
+ position: fixed;
1767
+ z-index: var(--layer-modal, 1000);
1768
+ top: 0;
1769
+ left: 0;
1770
+ bottom: 0;
1771
+ width: 100%;
1772
+ height: 100%;
1773
+ .bg {
1774
+ background-color: rgba(0, 0, 0, 0.85);
1775
+ @supports (backdrop-filter: blur(25px)) {
1776
+ filter: blur(0px);
1777
+ backdrop-filter: blur(25px);
1778
+ background-color: rgba(0, 0, 0, 0.7);
1779
+ }
1780
+ }
1781
+ &.fullscreen {
1782
+ .bg {
1783
+ background-color: black !important;
1784
+ opacity: 1 !important;
1785
+ }
1786
+ }
1787
+ :global(.carousel) {
1788
+ aspect-ratio: var(--aspect-ratio);
1789
+ height: calc(100% - 4.5rem);
1790
+ }
1791
+ @media (min-width: 768px) {
1792
+ :global(.carousel) {
1793
+ height: 100%;
1794
+ }
1795
+ }
1796
+
1797
+ .fullscreen-name {
1798
+ position: absolute;
1799
+ bottom: 0;
1800
+ left: 0;
1801
+ right: 0;
1802
+ z-index: 2;
1803
+ pointer-events: none;
1804
+ &::before {
1805
+ content: '';
1806
+ position: absolute;
1807
+ inset: 0;
1808
+ background-image: linear-gradient(to top, rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0));
1809
+ z-index: -1;
1810
+ }
1811
+ text-align: center;
1812
+ color: white;
1813
+ font-size: var(--text-base, 1rem);
1814
+ padding: 6rem 1rem 5rem;
1815
+ text-shadow:
1816
+ 0 1px 2px rgba(0, 0, 0, 0.5),
1817
+ 0 0 10px rgba(0, 0, 0, 0.3);
1818
+ white-space: nowrap;
1819
+ overflow: hidden;
1820
+ text-overflow: ellipsis;
1821
+ transition: opacity 1000ms ease;
1822
+ @starting-style {
1823
+ opacity: 0;
1824
+ }
1825
+ }
1826
+ @media (min-width: 768px) {
1827
+ .fullscreen-name {
1828
+ padding: 3rem 5rem 1rem;
1829
+ }
1830
+ }
1831
+
1832
+ .pagination {
1833
+ font-size: 1.3rem;
1834
+ color: white;
1835
+ text-shadow:
1836
+ 1px 1px 0 rgba(0, 0, 0, 0.5),
1837
+ 1px 1px 10px rgba(0, 0, 0, 0.5),
1838
+ 0 0 40px black;
1839
+ font-weight: bold;
1840
+ }
1841
+ }
1842
+
1843
+ .gallery.display-masonry {
1844
+ width: 100%;
1845
+ margin-inline: auto;
1846
+ display: grid;
1847
+ grid-auto-flow: dense;
1848
+ gap: var(--gallery-gap, 12px);
1849
+ padding: 0 var(--gallery-gap, 12px) var(--gallery-gap, 12px);
1850
+ max-width: 2160px;
1851
+ grid-auto-rows: 1fr;
1852
+ --cols: 4;
1853
+ --cols-per-image: 8;
1854
+ --cols-desktop: calc(var(--cols) * var(--cols-per-image));
1855
+ --cols-tablet: max(
1856
+ var(--cols-per-image),
1857
+ calc(
1858
+ round((var(--cols-desktop) * 0.75) / var(--cols-per-image), 1) *
1859
+ var(--cols-per-image)
1860
+ )
1861
+ );
1862
+ --cols-phone: max(
1863
+ var(--cols-per-image),
1864
+ calc(
1865
+ round((var(--cols-desktop) * 0.45) / var(--cols-per-image), 1) *
1866
+ var(--cols-per-image)
1867
+ )
1868
+ );
1869
+ grid-template-columns: repeat(var(--cols-phone), minmax(0, 1fr));
1870
+ @container (min-width: 768px) {
1871
+ grid-template-columns: repeat(var(--cols-tablet), minmax(0, 1fr));
1872
+ }
1873
+ @container (min-width: 1024px) {
1874
+ grid-template-columns: repeat(var(--cols-desktop), minmax(0, 1fr));
1875
+ }
1876
+
1877
+ &.radius-0 {
1878
+ --radius-lg: 0px;
1879
+ }
1880
+
1881
+ &.size-00 {
1882
+ --cols: 8;
1883
+ .name {
1884
+ font-size: 0.8rem;
1885
+ }
1886
+ &.radius-1 {
1887
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.4);
1888
+ }
1889
+ &.radius-2 {
1890
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.6);
1891
+ }
1892
+ &.radius-3 {
1893
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.8);
1894
+ }
1895
+ &.spacing-0 {
1896
+ --gallery-gap: 0px;
1897
+ }
1898
+ &.spacing-1 {
1899
+ --gallery-gap: min(6px, 1.5cqw);
1900
+ }
1901
+ &.spacing-2 {
1902
+ --gallery-gap: min(10px, 1.5cqw);
1903
+ }
1904
+ &.spacing-3 {
1905
+ --gallery-gap: min(16px, 1.5cqw);
1906
+ }
1907
+ }
1908
+ &.size-0 {
1909
+ --cols: 6;
1910
+ .name {
1911
+ font-size: 0.9rem;
1912
+ }
1913
+ &.radius-1 {
1914
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.45);
1915
+ }
1916
+ &.radius-2 {
1917
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.65);
1918
+ }
1919
+ &.radius-3 {
1920
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.85);
1921
+ }
1922
+ &.spacing-0 {
1923
+ --gallery-gap: 0px;
1924
+ }
1925
+ &.spacing-1 {
1926
+ --gallery-gap: min(8px, 1.5cqw);
1927
+ }
1928
+ &.spacing-2 {
1929
+ --gallery-gap: min(14px, 1.5cqw);
1930
+ }
1931
+ &.spacing-3 {
1932
+ --gallery-gap: min(20px, 1.5cqw);
1933
+ }
1934
+ }
1935
+ &.size-1 {
1936
+ --cols: 4;
1937
+ &.radius-1 {
1938
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.45);
1939
+ }
1940
+ &.radius-2 {
1941
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.8);
1942
+ }
1943
+ &.radius-3 {
1944
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.1);
1945
+ }
1946
+ &.spacing-0 {
1947
+ --gallery-gap: 0px;
1948
+ }
1949
+ &.spacing-1 {
1950
+ --gallery-gap: min(10px, 3cqw);
1951
+ }
1952
+ &.spacing-2 {
1953
+ --gallery-gap: min(16px, 3cqw);
1954
+ }
1955
+ &.spacing-3 {
1956
+ --gallery-gap: min(24px, 3cqw);
1957
+ }
1958
+ }
1959
+ &.size-2 {
1960
+ --cols: 3;
1961
+ &.radius-1 {
1962
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
1963
+ }
1964
+ &.radius-2 {
1965
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1);
1966
+ }
1967
+ &.radius-3 {
1968
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.4);
1969
+ }
1970
+ &.spacing-0 {
1971
+ --gallery-gap: 0px;
1972
+ }
1973
+ &.spacing-1 {
1974
+ --gallery-gap: min(12px, 5cqw);
1975
+ }
1976
+ &.spacing-2 {
1977
+ --gallery-gap: min(20px, 5cqw);
1978
+ }
1979
+ &.spacing-3 {
1980
+ --gallery-gap: min(28px, 5cqw);
1981
+ }
1982
+ }
1983
+ &.size-3 {
1984
+ --cols: 2;
1985
+ &.radius-1 {
1986
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
1987
+ }
1988
+ &.radius-2 {
1989
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.1);
1990
+ }
1991
+ &.radius-3 {
1992
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.5);
1993
+ }
1994
+ &.spacing-0 {
1995
+ --gallery-gap: 0px;
1996
+ }
1997
+ &.spacing-1 {
1998
+ --gallery-gap: min(14px, 5cqw);
1999
+ }
2000
+ &.spacing-2 {
2001
+ --gallery-gap: min(24px, 5cqw);
2002
+ }
2003
+ &.spacing-3 {
2004
+ --gallery-gap: min(34px, 5cqw);
2005
+ }
2006
+ }
2007
+
2008
+ &::before {
2009
+ content: '';
2010
+ width: 0;
2011
+ padding-bottom: 100%;
2012
+ grid-row: 1 / 1;
2013
+ grid-column: 1 / 1;
2014
+ aspect-ratio: 1;
2015
+ }
2016
+
2017
+ > .gallery-item {
2018
+ grid-column-end: span var(--cols-per-image);
2019
+ grid-row-end: span max(1, calc(var(--cols-per-image) * 1 / var(--ratio, 1)));
2020
+ &.favorite {
2021
+ --zero-if-one-column: min(
2022
+ round(down, calc((var(--cols-phone) / var(--cols-per-image)) - 1), 1),
2023
+ 1
2024
+ );
2025
+ --favorite-cols: calc(
2026
+ var(--cols-per-image) + var(--cols-per-image) * var(--zero-if-one-column)
2027
+ );
2028
+ grid-column-end: span calc(var(--cols-per-image) * 2);
2029
+ grid-row-end: span
2030
+ max(1, round(down, calc(var(--cols-per-image) * 2 / var(--ratio, 1)), 1));
2031
+ @container (max-width: 767px) {
2032
+ grid-column-end: span var(--favorite-cols);
2033
+ grid-row-end: span
2034
+ max(1, round(down, calc(var(--favorite-cols) * 1 / var(--ratio, 1)), 1));
2035
+ }
2036
+ }
2037
+ &:first-child {
2038
+ grid-column-start: 1;
2039
+ grid-row-start: 1;
2040
+ }
2041
+ }
2042
+ }
2043
+
2044
+ .gallery.display-grid {
2045
+ width: 100%;
2046
+ margin-inline: auto;
2047
+ display: grid;
2048
+ grid-auto-flow: dense;
2049
+ gap: var(--gallery-gap, 12px);
2050
+ padding: 0 var(--gallery-gap, 12px) var(--gallery-gap, 12px);
2051
+ max-width: 2160px;
2052
+ grid-auto-rows: 1fr;
2053
+ container: gallery-grid / inline-size;
2054
+
2055
+ &.radius-0 {
2056
+ --radius-lg: 0px;
2057
+ }
2058
+ &.size-00 {
2059
+ grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
2060
+ .name {
2061
+ font-size: 0.7rem;
2062
+ }
2063
+ &.radius-1 {
2064
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.3);
2065
+ }
2066
+ &.radius-2 {
2067
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.45);
2068
+ }
2069
+ &.radius-3 {
2070
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.6);
2071
+ }
2072
+ &.spacing-0 {
2073
+ --gallery-gap: 0px;
2074
+ }
2075
+ &.spacing-1 {
2076
+ --gallery-gap: min(6px, 1.5cqw);
2077
+ }
2078
+ &.spacing-2 {
2079
+ --gallery-gap: min(10px, 1.5cqw);
2080
+ }
2081
+ &.spacing-3 {
2082
+ --gallery-gap: min(16px, 1.5cqw);
2083
+ }
2084
+ @container (min-width: 768px) {
2085
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
2086
+ }
2087
+ }
2088
+ &.size-0 {
2089
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
2090
+ .name {
2091
+ font-size: 0.8rem;
2092
+ }
2093
+ &.radius-1 {
2094
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.35);
2095
+ }
2096
+ &.radius-2 {
2097
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.55);
2098
+ }
2099
+ &.radius-3 {
2100
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.75);
2101
+ }
2102
+ &.spacing-0 {
2103
+ --gallery-gap: 0px;
2104
+ }
2105
+ &.spacing-1 {
2106
+ --gallery-gap: min(8px, 1.5cqw);
2107
+ }
2108
+ &.spacing-2 {
2109
+ --gallery-gap: min(14px, 1.5cqw);
2110
+ }
2111
+ &.spacing-3 {
2112
+ --gallery-gap: min(20px, 1.5cqw);
2113
+ }
2114
+ @container (min-width: 768px) {
2115
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
2116
+ }
2117
+ }
2118
+ &.size-1 {
2119
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
2120
+ &.radius-1 {
2121
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.45);
2122
+ }
2123
+ &.radius-2 {
2124
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.8);
2125
+ }
2126
+ &.radius-3 {
2127
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.1);
2128
+ }
2129
+ &.spacing-0 {
2130
+ --gallery-gap: 0px;
2131
+ }
2132
+ &.spacing-1 {
2133
+ --gallery-gap: min(10px, 3cqw);
2134
+ }
2135
+ &.spacing-2 {
2136
+ --gallery-gap: min(16px, 3cqw);
2137
+ }
2138
+ &.spacing-3 {
2139
+ --gallery-gap: min(24px, 3cqw);
2140
+ }
2141
+ @container (min-width: 768px) {
2142
+ grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
2143
+ }
2144
+ }
2145
+ &.size-2 {
2146
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
2147
+ &.radius-1 {
2148
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
2149
+ }
2150
+ &.radius-2 {
2151
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1);
2152
+ }
2153
+ &.radius-3 {
2154
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.4);
2155
+ }
2156
+ &.spacing-0 {
2157
+ --gallery-gap: 0px;
2158
+ }
2159
+ &.spacing-1 {
2160
+ --gallery-gap: min(12px, 5cqw);
2161
+ }
2162
+ &.spacing-2 {
2163
+ --gallery-gap: min(20px, 5cqw);
2164
+ }
2165
+ &.spacing-3 {
2166
+ --gallery-gap: min(28px, 5cqw);
2167
+ }
2168
+ @container (min-width: 768px) {
2169
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
2170
+ }
2171
+ }
2172
+ &.size-3 {
2173
+ grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
2174
+ &.radius-1 {
2175
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
2176
+ }
2177
+ &.radius-2 {
2178
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.1);
2179
+ }
2180
+ &.radius-3 {
2181
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.5);
2182
+ }
2183
+ &.spacing-0 {
2184
+ --gallery-gap: 0px;
2185
+ }
2186
+ &.spacing-1 {
2187
+ --gallery-gap: min(14px, 5cqw);
2188
+ }
2189
+ &.spacing-2 {
2190
+ --gallery-gap: min(24px, 5cqw);
2191
+ }
2192
+ &.spacing-3 {
2193
+ --gallery-gap: min(34px, 5cqw);
2194
+ }
2195
+ @container (min-width: 768px) {
2196
+ grid-template-columns: repeat(auto-fill, minmax(520px, 1fr));
2197
+ }
2198
+ }
2199
+
2200
+ &::before {
2201
+ content: '';
2202
+ width: 0;
2203
+ padding-bottom: 100%;
2204
+ grid-row: 1 / 1;
2205
+ grid-column: 1 / 1;
2206
+ aspect-ratio: 1;
2207
+ }
2208
+
2209
+ > .gallery-item {
2210
+ grid-row-end: span 1;
2211
+ grid-column-end: span 1;
2212
+ &.favorite {
2213
+ grid-column-end: span 2;
2214
+ grid-row-end: span 2;
2215
+ }
2216
+ &:first-child {
2217
+ grid-column-start: 1;
2218
+ grid-row-start: 1;
2219
+ }
2220
+ }
2221
+ }
2222
+
2223
+ .gallery.display-masonry-row {
2224
+ --row-height: 250px;
2225
+ --max-row-height: 350px;
2226
+ display: flex;
2227
+ flex-wrap: wrap;
2228
+ justify-content: center;
2229
+ gap: var(--gallery-gap, 12px);
2230
+ padding: 0 var(--gallery-gap, 12px) var(--gallery-gap, 12px);
2231
+ max-width: 2160px;
2232
+ margin-inline: auto;
2233
+
2234
+ &.radius-0 {
2235
+ --radius-lg: 0px;
2236
+ }
2237
+ &.size-00 {
2238
+ --row-height: 45px;
2239
+ --max-row-height: 75px;
2240
+ &.radius-1 {
2241
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.4);
2242
+ }
2243
+ &.radius-2 {
2244
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
2245
+ }
2246
+ &.radius-3 {
2247
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.65);
2248
+ }
2249
+ &.spacing-0 {
2250
+ --gallery-gap: 0px;
2251
+ }
2252
+ &.spacing-1 {
2253
+ --gallery-gap: min(6px, 1.5cqw);
2254
+ }
2255
+ &.spacing-2 {
2256
+ --gallery-gap: min(10px, 1.5cqw);
2257
+ }
2258
+ &.spacing-3 {
2259
+ --gallery-gap: min(16px, 1.5cqw);
2260
+ }
2261
+ @container (min-width: 768px) {
2262
+ --row-height: 100px;
2263
+ --max-row-height: 140px;
2264
+ }
2265
+ }
2266
+ &.size-0 {
2267
+ --row-height: 70px;
2268
+ --max-row-height: 110px;
2269
+ &.radius-1 {
2270
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.45);
2271
+ }
2272
+ &.radius-2 {
2273
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.55);
2274
+ }
2275
+ &.radius-3 {
2276
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.75);
2277
+ }
2278
+ &.spacing-0 {
2279
+ --gallery-gap: 0px;
2280
+ }
2281
+ &.spacing-1 {
2282
+ --gallery-gap: min(8px, 1.5cqw);
2283
+ }
2284
+ &.spacing-2 {
2285
+ --gallery-gap: min(14px, 1.5cqw);
2286
+ }
2287
+ &.spacing-3 {
2288
+ --gallery-gap: min(20px, 1.5cqw);
2289
+ }
2290
+ @container (min-width: 768px) {
2291
+ --row-height: 150px;
2292
+ --max-row-height: 200px;
2293
+ }
2294
+ }
2295
+ &.size-1 {
2296
+ --row-height: 100px;
2297
+ --max-row-height: 150px;
2298
+ &.radius-1 {
2299
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.35);
2300
+ }
2301
+ &.radius-2 {
2302
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
2303
+ }
2304
+ &.radius-3 {
2305
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.7);
2306
+ }
2307
+
2308
+ &.spacing-0 {
2309
+ --gallery-gap: 0px;
2310
+ }
2311
+ &.spacing-1 {
2312
+ --gallery-gap: min(10px, 3cqw);
2313
+ }
2314
+ &.spacing-2 {
2315
+ --gallery-gap: min(16px, 3cqw);
2316
+ }
2317
+ &.spacing-3 {
2318
+ --gallery-gap: min(24px, 3cqw);
2319
+ }
2320
+ @container (min-width: 768px) {
2321
+ --row-height: 200px;
2322
+ --max-row-height: 300px;
2323
+ &.radius-1 {
2324
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.45);
2325
+ }
2326
+ &.radius-2 {
2327
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.8);
2328
+ }
2329
+ &.radius-3 {
2330
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.1);
2331
+ }
2332
+ }
2333
+ }
2334
+ &.size-2 {
2335
+ --row-height: 250px;
2336
+ --max-row-height: 350px;
2337
+ &.radius-1 {
2338
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
2339
+ }
2340
+ &.radius-2 {
2341
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1);
2342
+ }
2343
+ &.radius-3 {
2344
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.4);
2345
+ }
2346
+ &.spacing-0 {
2347
+ --gallery-gap: 0px;
2348
+ }
2349
+ &.spacing-1 {
2350
+ --gallery-gap: min(12px, 5cqw);
2351
+ }
2352
+ &.spacing-2 {
2353
+ --gallery-gap: min(20px, 5cqw);
2354
+ }
2355
+ &.spacing-3 {
2356
+ --gallery-gap: min(28px, 5cqw);
2357
+ }
2358
+ @container (max-width: 767px) {
2359
+ --max-row-height: 700px;
2360
+ }
2361
+ }
2362
+ &.size-3 {
2363
+ --row-height: 350px;
2364
+ --max-row-height: 500px;
2365
+ &.radius-1 {
2366
+ --radius-lg: calc(var(--gallery-gap, 12px) * 0.5);
2367
+ }
2368
+ &.radius-2 {
2369
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.1);
2370
+ }
2371
+ &.radius-3 {
2372
+ --radius-lg: calc(var(--gallery-gap, 12px) * 1.5);
2373
+ }
2374
+ &.spacing-0 {
2375
+ --gallery-gap: 0px;
2376
+ }
2377
+ &.spacing-1 {
2378
+ --gallery-gap: min(14px, 5cqw);
2379
+ }
2380
+ &.spacing-2 {
2381
+ --gallery-gap: min(24px, 5cqw);
2382
+ }
2383
+ &.spacing-3 {
2384
+ --gallery-gap: min(34px, 5cqw);
2385
+ }
2386
+ @container (max-width: 767px) {
2387
+ --max-row-height: 850px;
2388
+ }
2389
+ }
2390
+
2391
+ > .gallery-item {
2392
+ flex-basis: calc(var(--ratio, 1) * var(--row-height));
2393
+ flex-grow: calc(var(--ratio, 1) * 100);
2394
+ aspect-ratio: var(--ratio, 1);
2395
+ max-height: var(--max-row-height);
2396
+ max-width: calc(var(--ratio, 1) * var(--max-row-height) * 1.1);
2397
+ }
2398
+ }
2399
+
2400
+ .gallery.display-list {
2401
+ display: flex;
2402
+ flex-direction: column;
2403
+ max-width: 600px;
2404
+ margin-inline: auto;
2405
+ /* Size-driven scale: row height, horizontal padding, body text and the
2406
+ square thumbnail all key off these so smaller sizes feel uniformly
2407
+ tighter and larger sizes uniformly roomier. */
2408
+ --line-height: 3.5rem;
2409
+ --list-pad: 9px;
2410
+ --list-text-size: 1rem;
2411
+ --thumb-size: calc(var(--line-height) * 0.72);
2412
+
2413
+ &.radius-0 {
2414
+ --radius-lg: 0px;
2415
+ .info {
2416
+ border-radius: 0px !important;
2417
+ }
2418
+ }
2419
+ &.radius-1 {
2420
+ --radius-lg: calc(var(--line-height) * 0.1);
2421
+ }
2422
+ &.radius-2 {
2423
+ --radius-lg: calc(var(--line-height) * 0.15);
2424
+ }
2425
+ &.radius-3 {
2426
+ --radius-lg: calc(var(--line-height) * 0.2);
2427
+ }
2428
+ &.size-00 {
2429
+ --line-height: 2.25rem;
2430
+ --list-pad: 3px;
2431
+ --list-text-size: 0.78rem;
2432
+ }
2433
+ &.size-0 {
2434
+ --line-height: 2.75rem;
2435
+ --list-pad: 6px;
2436
+ --list-text-size: 0.88rem;
2437
+ }
2438
+ &.size-1 {
2439
+ --line-height: 3.5rem;
2440
+ --list-pad: 9px;
2441
+ --list-text-size: 1rem;
2442
+ }
2443
+ &.size-2 {
2444
+ --line-height: 4.5rem;
2445
+ --list-pad: 13px;
2446
+ --list-text-size: 1.15rem;
2447
+ }
2448
+ &.size-3 {
2449
+ --line-height: 5.5rem;
2450
+ --list-pad: 17px;
2451
+ --list-text-size: 1.3rem;
2452
+ }
2453
+
2454
+ > .list-item {
2455
+ display: flex;
2456
+ height: var(--line-height);
2457
+ align-items: center;
2458
+ /* No padding here: the clickable .info fills the row edge-to-edge so
2459
+ hover/press feedback can never appear on a non-clickable sliver.
2460
+ The content inset lives on .info instead. */
2461
+ padding: 0;
2462
+ position: relative;
2463
+ z-index: 1;
2464
+ overflow: hidden;
2465
+ /* Drives the subtle 3D push of the inner row on press, matching ListItem. */
2466
+ perspective: 100px;
2467
+
2468
+ /* Subtle text-tinted divider between rows, matching ListItem. */
2469
+ &::after {
2470
+ content: '';
2471
+ position: absolute;
2472
+ top: 0;
2473
+ left: var(--list-pad);
2474
+ right: var(--list-pad);
2475
+ border-top: solid 1px color-mix(in oklch, transparent, var(--color-text) 6%);
2476
+ pointer-events: none;
2477
+ z-index: 1;
2478
+ }
2479
+ &:first-child::after {
2480
+ content: none;
2481
+ }
2482
+
2483
+ .info {
2484
+ display: flex;
2485
+ flex: 1;
2486
+ min-width: 0;
2487
+ cursor: pointer;
2488
+ align-items: center;
2489
+ position: relative;
2490
+ overflow: hidden;
2491
+ /* Fill the full row height + width so the whole visible area of the
2492
+ row is the click target — feedback and clickability stay in sync. */
2493
+ align-self: stretch;
2494
+ padding: 0 calc(var(--list-pad) - 2px);
2495
+ border-radius: calc(var(--radius-lg) + var(--list-pad));
2496
+ @supports (corner-shape: squircle) {
2497
+ corner-shape: squircle;
2498
+ border-radius: calc(
2499
+ (var(--radius-lg) + var(--list-pad)) * var(--squircle-ratio, 2)
2500
+ );
2501
+ }
2502
+ /* Press effect, matching ListItem's translate-on-active. */
2503
+ transition: translate 200ms ease;
2504
+
2505
+ /* Hover/active background overlay (text @ 6%), matching ListItem.
2506
+ It lives on .info (the click target), not the row, so it only
2507
+ ever shows where the user can actually click. */
2508
+ &::before {
2509
+ content: '';
2510
+ position: absolute;
2511
+ top: 2px;
2512
+ left: 0;
2513
+ right: 0;
2514
+ bottom: 2px;
2515
+ background-color: var(--color-text);
2516
+ opacity: 0;
2517
+ border-radius: var(--radius-lg);
2518
+ @supports (corner-shape: squircle) {
2519
+ corner-shape: squircle;
2520
+ border-radius: calc(var(--radius-lg) * var(--squircle-ratio, 2));
2521
+ }
2522
+ z-index: -1;
2523
+ transition: opacity 300ms ease;
2524
+ }
2525
+ @media (hover: hover) and (pointer: fine) {
2526
+ &:hover {
2527
+ &::before {
2528
+ opacity: 0.06;
2529
+ transition: opacity 0ms ease;
2530
+ }
2531
+ /* Image gently zooms inside its (overflow-hidden) square. */
2532
+ .thumbnail-img {
2533
+ transform: scale(1.08);
2534
+ }
2535
+ .thumbnail-blur {
2536
+ transform: scale(1.32);
2537
+ }
2538
+ }
2539
+ }
2540
+ &:active {
2541
+ translate: 0px 2px clamp(-4px, calc(0.2em - 12px), -2px);
2542
+ }
2543
+ &:focus-visible {
2544
+ outline: none;
2545
+ &::after {
2546
+ content: '';
2547
+ position: absolute;
2548
+ inset: 2px 0;
2549
+ border: solid 1px var(--color-border-active);
2550
+ border-radius: var(--radius-lg);
2551
+ @supports (corner-shape: squircle) {
2552
+ corner-shape: squircle;
2553
+ border-radius: calc(var(--radius-lg) * var(--squircle-ratio, 2));
2554
+ }
2555
+ pointer-events: none;
2556
+ }
2557
+ }
2558
+ .thumbnail {
2559
+ flex-shrink: 0;
2560
+ width: var(--thumb-size);
2561
+ height: var(--thumb-size);
2562
+ position: relative;
2563
+ color: white;
2564
+ display: flex;
2565
+ align-items: center;
2566
+ justify-content: center;
2567
+ border-radius: var(--radius-lg);
2568
+ @supports (corner-shape: squircle) {
2569
+ corner-shape: squircle;
2570
+ border-radius: calc(var(--radius-lg) * var(--squircle-ratio, 2));
2571
+ }
2572
+ overflow: hidden;
2573
+ /* The square box behind contain-fit thumbnails so images of
2574
+ any aspect ratio read as consistently sized tiles. Falls back
2575
+ to a text-tinted fill so the square stays visible even when
2576
+ the surface tokens aren't defined by the host theme. */
2577
+ background-color: var(
2578
+ --color-bg-muted,
2579
+ color-mix(in oklch, var(--color-text, gray) 20%, transparent)
2580
+ );
2581
+ .thumbnail-blur,
2582
+ .thumbnail-img,
2583
+ .thumbnail-placeholder {
2584
+ position: absolute;
2585
+ inset: 0;
2586
+ width: 100%;
2587
+ height: 100%;
2588
+ object-fit: contain;
2589
+ display: block;
2590
+ transition: transform 350ms cubic-bezier(0.22, 1, 0.36, 1);
2591
+ }
2592
+ .thumbnail-blur {
2593
+ z-index: 0;
2594
+ object-fit: cover;
2595
+ filter: blur(8px) saturate(1.2);
2596
+ transform: scale(1.2);
2597
+ pointer-events: none;
2598
+ user-select: none;
2599
+ }
2600
+ .thumbnail-placeholder {
2601
+ z-index: 0;
2602
+ background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));
2603
+ }
2604
+ .thumbnail-img {
2605
+ z-index: 1;
2606
+ opacity: 1;
2607
+ transition:
2608
+ opacity 300ms ease,
2609
+ transform 350ms cubic-bezier(0.22, 1, 0.36, 1);
2610
+ &.no-blur {
2611
+ transition:
2612
+ opacity 200ms ease,
2613
+ transform 350ms cubic-bezier(0.22, 1, 0.36, 1);
2614
+ }
2615
+ &.fading {
2616
+ opacity: 0;
2617
+ transition: none;
2618
+ }
2619
+ }
2620
+ .icon {
2621
+ position: absolute;
2622
+ width: clamp(1rem, calc(var(--line-height) * 0.42), 2rem);
2623
+ height: clamp(1rem, calc(var(--line-height) * 0.42), 2rem);
2624
+ top: 50%;
2625
+ left: 50%;
2626
+ translate: -50% -50%;
2627
+ z-index: 2;
2628
+ background-color: rgba(0, 0, 0, 0.6);
2629
+ backdrop-filter: blur(10px);
2630
+ border-radius: 100%;
2631
+ display: flex;
2632
+ align-items: center;
2633
+ justify-content: center;
2634
+ :global(svg) {
2635
+ width: 80%;
2636
+ height: 80%;
2637
+ }
2638
+ }
2639
+ }
2640
+ .name {
2641
+ flex: 1;
2642
+ min-width: 0;
2643
+ padding-left: calc(var(--list-pad) + 0.5rem);
2644
+ padding-right: var(--list-pad);
2645
+ font-size: var(--list-text-size);
2646
+ color: var(--color-text);
2647
+ text-overflow: ellipsis;
2648
+ white-space: nowrap;
2649
+ overflow: hidden;
2650
+ }
2651
+ }
2652
+ /* Action buttons sit outside the clickable .info; give them the
2653
+ row inset that used to come from .list-item's own padding. */
2654
+ > .actions {
2655
+ flex-shrink: 0;
2656
+ margin-right: var(--list-pad);
2657
+ }
2658
+ }
2659
+ }
2660
+
2661
+ /* Reduced layouts when only a few images */
2662
+ .gallery.display-grid,
2663
+ .gallery.display-masonry,
2664
+ .gallery.display-masonry-row {
2665
+ &:has(.gallery-item:first-child:nth-last-child(1)) {
2666
+ display: flex;
2667
+ flex-wrap: wrap;
2668
+ align-items: start;
2669
+ justify-content: center;
2670
+ &:before {
2671
+ display: none;
2672
+ }
2673
+ &.radius-1 {
2674
+ --radius-lg: var(--radius-md, 0.375rem);
2675
+ }
2676
+ &.radius-2 {
2677
+ --radius-lg: var(--radius-lg, 0.5rem);
2678
+ }
2679
+ &.radius-3 {
2680
+ --radius-lg: var(--_rxl);
2681
+ }
2682
+ > .gallery-item {
2683
+ flex-basis: 100%;
2684
+ flex-grow: 1;
2685
+ max-width: none;
2686
+ max-height: none;
2687
+ aspect-ratio: max(var(--ratio, 1), 0.85);
2688
+ .name {
2689
+ font-size: 1rem;
2690
+ }
2691
+ }
2692
+ @container (min-width: 768px) {
2693
+ &.radius-1 {
2694
+ --radius-lg: var(--_rxl);
2695
+ }
2696
+ &.radius-2 {
2697
+ --radius-lg: var(--_r2xl);
2698
+ }
2699
+ &.radius-3 {
2700
+ --radius-lg: var(--_r3xl);
2701
+ }
2702
+ }
2703
+ }
2704
+
2705
+ &:has(.gallery-item:first-child:nth-last-child(2)) {
2706
+ display: flex;
2707
+ flex-wrap: wrap;
2708
+ align-items: start;
2709
+ justify-content: center;
2710
+ &:before {
2711
+ display: none;
2712
+ }
2713
+ &.radius-1 {
2714
+ --radius-lg: var(--radius-md, 0.375rem);
2715
+ }
2716
+ &.radius-2 {
2717
+ --radius-lg: var(--radius-lg, 0.5rem);
2718
+ }
2719
+ &.radius-3 {
2720
+ --radius-lg: var(--_rxl);
2721
+ }
2722
+ > .gallery-item {
2723
+ flex-basis: 0;
2724
+ flex-grow: 1;
2725
+ max-width: none;
2726
+ max-height: none;
2727
+ aspect-ratio: max(var(--ratio, 1), 0.75);
2728
+ .name {
2729
+ font-size: 1rem;
2730
+ }
2731
+ }
2732
+ @container (min-width: 768px) {
2733
+ &.radius-1 {
2734
+ --radius-lg: var(--radius-lg, 0.5rem);
2735
+ }
2736
+ &.radius-2 {
2737
+ --radius-lg: var(--_rxl);
2738
+ }
2739
+ &.radius-3 {
2740
+ --radius-lg: var(--_r2xl);
2741
+ }
2742
+ }
2743
+ }
2744
+
2745
+ &:has(.gallery-item:first-child:nth-last-child(3)) {
2746
+ display: flex;
2747
+ flex-wrap: wrap;
2748
+ align-items: start;
2749
+ justify-content: center;
2750
+ &:before {
2751
+ display: none;
2752
+ }
2753
+ &.radius-1 {
2754
+ --radius-lg: var(--radius-sm, 0.25rem);
2755
+ }
2756
+ &.radius-2 {
2757
+ --radius-lg: var(--radius-md, 0.375rem);
2758
+ }
2759
+ &.radius-3 {
2760
+ --radius-lg: var(--radius-lg, 0.5rem);
2761
+ }
2762
+ > .gallery-item {
2763
+ flex-basis: 0;
2764
+ flex-grow: 1;
2765
+ max-width: none;
2766
+ max-height: none;
2767
+ aspect-ratio: max(var(--ratio, 1), 0.75);
2768
+ .name {
2769
+ font-size: 1rem;
2770
+ }
2771
+ }
2772
+ @container (min-width: 768px) {
2773
+ &.radius-1 {
2774
+ --radius-lg: var(--radius-md, 0.375rem);
2775
+ }
2776
+ &.radius-2 {
2777
+ --radius-lg: var(--radius-lg, 0.5rem);
2778
+ }
2779
+ &.radius-3 {
2780
+ --radius-lg: var(--_rxl);
2781
+ }
2782
+ }
2783
+ &.size-2 {
2784
+ @container (max-width: 767px) {
2785
+ > .gallery-item {
2786
+ flex-basis: 100%;
2787
+ }
2788
+ }
2789
+ }
2790
+ }
2791
+
2792
+ &:has(.gallery-item:first-child:nth-last-child(4)) {
2793
+ &.size-0 {
2794
+ display: flex;
2795
+ flex-wrap: wrap;
2796
+ align-items: start;
2797
+ justify-content: center;
2798
+ &:before {
2799
+ display: none;
2800
+ }
2801
+ &.radius-1 {
2802
+ --radius-lg: var(--radius-sm, 0.25rem);
2803
+ }
2804
+ &.radius-2 {
2805
+ --radius-lg: var(--radius-md, 0.375rem);
2806
+ }
2807
+ &.radius-3 {
2808
+ --radius-lg: var(--radius-lg, 0.5rem);
2809
+ }
2810
+ > .gallery-item {
2811
+ flex-basis: 0;
2812
+ flex-grow: 1;
2813
+ max-width: none;
2814
+ max-height: none;
2815
+ aspect-ratio: max(var(--ratio, 1), 0.75);
2816
+ .name {
2817
+ font-size: 1rem;
2818
+ }
2819
+ }
2820
+ @container (min-width: 768px) {
2821
+ &.radius-1 {
2822
+ --radius-lg: var(--radius-md, 0.375rem);
2823
+ }
2824
+ &.radius-2 {
2825
+ --radius-lg: var(--radius-lg, 0.5rem);
2826
+ }
2827
+ &.radius-3 {
2828
+ --radius-lg: var(--_rxl);
2829
+ }
2830
+ }
2831
+ }
2832
+ &.size-2 {
2833
+ @container (max-width: 767px) {
2834
+ > .gallery-item {
2835
+ flex-basis: 100%;
2836
+ }
2837
+ }
2838
+ }
2839
+ }
2840
+
2841
+ &:has(.gallery-item:first-child:nth-last-child(5)).size-0:not(.display-grid) {
2842
+ display: flex;
2843
+ flex-wrap: wrap;
2844
+ align-items: start;
2845
+ justify-content: center;
2846
+ &:before {
2847
+ display: none;
2848
+ }
2849
+ &.radius-1 {
2850
+ --radius-lg: var(--radius-sm, 0.25rem);
2851
+ }
2852
+ &.radius-2 {
2853
+ --radius-lg: var(--radius-md, 0.375rem);
2854
+ }
2855
+ &.radius-3 {
2856
+ --radius-lg: var(--radius-lg, 0.5rem);
2857
+ }
2858
+ > .gallery-item {
2859
+ flex-basis: 0;
2860
+ flex-grow: 1;
2861
+ max-width: none;
2862
+ max-height: none;
2863
+ aspect-ratio: max(var(--ratio, 1), 0.75);
2864
+ .name {
2865
+ font-size: 1rem;
2866
+ }
2867
+ }
2868
+ @container (min-width: 768px) {
2869
+ &.radius-1 {
2870
+ --radius-lg: var(--radius-md, 0.375rem);
2871
+ }
2872
+ &.radius-2 {
2873
+ --radius-lg: var(--radius-lg, 0.5rem);
2874
+ }
2875
+ &.radius-3 {
2876
+ --radius-lg: var(--_rxl);
2877
+ }
2878
+ }
2879
+ }
2880
+ }
2881
+ </style>