@swiftbrowser/web 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 (84) hide show
  1. package/README.md +600 -0
  2. package/app/index.html +125 -0
  3. package/index.html +56 -0
  4. package/node/dist/node/apps.d.ts +58 -0
  5. package/node/dist/node/apps.d.ts.map +1 -0
  6. package/node/dist/node/apps.js +60 -0
  7. package/node/dist/node/apps.js.map +1 -0
  8. package/node/dist/node/config.d.ts +42 -0
  9. package/node/dist/node/config.d.ts.map +1 -0
  10. package/node/dist/node/config.js +99 -0
  11. package/node/dist/node/config.js.map +1 -0
  12. package/node/dist/node/examples.d.ts +39 -0
  13. package/node/dist/node/examples.d.ts.map +1 -0
  14. package/node/dist/node/examples.js +116 -0
  15. package/node/dist/node/examples.js.map +1 -0
  16. package/node/dist/node/index.d.ts +47 -0
  17. package/node/dist/node/index.d.ts.map +1 -0
  18. package/node/dist/node/index.js +52 -0
  19. package/node/dist/node/index.js.map +1 -0
  20. package/node/dist/plugins/app-manifests.d.ts +61 -0
  21. package/node/dist/plugins/app-manifests.d.ts.map +1 -0
  22. package/node/dist/plugins/app-manifests.js +168 -0
  23. package/node/dist/plugins/app-manifests.js.map +1 -0
  24. package/node/dist/plugins/asset-catalogs.d.ts +119 -0
  25. package/node/dist/plugins/asset-catalogs.d.ts.map +1 -0
  26. package/node/dist/plugins/asset-catalogs.js +488 -0
  27. package/node/dist/plugins/asset-catalogs.js.map +1 -0
  28. package/node/dist/plugins/bundle-resources.d.ts +49 -0
  29. package/node/dist/plugins/bundle-resources.d.ts.map +1 -0
  30. package/node/dist/plugins/bundle-resources.js +172 -0
  31. package/node/dist/plugins/bundle-resources.js.map +1 -0
  32. package/node/dist/plugins/swift-wasm.d.ts +33 -0
  33. package/node/dist/plugins/swift-wasm.d.ts.map +1 -0
  34. package/node/dist/plugins/swift-wasm.js +241 -0
  35. package/node/dist/plugins/swift-wasm.js.map +1 -0
  36. package/node/dist/src/assets.d.ts +60 -0
  37. package/node/dist/src/assets.d.ts.map +1 -0
  38. package/node/dist/src/assets.js +54 -0
  39. package/node/dist/src/assets.js.map +1 -0
  40. package/node/dist/src/devEvents.d.ts +31 -0
  41. package/node/dist/src/devEvents.d.ts.map +1 -0
  42. package/node/dist/src/devEvents.js +9 -0
  43. package/node/dist/src/devEvents.js.map +1 -0
  44. package/package.json +56 -0
  45. package/plugins/app-manifests.ts +192 -0
  46. package/plugins/asset-catalogs.ts +558 -0
  47. package/plugins/bundle-resources.ts +195 -0
  48. package/plugins/swift-wasm.ts +259 -0
  49. package/public/_headers +23 -0
  50. package/public/apple-touch-icon.png +0 -0
  51. package/public/icon.svg +14 -0
  52. package/public/manifest.webmanifest +14 -0
  53. package/src/animation.ts +521 -0
  54. package/src/assets.ts +86 -0
  55. package/src/chartDom.ts +481 -0
  56. package/src/devEvents.ts +35 -0
  57. package/src/deviceMode.ts +80 -0
  58. package/src/ghost.ts +25 -0
  59. package/src/interaction.ts +1113 -0
  60. package/src/landing.css +261 -0
  61. package/src/landing.ts +141 -0
  62. package/src/layout/chart.ts +477 -0
  63. package/src/layout/controls.ts +116 -0
  64. package/src/layout/engine.ts +3581 -0
  65. package/src/layout/index.ts +91 -0
  66. package/src/layout/measure.ts +86 -0
  67. package/src/layout/text.ts +124 -0
  68. package/src/layout/types.ts +153 -0
  69. package/src/layout/typography.ts +188 -0
  70. package/src/layoutDom.ts +549 -0
  71. package/src/layoutEvents.ts +110 -0
  72. package/src/locale.ts +64 -0
  73. package/src/main.ts +554 -0
  74. package/src/mock.ts +752 -0
  75. package/src/nodeQueries.ts +60 -0
  76. package/src/protocol.ts +1421 -0
  77. package/src/renderer.ts +2882 -0
  78. package/src/styles.css +2834 -0
  79. package/src/styles.ts +373 -0
  80. package/src/swipe.ts +110 -0
  81. package/src/symbols.ts +595 -0
  82. package/src/typography.ts +76 -0
  83. package/src/vite-env.d.ts +26 -0
  84. package/src/wasm.ts +400 -0
package/src/styles.css ADDED
@@ -0,0 +1,2834 @@
1
+ /* ---------------------------------------------------------------------------
2
+ iOS design tokens. Light values live on the device frame; dark ones switch
3
+ with the frame's data-theme so the page chrome and the "phone" are
4
+ independent.
5
+ ------------------------------------------------------------------------- */
6
+
7
+ :root {
8
+ /* The UINavigationController push curve: fast start, long settle. */
9
+ --sb-nav-easing: cubic-bezier(0.32, 0.72, 0, 1);
10
+ --sb-font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter,
11
+ system-ui, sans-serif;
12
+ /* Body size for the default (Large) Dynamic Type size; the device frame
13
+ overrides this with the current size (see .device below). */
14
+ --sb-font-size-body: 17px;
15
+
16
+ /* Page chrome (not iOS) */
17
+ --page-bg: #e9e9ee;
18
+ --page-fg: #1c1c1e;
19
+ --page-muted: #6e6e73;
20
+ --page-border: rgba(0, 0, 0, 0.08);
21
+ --page-surface: rgba(255, 255, 255, 0.8);
22
+ --page-error-bg: #ffe5e5;
23
+ --page-error-fg: #a1000d;
24
+
25
+ /* Device dimensions (iPhone 15 Pro logical points) */
26
+ --device-screen-width: 393px;
27
+ --device-screen-height: 852px;
28
+ --device-bezel: 12px;
29
+ --device-radius: 58px;
30
+ --device-safe-top: 59px;
31
+ --device-safe-bottom: 34px;
32
+
33
+ /* The real safe-area insets (device mode). env() is substituted at
34
+ computed-value time, so main.ts reads these with getComputedStyle; they
35
+ are 0 outside standalone (home screen) mode and landscape. */
36
+ --sb-inset-top: env(safe-area-inset-top, 0px);
37
+ --sb-inset-right: env(safe-area-inset-right, 0px);
38
+ --sb-inset-bottom: env(safe-area-inset-bottom, 0px);
39
+ --sb-inset-left: env(safe-area-inset-left, 0px);
40
+ }
41
+
42
+ @media (prefers-color-scheme: dark) {
43
+ :root {
44
+ --page-bg: #111114;
45
+ --page-fg: #f2f2f7;
46
+ --page-muted: #98989f;
47
+ --page-border: rgba(255, 255, 255, 0.1);
48
+ --page-surface: rgba(28, 28, 30, 0.85);
49
+ --page-error-bg: #3a0b10;
50
+ --page-error-fg: #ff8a8a;
51
+ }
52
+ }
53
+
54
+ .device {
55
+ /* Dynamic Type: main.ts writes --sb-ts-<style>-size / -lh / -scale on the
56
+ device frame for the selected size (typography.ts); the fallbacks are the
57
+ Large values. Unstyled text is `body` and scales with it. */
58
+ --sb-font-size-body: var(--sb-ts-body-size, 17px);
59
+ }
60
+
61
+ .device,
62
+ .device[data-theme="light"] {
63
+ color-scheme: light;
64
+ --sb-color-primary: #000000;
65
+ --sb-color-secondary: rgba(60, 60, 67, 0.6);
66
+ --sb-color-accent: #007aff;
67
+ --sb-color-system-background: #ffffff;
68
+ --sb-color-secondary-system-background: #f2f2f7;
69
+ --sb-color-separator: rgba(60, 60, 67, 0.29);
70
+ --sb-color-tertiary-label: rgba(60, 60, 67, 0.3);
71
+ --sb-color-quaternary-label: rgba(60, 60, 67, 0.18);
72
+ --sb-color-red: #ff3b30;
73
+ --sb-color-green: #34c759;
74
+ --sb-color-grouped-background: #f2f2f7;
75
+ --sb-color-grouped-cell: #ffffff;
76
+ --sb-color-fill: rgba(120, 120, 128, 0.2);
77
+ --sb-color-secondary-fill: rgba(120, 120, 128, 0.16);
78
+ --sb-color-tertiary-fill: rgba(118, 118, 128, 0.12);
79
+ --sb-color-quaternary-fill: rgba(116, 116, 128, 0.08);
80
+ /* Phase 6 materials: rgb components of the color blurred over the backdrop. */
81
+ --sb-color-material-base: 255, 255, 255;
82
+ --sb-color-switch-off: #e9e9ea;
83
+ --sb-color-field-border: #c6c6c8;
84
+ --sb-color-field-background: #ffffff;
85
+ --sb-color-placeholder: rgba(60, 60, 67, 0.3);
86
+ --sb-color-sheet-background: #ffffff;
87
+ --sb-color-grabber: rgba(60, 60, 67, 0.3);
88
+ /* Phase 4 */
89
+ --sb-color-tabbar-background: rgba(249, 249, 249, 0.94);
90
+ --sb-color-segment-track: rgba(118, 118, 128, 0.12);
91
+ --sb-color-segment-selected: #ffffff;
92
+ --sb-color-menu-background: rgba(250, 250, 250, 0.96);
93
+ }
94
+
95
+ .device[data-theme="dark"] {
96
+ color-scheme: dark;
97
+ --sb-color-primary: #ffffff;
98
+ --sb-color-secondary: rgba(235, 235, 245, 0.6);
99
+ --sb-color-accent: #0a84ff;
100
+ --sb-color-system-background: #000000;
101
+ --sb-color-secondary-system-background: #1c1c1e;
102
+ --sb-color-separator: rgba(84, 84, 88, 0.6);
103
+ --sb-color-tertiary-label: rgba(235, 235, 245, 0.3);
104
+ --sb-color-quaternary-label: rgba(235, 235, 245, 0.16);
105
+ --sb-color-red: #ff453a;
106
+ --sb-color-green: #30d158;
107
+ --sb-color-grouped-background: #000000;
108
+ --sb-color-grouped-cell: #1c1c1e;
109
+ --sb-color-fill: rgba(120, 120, 128, 0.36);
110
+ --sb-color-secondary-fill: rgba(120, 120, 128, 0.32);
111
+ --sb-color-tertiary-fill: rgba(118, 118, 128, 0.24);
112
+ --sb-color-quaternary-fill: rgba(118, 118, 128, 0.18);
113
+ --sb-color-material-base: 30, 30, 30;
114
+ --sb-color-switch-off: #39393d;
115
+ --sb-color-field-border: #3a3a3c;
116
+ --sb-color-field-background: #1c1c1e;
117
+ --sb-color-placeholder: rgba(235, 235, 245, 0.3);
118
+ /* Sheets float above the window and take the elevated system background. */
119
+ --sb-color-sheet-background: #1c1c1e;
120
+ --sb-color-grabber: rgba(235, 235, 245, 0.3);
121
+ /* Phase 4 */
122
+ --sb-color-tabbar-background: rgba(29, 29, 29, 0.94);
123
+ --sb-color-segment-track: rgba(118, 118, 128, 0.24);
124
+ --sb-color-segment-selected: #636366;
125
+ --sb-color-menu-background: rgba(37, 37, 37, 0.96);
126
+ }
127
+
128
+ /* ---------------------------------------------------------------------------
129
+ Page layout
130
+ ------------------------------------------------------------------------- */
131
+
132
+ *,
133
+ *::before,
134
+ *::after {
135
+ box-sizing: border-box;
136
+ }
137
+
138
+ html,
139
+ body {
140
+ margin: 0;
141
+ min-height: 100%;
142
+ }
143
+
144
+ body {
145
+ display: flex;
146
+ flex-direction: column;
147
+ min-height: 100vh;
148
+ background: var(--page-bg);
149
+ color: var(--page-fg);
150
+ font-family: var(--sb-font-family);
151
+ font-size: 14px;
152
+ -webkit-font-smoothing: antialiased;
153
+ }
154
+
155
+ .toolbar {
156
+ position: sticky;
157
+ top: 0;
158
+ z-index: 10;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 10px;
162
+ padding: 10px 16px;
163
+ background: var(--page-surface);
164
+ border-bottom: 1px solid var(--page-border);
165
+ backdrop-filter: blur(16px);
166
+ }
167
+
168
+ .toolbar-brand {
169
+ font-weight: 600;
170
+ letter-spacing: -0.01em;
171
+ }
172
+
173
+ .toolbar-sep {
174
+ width: 1px;
175
+ height: 18px;
176
+ background: var(--page-border);
177
+ }
178
+
179
+ .toolbar-label {
180
+ color: var(--page-muted);
181
+ }
182
+
183
+ .toolbar-value {
184
+ font-variant-numeric: tabular-nums;
185
+ padding: 2px 8px;
186
+ border-radius: 6px;
187
+ background: var(--page-border);
188
+ }
189
+
190
+ .toolbar-spacer {
191
+ flex: 1 1 0;
192
+ }
193
+
194
+ /* Hot-reload indicator, driven by the dev server's sb:build-* events. */
195
+ .toolbar-build {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 6px;
199
+ color: var(--page-muted);
200
+ }
201
+
202
+ .toolbar-build[hidden] {
203
+ display: none;
204
+ }
205
+
206
+ .toolbar-build::before {
207
+ content: "";
208
+ width: 10px;
209
+ height: 10px;
210
+ border-radius: 50%;
211
+ border: 1.5px solid currentColor;
212
+ border-top-color: transparent;
213
+ animation: sb-spin 0.9s linear infinite;
214
+ }
215
+
216
+ .toolbar-build[data-state="error"] {
217
+ color: var(--page-error-fg);
218
+ }
219
+
220
+ .toolbar-build[data-state="error"]::before {
221
+ animation: none;
222
+ border-color: currentColor;
223
+ }
224
+
225
+ .toolbar-button {
226
+ font: inherit;
227
+ padding: 6px 12px;
228
+ border: 1px solid var(--page-border);
229
+ border-radius: 8px;
230
+ background: transparent;
231
+ color: var(--page-fg);
232
+ cursor: pointer;
233
+ }
234
+
235
+ .toolbar-button:hover {
236
+ background: var(--page-border);
237
+ }
238
+
239
+ .toolbar-button[aria-pressed="true"] {
240
+ background: var(--page-fg);
241
+ color: var(--page-bg);
242
+ border-color: transparent;
243
+ }
244
+
245
+ .toolbar-select {
246
+ font: inherit;
247
+ padding: 5px 8px;
248
+ border: 1px solid var(--page-border);
249
+ border-radius: 8px;
250
+ background: transparent;
251
+ color: var(--page-fg);
252
+ cursor: pointer;
253
+ }
254
+
255
+ .stage {
256
+ display: flex;
257
+ flex-direction: column;
258
+ align-items: center;
259
+ gap: 16px;
260
+ padding: 32px 16px 48px;
261
+ }
262
+
263
+ .status {
264
+ max-width: 560px;
265
+ margin: 0;
266
+ padding: 10px 14px;
267
+ border-radius: 10px;
268
+ background: var(--page-error-bg);
269
+ color: var(--page-error-fg);
270
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
271
+ font-size: 12px;
272
+ white-space: pre-wrap;
273
+ word-break: break-word;
274
+ }
275
+
276
+ /* ---------------------------------------------------------------------------
277
+ Device frame
278
+ ------------------------------------------------------------------------- */
279
+
280
+ .device {
281
+ position: relative;
282
+ flex: 0 0 auto;
283
+ width: calc(var(--device-screen-width) + 2 * var(--device-bezel));
284
+ height: calc(var(--device-screen-height) + 2 * var(--device-bezel));
285
+ padding: var(--device-bezel);
286
+ border-radius: var(--device-radius);
287
+ background: linear-gradient(160deg, #3a3a3f 0%, #141417 45%, #0c0c0e 100%);
288
+ box-shadow:
289
+ 0 0 0 1.5px #5a5a60,
290
+ 0 0 0 3px #1a1a1d,
291
+ 0 30px 60px -20px rgba(0, 0, 0, 0.55),
292
+ 0 12px 24px -12px rgba(0, 0, 0, 0.4);
293
+ }
294
+
295
+ /* Side buttons */
296
+ .device::before,
297
+ .device::after {
298
+ content: "";
299
+ position: absolute;
300
+ background: #2a2a2e;
301
+ border-radius: 2px;
302
+ }
303
+
304
+ .device::before {
305
+ /* volume up + down (one element, drawn with a gradient gap) */
306
+ left: -4.5px;
307
+ top: 180px;
308
+ width: 3px;
309
+ height: 160px;
310
+ background: linear-gradient(#2a2a2e 0 40%, transparent 40% 60%, #2a2a2e 60%);
311
+ }
312
+
313
+ .device::after {
314
+ /* power */
315
+ right: -4.5px;
316
+ top: 220px;
317
+ width: 3px;
318
+ height: 100px;
319
+ }
320
+
321
+ .device-screen {
322
+ position: relative;
323
+ width: var(--device-screen-width);
324
+ height: var(--device-screen-height);
325
+ overflow: hidden;
326
+ border-radius: calc(var(--device-radius) - var(--device-bezel));
327
+ background: var(--sb-color-system-background);
328
+ color: var(--sb-color-primary);
329
+ font-size: var(--sb-font-size-body);
330
+ /* Unitless so fixed-size fonts keep proportional line boxes; text styles
331
+ set their iOS line height in px on the styled box. */
332
+ line-height: 1.3;
333
+ }
334
+
335
+ .dynamic-island {
336
+ position: absolute;
337
+ top: 11px;
338
+ left: 50%;
339
+ width: 126px;
340
+ height: 37px;
341
+ transform: translateX(-50%);
342
+ border-radius: 20px;
343
+ background: #000;
344
+ z-index: 3;
345
+ }
346
+
347
+ .status-bar {
348
+ position: absolute;
349
+ top: 0;
350
+ left: 0;
351
+ right: 0;
352
+ height: var(--device-safe-top);
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: space-between;
356
+ padding: 0 32px 6px 40px;
357
+ pointer-events: none;
358
+ z-index: 2;
359
+ color: var(--sb-color-primary);
360
+ }
361
+
362
+ .status-time {
363
+ font-size: 17px;
364
+ font-weight: 600;
365
+ letter-spacing: -0.02em;
366
+ font-variant-numeric: tabular-nums;
367
+ }
368
+
369
+ .status-glyphs {
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 7px;
373
+ }
374
+
375
+ .glyph {
376
+ display: block;
377
+ fill: currentColor;
378
+ }
379
+
380
+ .glyph-battery .battery-shell {
381
+ fill: none;
382
+ stroke: currentColor;
383
+ stroke-opacity: 0.4;
384
+ }
385
+
386
+ .glyph-battery .battery-cap {
387
+ fill: currentColor;
388
+ fill-opacity: 0.4;
389
+ }
390
+
391
+ .home-indicator {
392
+ position: absolute;
393
+ bottom: 8px;
394
+ left: 50%;
395
+ width: 139px;
396
+ height: 5px;
397
+ transform: translateX(-50%);
398
+ border-radius: 3px;
399
+ background: var(--sb-color-primary);
400
+ pointer-events: none;
401
+ z-index: 2;
402
+ }
403
+
404
+ /* Root container (element id 0). The layout engine positions every element
405
+ absolutely in screen coordinates (safe areas are part of the proposal), so
406
+ the root is just a clipping box. */
407
+ .screen {
408
+ position: absolute;
409
+ inset: 0;
410
+ overflow: hidden;
411
+ z-index: 1;
412
+ }
413
+
414
+ .device[data-state="loading"] .screen::after {
415
+ content: "";
416
+ position: absolute;
417
+ left: 50%;
418
+ top: 50%;
419
+ width: 22px;
420
+ height: 22px;
421
+ margin: -11px 0 0 -11px;
422
+ border-radius: 50%;
423
+ border: 2.5px solid var(--sb-color-secondary);
424
+ border-top-color: transparent;
425
+ animation: sb-spin 0.9s linear infinite;
426
+ }
427
+
428
+ @keyframes sb-spin {
429
+ to {
430
+ transform: rotate(360deg);
431
+ }
432
+ }
433
+
434
+ /* ---------------------------------------------------------------------------
435
+ Device mode (html[data-sb-mode="device"], see src/deviceMode.ts)
436
+
437
+ On a real phone there is no toolbar and no fake bezel: the OS draws the
438
+ status bar and #screen fills the viewport. The layout engine still gets a
439
+ status-bar / home-indicator safe area, but from env(safe-area-inset-*)
440
+ (via --sb-inset-*), so it is only non-zero in standalone mode / landscape.
441
+ ------------------------------------------------------------------------- */
442
+
443
+ html[data-sb-mode="device"] {
444
+ height: 100%;
445
+ overflow: hidden;
446
+ overscroll-behavior: none;
447
+ /* No double-tap zoom, no text inflation in landscape. */
448
+ touch-action: manipulation;
449
+ -webkit-text-size-adjust: 100%;
450
+ text-size-adjust: 100%;
451
+ }
452
+
453
+ html[data-sb-mode="device"] body {
454
+ min-height: 100dvh;
455
+ overflow: hidden;
456
+ overscroll-behavior: none;
457
+ touch-action: manipulation;
458
+ background: var(--sb-color-system-background, var(--page-bg));
459
+ }
460
+
461
+ html[data-sb-mode="device"] .toolbar {
462
+ display: none;
463
+ }
464
+
465
+ html[data-sb-mode="device"] .stage {
466
+ padding: 0;
467
+ gap: 0;
468
+ }
469
+
470
+ html[data-sb-mode="device"] .device {
471
+ position: fixed;
472
+ top: 0;
473
+ left: 0;
474
+ width: 100dvw;
475
+ height: 100dvh;
476
+ padding: 0;
477
+ border-radius: 0;
478
+ background: var(--sb-color-system-background);
479
+ box-shadow: none;
480
+ }
481
+
482
+ html[data-sb-mode="device"] .device::before,
483
+ html[data-sb-mode="device"] .device::after,
484
+ html[data-sb-mode="device"] .dynamic-island,
485
+ html[data-sb-mode="device"] .status-bar,
486
+ html[data-sb-mode="device"] .home-indicator {
487
+ display: none;
488
+ }
489
+
490
+ html[data-sb-mode="device"] .device-screen {
491
+ width: 100dvw;
492
+ height: 100dvh;
493
+ border-radius: 0;
494
+ }
495
+
496
+ /* The error banner floats over the app, clear of the home indicator. */
497
+ html[data-sb-mode="device"] .status {
498
+ position: fixed;
499
+ left: 16px;
500
+ right: 16px;
501
+ bottom: calc(16px + var(--sb-inset-bottom));
502
+ z-index: 10;
503
+ max-width: none;
504
+ max-height: 40dvh;
505
+ overflow: auto;
506
+ }
507
+
508
+ /* ---------------------------------------------------------------------------
509
+ Rendered elements
510
+
511
+ Every protocol element is an absolutely positioned box whose left/top/
512
+ width/height come from the layout engine (src/layoutDom.ts). Slots of
513
+ compound kinds (.sb-slot) stay static so their children are positioned
514
+ against the compound element itself; the content boxes of lists and scroll
515
+ views are the exception (they scroll).
516
+ ------------------------------------------------------------------------- */
517
+
518
+ .screen [data-sb-id],
519
+ .screen [data-sb-ghost] {
520
+ position: absolute;
521
+ box-sizing: border-box;
522
+ margin: 0;
523
+ }
524
+
525
+ .sb-slot {
526
+ position: static;
527
+ }
528
+
529
+ /* Text: the engine hands over the exact lines, joined with newlines.
530
+ Phase 6: a gradient foreground is painted as the text's own background
531
+ clipped to the glyphs. The styled box sets `--sb-text-gradient`,
532
+ `--sb-text-clip` and `--sb-text-color` (inherited by every text below it;
533
+ a nested flat foreground resets them to `initial`). With the properties
534
+ unset the declarations are invalid at computed-value time: the image and
535
+ clip fall back, and `color` becomes `unset`, i.e. inherits as before. */
536
+ .sb-text {
537
+ display: block;
538
+ white-space: pre;
539
+ overflow: visible;
540
+ text-align: left;
541
+ background-image: var(--sb-text-gradient, none);
542
+ -webkit-background-clip: var(--sb-text-clip, border-box);
543
+ background-clip: var(--sb-text-clip, border-box);
544
+ color: var(--sb-text-color);
545
+ }
546
+
547
+ .sb-vstack,
548
+ .sb-hstack,
549
+ .sb-zstack,
550
+ .sb-styled,
551
+ .sb-spacer {
552
+ display: block;
553
+ }
554
+
555
+ .sb-button {
556
+ appearance: none;
557
+ display: block;
558
+ padding: 0;
559
+ border: 0;
560
+ background: none;
561
+ color: var(--sb-button-color, var(--sb-tint, var(--sb-color-accent)));
562
+ font: inherit;
563
+ font-size: var(--sb-font-size-body);
564
+ line-height: 1.3;
565
+ text-align: left;
566
+ cursor: pointer;
567
+ touch-action: manipulation;
568
+ -webkit-tap-highlight-color: transparent;
569
+ transition: opacity 0.1s ease;
570
+ }
571
+
572
+ .sb-button:active {
573
+ opacity: 0.3;
574
+ }
575
+
576
+ .sb-button:focus-visible {
577
+ outline: 2px solid var(--sb-color-accent);
578
+ outline-offset: 4px;
579
+ border-radius: 6px;
580
+ }
581
+
582
+ /* ---------------------------------------------------------------------------
583
+ Phase 2: button styles, disabled, image, divider, scrollview
584
+ ------------------------------------------------------------------------- */
585
+
586
+ .sb-button[data-sb-style="bordered"],
587
+ .sb-button[data-sb-style="borderedProminent"] {
588
+ border-radius: 9999px;
589
+ }
590
+
591
+ .sb-button[data-sb-style="bordered"] {
592
+ background: var(--sb-color-fill);
593
+ color: var(--sb-button-color, var(--sb-tint, var(--sb-color-accent)));
594
+ }
595
+
596
+ .sb-button[data-sb-style="borderedProminent"] {
597
+ background: var(--sb-tint, var(--sb-color-accent));
598
+ color: #ffffff;
599
+ }
600
+
601
+ .sb-button[data-sb-role="destructive"] {
602
+ color: var(--sb-color-red);
603
+ }
604
+
605
+ .sb-button[data-sb-style="borderedProminent"][data-sb-role="destructive"] {
606
+ background: var(--sb-color-red);
607
+ color: #ffffff;
608
+ }
609
+
610
+ /* .disabled(true): opacity and pointer-events are set inline by the renderer
611
+ (so update() clears them); the attribute carries the state for tooling. */
612
+ [data-sb-disabled] {
613
+ user-select: none;
614
+ }
615
+
616
+ /* Image(systemName:): a 1em glyph centered in its line-height square; the
617
+ renderer sets font-size from the resolved font. */
618
+ .sb-image {
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: center;
622
+ line-height: 1;
623
+ }
624
+
625
+ .sb-image > svg {
626
+ display: block;
627
+ width: 1em;
628
+ height: 1em;
629
+ stroke: currentColor;
630
+ }
631
+
632
+ /* Unknown symbol or catalog image: a dashed box named by `title`. */
633
+ .sb-image[data-sb-missing] {
634
+ border: 1px dashed currentColor;
635
+ border-radius: 2px;
636
+ opacity: 0.6;
637
+ }
638
+
639
+ /* Phase 5: a catalog image. The layout engine decides the frame (intrinsic
640
+ size, or the proposal for `.resizable()`), so the bitmap fills the box;
641
+ `.aspectRatio` fitting is a layout concern, not object-fit's. */
642
+ .sb-image > img {
643
+ display: block;
644
+ width: 100%;
645
+ height: 100%;
646
+ object-fit: fill;
647
+ pointer-events: none;
648
+ user-select: none;
649
+ -webkit-user-select: none;
650
+ }
651
+
652
+ /* Phase 5: a `.symbolset` SVG drawn as a template: the SVG masks a box
653
+ filled with the current foreground color, like an SF Symbol. */
654
+ .sb-image > .sb-image-mask {
655
+ display: block;
656
+ width: 1em;
657
+ height: 1em;
658
+ background-color: currentColor;
659
+ mask-size: contain;
660
+ mask-repeat: no-repeat;
661
+ mask-position: center;
662
+ -webkit-mask-size: contain;
663
+ -webkit-mask-repeat: no-repeat;
664
+ -webkit-mask-position: center;
665
+ }
666
+
667
+ .sb-divider {
668
+ background: var(--sb-color-separator);
669
+ }
670
+
671
+ /* ScrollView: the engine sizes the viewport; the inner content box is sized
672
+ to the content so the browser scrolls it. iOS-style invisible scrollbars. */
673
+ .sb-scrollview {
674
+ overflow: hidden;
675
+ scrollbar-width: none;
676
+ -webkit-overflow-scrolling: touch;
677
+ }
678
+
679
+ .sb-scrollview::-webkit-scrollbar,
680
+ .sb-list::-webkit-scrollbar {
681
+ display: none;
682
+ }
683
+
684
+ .sb-scrollview[data-sb-axes="vertical"] {
685
+ overflow-y: auto;
686
+ }
687
+
688
+ .sb-scrollview[data-sb-axes="horizontal"] {
689
+ overflow-x: auto;
690
+ }
691
+
692
+ .sb-scrollview[data-sb-axes="both"] {
693
+ overflow: auto;
694
+ }
695
+
696
+ /* Phase 12: `.scrollTargetBehavior(.viewAligned)` / `.paging` snap the scroll
697
+ view on its axis; the children of the `.scrollTargetLayout()` stack (or of
698
+ the content, for paging) are the snap points. */
699
+ .sb-scrollview[data-sb-snap][data-sb-axes="horizontal"] {
700
+ scroll-snap-type: x mandatory;
701
+ }
702
+
703
+ .sb-scrollview[data-sb-snap][data-sb-axes="vertical"] {
704
+ scroll-snap-type: y mandatory;
705
+ }
706
+
707
+ .sb-scrollview[data-sb-snap][data-sb-axes="both"] {
708
+ scroll-snap-type: both mandatory;
709
+ }
710
+
711
+ [data-sb-scroll-target] > * {
712
+ scroll-snap-align: start;
713
+ }
714
+
715
+ .sb-scrollview[data-sb-snap="paging"] > .sb-scroll-content > * {
716
+ scroll-snap-align: start;
717
+ scroll-snap-stop: always;
718
+ }
719
+
720
+ .sb-scroll-content,
721
+ .sb-list-content {
722
+ position: relative;
723
+ display: block;
724
+ }
725
+
726
+ /* ---------------------------------------------------------------------------
727
+ Phase 2: text fields and toggles
728
+ ------------------------------------------------------------------------- */
729
+
730
+ .sb-textfield {
731
+ appearance: none;
732
+ display: block;
733
+ font: inherit;
734
+ font-size: var(--sb-font-size-body);
735
+ line-height: 1.3;
736
+ color: var(--sb-color-primary);
737
+ background: transparent;
738
+ border: 0;
739
+ outline: none;
740
+ }
741
+
742
+ .sb-textfield::placeholder {
743
+ color: var(--sb-color-placeholder);
744
+ }
745
+
746
+ .sb-textfield[data-sb-style="roundedBorder"] {
747
+ padding: 0 7px;
748
+ border: 1px solid var(--sb-color-field-border);
749
+ border-radius: 5px;
750
+ background: var(--sb-color-field-background);
751
+ }
752
+
753
+ .sb-textfield[data-sb-style="roundedBorder"]:focus-visible {
754
+ border-color: var(--sb-color-accent);
755
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--sb-color-accent) 30%, transparent);
756
+ }
757
+
758
+ /* Toggle: the label children are positioned by their frames; the switch sits
759
+ at the engine's `switch` rect (51×31 at the trailing edge). */
760
+ .sb-switch {
761
+ appearance: none;
762
+ position: absolute;
763
+ width: 51px;
764
+ height: 31px;
765
+ margin: 0;
766
+ padding: 0;
767
+ border: 0;
768
+ border-radius: 9999px;
769
+ background: var(--sb-color-switch-off);
770
+ cursor: pointer;
771
+ transition: background-color 0.25s ease;
772
+ touch-action: manipulation;
773
+ -webkit-tap-highlight-color: transparent;
774
+ }
775
+
776
+ .sb-switch[aria-checked="true"] {
777
+ background: var(--sb-tint, var(--sb-color-green));
778
+ }
779
+
780
+ .sb-switch:focus-visible {
781
+ outline: 2px solid var(--sb-color-accent);
782
+ outline-offset: 2px;
783
+ }
784
+
785
+ .sb-switch-knob {
786
+ position: absolute;
787
+ top: 2px;
788
+ left: 2px;
789
+ width: 27px;
790
+ height: 27px;
791
+ border-radius: 50%;
792
+ background: #ffffff;
793
+ box-shadow:
794
+ 0 3px 8px rgba(0, 0, 0, 0.15),
795
+ 0 3px 1px rgba(0, 0, 0, 0.06);
796
+ transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
797
+ }
798
+
799
+ .sb-switch[aria-checked="true"] .sb-switch-knob {
800
+ transform: translateX(20px);
801
+ }
802
+
803
+ /* ---------------------------------------------------------------------------
804
+ Phase 2: lists (iOS inset grouped). Geometry comes from the engine: the
805
+ list is the scrolling viewport, sections carry the inset card (`rows`),
806
+ every row's DOM box is the full-width row inside the card.
807
+ ------------------------------------------------------------------------- */
808
+
809
+ .sb-list {
810
+ background: var(--sb-color-grouped-background);
811
+ overflow-y: auto;
812
+ scrollbar-width: none;
813
+ --sb-icon-knockout: var(--sb-color-grouped-cell);
814
+ }
815
+
816
+ .sb-section-header,
817
+ .sb-section-footer {
818
+ position: absolute;
819
+ white-space: pre;
820
+ color: var(--sb-color-secondary);
821
+ text-align: left;
822
+ }
823
+
824
+ .sb-section-header[hidden],
825
+ .sb-section-footer[hidden] {
826
+ display: none;
827
+ }
828
+
829
+ .sb-section-rows {
830
+ position: absolute;
831
+ border-radius: 10px;
832
+ background: var(--sb-color-grouped-cell);
833
+ overflow: hidden;
834
+ }
835
+
836
+ /* Rows: every direct child of a section's card, and every non-section child
837
+ of a list (loose rows form their own card). */
838
+ .sb-section-rows > [data-sb-id],
839
+ .sb-section-rows > [data-sb-ghost],
840
+ .sb-list-content > :not(.sb-section) {
841
+ background: var(--sb-color-grouped-cell);
842
+ text-align: left;
843
+ }
844
+
845
+ .sb-list-content > :not(.sb-section):first-child,
846
+ .sb-list-content > .sb-section + :not(.sb-section) {
847
+ border-top-left-radius: 10px;
848
+ border-top-right-radius: 10px;
849
+ }
850
+
851
+ .sb-list-content > :not(.sb-section):last-child,
852
+ .sb-list-content > :not(.sb-section):has(+ .sb-section) {
853
+ border-bottom-left-radius: 10px;
854
+ border-bottom-right-radius: 10px;
855
+ }
856
+
857
+ /* Hairline separators, inset 16px from the leading edge. */
858
+ .sb-section-rows > :not(:last-child)::after,
859
+ .sb-list-content > :not(.sb-section):not(:last-child):not(:has(+ .sb-section))::after {
860
+ content: "";
861
+ position: absolute;
862
+ left: 16px;
863
+ right: 0;
864
+ bottom: 0;
865
+ height: 1px;
866
+ background: var(--sb-color-separator);
867
+ pointer-events: none;
868
+ }
869
+
870
+ .sb-section-rows > .sb-button,
871
+ .sb-list-content > .sb-button {
872
+ border-radius: 0;
873
+ font-size: var(--sb-font-size-body);
874
+ }
875
+
876
+ .sb-section-rows > .sb-button:active,
877
+ .sb-list-content > .sb-button:active {
878
+ opacity: 1;
879
+ background: var(--sb-color-fill);
880
+ }
881
+
882
+ .sb-section-rows > .sb-navlink:active,
883
+ .sb-list-content > .sb-navlink:active {
884
+ background: var(--sb-color-fill);
885
+ }
886
+
887
+ /* ---------------------------------------------------------------------------
888
+ Phase 2: navigation
889
+ ------------------------------------------------------------------------- */
890
+
891
+ .sb-navstack {
892
+ overflow: hidden;
893
+ }
894
+
895
+ .sb-navscreen {
896
+ background: var(--sb-color-system-background);
897
+ will-change: transform;
898
+ transition:
899
+ transform 0.4s var(--sb-nav-easing),
900
+ visibility 0s linear 0s;
901
+ }
902
+
903
+ /* A screen with a list as its content takes the grouped background, like a
904
+ UINavigationController hosting an inset-grouped table view. */
905
+ .sb-navscreen:has(> .sb-navscreen-body > .sb-navscreen-content > .sb-list) {
906
+ background: var(--sb-color-grouped-background);
907
+ }
908
+
909
+ /* Screens below the top one slide partially left and are hidden once the
910
+ transition has finished. */
911
+ .sb-navscreen[data-sb-nav-position="below"] {
912
+ transform: translateX(-30%);
913
+ visibility: hidden;
914
+ transition:
915
+ transform 0.4s var(--sb-nav-easing),
916
+ visibility 0s linear 0.4s;
917
+ }
918
+
919
+ .sb-navscreen--push {
920
+ animation: sb-nav-push 0.4s var(--sb-nav-easing) both;
921
+ }
922
+
923
+ .sb-navstack > .sb-navscreen--pop {
924
+ z-index: 2;
925
+ pointer-events: none;
926
+ visibility: visible;
927
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.12);
928
+ animation: sb-nav-pop 0.4s var(--sb-nav-easing) both;
929
+ }
930
+
931
+ @keyframes sb-nav-push {
932
+ from {
933
+ transform: translateX(100%);
934
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.12);
935
+ }
936
+ to {
937
+ transform: translateX(0);
938
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0);
939
+ }
940
+ }
941
+
942
+ @keyframes sb-nav-pop {
943
+ from {
944
+ transform: translateX(0);
945
+ }
946
+ to {
947
+ transform: translateX(100%);
948
+ }
949
+ }
950
+
951
+ /* Navigation bar: the engine's `bar` rect (status bar + 44); the renderer
952
+ pads the status bar height so the 44px row sits at the bottom. */
953
+ .sb-navbar {
954
+ position: absolute;
955
+ display: grid;
956
+ grid-template-columns: 1fr auto 1fr;
957
+ align-items: center;
958
+ padding: 0 8px;
959
+ box-sizing: border-box;
960
+ background: transparent;
961
+ /* Phase 8: content that ignores the safe area paints under the bar; the
962
+ bar itself lets pointers through to toolbar items laid out over it. */
963
+ z-index: 1;
964
+ pointer-events: none;
965
+ }
966
+
967
+ .sb-navbar > * {
968
+ pointer-events: auto;
969
+ }
970
+
971
+ .sb-navbar-trailing:empty {
972
+ pointer-events: none;
973
+ }
974
+
975
+ .sb-navbar-back {
976
+ appearance: none;
977
+ grid-column: 1;
978
+ justify-self: start;
979
+ display: inline-flex;
980
+ align-items: center;
981
+ gap: 2px;
982
+ /* Width comes from the layout engine (chrome.back); this is only a fallback. */
983
+ max-width: 45%;
984
+ margin: 0;
985
+ padding: 0;
986
+ border: 0;
987
+ background: none;
988
+ color: var(--sb-color-accent);
989
+ font: inherit;
990
+ font-size: var(--sb-font-size-body);
991
+ line-height: 1.3;
992
+ cursor: pointer;
993
+ touch-action: manipulation;
994
+ -webkit-tap-highlight-color: transparent;
995
+ }
996
+
997
+ .sb-navbar-back[hidden] {
998
+ display: none;
999
+ }
1000
+
1001
+ .sb-navbar-back:active {
1002
+ opacity: 0.3;
1003
+ }
1004
+
1005
+ .sb-navbar-back > svg {
1006
+ width: 1.4em;
1007
+ height: 1.4em;
1008
+ margin-left: -4px;
1009
+ stroke-width: 2.5;
1010
+ flex-shrink: 0;
1011
+ }
1012
+
1013
+ .sb-navbar-back-label {
1014
+ overflow: hidden;
1015
+ text-overflow: ellipsis;
1016
+ white-space: nowrap;
1017
+ }
1018
+
1019
+ .sb-navbar-title {
1020
+ /* Positioned at the engine's `title` rect (Phase 6), centered and truncated
1021
+ to the room the back button and the toolbar items leave. Not a grid item:
1022
+ an absolutely positioned grid child would be placed against its grid
1023
+ area, not the bar's padding box. */
1024
+ position: absolute;
1025
+ display: block;
1026
+ max-width: 220px;
1027
+ overflow: hidden;
1028
+ text-overflow: ellipsis;
1029
+ white-space: nowrap;
1030
+ text-align: center;
1031
+ font-size: var(--sb-ts-headline-size, 17px);
1032
+ font-weight: 600;
1033
+ color: var(--sb-color-primary);
1034
+ }
1035
+
1036
+ .sb-navbar-title[hidden] {
1037
+ display: none;
1038
+ }
1039
+
1040
+ .sb-navbar-trailing {
1041
+ grid-column: 3;
1042
+ }
1043
+
1044
+ .sb-navscreen-body,
1045
+ .sb-navscreen-content {
1046
+ position: static;
1047
+ display: block;
1048
+ }
1049
+
1050
+ /* Large title: the engine's `largeTitle` rect (52px row), text already
1051
+ truncated to one line. */
1052
+ .sb-navbar-large-title {
1053
+ position: absolute;
1054
+ z-index: 1; /* Phase 8: above content that ignores the safe area */
1055
+ display: flex;
1056
+ align-items: center;
1057
+ padding: 0 16px;
1058
+ box-sizing: border-box;
1059
+ font-size: var(--sb-ts-largeTitle-size, 34px);
1060
+ font-weight: 700;
1061
+ letter-spacing: -0.01em;
1062
+ text-align: left;
1063
+ color: var(--sb-color-primary);
1064
+ white-space: pre;
1065
+ overflow: hidden;
1066
+ }
1067
+
1068
+ .sb-navbar-large-title[hidden] {
1069
+ display: none;
1070
+ }
1071
+
1072
+ /* NavigationLink: a tappable label. Inside a list it is a row with a chevron
1073
+ at the engine's `chevron` rect; elsewhere it reads as plain accent text. */
1074
+ .sb-navlink {
1075
+ color: var(--sb-color-accent);
1076
+ cursor: pointer;
1077
+ touch-action: manipulation;
1078
+ -webkit-tap-highlight-color: transparent;
1079
+ }
1080
+
1081
+ .sb-navlink:focus-visible {
1082
+ outline: 2px solid var(--sb-color-accent);
1083
+ outline-offset: 2px;
1084
+ border-radius: 6px;
1085
+ }
1086
+
1087
+ .sb-navlink-chevron {
1088
+ position: absolute;
1089
+ stroke-width: 2.5;
1090
+ color: var(--sb-color-tertiary-label);
1091
+ }
1092
+
1093
+ .sb-navlink-chevron[hidden] {
1094
+ display: none;
1095
+ }
1096
+
1097
+ .sb-section-rows > .sb-navlink,
1098
+ .sb-list-content > .sb-navlink {
1099
+ color: var(--sb-color-primary);
1100
+ }
1101
+
1102
+ @media (prefers-reduced-motion: reduce) {
1103
+ .sb-navscreen,
1104
+ .sb-navscreen--push,
1105
+ .sb-navstack > .sb-navscreen--pop,
1106
+ .sb-switch,
1107
+ .sb-switch-knob {
1108
+ animation-duration: 0.01s;
1109
+ transition-duration: 0.01s;
1110
+ }
1111
+ }
1112
+
1113
+ /* ---------------------------------------------------------------------------
1114
+ Phase 3: color and shape views (the engine sizes them to their proposal)
1115
+ ------------------------------------------------------------------------- */
1116
+
1117
+ .sb-color,
1118
+ .sb-shape {
1119
+ display: block;
1120
+ }
1121
+
1122
+ /* An exit clone of a removed element, positioned by the Animator. */
1123
+ .sb-exit-clone {
1124
+ pointer-events: none;
1125
+ user-select: none;
1126
+ }
1127
+
1128
+ /* ---------------------------------------------------------------------------
1129
+ Phase 3: sheets
1130
+ ------------------------------------------------------------------------- */
1131
+
1132
+ /* A sheet is a root-level layer covering the screen: a scrim over the
1133
+ presenting tree and the card at the engine's frame (large: screen height −
1134
+ status bar − 10; medium: half), sliding up from the bottom. */
1135
+ .screen > .sb-sheet,
1136
+ .sb-sheet {
1137
+ position: absolute;
1138
+ inset: 0;
1139
+ width: auto;
1140
+ height: auto;
1141
+ z-index: 5;
1142
+ display: block;
1143
+ }
1144
+
1145
+ .sb-sheet-scrim {
1146
+ position: absolute;
1147
+ inset: 0;
1148
+ background: rgba(0, 0, 0, 0.4);
1149
+ animation: sb-sheet-scrim-in 0.4s ease-out both;
1150
+ }
1151
+
1152
+ .sb-sheet-card {
1153
+ position: absolute;
1154
+ box-sizing: border-box;
1155
+ border-radius: 10px 10px 0 0;
1156
+ background: var(--sb-color-sheet-background);
1157
+ box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.15);
1158
+ overflow: hidden;
1159
+ will-change: transform;
1160
+ touch-action: none;
1161
+ transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
1162
+ }
1163
+
1164
+ .sb-sheet-grabber {
1165
+ position: absolute;
1166
+ border-radius: 2.5px;
1167
+ background: var(--sb-color-grabber);
1168
+ z-index: 1;
1169
+ }
1170
+
1171
+ .sb-sheet-content {
1172
+ position: static;
1173
+ display: block;
1174
+ }
1175
+
1176
+ /* Present: the card slides up over 0.4s while the scrim fades in. */
1177
+ .sb-sheet--in > .sb-sheet-card {
1178
+ animation: sb-sheet-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) both;
1179
+ }
1180
+
1181
+ /* Dismiss: the clone slides down from where a drag left it and fades its scrim. */
1182
+ .sb-sheet--out {
1183
+ pointer-events: none;
1184
+ }
1185
+
1186
+ .sb-sheet--out > .sb-sheet-card {
1187
+ transition: none;
1188
+ animation: sb-sheet-out 0.4s cubic-bezier(0.4, 0, 1, 1) both;
1189
+ }
1190
+
1191
+ .sb-sheet--out > .sb-sheet-scrim {
1192
+ animation: sb-sheet-scrim-out 0.4s ease-out both;
1193
+ }
1194
+
1195
+ /* While dragging the card follows the pointer (inline transform). */
1196
+ .sb-sheet[data-sb-dragging] > .sb-sheet-card {
1197
+ transition: none;
1198
+ }
1199
+
1200
+ /* Sheets under the top one park slightly scaled, like iOS stacks them. */
1201
+ .sb-sheet[data-sb-sheet-position="below"] > .sb-sheet-card {
1202
+ transform: translateY(-10px) scale(0.92);
1203
+ transform-origin: 50% 0;
1204
+ }
1205
+
1206
+ /* Behind a `large` sheet the presenting tree scales to 0.92 and dims. */
1207
+ .screen > :not(.sb-sheet) {
1208
+ transform-origin: 50% 0;
1209
+ transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
1210
+ }
1211
+
1212
+ .screen[data-sb-sheet="large"] > :not(.sb-sheet, .sb-exit-clone, .sb-picker-menu) {
1213
+ transform: scale(0.92);
1214
+ }
1215
+
1216
+ @keyframes sb-sheet-in {
1217
+ from {
1218
+ transform: translateY(100%);
1219
+ }
1220
+ to {
1221
+ transform: translateY(0);
1222
+ }
1223
+ }
1224
+
1225
+ @keyframes sb-sheet-out {
1226
+ from {
1227
+ transform: translateY(var(--sb-sheet-drag, 0px));
1228
+ }
1229
+ to {
1230
+ transform: translateY(100%);
1231
+ }
1232
+ }
1233
+
1234
+ @keyframes sb-sheet-scrim-in {
1235
+ from {
1236
+ opacity: 0;
1237
+ }
1238
+ to {
1239
+ opacity: 1;
1240
+ }
1241
+ }
1242
+
1243
+ @keyframes sb-sheet-scrim-out {
1244
+ from {
1245
+ opacity: 1;
1246
+ }
1247
+ to {
1248
+ opacity: 0;
1249
+ }
1250
+ }
1251
+
1252
+ @media (prefers-reduced-motion: reduce) {
1253
+ .sb-sheet-scrim,
1254
+ .sb-sheet-card,
1255
+ .sb-sheet--in > .sb-sheet-card,
1256
+ .sb-sheet--out > .sb-sheet-card,
1257
+ .sb-sheet--out > .sb-sheet-scrim,
1258
+ .screen > :not(.sb-sheet) {
1259
+ animation-duration: 0.01s;
1260
+ transition-duration: 0.01s;
1261
+ }
1262
+ }
1263
+
1264
+ /* ---------------------------------------------------------------------------
1265
+ Phase 4: list styles. `grouped` sections are full width with hairlines
1266
+ instead of inset cards; `plain` has no cards and the system background.
1267
+ The geometry (no inset, no gaps) comes from the engine; this only drops the
1268
+ radii and swaps the backgrounds.
1269
+ ------------------------------------------------------------------------- */
1270
+
1271
+ .sb-list[data-sb-list-style="grouped"] .sb-section-rows,
1272
+ .sb-list[data-sb-list-style="plain"] .sb-section-rows,
1273
+ .sb-list[data-sb-list-style="grouped"] > .sb-list-content > :not(.sb-section),
1274
+ .sb-list[data-sb-list-style="plain"] > .sb-list-content > :not(.sb-section) {
1275
+ border-radius: 0;
1276
+ }
1277
+
1278
+ .sb-list[data-sb-list-style="grouped"] .sb-section-rows {
1279
+ box-shadow:
1280
+ 0 -1px 0 var(--sb-color-separator),
1281
+ 0 1px 0 var(--sb-color-separator);
1282
+ }
1283
+
1284
+ .sb-list[data-sb-list-style="plain"] {
1285
+ background: var(--sb-color-system-background);
1286
+ --sb-icon-knockout: var(--sb-color-system-background);
1287
+ }
1288
+
1289
+ .sb-list[data-sb-list-style="plain"] .sb-section-rows,
1290
+ .sb-list[data-sb-list-style="plain"] .sb-section-rows > [data-sb-id],
1291
+ .sb-list[data-sb-list-style="plain"] .sb-section-rows > [data-sb-ghost],
1292
+ .sb-list[data-sb-list-style="plain"] > .sb-list-content > :not(.sb-section) {
1293
+ background: var(--sb-color-system-background);
1294
+ }
1295
+
1296
+ .sb-navscreen:has(> .sb-navscreen-body > .sb-navscreen-content > .sb-list[data-sb-list-style="plain"]) {
1297
+ background: var(--sb-color-system-background);
1298
+ }
1299
+
1300
+ /* ---------------------------------------------------------------------------
1301
+ Phase 4: tab views. The tabs are stacked in the content slot (every tab has
1302
+ the tab view's frame); the selected one is visible, the others keep their
1303
+ DOM hidden. The bar is the engine's `bar` rect (49 + bottom safe area):
1304
+ translucent system background, hairline on top, items in the 49px row.
1305
+ ------------------------------------------------------------------------- */
1306
+
1307
+ .sb-tabview {
1308
+ overflow: hidden;
1309
+ }
1310
+
1311
+ .sb-tabview-content {
1312
+ position: static;
1313
+ display: block;
1314
+ }
1315
+
1316
+ .sb-tab {
1317
+ display: block;
1318
+ }
1319
+
1320
+ .sb-tab[data-sb-tab-hidden] {
1321
+ visibility: hidden;
1322
+ pointer-events: none;
1323
+ }
1324
+
1325
+ .sb-tabbar {
1326
+ position: absolute;
1327
+ z-index: 3;
1328
+ background: var(--sb-color-tabbar-background);
1329
+ backdrop-filter: blur(20px) saturate(180%);
1330
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
1331
+ }
1332
+
1333
+ .sb-tabbar::before {
1334
+ content: "";
1335
+ position: absolute;
1336
+ left: 0;
1337
+ right: 0;
1338
+ top: 0;
1339
+ height: 1px;
1340
+ background: var(--sb-color-separator);
1341
+ pointer-events: none;
1342
+ }
1343
+
1344
+ .sb-tabbar-item {
1345
+ appearance: none;
1346
+ position: absolute;
1347
+ display: flex;
1348
+ flex-direction: column;
1349
+ align-items: center;
1350
+ justify-content: flex-start;
1351
+ gap: 2px;
1352
+ margin: 0;
1353
+ padding: 6px 0 0;
1354
+ border: 0;
1355
+ background: none;
1356
+ color: var(--sb-color-secondary);
1357
+ font-family: var(--sb-font-family);
1358
+ cursor: pointer;
1359
+ touch-action: manipulation;
1360
+ -webkit-tap-highlight-color: transparent;
1361
+ }
1362
+
1363
+ .sb-tabbar-item[aria-selected="true"] {
1364
+ color: var(--sb-color-accent);
1365
+ }
1366
+
1367
+ .sb-tabbar-item:focus-visible {
1368
+ outline: 2px solid var(--sb-color-accent);
1369
+ outline-offset: -2px;
1370
+ border-radius: 8px;
1371
+ }
1372
+
1373
+ .sb-tabbar-icon {
1374
+ display: flex;
1375
+ align-items: center;
1376
+ justify-content: center;
1377
+ width: 24px;
1378
+ height: 24px;
1379
+ font-size: 24px;
1380
+ line-height: 1;
1381
+ }
1382
+
1383
+ .sb-tabbar-icon[hidden] {
1384
+ display: none;
1385
+ }
1386
+
1387
+ .sb-tabbar-icon > svg {
1388
+ display: block;
1389
+ width: 1em;
1390
+ height: 1em;
1391
+ stroke: currentColor;
1392
+ stroke-width: 1.75;
1393
+ }
1394
+
1395
+ .sb-tabbar-icon[data-sb-missing] {
1396
+ border: 1px dashed currentColor;
1397
+ border-radius: 4px;
1398
+ opacity: 0.6;
1399
+ }
1400
+
1401
+ .sb-tabbar-title {
1402
+ font-size: 10px;
1403
+ line-height: 12px;
1404
+ font-weight: 500;
1405
+ letter-spacing: -0.01em;
1406
+ white-space: nowrap;
1407
+ }
1408
+
1409
+ /* ---------------------------------------------------------------------------
1410
+ Phase 4: pickers. Segmented: a gray track with equal segments at the
1411
+ engine's `segment<i>` rects; the selected one carries a raised pill inset
1412
+ 2px. Menu: label leading, value + chevron trailing (engine `label`/`value`
1413
+ rects); the control opens a menu appended to #screen.
1414
+ ------------------------------------------------------------------------- */
1415
+
1416
+ .sb-picker[data-sb-style="segmented"] {
1417
+ display: block;
1418
+ }
1419
+
1420
+ /* The track is placed at the control's frame (in a list row the element's
1421
+ box is the whole row, so the track cannot be the element's background). */
1422
+ .sb-segment-track {
1423
+ position: absolute;
1424
+ border-radius: 8px;
1425
+ background: var(--sb-color-segment-track);
1426
+ pointer-events: none;
1427
+ }
1428
+
1429
+ .sb-segment {
1430
+ appearance: none;
1431
+ position: absolute;
1432
+ display: flex;
1433
+ align-items: center;
1434
+ justify-content: center;
1435
+ margin: 0;
1436
+ padding: 0;
1437
+ border: 0;
1438
+ background: none;
1439
+ color: var(--sb-color-primary);
1440
+ font: 600 13px/16px var(--sb-font-family);
1441
+ letter-spacing: -0.01em;
1442
+ white-space: nowrap;
1443
+ cursor: pointer;
1444
+ touch-action: manipulation;
1445
+ -webkit-tap-highlight-color: transparent;
1446
+ }
1447
+
1448
+ .sb-segment::before {
1449
+ content: "";
1450
+ position: absolute;
1451
+ inset: 2px;
1452
+ border-radius: 7px;
1453
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
1454
+ }
1455
+
1456
+ .sb-segment[aria-checked="true"]::before {
1457
+ background: var(--sb-color-segment-selected);
1458
+ box-shadow:
1459
+ 0 3px 8px rgba(0, 0, 0, 0.12),
1460
+ 0 3px 1px rgba(0, 0, 0, 0.04);
1461
+ }
1462
+
1463
+ .sb-segment-title {
1464
+ position: relative;
1465
+ overflow: hidden;
1466
+ text-overflow: ellipsis;
1467
+ max-width: calc(100% - 8px);
1468
+ }
1469
+
1470
+ .sb-segment:focus-visible {
1471
+ outline: 2px solid var(--sb-color-accent);
1472
+ outline-offset: -2px;
1473
+ border-radius: 7px;
1474
+ }
1475
+
1476
+ .sb-picker[data-sb-style="menu"] {
1477
+ display: block;
1478
+ color: var(--sb-color-primary);
1479
+ cursor: pointer;
1480
+ touch-action: manipulation;
1481
+ -webkit-tap-highlight-color: transparent;
1482
+ }
1483
+
1484
+ .sb-picker[data-sb-style="menu"]:focus-visible {
1485
+ outline: 2px solid var(--sb-color-accent);
1486
+ outline-offset: 2px;
1487
+ border-radius: 6px;
1488
+ }
1489
+
1490
+ .sb-section-rows > .sb-picker[data-sb-style="menu"]:active,
1491
+ .sb-list-content > .sb-picker[data-sb-style="menu"]:active {
1492
+ background: var(--sb-color-fill);
1493
+ }
1494
+
1495
+ .sb-picker-label,
1496
+ .sb-picker-value {
1497
+ position: absolute;
1498
+ display: flex;
1499
+ align-items: center;
1500
+ white-space: nowrap;
1501
+ overflow: hidden;
1502
+ }
1503
+
1504
+ .sb-picker-label {
1505
+ text-overflow: ellipsis;
1506
+ }
1507
+
1508
+ .sb-picker-value {
1509
+ justify-content: flex-end;
1510
+ gap: 4px;
1511
+ color: var(--sb-color-secondary);
1512
+ }
1513
+
1514
+ .sb-picker-value-text {
1515
+ overflow: hidden;
1516
+ text-overflow: ellipsis;
1517
+ }
1518
+
1519
+ .sb-picker-value > svg {
1520
+ display: block;
1521
+ width: 16px;
1522
+ height: 16px;
1523
+ flex-shrink: 0;
1524
+ stroke: currentColor;
1525
+ stroke-width: 2.5;
1526
+ }
1527
+
1528
+ /* The option menu: anchored under (or above) the control by the renderer. */
1529
+ .sb-picker-menu {
1530
+ position: absolute;
1531
+ z-index: 6;
1532
+ border-radius: 13px;
1533
+ background: var(--sb-color-menu-background);
1534
+ backdrop-filter: blur(30px) saturate(180%);
1535
+ -webkit-backdrop-filter: blur(30px) saturate(180%);
1536
+ box-shadow:
1537
+ 0 0 0 0.5px rgba(0, 0, 0, 0.08),
1538
+ 0 10px 40px rgba(0, 0, 0, 0.22);
1539
+ overflow: hidden;
1540
+ transform-origin: 50% 0;
1541
+ animation: sb-menu-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1) both;
1542
+ }
1543
+
1544
+ .sb-picker-menu[data-sb-placement="above"] {
1545
+ transform-origin: 50% 100%;
1546
+ }
1547
+
1548
+ .sb-picker-menu-item {
1549
+ appearance: none;
1550
+ display: flex;
1551
+ align-items: center;
1552
+ gap: 8px;
1553
+ width: 100%;
1554
+ height: 44px;
1555
+ margin: 0;
1556
+ padding: 0 16px;
1557
+ border: 0;
1558
+ background: none;
1559
+ color: var(--sb-color-primary);
1560
+ font: 400 var(--sb-font-size-body) / 1.3 var(--sb-font-family);
1561
+ text-align: left;
1562
+ cursor: pointer;
1563
+ touch-action: manipulation;
1564
+ -webkit-tap-highlight-color: transparent;
1565
+ }
1566
+
1567
+ :is(.sb-picker-menu-item, .sb-picker-menu-static) + :is(.sb-picker-menu-item, .sb-picker-menu-static) {
1568
+ box-shadow: inset 0 1px 0 var(--sb-color-separator);
1569
+ }
1570
+
1571
+ .sb-picker-menu-item:active {
1572
+ background: var(--sb-color-fill);
1573
+ }
1574
+
1575
+ .sb-picker-menu-check {
1576
+ display: inline-flex;
1577
+ align-items: center;
1578
+ justify-content: center;
1579
+ width: 20px;
1580
+ flex-shrink: 0;
1581
+ visibility: hidden;
1582
+ }
1583
+
1584
+ .sb-picker-menu-item[aria-checked="true"] > .sb-picker-menu-check {
1585
+ visibility: visible;
1586
+ }
1587
+
1588
+ .sb-picker-menu-check > svg {
1589
+ width: 1em;
1590
+ height: 1em;
1591
+ stroke: currentColor;
1592
+ stroke-width: 2.5;
1593
+ }
1594
+
1595
+ .sb-picker-menu-title {
1596
+ flex: 1 1 auto;
1597
+ overflow: hidden;
1598
+ text-overflow: ellipsis;
1599
+ white-space: nowrap;
1600
+ }
1601
+
1602
+ @keyframes sb-menu-in {
1603
+ from {
1604
+ opacity: 0;
1605
+ transform: scale(0.9);
1606
+ }
1607
+ to {
1608
+ opacity: 1;
1609
+ transform: scale(1);
1610
+ }
1611
+ }
1612
+
1613
+ /* GeometryReader: a plain box the engine sizes to its proposal. */
1614
+ .sb-geometry {
1615
+ display: block;
1616
+ }
1617
+
1618
+ /* ---------------------------------------------------------------------------
1619
+ Phase 6: layout containers (docs/phase6/layout-containers.md). Grids, the
1620
+ custom `layout` element and `decorated` are plain boxes whose children the
1621
+ engine positions; the gauge draws its bar as chrome.
1622
+ ------------------------------------------------------------------------- */
1623
+
1624
+ .sb-layout,
1625
+ .sb-grid,
1626
+ .sb-gridrow,
1627
+ .sb-lazyvgrid,
1628
+ .sb-decorated {
1629
+ display: block;
1630
+ }
1631
+
1632
+ /* `.background { }`: the decoration (the last child) draws under the content.
1633
+ The box is its own stacking context so the content's z-index stays local. */
1634
+ .sb-decorated {
1635
+ isolation: isolate;
1636
+ }
1637
+
1638
+ .sb-decorated[data-sb-role="background"] > [data-sb-id]:first-child {
1639
+ z-index: 1;
1640
+ }
1641
+
1642
+ .sb-gauge {
1643
+ display: block;
1644
+ }
1645
+
1646
+ .sb-gauge-track,
1647
+ .sb-gauge-fill {
1648
+ position: absolute;
1649
+ border-radius: 2px;
1650
+ pointer-events: none;
1651
+ }
1652
+
1653
+ .sb-gauge-track {
1654
+ background: var(--sb-color-fill);
1655
+ }
1656
+
1657
+ .sb-gauge-fill {
1658
+ background: var(--sb-tint, var(--sb-color-accent));
1659
+ }
1660
+
1661
+ @media (prefers-reduced-motion: reduce) {
1662
+ .sb-picker-menu,
1663
+ .sb-segment::before {
1664
+ animation-duration: 0.01s;
1665
+ transition-duration: 0.01s;
1666
+ }
1667
+ }
1668
+
1669
+ /* ---------------------------------------------------------------------------
1670
+ Phase 6 (docs/phase6/chrome.md): menus, inline pickers, navigation bar
1671
+ toolbars, search fields, tap gestures and list row badges.
1672
+ ------------------------------------------------------------------------- */
1673
+
1674
+ /* Menu: the label (first child) is the tappable control, the item children
1675
+ are listed in the popover and never shown inline. */
1676
+ .sb-menu {
1677
+ display: block;
1678
+ color: var(--sb-color-accent);
1679
+ cursor: pointer;
1680
+ touch-action: manipulation;
1681
+ -webkit-tap-highlight-color: transparent;
1682
+ }
1683
+
1684
+ .sb-menu > [data-sb-id]:not(:first-child),
1685
+ .sb-menu > [data-sb-id][hidden] {
1686
+ display: none;
1687
+ }
1688
+
1689
+ .sb-menu:active {
1690
+ opacity: 0.3;
1691
+ }
1692
+
1693
+ .sb-menu:focus-visible {
1694
+ outline: 2px solid var(--sb-color-accent);
1695
+ outline-offset: 4px;
1696
+ border-radius: 6px;
1697
+ }
1698
+
1699
+ /* Popover rows for menu items: a tap row shows its symbol trailing; a
1700
+ destructive role is red; a disabled item dims; section headers are 32px
1701
+ captions; `text` items are static 44px rows; groups are split by 8px bands. */
1702
+ .sb-picker-menu-symbol {
1703
+ display: inline-flex;
1704
+ align-items: center;
1705
+ justify-content: center;
1706
+ width: 22px;
1707
+ margin-left: auto;
1708
+ flex-shrink: 0;
1709
+ }
1710
+
1711
+ .sb-picker-menu-symbol > svg {
1712
+ width: 1.15em;
1713
+ height: 1.15em;
1714
+ stroke: currentColor;
1715
+ }
1716
+
1717
+ .sb-picker-menu-item[data-sb-role="destructive"] {
1718
+ color: var(--sb-color-red);
1719
+ }
1720
+
1721
+ .sb-picker-menu-item:disabled {
1722
+ color: var(--sb-color-tertiary-label);
1723
+ cursor: default;
1724
+ }
1725
+
1726
+ .sb-picker-menu-caption {
1727
+ display: flex;
1728
+ align-items: center;
1729
+ height: 32px;
1730
+ padding: 0 16px;
1731
+ color: var(--sb-color-secondary);
1732
+ font: 400 13px/16px var(--sb-font-family);
1733
+ white-space: nowrap;
1734
+ overflow: hidden;
1735
+ text-overflow: ellipsis;
1736
+ }
1737
+
1738
+ .sb-picker-menu-static {
1739
+ display: flex;
1740
+ align-items: center;
1741
+ height: 44px;
1742
+ padding: 0 16px;
1743
+ color: var(--sb-color-secondary);
1744
+ font: 400 var(--sb-font-size-body) / 1.3 var(--sb-font-family);
1745
+ white-space: nowrap;
1746
+ overflow: hidden;
1747
+ text-overflow: ellipsis;
1748
+ }
1749
+
1750
+ .sb-picker-menu-separator {
1751
+ height: 8px;
1752
+ background: var(--sb-color-fill);
1753
+ }
1754
+
1755
+ /* Inline picker: a dim label row (when the label is non-empty) and 44px
1756
+ option rows at the engine's `label` / `option<i>` rects; inside a list the
1757
+ rows span the list row, with the usual 16px content inset and hairlines. */
1758
+ .sb-picker[data-sb-style="inline"] {
1759
+ display: block;
1760
+ color: var(--sb-color-primary);
1761
+ }
1762
+
1763
+ .sb-picker-inline-label {
1764
+ position: absolute;
1765
+ display: flex;
1766
+ align-items: center;
1767
+ color: var(--sb-color-secondary);
1768
+ white-space: nowrap;
1769
+ overflow: hidden;
1770
+ text-overflow: ellipsis;
1771
+ }
1772
+
1773
+ .sb-picker-option {
1774
+ appearance: none;
1775
+ position: absolute;
1776
+ display: flex;
1777
+ align-items: center;
1778
+ gap: 8px;
1779
+ margin: 0;
1780
+ padding: 0;
1781
+ border: 0;
1782
+ background: none;
1783
+ color: var(--sb-color-primary);
1784
+ font: inherit;
1785
+ text-align: left;
1786
+ cursor: pointer;
1787
+ touch-action: manipulation;
1788
+ -webkit-tap-highlight-color: transparent;
1789
+ }
1790
+
1791
+ .sb-picker-option:active {
1792
+ background: var(--sb-color-fill);
1793
+ }
1794
+
1795
+ .sb-picker-option:focus-visible {
1796
+ outline: 2px solid var(--sb-color-accent);
1797
+ outline-offset: -2px;
1798
+ }
1799
+
1800
+ .sb-picker-option-title {
1801
+ flex: 1 1 auto;
1802
+ overflow: hidden;
1803
+ text-overflow: ellipsis;
1804
+ white-space: nowrap;
1805
+ }
1806
+
1807
+ .sb-picker-option-check {
1808
+ display: inline-flex;
1809
+ align-items: center;
1810
+ justify-content: center;
1811
+ width: 20px;
1812
+ flex-shrink: 0;
1813
+ color: var(--sb-color-accent);
1814
+ visibility: hidden;
1815
+ }
1816
+
1817
+ .sb-picker-option[aria-checked="true"] > .sb-picker-option-check {
1818
+ visibility: visible;
1819
+ }
1820
+
1821
+ .sb-picker-option-check > svg {
1822
+ width: 1.1em;
1823
+ height: 1.1em;
1824
+ stroke: currentColor;
1825
+ stroke-width: 2.5;
1826
+ }
1827
+
1828
+ /* Hairlines between the inline rows (the first row has the row above it). */
1829
+ .sb-picker-inline-label + .sb-picker-option::before,
1830
+ .sb-picker-option + .sb-picker-option::before {
1831
+ content: "";
1832
+ position: absolute;
1833
+ left: 0;
1834
+ right: 0;
1835
+ top: 0;
1836
+ height: 1px;
1837
+ background: var(--sb-color-separator);
1838
+ pointer-events: none;
1839
+ }
1840
+
1841
+ .sb-section-rows > .sb-picker[data-sb-style="inline"] > .sb-picker-inline-label,
1842
+ .sb-list-content > .sb-picker[data-sb-style="inline"] > .sb-picker-inline-label,
1843
+ .sb-section-rows > .sb-picker[data-sb-style="inline"] > .sb-picker-option,
1844
+ .sb-list-content > .sb-picker[data-sb-style="inline"] > .sb-picker-option {
1845
+ padding: 0 16px;
1846
+ }
1847
+
1848
+ .sb-section-rows > .sb-picker[data-sb-style="inline"] > .sb-picker-option::before,
1849
+ .sb-list-content > .sb-picker[data-sb-style="inline"] > .sb-picker-option::before {
1850
+ left: 16px;
1851
+ }
1852
+
1853
+ /* Toolbars: the element's frame is its item group in the bar (items are
1854
+ ordinary children with frames). A bottom toolbar is the whole 49px + safe
1855
+ area bar, translucent with a hairline like the tab bar. */
1856
+ .sb-toolbar {
1857
+ display: block;
1858
+ }
1859
+
1860
+ .sb-toolbar[data-sb-placement="bottom"] {
1861
+ z-index: 3;
1862
+ background: var(--sb-color-tabbar-background);
1863
+ backdrop-filter: blur(20px) saturate(180%);
1864
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
1865
+ }
1866
+
1867
+ .sb-toolbar[data-sb-placement="bottom"]::before {
1868
+ content: "";
1869
+ position: absolute;
1870
+ left: 0;
1871
+ right: 0;
1872
+ top: 0;
1873
+ height: 1px;
1874
+ background: var(--sb-color-separator);
1875
+ pointer-events: none;
1876
+ }
1877
+
1878
+ /* Search field: a 36px rounded gray field (engine `searchField` rect) with a
1879
+ magnifier leading; the input sends `text` events like a textfield. */
1880
+ .sb-searchfield {
1881
+ display: flex;
1882
+ align-items: center;
1883
+ gap: 6px;
1884
+ padding: 0 8px;
1885
+ border-radius: 10px;
1886
+ background: var(--sb-color-fill);
1887
+ color: var(--sb-color-primary);
1888
+ }
1889
+
1890
+ .sb-searchfield > svg {
1891
+ display: block;
1892
+ width: 17px;
1893
+ height: 17px;
1894
+ flex-shrink: 0;
1895
+ color: var(--sb-color-secondary);
1896
+ stroke: currentColor;
1897
+ stroke-width: 2.25;
1898
+ }
1899
+
1900
+ .sb-searchfield-input {
1901
+ flex: 1 1 auto;
1902
+ min-width: 0;
1903
+ appearance: none;
1904
+ margin: 0;
1905
+ padding: 0;
1906
+ border: 0;
1907
+ background: transparent;
1908
+ outline: none;
1909
+ color: var(--sb-color-primary);
1910
+ font: inherit;
1911
+ font-size: var(--sb-font-size-body);
1912
+ line-height: 1.3;
1913
+ }
1914
+
1915
+ .sb-searchfield-input::placeholder {
1916
+ color: var(--sb-color-secondary);
1917
+ }
1918
+
1919
+ /* Tap gesture: a plain box the size of its child; the renderer sends `tap`
1920
+ for clicks no inner control claimed. */
1921
+ .sb-tapgesture {
1922
+ display: block;
1923
+ cursor: pointer;
1924
+ touch-action: manipulation;
1925
+ -webkit-tap-highlight-color: transparent;
1926
+ }
1927
+
1928
+ /* List row badge: trailing secondary text at the engine's `badge` rect. */
1929
+ .sb-badge {
1930
+ position: absolute;
1931
+ display: flex;
1932
+ align-items: center;
1933
+ justify-content: flex-end;
1934
+ color: var(--sb-color-secondary);
1935
+ white-space: pre;
1936
+ pointer-events: none;
1937
+ }
1938
+
1939
+ /* ---------------------------------------------------------------------------
1940
+ Phase 7: split navigation, selected rows, ViewThatFits, hidden and masks
1941
+ (docs/ops-protocol.md, "Phase 7").
1942
+ ------------------------------------------------------------------------- */
1943
+
1944
+ /* NavigationSplitView: two navstack columns (protocol children, positioned by
1945
+ the engine) with a hairline between them at the engine's `separator` rect. */
1946
+ .sb-navsplit {
1947
+ overflow: hidden;
1948
+ background: var(--sb-color-system-background);
1949
+ }
1950
+
1951
+ .sb-navsplit-separator {
1952
+ position: absolute;
1953
+ background: var(--sb-color-separator);
1954
+ pointer-events: none;
1955
+ }
1956
+
1957
+ /* A selected `navlink` row (`List(selection:)`): the sidebar-style rounded
1958
+ fill. The engine omits the chevron inside a navsplit sidebar; elsewhere
1959
+ the chevron stays. The list-row selectors outrank the per-style row
1960
+ background and `border-radius: 0` rules above. */
1961
+ .sb-navlink[data-sb-selected],
1962
+ .sb-list[data-sb-list-style] > .sb-list-content > .sb-navlink[data-sb-selected],
1963
+ .sb-list[data-sb-list-style] .sb-section-rows > .sb-navlink[data-sb-selected] {
1964
+ background: var(--sb-color-tertiary-fill);
1965
+ border-radius: 10px;
1966
+ }
1967
+
1968
+ /* ViewThatFits: the candidates it did not choose have no frame and carry the
1969
+ `hidden` attribute (src/layoutDom.ts). */
1970
+ .sb-viewthatfits > [data-sb-id][hidden] {
1971
+ display: none;
1972
+ }
1973
+
1974
+ /* `.hidden()`: the styled box keeps its layout but is invisible and inert
1975
+ (inline `visibility: hidden; pointer-events: none`, see applyStyle). */
1976
+ .sb-styled[data-sb-hidden] {
1977
+ visibility: hidden;
1978
+ pointer-events: none;
1979
+ }
1980
+
1981
+ /* ---------------------------------------------------------------------------
1982
+ Phase 7: charts (docs/ops-protocol.md, "Phase 7: charts"). The engine
1983
+ frames the chart and its `chartitem` children; src/chartDom.ts draws the
1984
+ plot SVG, ticks, text labels and legend as chrome inside .sb-chart-chrome,
1985
+ which stays static so its absolutely positioned pieces resolve against
1986
+ the chart box. The slot comes after the chrome, so annotations draw above
1987
+ the marks.
1988
+ ------------------------------------------------------------------------- */
1989
+
1990
+ .sb-chart,
1991
+ .sb-chartitem {
1992
+ display: block;
1993
+ }
1994
+
1995
+ .sb-chart-chrome {
1996
+ position: static;
1997
+ }
1998
+
1999
+ .sb-chart-plot {
2000
+ position: absolute;
2001
+ display: block;
2002
+ overflow: hidden;
2003
+ pointer-events: none;
2004
+ }
2005
+
2006
+ .sb-chart-gridline {
2007
+ stroke: var(--sb-color-separator);
2008
+ stroke-width: 1;
2009
+ shape-rendering: crispEdges;
2010
+ }
2011
+
2012
+ .sb-chart-tick {
2013
+ position: absolute;
2014
+ background: var(--sb-color-separator);
2015
+ pointer-events: none;
2016
+ }
2017
+
2018
+ .sb-chart-label {
2019
+ position: absolute;
2020
+ color: var(--sb-color-secondary);
2021
+ white-space: pre;
2022
+ pointer-events: none;
2023
+ }
2024
+
2025
+ .sb-chart-legend-item {
2026
+ position: absolute;
2027
+ display: flex;
2028
+ align-items: center;
2029
+ gap: 4px;
2030
+ color: var(--sb-color-secondary);
2031
+ white-space: pre;
2032
+ pointer-events: none;
2033
+ }
2034
+
2035
+ .sb-chart-legend-glyph {
2036
+ display: block;
2037
+ flex: none;
2038
+ }
2039
+
2040
+ .sb-chart-legend-label {
2041
+ flex: none;
2042
+ }
2043
+
2044
+ /* The SwiftUI chart palette (and the other system colors) as `--sb-color-<name>`,
2045
+ which a named `Color` without a semantic token resolves to (src/styles.ts). */
2046
+ .device,
2047
+ .device[data-theme="light"] {
2048
+ --sb-color-blue: #007aff;
2049
+ --sb-color-orange: #ff9500;
2050
+ --sb-color-yellow: #ffcc00;
2051
+ --sb-color-purple: #af52de;
2052
+ --sb-color-pink: #ff2d55;
2053
+ --sb-color-teal: #30b0c7;
2054
+ --sb-color-indigo: #5856d6;
2055
+ --sb-color-mint: #00c7be;
2056
+ --sb-color-cyan: #32ade6;
2057
+ --sb-color-brown: #a2845e;
2058
+ --sb-color-gray: #8e8e93;
2059
+ }
2060
+
2061
+ .device[data-theme="dark"] {
2062
+ --sb-color-blue: #0a84ff;
2063
+ --sb-color-orange: #ff9f0a;
2064
+ --sb-color-yellow: #ffd60a;
2065
+ --sb-color-purple: #bf5af2;
2066
+ --sb-color-pink: #ff375f;
2067
+ --sb-color-teal: #40c8e0;
2068
+ --sb-color-indigo: #5e5ce6;
2069
+ --sb-color-mint: #63e6e2;
2070
+ --sb-color-cyan: #64d2ff;
2071
+ --sb-color-brown: #ac8e68;
2072
+ --sb-color-gray: #8e8e93;
2073
+ }
2074
+
2075
+ /* ---------------------------------------------------------------------------
2076
+ Phase 8: gestures, rows, navigation (docs/ops-protocol.md, "Phase 8:
2077
+ interaction"; src/interaction.ts drives the data attributes).
2078
+ ------------------------------------------------------------------------- */
2079
+
2080
+ /* `gesture`: a drag gesture owns touches in both axes; long-press subtrees
2081
+ never show the browser's text selection or callout. */
2082
+ .sb-gesture,
2083
+ .sb-contextmenu {
2084
+ user-select: none;
2085
+ -webkit-user-select: none;
2086
+ -webkit-touch-callout: none;
2087
+ -webkit-tap-highlight-color: transparent;
2088
+ }
2089
+
2090
+ .sb-gesture[data-sb-drag] {
2091
+ touch-action: none;
2092
+ }
2093
+
2094
+ /* `contextmenu` / `swipeactions`: only the content (the first child) shows
2095
+ inline; the items are listed in the popover / drawn as action buttons
2096
+ (they are in `hidden` without frames). */
2097
+ .sb-contextmenu > [data-sb-id]:not(:first-child),
2098
+ .sb-contextmenu > [data-sb-id][hidden],
2099
+ .sb-swipeactions > .sb-swipe-content > [data-sb-id]:not(:first-child),
2100
+ .sb-swipeactions > .sb-swipe-content > [data-sb-id][hidden] {
2101
+ display: none;
2102
+ }
2103
+
2104
+ /* `.allowsHitTesting(false)` (set inline by the renderer as well). */
2105
+ [data-sb-hit-testing="false"] {
2106
+ pointer-events: none;
2107
+ }
2108
+
2109
+ /* Hit testing like SwiftUI's: a stack, spacer or plain modifier box is not
2110
+ hit in its empty space (what is under it is), only content is: text, images,
2111
+ shapes, controls, rows. A `styled` with a `background` paint is content too.
2112
+ Containers therefore drop pointer events and their children opt back in;
2113
+ subtrees that must stay inert (hidden, disabled, hit testing off, exit and
2114
+ pop animations) are blocked explicitly, since the opt-in breaks inheritance. */
2115
+ .sb-vstack,
2116
+ .sb-hstack,
2117
+ .sb-zstack,
2118
+ .sb-spacer,
2119
+ .sb-styled,
2120
+ .sb-geometry,
2121
+ .sb-layout,
2122
+ .sb-grid,
2123
+ .sb-gridrow,
2124
+ .sb-lazyvgrid,
2125
+ .sb-viewthatfits,
2126
+ .sb-decorated,
2127
+ .sb-gesture,
2128
+ .sb-tapgesture {
2129
+ pointer-events: none;
2130
+ }
2131
+
2132
+ :is(.sb-vstack, .sb-hstack, .sb-zstack, .sb-styled, .sb-geometry, .sb-layout, .sb-grid, .sb-gridrow, .sb-lazyvgrid, .sb-viewthatfits, .sb-decorated, .sb-gesture, .sb-tapgesture)
2133
+ > [data-sb-kind]:not(.sb-vstack, .sb-hstack, .sb-zstack, .sb-spacer, .sb-styled, .sb-geometry, .sb-layout, .sb-grid, .sb-gridrow, .sb-lazyvgrid, .sb-viewthatfits, .sb-decorated, .sb-gesture, .sb-tapgesture) {
2134
+ pointer-events: auto;
2135
+ }
2136
+
2137
+ .sb-styled[data-sb-background] {
2138
+ pointer-events: auto;
2139
+ }
2140
+
2141
+ :is([data-sb-hidden], [data-sb-disabled], [data-sb-hit-testing="false"], .sb-navscreen--pop, .sb-exit-clone, .sb-sheet--out, .sb-alert--out, .sb-tab[data-sb-tab-hidden]) [data-sb-kind],
2142
+ :is([data-sb-hidden], [data-sb-disabled], [data-sb-hit-testing="false"], .sb-navscreen--pop, .sb-exit-clone, .sb-sheet--out, .sb-alert--out, .sb-tab[data-sb-tab-hidden]) .sb-navbar > * {
2143
+ pointer-events: none !important;
2144
+ }
2145
+
2146
+ /* While a pointer session runs (src/interaction.ts) nothing on the screen is
2147
+ selectable: a mouse drag is a gesture, not a text selection. */
2148
+ [data-sb-pointer-session],
2149
+ [data-sb-pointer-session] * {
2150
+ user-select: none;
2151
+ -webkit-user-select: none;
2152
+ }
2153
+
2154
+ /* Swipe rows (`swipeactions`, deletable `listrow`): the content slot paints
2155
+ the row background and slides over the action buttons behind it. */
2156
+ .sb-swipeactions,
2157
+ .sb-listrow {
2158
+ overflow: hidden;
2159
+ touch-action: pan-y;
2160
+ }
2161
+
2162
+ .sb-swipeactions > .sb-swipe-content,
2163
+ .sb-listrow > .sb-swipe-content {
2164
+ position: absolute;
2165
+ inset: 0;
2166
+ z-index: 1;
2167
+ background-color: inherit;
2168
+ border-radius: inherit;
2169
+ transition: transform 0.3s var(--sb-nav-easing);
2170
+ will-change: transform;
2171
+ }
2172
+
2173
+ .sb-swipeactions[data-sb-swiping] > .sb-swipe-content,
2174
+ .sb-listrow[data-sb-swiping] > .sb-swipe-content {
2175
+ transition: none;
2176
+ }
2177
+
2178
+ /* The hairline separator stays above the sliding content. */
2179
+ .sb-swipeactions::after,
2180
+ .sb-listrow::after {
2181
+ z-index: 2;
2182
+ }
2183
+
2184
+ .sb-swipe-actions {
2185
+ position: absolute;
2186
+ top: 0;
2187
+ bottom: 0;
2188
+ z-index: 0;
2189
+ display: flex;
2190
+ overflow: hidden;
2191
+ }
2192
+
2193
+ .sb-swipe-actions[data-sb-edge="trailing"] {
2194
+ right: 0;
2195
+ }
2196
+
2197
+ /* Leading: the first declared action is nearest the content, so the row is reversed. */
2198
+ .sb-swipe-actions[data-sb-edge="leading"] {
2199
+ left: 0;
2200
+ flex-direction: row-reverse;
2201
+ }
2202
+
2203
+ .sb-swipe-action {
2204
+ appearance: none;
2205
+ flex: 1 1 74px;
2206
+ min-width: 0;
2207
+ display: flex;
2208
+ flex-direction: column;
2209
+ align-items: center;
2210
+ justify-content: center;
2211
+ gap: 3px;
2212
+ margin: 0;
2213
+ padding: 0 4px;
2214
+ border: 0;
2215
+ background: var(--sb-swipe-tint, var(--sb-color-gray));
2216
+ color: #fff;
2217
+ font: 500 var(--sb-ts-footnote-size, 13px) / 1.2 var(--sb-font-family);
2218
+ white-space: nowrap;
2219
+ overflow: hidden;
2220
+ cursor: pointer;
2221
+ touch-action: manipulation;
2222
+ -webkit-tap-highlight-color: transparent;
2223
+ }
2224
+
2225
+ .sb-swipe-action[data-sb-role="destructive"] {
2226
+ background: var(--sb-swipe-tint, var(--sb-color-red));
2227
+ }
2228
+
2229
+ .sb-swipe-action:active {
2230
+ filter: brightness(0.85);
2231
+ }
2232
+
2233
+ /* Once every button has its 74px, only the one nearest the content grows. */
2234
+ .sb-swipe-actions[data-sb-stretch] > .sb-swipe-action {
2235
+ flex: 0 0 74px;
2236
+ }
2237
+
2238
+ .sb-swipe-actions[data-sb-stretch] > .sb-swipe-action:first-child {
2239
+ flex: 1 1 74px;
2240
+ }
2241
+
2242
+ .sb-swipe-action-symbol {
2243
+ display: inline-flex;
2244
+ }
2245
+
2246
+ .sb-swipe-action-symbol > svg {
2247
+ width: 22px;
2248
+ height: 22px;
2249
+ stroke-width: 2;
2250
+ }
2251
+
2252
+ .sb-swipe-action-title {
2253
+ overflow: hidden;
2254
+ text-overflow: ellipsis;
2255
+ max-width: 100%;
2256
+ }
2257
+
2258
+ /* `listrow`: the edit-mode control at the engine's `editControl` rect (hidden
2259
+ otherwise); a selected row reuses the Phase 7 selected-row fill. */
2260
+ .sb-listrow-control {
2261
+ appearance: none;
2262
+ position: absolute;
2263
+ z-index: 2;
2264
+ display: flex;
2265
+ align-items: center;
2266
+ justify-content: center;
2267
+ margin: 0;
2268
+ padding: 0;
2269
+ border: 0;
2270
+ background: none;
2271
+ cursor: pointer;
2272
+ touch-action: manipulation;
2273
+ -webkit-tap-highlight-color: transparent;
2274
+ animation: sb-fade-in 0.25s ease-out both;
2275
+ }
2276
+
2277
+ .sb-listrow-control[hidden] {
2278
+ display: none;
2279
+ }
2280
+
2281
+ .sb-listrow-circle {
2282
+ display: flex;
2283
+ align-items: center;
2284
+ justify-content: center;
2285
+ width: 22px;
2286
+ height: 22px;
2287
+ border-radius: 50%;
2288
+ box-sizing: border-box;
2289
+ border: 1.5px solid var(--sb-color-tertiary-label);
2290
+ color: #fff;
2291
+ transition:
2292
+ background-color 0.2s,
2293
+ border-color 0.2s;
2294
+ }
2295
+
2296
+ .sb-listrow-circle > svg {
2297
+ display: none;
2298
+ width: 14px;
2299
+ height: 14px;
2300
+ stroke: currentColor;
2301
+ stroke-width: 3;
2302
+ }
2303
+
2304
+ .sb-listrow[data-sb-selectable][data-sb-selected] .sb-listrow-circle {
2305
+ background: var(--sb-color-accent);
2306
+ border-color: var(--sb-color-accent);
2307
+ }
2308
+
2309
+ .sb-listrow[data-sb-selectable][data-sb-selected] .sb-listrow-circle > .sb-listrow-check {
2310
+ display: block;
2311
+ }
2312
+
2313
+ /* Deletable only: the red minus. */
2314
+ .sb-listrow[data-sb-deletable]:not([data-sb-selectable]) .sb-listrow-circle {
2315
+ background: var(--sb-color-red);
2316
+ border-color: var(--sb-color-red);
2317
+ }
2318
+
2319
+ .sb-listrow[data-sb-deletable]:not([data-sb-selectable]) .sb-listrow-circle > .sb-listrow-minus {
2320
+ display: block;
2321
+ }
2322
+
2323
+ .sb-listrow[data-sb-selectable] {
2324
+ cursor: pointer;
2325
+ }
2326
+
2327
+ .sb-listrow[data-sb-selected],
2328
+ .sb-list[data-sb-list-style] > .sb-list-content > .sb-listrow[data-sb-selected],
2329
+ .sb-list[data-sb-list-style] .sb-section-rows > .sb-listrow[data-sb-selected] {
2330
+ background: var(--sb-color-tertiary-fill);
2331
+ border-radius: 10px;
2332
+ }
2333
+
2334
+ @keyframes sb-fade-in {
2335
+ from {
2336
+ opacity: 0;
2337
+ }
2338
+ to {
2339
+ opacity: 1;
2340
+ }
2341
+ }
2342
+
2343
+ /* Pull to refresh: a 40px spinner row above the content while `refreshing`
2344
+ (the content moves down by 40 like an iOS content inset); during a pull the
2345
+ controller fades it in by `--sb-pull` (0...1). */
2346
+ .sb-refresh {
2347
+ position: absolute;
2348
+ left: 0;
2349
+ right: 0;
2350
+ top: 0;
2351
+ height: 40px;
2352
+ display: flex;
2353
+ align-items: center;
2354
+ justify-content: center;
2355
+ pointer-events: none;
2356
+ opacity: var(--sb-pull, 0);
2357
+ transition: opacity 0.2s;
2358
+ }
2359
+
2360
+ .sb-refresh-spinner {
2361
+ width: 20px;
2362
+ height: 20px;
2363
+ border-radius: 50%;
2364
+ border: 2.5px solid var(--sb-color-secondary);
2365
+ border-top-color: transparent;
2366
+ transform: rotate(calc(var(--sb-pull, 0) * 360deg));
2367
+ }
2368
+
2369
+ [data-sb-refreshing] > .sb-refresh {
2370
+ opacity: 1;
2371
+ }
2372
+
2373
+ [data-sb-refreshing] > .sb-refresh > .sb-refresh-spinner {
2374
+ animation: sb-spin 0.9s linear infinite;
2375
+ }
2376
+
2377
+ .sb-list[data-sb-refreshable] > .sb-list-content,
2378
+ .sb-scrollview[data-sb-refreshable] > .sb-scroll-content {
2379
+ transition: transform 0.3s var(--sb-nav-easing);
2380
+ }
2381
+
2382
+ [data-sb-pulling] > .sb-list-content,
2383
+ [data-sb-pulling] > .sb-scroll-content {
2384
+ transition: none;
2385
+ }
2386
+
2387
+ [data-sb-refreshing] > .sb-list-content,
2388
+ [data-sb-refreshing] > .sb-scroll-content {
2389
+ transform: translateY(40px);
2390
+ }
2391
+
2392
+ /* Swipe back: the strip along a pushed screen's leading edge where a
2393
+ rightward pan starts. Only touch needs it (to keep the browser from
2394
+ scrolling): for the mouse the controller reads the pointer position. */
2395
+ .sb-navscreen-edge {
2396
+ position: absolute;
2397
+ left: 0;
2398
+ top: 0;
2399
+ bottom: 0;
2400
+ width: 24px;
2401
+ z-index: 3;
2402
+ display: none;
2403
+ pointer-events: none;
2404
+ touch-action: none;
2405
+ }
2406
+
2407
+ .sb-navscreen[data-sb-nav-position="top"]:not([data-sb-depth="0"]) > .sb-navscreen-edge {
2408
+ display: block;
2409
+ }
2410
+
2411
+ @media (pointer: coarse) {
2412
+ .sb-navscreen[data-sb-nav-position="top"]:not([data-sb-depth="0"]) > .sb-navscreen-edge {
2413
+ pointer-events: auto;
2414
+ }
2415
+ }
2416
+
2417
+ /* The screen below dims by `--sb-nav-dim` while it sits behind the top one
2418
+ (0.12 at rest; the swipe back fades it out as the top screen uncovers it). */
2419
+ .sb-navscreen::after {
2420
+ content: "";
2421
+ position: absolute;
2422
+ inset: 0;
2423
+ z-index: 4;
2424
+ background-color: rgba(0, 0, 0, var(--sb-nav-dim, 0));
2425
+ pointer-events: none;
2426
+ transition: background-color 0.4s var(--sb-nav-easing);
2427
+ }
2428
+
2429
+ .sb-navscreen[data-sb-nav-position="below"] {
2430
+ --sb-nav-dim: 0.12;
2431
+ }
2432
+
2433
+ /* While dragging, the screens follow the pointer (inline transforms); the
2434
+ dragged screen casts the push shadow. */
2435
+ .sb-navscreen[data-sb-swipe-dragging],
2436
+ .sb-navscreen[data-sb-swipe-pop] {
2437
+ z-index: 2;
2438
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.12);
2439
+ }
2440
+
2441
+ /* The pop clone starts where a swipe back left the screen (overrides the
2442
+ Phase 2 keyframes of the same name). */
2443
+ @keyframes sb-nav-pop {
2444
+ from {
2445
+ transform: translateX(var(--sb-nav-from, 0px));
2446
+ }
2447
+ to {
2448
+ transform: translateX(100%);
2449
+ }
2450
+ }
2451
+
2452
+
2453
+ /* Phase 8: alerts, controls, effects */
2454
+
2455
+ /* ---------------------------------------------------------------------------
2456
+ Alerts and confirmation dialogs: a root-level layer above sheets (z 6 > 5).
2457
+ The scrim fades in over 0.25s; the layer (cards, texts, buttons, OK) fades
2458
+ and, for the `alert` style, scales from 1.1 about the card's center (the
2459
+ renderer sets the layer's transform-origin from the engine's `card` rect).
2460
+ A dismissed alert leaves a ghost clone that fades out.
2461
+ ------------------------------------------------------------------------- */
2462
+ .screen > .sb-alert,
2463
+ .sb-alert {
2464
+ position: absolute;
2465
+ inset: 0;
2466
+ width: auto;
2467
+ height: auto;
2468
+ z-index: 6;
2469
+ display: block;
2470
+ }
2471
+
2472
+ /* Above a `large` sheet the presenting tree scales down; an alert never does. */
2473
+ .screen[data-sb-sheet] > .sb-alert,
2474
+ .screen > .sb-alert {
2475
+ transform: none;
2476
+ transition: none;
2477
+ }
2478
+
2479
+ .sb-alert-scrim {
2480
+ position: absolute;
2481
+ inset: 0;
2482
+ background: rgba(0, 0, 0, 0.3);
2483
+ animation: sb-alert-fade-in 0.25s ease-out both;
2484
+ }
2485
+
2486
+ /* Chrome the engine gave no rect gets the `hidden` attribute (layoutDom);
2487
+ the display rules below must not override it. */
2488
+ .sb-alert-layer > [hidden],
2489
+ .sb-datepicker > [hidden],
2490
+ .sb-stepper > [hidden],
2491
+ .sb-slider > [hidden] {
2492
+ display: none;
2493
+ }
2494
+
2495
+ /* The layer spans the screen only to be animated as one; taps between its
2496
+ parts fall through to the scrim. */
2497
+ .sb-alert-layer {
2498
+ position: absolute;
2499
+ inset: 0;
2500
+ pointer-events: none;
2501
+ }
2502
+
2503
+ .sb-alert-layer > * {
2504
+ pointer-events: auto;
2505
+ }
2506
+
2507
+ .sb-alert--in > .sb-alert-layer {
2508
+ animation: sb-alert-fade-in 0.25s ease-out both;
2509
+ }
2510
+
2511
+ .sb-alert[data-sb-style="alert"].sb-alert--in > .sb-alert-layer {
2512
+ animation: sb-alert-in 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) both;
2513
+ }
2514
+
2515
+ .sb-alert--out {
2516
+ pointer-events: none;
2517
+ animation: sb-alert-fade-out 0.25s ease-out both;
2518
+ }
2519
+
2520
+ .sb-alert--out > .sb-alert-scrim,
2521
+ .sb-alert--out > .sb-alert-layer {
2522
+ animation: none;
2523
+ }
2524
+
2525
+ .sb-alert-card,
2526
+ .sb-alert-cancel-card {
2527
+ position: absolute;
2528
+ box-sizing: border-box;
2529
+ border-radius: 14px;
2530
+ background: color-mix(in srgb, var(--sb-color-secondary-system-background) 85%, transparent);
2531
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
2532
+ backdrop-filter: blur(24px) saturate(180%);
2533
+ overflow: hidden;
2534
+ }
2535
+
2536
+ .sb-alert-cancel-card {
2537
+ background: var(--sb-color-sheet-background);
2538
+ }
2539
+
2540
+ .sb-alert-title,
2541
+ .sb-alert-message {
2542
+ position: absolute;
2543
+ display: block;
2544
+ white-space: pre;
2545
+ text-align: center;
2546
+ color: var(--sb-color-primary);
2547
+ overflow: hidden;
2548
+ }
2549
+
2550
+ /* The action sheet's title and message are a dim caption. */
2551
+ .sb-alert[data-sb-style="dialog"] .sb-alert-title,
2552
+ .sb-alert[data-sb-style="dialog"] .sb-alert-message {
2553
+ color: var(--sb-color-secondary);
2554
+ }
2555
+
2556
+ /* Button children: the engine gives each its row and centers its label;
2557
+ every row has a hairline above it. Two side-by-side buttons (`row` layout)
2558
+ also get a hairline between them. */
2559
+ .sb-alert-buttons > .sb-button,
2560
+ .sb-alert-ok {
2561
+ color: var(--sb-tint, var(--sb-color-accent));
2562
+ font-size: var(--sb-font-size-body);
2563
+ text-align: center;
2564
+ overflow: hidden;
2565
+ }
2566
+
2567
+ .sb-alert-buttons > .sb-button::before,
2568
+ .sb-alert-ok::before {
2569
+ content: "";
2570
+ position: absolute;
2571
+ left: 0;
2572
+ right: 0;
2573
+ top: 0;
2574
+ height: 1px;
2575
+ background: var(--sb-color-separator);
2576
+ pointer-events: none;
2577
+ }
2578
+
2579
+ .sb-alert[data-sb-layout="row"] .sb-alert-buttons > .sb-button:nth-child(2)::after {
2580
+ content: "";
2581
+ position: absolute;
2582
+ left: 0;
2583
+ top: 0;
2584
+ bottom: 0;
2585
+ width: 1px;
2586
+ background: var(--sb-color-separator);
2587
+ pointer-events: none;
2588
+ }
2589
+
2590
+ .sb-alert-buttons > .sb-button[data-sb-role="cancel"] {
2591
+ font-weight: 600;
2592
+ }
2593
+
2594
+ .sb-alert-buttons > .sb-button[data-sb-role="destructive"] {
2595
+ color: var(--sb-color-red);
2596
+ }
2597
+
2598
+ .sb-alert-buttons > .sb-button:active,
2599
+ .sb-alert-ok:active {
2600
+ opacity: 1;
2601
+ background: var(--sb-color-fill);
2602
+ }
2603
+
2604
+ .sb-alert-ok {
2605
+ appearance: none;
2606
+ position: absolute;
2607
+ margin: 0;
2608
+ padding: 0;
2609
+ border: 0;
2610
+ background: none;
2611
+ font: inherit;
2612
+ font-size: var(--sb-font-size-body);
2613
+ cursor: pointer;
2614
+ touch-action: manipulation;
2615
+ -webkit-tap-highlight-color: transparent;
2616
+ }
2617
+
2618
+ /* Action sheet rows are taller and 20pt; its cancel button sits in its own card (no hairline). */
2619
+ .sb-alert[data-sb-style="dialog"] .sb-alert-buttons > .sb-button,
2620
+ .sb-alert[data-sb-style="dialog"] .sb-alert-ok {
2621
+ font-size: 20px;
2622
+ }
2623
+
2624
+ .sb-alert[data-sb-style="dialog"] .sb-alert-buttons > .sb-button[data-sb-role="cancel"]::before {
2625
+ display: none;
2626
+ }
2627
+
2628
+ /* Without a title or message the first row is flush with the card's top: no hairline. */
2629
+ .sb-alert[data-sb-style="dialog"]:not([data-sb-header]) .sb-alert-buttons > .sb-button:first-child::before,
2630
+ .sb-alert[data-sb-style="dialog"]:not([data-sb-header]) .sb-alert-ok::before {
2631
+ display: none;
2632
+ }
2633
+
2634
+ @keyframes sb-alert-fade-in {
2635
+ from {
2636
+ opacity: 0;
2637
+ }
2638
+ to {
2639
+ opacity: 1;
2640
+ }
2641
+ }
2642
+
2643
+ @keyframes sb-alert-in {
2644
+ from {
2645
+ opacity: 0;
2646
+ transform: scale(1.1);
2647
+ }
2648
+ to {
2649
+ opacity: 1;
2650
+ transform: scale(1);
2651
+ }
2652
+ }
2653
+
2654
+ @keyframes sb-alert-fade-out {
2655
+ from {
2656
+ opacity: 1;
2657
+ }
2658
+ to {
2659
+ opacity: 0;
2660
+ }
2661
+ }
2662
+
2663
+ /* ---------------------------------------------------------------------------
2664
+ Full-screen cover: a sheet with `detents: ["full"]` has no radius, grabber
2665
+ (the engine gives it none) or drag to dismiss.
2666
+ ------------------------------------------------------------------------- */
2667
+ .sb-sheet[data-sb-detent="full"] > .sb-sheet-card {
2668
+ border-radius: 0;
2669
+ box-shadow: none;
2670
+ touch-action: auto;
2671
+ }
2672
+
2673
+ /* ---------------------------------------------------------------------------
2674
+ Stepper: the label children are positioned by their frames; the 94×32
2675
+ segmented control sits at the engine's `control` rect, its halves divided
2676
+ by a hairline. A dimmed half (`aria-disabled`) swallows taps.
2677
+ ------------------------------------------------------------------------- */
2678
+ .sb-stepper-control {
2679
+ position: absolute;
2680
+ display: flex;
2681
+ box-sizing: border-box;
2682
+ border-radius: 8px;
2683
+ background: var(--sb-color-tertiary-fill);
2684
+ overflow: hidden;
2685
+ }
2686
+
2687
+ .sb-stepper-button {
2688
+ appearance: none;
2689
+ flex: 1 1 0;
2690
+ display: flex;
2691
+ align-items: center;
2692
+ justify-content: center;
2693
+ margin: 0;
2694
+ padding: 0;
2695
+ border: 0;
2696
+ background: none;
2697
+ color: var(--sb-color-primary);
2698
+ font: inherit;
2699
+ font-size: var(--sb-font-size-body);
2700
+ cursor: pointer;
2701
+ touch-action: manipulation;
2702
+ -webkit-tap-highlight-color: transparent;
2703
+ }
2704
+
2705
+ .sb-stepper-button + .sb-stepper-button {
2706
+ box-shadow: -1px 0 0 var(--sb-color-separator);
2707
+ }
2708
+
2709
+ .sb-stepper-button > svg {
2710
+ display: block;
2711
+ width: 16px;
2712
+ height: 16px;
2713
+ stroke: currentColor;
2714
+ stroke-width: 2.5;
2715
+ }
2716
+
2717
+ .sb-stepper-button:active:not([aria-disabled="true"]) {
2718
+ background: var(--sb-color-fill);
2719
+ }
2720
+
2721
+ .sb-stepper-button[aria-disabled="true"] {
2722
+ opacity: 0.35;
2723
+ cursor: default;
2724
+ }
2725
+
2726
+ /* ---------------------------------------------------------------------------
2727
+ Slider: the label children are positioned by their frames; the 4pt track,
2728
+ the accent fill and the 27pt thumb are chrome at the engine's rects. A
2729
+ pointer anywhere on the slider moves the thumb (touch-action none).
2730
+ ------------------------------------------------------------------------- */
2731
+ .sb-slider {
2732
+ touch-action: none;
2733
+ cursor: pointer;
2734
+ -webkit-tap-highlight-color: transparent;
2735
+ }
2736
+
2737
+ .sb-slider-track {
2738
+ position: absolute;
2739
+ border-radius: 2px;
2740
+ background: var(--sb-color-tertiary-fill);
2741
+ }
2742
+
2743
+ .sb-slider-fill {
2744
+ position: absolute;
2745
+ border-radius: 2px;
2746
+ background: var(--sb-tint, var(--sb-color-accent));
2747
+ }
2748
+
2749
+ .sb-slider-thumb {
2750
+ position: absolute;
2751
+ box-sizing: border-box;
2752
+ border-radius: 50%;
2753
+ background: #ffffff;
2754
+ box-shadow:
2755
+ 0 3px 8px rgba(0, 0, 0, 0.15),
2756
+ 0 3px 1px rgba(0, 0, 0, 0.06),
2757
+ 0 0 0 0.5px rgba(0, 0, 0, 0.04);
2758
+ }
2759
+
2760
+ /* ---------------------------------------------------------------------------
2761
+ Date picker (compact): the label children are positioned by their frames;
2762
+ the date / time capsules are chrome at the engine's rects, each with an
2763
+ invisible native input stretched over it that opens the browser's picker.
2764
+ ------------------------------------------------------------------------- */
2765
+ .sb-datepicker-capsule {
2766
+ position: absolute;
2767
+ display: flex;
2768
+ align-items: center;
2769
+ justify-content: center;
2770
+ box-sizing: border-box;
2771
+ border-radius: 8px;
2772
+ background: var(--sb-color-tertiary-fill);
2773
+ color: var(--sb-color-primary);
2774
+ font-size: var(--sb-font-size-body);
2775
+ line-height: 1.3;
2776
+ white-space: nowrap;
2777
+ overflow: hidden;
2778
+ }
2779
+
2780
+ .sb-datepicker-text {
2781
+ display: block;
2782
+ overflow: hidden;
2783
+ text-overflow: ellipsis;
2784
+ }
2785
+
2786
+ .sb-datepicker-input {
2787
+ position: absolute;
2788
+ inset: 0;
2789
+ width: 100%;
2790
+ height: 100%;
2791
+ margin: 0;
2792
+ padding: 0;
2793
+ border: 0;
2794
+ opacity: 0;
2795
+ cursor: pointer;
2796
+ color-scheme: light dark;
2797
+ }
2798
+
2799
+ .sb-datepicker-input::-webkit-calendar-picker-indicator {
2800
+ position: absolute;
2801
+ inset: 0;
2802
+ width: 100%;
2803
+ height: 100%;
2804
+ cursor: pointer;
2805
+ }
2806
+
2807
+ /* ---------------------------------------------------------------------------
2808
+ Text editor: a borderless textarea filling its frame in the body font (its
2809
+ parent provides any frame or background).
2810
+ ------------------------------------------------------------------------- */
2811
+ .sb-texteditor {
2812
+ appearance: none;
2813
+ display: block;
2814
+ margin: 0;
2815
+ padding: 0;
2816
+ border: 0;
2817
+ outline: none;
2818
+ resize: none;
2819
+ overflow: auto;
2820
+ background: transparent;
2821
+ color: var(--sb-color-primary);
2822
+ font: inherit;
2823
+ font-size: var(--sb-font-size-body);
2824
+ line-height: 1.3;
2825
+ }
2826
+
2827
+ @media (prefers-reduced-motion: reduce) {
2828
+ .sb-alert-scrim,
2829
+ .sb-alert--in > .sb-alert-layer,
2830
+ .sb-alert[data-sb-style="alert"].sb-alert--in > .sb-alert-layer,
2831
+ .sb-alert--out {
2832
+ animation-duration: 0.01s;
2833
+ }
2834
+ }