actual-css 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +67 -3
  2. package/README.md +35 -30
  3. package/dist/actual.css +73 -4946
  4. package/dist/actual.full.css +7090 -0
  5. package/dist/actual.full.js +1 -0
  6. package/dist/actual.full.min.css +1 -0
  7. package/dist/actual.js +1 -1
  8. package/dist/actual.min.css +1 -1
  9. package/docs/design-notes/cascade-layer.md +34 -13
  10. package/docs/design-notes/column-layout.md +677 -0
  11. package/docs/design-notes/forced-colors.md +3 -0
  12. package/docs/design-notes/grid.md +612 -0
  13. package/docs/design-notes/naming.md +15 -0
  14. package/docs/design-notes/widget-primitives.md +1 -0
  15. package/docs/navigation.json +5 -1
  16. package/docs/pages/components/alert.md +16 -14
  17. package/docs/pages/components/aura.md +1 -1
  18. package/docs/pages/components/avatar.md +6 -6
  19. package/docs/pages/components/badge.md +7 -7
  20. package/docs/pages/components/button.md +16 -16
  21. package/docs/pages/components/card.md +13 -9
  22. package/docs/pages/components/chat.md +8 -8
  23. package/docs/pages/components/dialog.md +7 -7
  24. package/docs/pages/components/drawer.md +3 -3
  25. package/docs/pages/components/fab.md +7 -7
  26. package/docs/pages/components/navbar.md +9 -8
  27. package/docs/pages/components/pagination.md +4 -3
  28. package/docs/pages/components/skeleton.md +4 -4
  29. package/docs/pages/components/spinner.md +5 -5
  30. package/docs/pages/components/table.md +30 -5
  31. package/docs/pages/enhancements/flyout.md +13 -13
  32. package/docs/pages/enhancements/overview.md +1 -1
  33. package/docs/pages/enhancements/scrollspy.md +6 -6
  34. package/docs/pages/enhancements/status-bar.md +5 -1
  35. package/docs/pages/enhancements/tabs.md +6 -6
  36. package/docs/pages/enhancements/tooltip.md +1 -1
  37. package/docs/pages/examples/overview.md +39 -22
  38. package/docs/pages/forms/choice-cards.md +11 -6
  39. package/docs/pages/forms/controls.md +1 -1
  40. package/docs/pages/forms/detached-actions.md +1 -1
  41. package/docs/pages/forms/floating-field.md +10 -5
  42. package/docs/pages/forms/form-actions.md +1 -1
  43. package/docs/pages/forms/input-policy.md +1 -1
  44. package/docs/pages/forms/joined-controls.md +4 -4
  45. package/docs/pages/forms/labels.md +6 -6
  46. package/docs/pages/forms/otp.md +1 -1
  47. package/docs/pages/forms/overview.md +19 -19
  48. package/docs/pages/forms/password.md +1 -1
  49. package/docs/pages/forms/range.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +41 -45
  51. package/docs/pages/forms/sticky-actions.md +3 -3
  52. package/docs/pages/forms/switch.md +110 -2
  53. package/docs/pages/forms/validation.md +9 -9
  54. package/docs/pages/foundations/philosophy.md +1 -1
  55. package/docs/pages/foundations/theming.md +17 -2
  56. package/docs/pages/foundations/tokens.md +22 -10
  57. package/docs/pages/foundations/typography.md +50 -20
  58. package/docs/pages/guides/bootstrap.md +11 -11
  59. package/docs/pages/guides/integrating-actual-css.md +186 -0
  60. package/docs/pages/guides/introduction.md +23 -26
  61. package/docs/pages/guides/modular-import.md +173 -0
  62. package/docs/pages/guides/progressive-enhancement.md +81 -78
  63. package/docs/pages/guides/tailwind.md +6 -8
  64. package/docs/pages/layout/app-shell.md +1 -1
  65. package/docs/pages/layout/center.md +1 -1
  66. package/docs/pages/layout/choosing.md +39 -0
  67. package/docs/pages/layout/column-layout.md +206 -0
  68. package/docs/pages/layout/frame.md +6 -1
  69. package/docs/pages/layout/grid.md +100 -102
  70. package/docs/pages/layout/list.md +1 -1
  71. package/docs/pages/layout/media.md +1 -1
  72. package/docs/pages/layout/overview.md +15 -2
  73. package/docs/pages/layout/scroll-snap.md +2 -2
  74. package/docs/pages/layout/scroll-target.md +45 -0
  75. package/docs/pages/layout/sidebar-layout.md +20 -19
  76. package/docs/pages/layout/stack.md +53 -1
  77. package/docs/pages/layout/switcher.md +10 -26
  78. package/docs/pages/layout/topbar.md +2 -2
  79. package/docs/pages/patterns/actions.md +1 -1
  80. package/docs/pages/patterns/code-block.md +1 -1
  81. package/docs/pages/patterns/exclusive-accordion.md +1 -1
  82. package/docs/pages/patterns/hierarchical-nav.md +1 -1
  83. package/docs/pages/patterns/icon-input.md +1 -1
  84. package/docs/pages/patterns/intent-nesting.md +1 -1
  85. package/docs/pages/patterns/navigation-list.md +1 -1
  86. package/docs/pages/patterns/overview.md +1 -1
  87. package/docs/pages/patterns/structured-lists.md +1 -1
  88. package/docs/pages/utilities/accessibility.md +3 -3
  89. package/docs/pages/utilities/flex.md +15 -15
  90. package/docs/pages/utilities/links.md +4 -4
  91. package/docs/pages/utilities/list.md +8 -8
  92. package/docs/pages/utilities/overflow.md +8 -8
  93. package/docs/pages/utilities/overview.md +1 -1
  94. package/docs/pages/utilities/shape.md +3 -3
  95. package/docs/pages/utilities/sizing-wrapping.md +23 -16
  96. package/docs/pages/utilities/spacing.md +20 -20
  97. package/docs/pages/utilities/text.md +10 -10
  98. package/llms.txt +46 -4
  99. package/package.json +42 -13
  100. package/scripts/reserved-classes.json +261 -0
  101. package/src/cli/actual-css.js +133 -0
  102. package/src/css/actual.css +1 -31
  103. package/src/css/actual.full.css +11 -2
  104. package/src/css/components/alert.css +17 -4
  105. package/src/css/components/badge.css +18 -6
  106. package/src/css/components/breadcrumb.css +1 -1
  107. package/src/css/components/button.css +19 -1
  108. package/src/css/components/card.css +27 -2
  109. package/src/css/components/drawer.css +27 -2
  110. package/src/css/components/flyout.css +5 -0
  111. package/src/css/components/index.css +32 -27
  112. package/src/css/components/join.css +8 -1
  113. package/src/css/components/key.css +2 -0
  114. package/src/css/components/menu.css +11 -1
  115. package/src/css/components/meter.css +4 -1
  116. package/src/css/components/modal.css +5 -0
  117. package/src/css/components/pagination.css +11 -0
  118. package/src/css/components/spinner.css +5 -0
  119. package/src/css/components/status-bar.css +5 -0
  120. package/src/css/components/surface.css +5 -0
  121. package/src/css/components/table.css +6 -0
  122. package/src/css/components/tooltip.css +5 -0
  123. package/src/css/core/index.css +9 -0
  124. package/src/css/core/print.css +18 -0
  125. package/src/css/{theme.css → core/theme.css} +20 -10
  126. package/src/css/{tokens.css → core/tokens.css} +15 -2
  127. package/src/css/{variants.css → core/variants.css} +3 -2
  128. package/src/css/effects/index.css +2 -0
  129. package/src/css/forms/base.css +8 -0
  130. package/src/css/forms/choice-card.css +5 -4
  131. package/src/css/forms/choice.css +49 -37
  132. package/src/css/forms/control.css +4 -1
  133. package/src/css/forms/custom-select.css +2 -0
  134. package/src/css/{optional → forms}/floating-field.css +25 -7
  135. package/src/css/forms/index.css +10 -13
  136. package/src/css/forms/native.css +15 -2
  137. package/src/css/{optional → forms}/otp.css +20 -16
  138. package/src/css/forms/switch.css +31 -29
  139. package/src/css/layout/app-shell.css +6 -0
  140. package/src/css/layout/center.css +13 -0
  141. package/src/css/layout/cluster.css +21 -0
  142. package/src/css/layout/column-layout.css +144 -0
  143. package/src/css/layout/container-query.css +13 -0
  144. package/src/css/layout/frame.css +19 -0
  145. package/src/css/layout/grid.css +141 -0
  146. package/src/css/layout/index.css +42 -0
  147. package/src/css/layout/measure.css +9 -0
  148. package/src/css/layout/media.css +10 -0
  149. package/src/css/layout/scroll-target.css +15 -0
  150. package/src/css/layout/scroller.css +41 -0
  151. package/src/css/layout/sidebar-layout.css +31 -0
  152. package/src/css/layout/stack.css +21 -0
  153. package/src/css/layout/switcher.css +23 -0
  154. package/src/css/{optional/layout-extra.css → layout/topbar.css} +0 -20
  155. package/src/css/typography/index.css +5 -0
  156. package/src/css/typography/lead.css +7 -0
  157. package/src/css/{components → typography}/overline.css +2 -2
  158. package/src/css/{prose.css → typography/prose.css} +53 -38
  159. package/src/css/{utilities.css → utilities/base.css} +2 -0
  160. package/src/css/utilities/index.css +3 -0
  161. package/src/js/escape.js +36 -0
  162. package/src/js/flyout.js +22 -12
  163. package/src/js/full.js +26 -0
  164. package/src/js/index.js +5 -21
  165. package/src/js/surface.js +1 -31
  166. package/src/js/tooltip.js +46 -18
  167. package/src/js/validation.js +1 -1
  168. package/src/tooling/css-bundle.js +308 -0
  169. package/dist/actual-themes.min.css +0 -1
  170. package/dist/optional.css +0 -1048
  171. package/dist/optional.min.css +0 -1
  172. package/docs/pages/guides/optional-components.md +0 -33
  173. package/src/css/grid.css +0 -109
  174. package/src/css/layout.css +0 -123
  175. package/src/css/optional/index.css +0 -10
  176. package/src/css/optional/scroller.css +0 -63
  177. package/src/css/print.css +0 -47
  178. package/src/css/themes/corporate.css +0 -60
  179. package/src/css/themes/cyberpunk.css +0 -60
  180. package/src/css/themes/dim.css +0 -33
  181. package/src/css/themes/edge.css +0 -117
  182. package/src/css/themes/forest.css +0 -65
  183. package/src/css/themes/gradient.css +0 -97
  184. package/src/css/themes/index.css +0 -47
  185. package/src/css/themes/indigo.css +0 -33
  186. package/src/css/themes/lavender.css +0 -66
  187. package/src/css/themes/mono.css +0 -60
  188. package/src/css/themes/neon.css +0 -66
  189. package/src/css/themes/ocean.css +0 -60
  190. package/src/css/themes/petrol.css +0 -64
  191. package/src/css/themes/spruce.css +0 -64
  192. package/src/css/themes/square.css +0 -65
  193. package/src/css/themes/sunset.css +0 -66
  194. /package/src/css/{optional → components}/chat.css +0 -0
  195. /package/src/css/{optional → components}/fab.css +0 -0
  196. /package/src/css/{base.css → core/base.css} +0 -0
  197. /package/src/css/{focus.css → core/focus.css} +0 -0
  198. /package/src/css/{intents.css → core/intents.css} +0 -0
  199. /package/src/css/{reset.css → core/reset.css} +0 -0
  200. /package/src/css/{optional → effects}/aura.css +0 -0
  201. /package/src/css/{optional → layout}/scroll-snap.css +0 -0
  202. /package/src/css/{optional/typography-fluid.css → typography/fluid.css} +0 -0
  203. /package/src/css/{optional/utilities-extra.css → utilities/extra.css} +0 -0
package/dist/actual.css CHANGED
@@ -1,3 +1,4 @@
1
+ /* Core — the minimal Actual language. Every module depends on this order. */
1
2
  /* Mild reset: useful defaults without classless magic. */
2
3
  *,
3
4
  *::before,
@@ -130,13 +131,21 @@ select {
130
131
 
131
132
  /* Controls — inputs and buttons share --control-size so heights always match.
132
133
  .sm/.lg (variants.css) switch --control-size; --control-font-size stays
133
- at its baseline because density does not change typography. */
134
+ at its baseline because density does not change typography.
135
+
136
+ Inline choices (checkbox, radio, switch) are typographic, not field-sized:
137
+ they sit on a line of text, so they follow --control-font-size through
138
+ --choice-control-size instead of --control-size. Being an em value in
139
+ :root is deliberate — an unregistered custom property resolves its units
140
+ at the element that consumes it, so one declaration tracks whatever font
141
+ size the choice actually renders at. */
134
142
  --control-size-sm: 2rem;
135
143
  --control-size-md: 2.375rem;
136
144
  --control-size-lg: 2.75rem;
137
145
  --control-size: var(--control-size-md);
138
146
  --control-pad-x: 1em;
139
147
  --control-font-size: var(--font-size-md);
148
+ --choice-control-size: 1.125em;
140
149
  --disabled-opacity: 0.65;
141
150
  }
142
151
 
@@ -165,6 +174,10 @@ select {
165
174
  --soft-bg-mix: 88%;
166
175
  --soft-border-mix: 65%;
167
176
  --soft-hover-alpha: 12%;
177
+ /* Soft foreground: the share of raw intent in soft text. 100% keeps the
178
+ pure intent color; lower values rebate it toward --text, which vivid or
179
+ light intent palettes need to stay legible on their own soft surface. */
180
+ --soft-fg-mix: 100%;
168
181
 
169
182
  /* Shared icon assets. Override these URLs when a strict CSP disallows data:. */
170
183
  --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
@@ -189,7 +202,8 @@ select {
189
202
  }
190
203
 
191
204
  @media (forced-colors: active) {
192
- :root {
205
+ :root,
206
+ [data-theme] {
193
207
  --state-selected: Highlight;
194
208
  --state-selected-fg: HighlightText;
195
209
  --state-disabled: GrayText;
@@ -331,6 +345,16 @@ select {
331
345
  --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
332
346
  }
333
347
 
348
+ /* Native platform controls (checkbox, radio, range, progress) that are not
349
+ restyled by a component class still follow the theme accent. Declared on
350
+ every island: accent-color inherits as a computed value, so islands must
351
+ recompute it from their own --primary (same resolution rule as above).
352
+ Unsupported browsers ignore it; forced-colors keeps browser control. */
353
+ :root,
354
+ [data-theme] {
355
+ accent-color: var(--primary);
356
+ }
357
+
334
358
  /* Tone subsystem — fixed-backdrop foreground for components placed over media
335
359
  or other surfaces outside the theme token stack. Use data-theme islands for
336
360
  full dark/light subtree inversion. */
@@ -345,13 +369,13 @@ select {
345
369
  /* Focus ring derivation — color-mix tier. Runs whether or not light-dark()
346
370
  is also supported, so it covers the color-mix-without-light-dark browser
347
371
  tier on its own; the light-dark block below refines it further with the
348
- light/dark alpha split once both features are available. Deriving from
349
- --focus (not a literal) means overriding --primary/--focus, in the
350
- default theme or any custom one, keeps the ring in sync automatically. */
372
+ light/dark alpha split once both features are available. Declared on every
373
+ [data-theme] island (not just the built-in light/dark pair) so custom
374
+ islands recolor the ring for free; a preset that re-declares --focus-ring
375
+ later in source order intentionally pins its own ring instead. */
351
376
  @supports (color: color-mix(in oklch, red, white)) {
352
377
  :root,
353
- [data-theme="light"],
354
- [data-theme="dark"] {
378
+ [data-theme] {
355
379
  --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent);
356
380
  }
357
381
  }
@@ -400,7 +424,7 @@ select {
400
424
 
401
425
  @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) {
402
426
  :root,
403
- [data-theme="light"] {
427
+ [data-theme] {
404
428
  --focus-ring: light-dark(
405
429
  color-mix(in oklch, var(--focus) 30%, transparent),
406
430
  color-mix(in oklch, var(--focus) 28%, transparent)
@@ -410,8 +434,7 @@ select {
410
434
 
411
435
  @media (forced-colors: active) {
412
436
  :root,
413
- [data-theme="light"],
414
- [data-theme="dark"] {
437
+ [data-theme] {
415
438
  --focus-outline-color: Highlight;
416
439
  --focus-ring-shadow: none;
417
440
 
@@ -421,8 +444,9 @@ select {
421
444
 
422
445
  @media (prefers-contrast: more) {
423
446
  :root,
424
- [data-theme="light"],
425
- [data-theme="dark"] {
447
+ /* The repeated attribute intentionally outranks single-attribute preset
448
+ palettes, including a preset stylesheet loaded after the core. */
449
+ [data-theme][data-theme] {
426
450
  --border-width: 2px;
427
451
  --focus-outline-offset: 3px;
428
452
  --focus-ring-width: 4px;
@@ -530,441 +554,6 @@ small {
530
554
  --intent-fg: var(--neutral-fg);
531
555
  }
532
556
 
533
- /* Prose before layout: layout primitives (.stack, .cluster) reset child
534
- margins and must win over .prose's flow margins at equal specificity —
535
- source order is the tie-breaker, so prose loads first. */
536
- /*
537
- * Prose is the opt-in rich text scope for authored content.
538
- *
539
- * Global element styles stay mild so app UI is not shaped accidentally. Page
540
- * layout, forms, cards, navigation, and app tables remain outside this scope.
541
- *
542
- * Public hooks:
543
- * --prose-size
544
- * --prose-line-height
545
- * --prose-measure
546
- * --prose-flow
547
- * --prose-heading-line-height
548
- * --prose-h1
549
- * --prose-h2
550
- * --prose-h3
551
- * --prose-h4
552
- */
553
-
554
- .prose {
555
- /* Prose scale */
556
- --prose-size: 1rem;
557
- --prose-line-height: 1.7;
558
- --prose-measure: 70ch;
559
- --prose-flow: 1em;
560
- --prose-heading-line-height: 1.2;
561
- --prose-h1: 2rem;
562
- --prose-h2: 1.5rem;
563
- --prose-h3: 1.25rem;
564
- --prose-h4: 1.125rem;
565
-
566
- max-inline-size: var(--prose-measure);
567
- color: var(--text);
568
- font-size: var(--prose-size);
569
- line-height: var(--prose-line-height);
570
- overflow-wrap: break-word;
571
- }
572
-
573
- .prose :where(p, ul, ol, dl, blockquote, pre, table, figure) {
574
- margin-block: var(--prose-flow) 0;
575
- }
576
-
577
- .prose :where(h1, h2, h3, h4, h5, h6) {
578
- margin-block: 1.5em 0;
579
- color: var(--text);
580
- font-weight: var(--font-weight-strong);
581
- line-height: var(--prose-heading-line-height);
582
- }
583
-
584
- .prose :where(p, li, blockquote) {
585
- text-wrap: pretty;
586
- }
587
-
588
- .prose > :first-child {
589
- margin-block-start: 0;
590
- }
591
-
592
- .prose h1 {
593
- font-size: var(--prose-h1);
594
- }
595
-
596
- .prose h2 {
597
- font-size: var(--prose-h2);
598
- }
599
-
600
- .prose h3 {
601
- font-size: var(--prose-h3);
602
- }
603
-
604
- .prose h4 {
605
- font-size: var(--prose-h4);
606
- }
607
-
608
- .prose a {
609
- color: var(--primary);
610
- text-decoration-line: underline;
611
- text-decoration-thickness: 0.08em;
612
- text-underline-offset: 0.18em;
613
- }
614
-
615
- .prose a:hover {
616
- text-decoration-thickness: 0.12em;
617
- }
618
-
619
- @supports (color: color-mix(in oklch, red, white)) {
620
- .prose a:hover {
621
- color: color-mix(in oklch, var(--primary), var(--text) 15%);
622
- }
623
- }
624
-
625
- .prose :where(ul, ol) {
626
- padding-inline-start: 1.5em;
627
- }
628
-
629
- .prose li + li {
630
- margin-block-start: 0.35em;
631
- }
632
-
633
- .prose code {
634
- padding: 0.15em 0.3em;
635
- border-radius: var(--radius-sm);
636
- background: var(--surface-subtle);
637
- color: var(--text);
638
- font-size: 0.875em;
639
- }
640
-
641
- /* Visual twin of .key (components/key.css) — keep the two recipes in sync. */
642
- .prose kbd {
643
- display: inline-flex;
644
- align-items: center;
645
- justify-content: center;
646
- min-inline-size: 1.75em;
647
- min-block-size: 1.6em;
648
- padding-inline: 0.45em;
649
- border: var(--border-width) solid var(--border);
650
- border-block-end-width: calc(var(--border-width) * 2);
651
- border-radius: var(--radius-sm);
652
- background: var(--surface-raised);
653
- color: var(--text);
654
- font-family: var(--font-mono);
655
- font-size: 0.875em;
656
- font-weight: var(--font-weight-medium);
657
- line-height: 1;
658
- white-space: nowrap;
659
- vertical-align: 0.08em;
660
- }
661
-
662
- .prose pre {
663
- overflow-x: auto;
664
- padding: 1em;
665
- border: var(--border-width) solid var(--border);
666
- border-radius: var(--radius);
667
- background: var(--surface-subtle);
668
- }
669
-
670
- .prose pre code {
671
- padding: 0;
672
- background: transparent;
673
- }
674
-
675
- .prose blockquote {
676
- padding-inline-start: 1em;
677
- border-inline-start: var(--border-width) solid var(--border);
678
- color: var(--text-muted);
679
- }
680
-
681
- .prose figure {
682
- margin-inline: 0;
683
- }
684
-
685
- .prose figcaption {
686
- margin-block-start: 0.5em;
687
- color: var(--text-muted);
688
- font-size: 0.875em;
689
- }
690
-
691
- .prose table {
692
- inline-size: 100%;
693
- border-collapse: collapse;
694
- }
695
-
696
- .prose th,
697
- .prose td {
698
- padding: 0.5em;
699
- border-block-end: var(--border-width) solid var(--border);
700
- text-align: start;
701
- }
702
-
703
- /* Lead paragraph: a slightly larger, more relaxed reading-introduction text.
704
- Non-fluid and always available — use .text-lead from the optional fluid
705
- module for marketing/display surfaces. */
706
- .lead {
707
- font-size: 1.125em;
708
- line-height: var(--line-height-relaxed);
709
- }
710
-
711
- /* Readable content measure. Composes with .center for centering or with
712
- margin-inline: auto for a one-off centered block. */
713
- .measure {
714
- max-inline-size: var(--prose-measure, 70ch);
715
- }
716
-
717
- .prose.sm {
718
- --prose-size: 0.9375rem;
719
- --prose-h1: 1.75rem;
720
- --prose-h2: 1.375rem;
721
- --prose-h3: 1.125rem;
722
- --prose-h4: 1rem;
723
- }
724
-
725
- .prose.lg {
726
- --prose-size: 1.125rem;
727
- --prose-measure: 72ch;
728
- --prose-h1: 2.25rem;
729
- --prose-h2: 1.625rem;
730
- --prose-h3: 1.375rem;
731
- --prose-h4: 1.25rem;
732
- }
733
-
734
- /*
735
- * Core layout primitives: stack, cluster, center, frame, app-shell, media,
736
- * sidebar-layout. Each primitive owns a single, readable behavior; they are
737
- * meant to compose in markup.
738
- *
739
- * Public hooks:
740
- * --cluster-align
741
- * --cluster-justify
742
- * --cluster-wrap
743
- * --center-size
744
- * --center-pad
745
- * --frame-ratio
746
- * --sidebar-layout-size
747
- *
748
- * Child contracts:
749
- * .stack owns vertical rhythm: direct-child block-axis margins are reset,
750
- * spacing comes from --gap, inline margins stay free.
751
- * .cluster owns inline rhythm: direct-child margins are reset, spacing comes
752
- * from --gap.
753
- * .frame sizes direct media children (img/video/iframe/canvas/svg) to fill the
754
- * frame.
755
- */
756
-
757
- /* Responsive recipes:
758
- - avatar/icon + flexible content -> .media
759
- - inline items that wrap when space runs out -> .cluster (keep wrapping)
760
- - content + aside -> .sidebar-layout inside .container-query
761
- - row that becomes a column when constrained -> .switcher (optional module)
762
-
763
- Anti-patterns:
764
- - viewport breakpoints for ordinary composition
765
- - fixed grid templates (--grid-columns) where .grid/.sidebar-layout express
766
- the layout
767
- - nowrap on page-level compositions; prefer wrapping and container-aware
768
- primitives */
769
- .container-query {
770
- container-type: inline-size;
771
- }
772
-
773
- .stack {
774
- display: flex;
775
- flex-direction: column;
776
- gap: var(--gap);
777
- }
778
-
779
- /* .stack owns the block axis only: block margins reset, inline margins stay
780
- free (self-centering children via margin-inline: auto keep working). */
781
- .stack > * {
782
- margin-block: 0;
783
- }
784
-
785
- .cluster {
786
- display: flex;
787
- flex-wrap: var(--cluster-wrap, wrap);
788
- align-items: var(--cluster-align, center);
789
- justify-content: var(--cluster-justify, flex-start);
790
- gap: var(--gap);
791
- }
792
-
793
- .cluster > * {
794
- margin: 0;
795
- }
796
-
797
- .center {
798
- box-sizing: content-box;
799
- max-inline-size: var(--center-size, 72rem);
800
- margin-inline: auto;
801
- padding-inline: var(--center-pad, 1rem);
802
- }
803
-
804
- .frame {
805
- overflow: hidden;
806
- aspect-ratio: var(--frame-ratio, 16 / 9);
807
- }
808
-
809
- .frame > :where(img, video, iframe, canvas, svg) {
810
- inline-size: 100%;
811
- block-size: 100%;
812
- object-fit: cover;
813
- }
814
-
815
- .app-shell {
816
- min-block-size: var(--viewport-block);
817
- display: flex;
818
- flex-direction: column;
819
- }
820
-
821
- /* Media object: fixed-width leading element (avatar, icon, image) + flexible
822
- trailing content. Replaces the repeated "auto minmax(0, 1fr)" pattern in
823
- author cards, comments, meta rows, and similar compositions. */
824
- .media {
825
- display: grid;
826
- grid-template-columns: auto minmax(0, 1fr);
827
- gap: var(--gap);
828
- align-items: start;
829
- }
830
-
831
- /* Sidebar layout: content + aside using grid. Use this when the aside is
832
- sticky or the content is multi-section. The aside width is tunable with
833
- --sidebar-layout-size. */
834
- .sidebar-layout {
835
- display: grid;
836
- gap: var(--gap);
837
- grid-template-columns: minmax(0, 1fr);
838
- }
839
-
840
- /* Container queries are the preferred path; without them the @supports not
841
- block below restores the same columns at the same viewport breakpoint. */
842
- @container (width >= 64rem) {
843
- .container-query .sidebar-layout {
844
- grid-template-columns: minmax(0, 1fr) var(--sidebar-layout-size, 18rem);
845
- align-items: start;
846
- }
847
- }
848
-
849
- @supports not (container-type: inline-size) {
850
- @media (min-width: 64rem) {
851
- .container-query .sidebar-layout {
852
- grid-template-columns: minmax(0, 1fr) var(--sidebar-layout-size, 18rem);
853
- align-items: start;
854
- }
855
- }
856
- }
857
-
858
- /*
859
- * Responsive equal-width grids, with fixed-column presets below.
860
- *
861
- * Public hooks:
862
- * --grid-min
863
- * --grid-columns
864
- */
865
-
866
- /* --grid-columns replaces the intrinsic auto-fit template: once set, the
867
- grid no longer collapses on its own — the author owns narrow-container
868
- collapse. Prefer .sidebar-layout inside .container-query for content +
869
- aside layouts; reserve --grid-columns for genuinely fixed or asymmetric
870
- templates. */
871
-
872
- /* Recipe:
873
- - N equal cards that collapse when narrow -> .grid (auto-fit, tune
874
- --grid-min) or .grid-2/3/4/6 inside .container-query
875
- - content + aside -> .sidebar-layout inside .container-query
876
- - fixed or asymmetric columns -> --grid-columns with an explicit collapse
877
- rule of your own */
878
- .grid {
879
- display: grid;
880
- gap: var(--gap);
881
- grid-template-columns: var(
882
- --grid-columns,
883
- repeat(auto-fit, minmax(min(100%, var(--grid-min, 16rem)), 1fr))
884
- );
885
- }
886
-
887
- /* Fixed-column grids at their full count by default: .grid-2/3/4/6 stay at
888
- 2/3/4/6 columns unless wrapped in .container-query, which collapses the
889
- column count on narrow wrappers (a 2-col intermediate step for
890
- .grid-3/.grid-4; .grid-6 starts at 2 columns because it is meant for
891
- compact items that do not need a full single-column stack). */
892
- .grid-2,
893
- .grid-3,
894
- .grid-4,
895
- .grid-6 {
896
- display: grid;
897
- gap: var(--gap);
898
- }
899
-
900
- .grid-2 {
901
- grid-template-columns: repeat(2, minmax(0, 1fr));
902
- }
903
- .grid-3 {
904
- grid-template-columns: repeat(3, minmax(0, 1fr));
905
- }
906
- .grid-4 {
907
- grid-template-columns: repeat(4, minmax(0, 1fr));
908
- }
909
- .grid-6 {
910
- grid-template-columns: repeat(6, minmax(0, 1fr));
911
- }
912
-
913
- /* Container queries are the preferred responsive path; browsers without them
914
- ignore these rules and get the equivalent @media rules from the
915
- @supports not (container-type) block below — same columns, same breakpoints. */
916
- @container (width < 28rem) {
917
- .container-query .grid-2,
918
- .container-query .grid-3,
919
- .container-query .grid-4 {
920
- grid-template-columns: 1fr;
921
- }
922
-
923
- .container-query .grid-6 {
924
- grid-template-columns: repeat(2, minmax(0, 1fr));
925
- }
926
- }
927
-
928
- @container (28rem <= width < 48rem) {
929
- .container-query .grid-3,
930
- .container-query .grid-4 {
931
- grid-template-columns: repeat(2, minmax(0, 1fr));
932
- }
933
- }
934
-
935
- @container (28rem <= width < 64rem) {
936
- .container-query .grid-6 {
937
- grid-template-columns: repeat(3, minmax(0, 1fr));
938
- }
939
- }
940
-
941
- @supports not (container-type: inline-size) {
942
- @media (max-width: 63.999rem) {
943
- .container-query .grid-6 {
944
- grid-template-columns: repeat(3, minmax(0, 1fr));
945
- }
946
- }
947
-
948
- @media (max-width: 47.999rem) {
949
- .container-query .grid-3,
950
- .container-query .grid-4 {
951
- grid-template-columns: repeat(2, minmax(0, 1fr));
952
- }
953
- }
954
-
955
- @media (max-width: 27.999rem) {
956
- .container-query .grid-2,
957
- .container-query .grid-3,
958
- .container-query .grid-4 {
959
- grid-template-columns: 1fr;
960
- }
961
-
962
- .container-query .grid-6 {
963
- grid-template-columns: repeat(2, minmax(0, 1fr));
964
- }
965
- }
966
- }
967
-
968
557
  /*
969
558
  * Cross-cutting variant token mapping.
970
559
  *
@@ -1052,12 +641,13 @@ small {
1052
641
  @supports (color: color-mix(in oklch, red, white)) {
1053
642
  .soft {
1054
643
  /* @sync soft-recipe */
1055
- --ui-bg: color-mix(in oklch, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
644
+ --ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
1056
645
  --ui-border: color-mix(
1057
- in oklch,
646
+ in oklab,
1058
647
  var(--surface) var(--soft-border-mix),
1059
648
  var(--intent, var(--neutral))
1060
649
  );
650
+ --ui-fg: color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix));
1061
651
  /* @sync soft-recipe */
1062
652
  }
1063
653
 
@@ -1069,4511 +659,48 @@ small {
1069
659
  /* Interactive hover recipes live in the components that consume them:
1070
660
  components/button.css and components/badge.css. */
1071
661
 
1072
- /* Controls */
1073
- /*
1074
- * Buttons for actions and button-like links.
1075
- *
1076
- * .btn is primarily for actionable elements: <button>, <a href>,
1077
- * input[type="button"|"submit"|"reset"].
1078
- *
1079
- * Keeps shared intent, variant, and size behavior with a solid default.
1080
- *
1081
- * Disabled state uses the native disabled attribute for buttons and inputs.
1082
- * Anchors have no native disabled state, so a.btn uses aria-disabled="true"
1083
- * for disabled styling and activation guards.
1084
- *
1085
- * Public hooks:
1086
- * --btn-font-size
1087
- * --btn-min-size
1088
- * --btn-pad-x
1089
- * --btn-radius
1090
- * --btn-focus-color
1091
- * --btn-gap
1092
- * --btn-hover-overlay
1093
- * --btn-default-bg
1094
- * --btn-default-fg
1095
- * --btn-default-border
1096
- * --btn-border-color
1097
- *
1098
- * Internal:
1099
- * --btn-bg
1100
- * --btn-fg
1101
- * --btn-border
1102
- * --btn-default-hover-bg
1103
- * --btn-focus-ring-color
1104
- * --btn-border-block-end-color
1105
- * --btn-border-block-end-width
1106
- * --spinner-width
1107
- */
1108
-
1109
- /* Intent boundary — inherited intents do not tint this component.
1110
- Local intent classes still win because :where() has zero specificity. */
1111
- :where(.btn) {
1112
- --intent: initial;
1113
- --intent-fg: initial;
1114
- --ui-bg: initial;
1115
- --ui-fg: initial;
1116
- --ui-border: initial;
1117
- --ui-hover-bg: initial;
1118
- }
1119
-
1120
- .btn {
1121
- --btn-font-size: var(--control-font-size);
1122
- --btn-min-size: var(--control-size);
1123
- --btn-pad-x: var(--control-pad-x, 1em);
1124
- --btn-bg: var(--ui-bg, var(--btn-default-bg, var(--intent, var(--neutral))));
1125
- --btn-fg: var(--ui-fg, var(--btn-default-fg, var(--intent-fg, var(--neutral-fg))));
1126
- --btn-border: var(--ui-border, var(--btn-default-border, transparent));
1127
- --btn-default-hover-bg: var(--btn-bg);
1128
- --btn-radius: var(--radius);
1129
- --btn-focus-color: var(--intent, var(--focus));
1130
- --btn-hover-overlay: var(--hover-overlay, rgb(0 0 0 / 0.08));
1131
-
1132
- /* Positioning context for indicators, overlays, and grouped states. */
1133
- position: relative;
1134
- display: inline-flex;
1135
- align-items: center;
1136
- justify-content: center;
1137
- gap: var(--btn-gap, 0.375em);
1138
- min-block-size: var(--btn-min-size);
1139
- padding-block: var(--space-20);
1140
- padding-block: max(var(--space-10), calc((var(--btn-min-size) - 1lh) / 2));
1141
- padding-inline: var(--btn-pad-x);
1142
- border: var(--border-width) solid var(--btn-border-color, var(--btn-border));
1143
- border-block-end-color: var(
1144
- --btn-border-block-end-color,
1145
- var(--btn-border-color, var(--btn-border))
1146
- );
1147
- border-block-end-width: var(--btn-border-block-end-width, var(--border-width));
1148
- border-radius: var(--btn-radius);
1149
- background: var(--btn-bg);
1150
- color: var(--btn-fg);
1151
- font: inherit;
1152
- font-size: var(--btn-font-size);
1153
- font-weight: var(--font-weight-medium);
1154
- line-height: 1;
1155
- text-decoration: none;
1156
- cursor: pointer;
1157
- transition:
1158
- background-color var(--duration),
1159
- border-color var(--duration),
1160
- color var(--duration),
1161
- box-shadow var(--duration),
1162
- transform var(--duration);
1163
- }
662
+ /* Focus baseline — a visible outline is the guaranteed focus affordance. The
663
+ :focus outline covers legacy browsers; modern browsers hide it for
664
+ pointer/mouse focus and interactive components layer their ring on top.
1164
665
 
1165
- .btn.icon-only {
1166
- inline-size: var(--btn-min-size);
1167
- padding-inline: 0;
666
+ Invariant: an interactive component must never rely on box-shadow alone
667
+ for its keyboard focus indicator. Components that remove the native
668
+ outline on :focus-visible replace it with a transparent outline — invisible
669
+ normally, forced to the system focus color under forced-colors. */
670
+ :focus {
671
+ outline: var(--focus-outline);
672
+ outline-offset: var(--focus-outline-offset);
1168
673
  }
1169
674
 
1170
- @supports (color: color-mix(in oklch, red, white)) {
1171
- .btn {
1172
- --btn-focus-ring-color: color-mix(in oklch, var(--btn-focus-color) 28%, transparent);
675
+ @supports selector(:focus-visible) {
676
+ :focus:not(:focus-visible) {
677
+ outline: none;
1173
678
  }
1174
- }
1175
-
1176
- /* .solid/.soft/.outline are shared via variants.css. The baseline --btn-*
1177
- fallbacks make .btn solid by default; .ghost/.link are .btn-only variants. */
1178
- .btn.ghost {
1179
- --ui-bg: transparent;
1180
- --ui-fg: var(--intent, currentColor);
1181
- --ui-border: transparent;
1182
- }
1183
-
1184
- /* Solid/soft get the overlay darkening on hover; outline/ghost/link stay flat.
1185
- Routed through a variable instead of a :not(.outline, .ghost, .link)
1186
- exclusion so the hover rule below only has to state the interactive
1187
- element list once. */
1188
- .btn.outline,
1189
- .btn.ghost,
1190
- .btn.link {
1191
- --btn-hover-overlay: transparent;
1192
- }
1193
679
 
1194
- /* Shared hover recipe — sets --ui-hover-bg consumed by the hover rule below.
1195
- Declared outside :hover: the variables resolve only when the hover rule
1196
- reads them, so the values are inert until interaction. */
1197
- .btn:is(.outline, .ghost) {
1198
- --ui-hover-bg: transparent;
1199
- }
1200
-
1201
- @supports (color: color-mix(in oklch, red, white)) {
1202
- .btn {
1203
- --btn-default-hover-bg: color-mix(in oklch, var(--btn-bg) 88%, black);
680
+ /* Pointer focus must still signal in high-contrast mode, where the shared
681
+ box-shadow rings are disabled (--focus-ring-shadow: none). */
682
+ @media (forced-colors: active) {
683
+ :focus:not(:focus-visible) {
684
+ outline: var(--focus-outline);
685
+ }
1204
686
  }
687
+ }
1205
688
 
1206
- .btn.soft {
1207
- --ui-hover-bg: color-mix(
1208
- in oklch,
1209
- var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
1210
- var(--intent, var(--neutral))
1211
- );
689
+ @media print {
690
+ :root,
691
+ body {
692
+ /* Save some ink in dark mode */
693
+ color-scheme: light;
694
+ background: #fff;
695
+ color: #000;
1212
696
  }
1213
697
 
1214
- .btn.outline,
1215
- .btn.ghost {
1216
- --ui-hover-bg: color-mix(in oklch, currentColor 10%, transparent);
698
+ h1,
699
+ h2,
700
+ h3,
701
+ h4,
702
+ h5,
703
+ h6 {
704
+ break-after: avoid;
1217
705
  }
1218
706
  }
1219
-
1220
- /* The .soft/.outline/.ghost hover recipe (--ui-hover-bg) is set locally above;
1221
- the overlay darkening for solid/soft is .btn-specific. */
1222
- .btn:not(:where(:disabled, [aria-disabled="true"])):hover {
1223
- background: var(--ui-hover-bg, var(--btn-default-hover-bg));
1224
- box-shadow: inset 0 0 0 999px var(--btn-hover-overlay);
1225
- }
1226
-
1227
- .btn:not(:where(:disabled, [aria-disabled="true"])):active {
1228
- transform: translateY(1px);
1229
- }
1230
-
1231
- .btn.link {
1232
- --ui-bg: transparent;
1233
- --ui-fg: var(--intent, currentColor);
1234
- --ui-border: transparent;
1235
- --btn-radius: var(--radius-sm);
1236
- --btn-border-block-end-color: transparent;
1237
- --btn-border-block-end-width: var(--border-width);
1238
-
1239
- min-block-size: auto;
1240
- padding-inline: 0;
1241
- inline-size: fit-content;
1242
- max-inline-size: 100%;
1243
- border-color: transparent;
1244
- text-decoration: underline;
1245
- text-underline-offset: 0.18em;
1246
- }
1247
-
1248
- /* .btn.circle needs no local rule: the .circle utility (utilities.css) loads
1249
- after components and overrides border-radius at equal specificity. */
1250
-
1251
- .btn.link:not(:where(:disabled, [aria-disabled="true"])):hover {
1252
- background: transparent;
1253
- text-decoration-thickness: 0.12em;
1254
- }
1255
-
1256
- .btn:focus-visible {
1257
- outline: 2px solid transparent;
1258
- box-shadow: 0 0 0 var(--focus-ring-width) var(--btn-focus-ring-color, var(--focus-ring));
1259
- }
1260
-
1261
- .btn.link:focus-visible {
1262
- outline: var(--focus-outline);
1263
- outline-offset: 0.2em;
1264
- box-shadow: none;
1265
- }
1266
-
1267
- .btn:disabled {
1268
- cursor: not-allowed;
1269
- opacity: var(--disabled-opacity);
1270
- }
1271
-
1272
- /* Anchors have no native disabled state.
1273
- a.btn[aria-disabled="true"] only provides disabled styling and blocks pointer
1274
- activation. For a truly disabled link, remove href or prevent activation in
1275
- app code. */
1276
- a.btn[aria-disabled="true"] {
1277
- cursor: not-allowed;
1278
- opacity: var(--disabled-opacity);
1279
- pointer-events: none;
1280
- }
1281
-
1282
- /* Busy */
1283
- .btn[aria-busy="true"]:not(:disabled) {
1284
- cursor: progress;
1285
- }
1286
-
1287
- .btn[aria-busy="true"] > .spinner {
1288
- --spinner-width: 0.15em;
1289
- }
1290
-
1291
- /* Toggle and current state.
1292
- State must outrank the shared .outline/.ghost hover recipes: a pressed or
1293
- current button keeps its intent background and foreground while hovered.
1294
- aria-current="page" is the pagination current-page marker; aria-pressed
1295
- covers toggle buttons. */
1296
- .btn:is([aria-pressed="true"], [aria-current="page"]):not(.link) {
1297
- --btn-bg: var(--intent, var(--state-selected));
1298
- --btn-fg: var(--intent-fg, var(--state-selected-fg));
1299
- --btn-border: var(--intent, var(--state-selected));
1300
- --ui-hover-bg: var(--btn-bg);
1301
- }
1302
-
1303
- @media (forced-colors: active) {
1304
- .btn {
1305
- border-color: ButtonText;
1306
- background: ButtonFace;
1307
- color: ButtonText;
1308
- box-shadow: none;
1309
- }
1310
-
1311
- .btn.link {
1312
- border-color: transparent;
1313
- background: Canvas;
1314
- color: LinkText;
1315
- }
1316
-
1317
- .btn:is([aria-pressed="true"], [aria-current="page"]):not(.link) {
1318
- border-color: Highlight;
1319
- background: Highlight;
1320
- color: HighlightText;
1321
- forced-color-adjust: none;
1322
- }
1323
-
1324
- .btn:hover:not(:where(:disabled, [aria-disabled="true"], .link)) {
1325
- outline: var(--border-width) solid ButtonText;
1326
- outline-offset: 2px;
1327
- }
1328
-
1329
- .btn:disabled,
1330
- a.btn[aria-disabled="true"] {
1331
- border-color: GrayText;
1332
- background: Canvas;
1333
- color: GrayText;
1334
- forced-color-adjust: auto;
1335
- }
1336
- }
1337
-
1338
- /*
1339
- * Badges for compact labels, counts, and state hints.
1340
- *
1341
- * Soft by default. .solid for counters and notification badges. The dot
1342
- * (.badge:empty) stays solid regardless — a soft dot is pointless.
1343
- * Shared .outline variant stays available.
1344
- *
1345
- * Public hooks:
1346
- * --badge-size
1347
- * --badge-pad-x
1348
- * --badge-font-size
1349
- * --badge-dot-size
1350
- * --badge-radius
1351
- *
1352
- * Internal:
1353
- * --badge-default-bg
1354
- * --badge-default-fg
1355
- * --badge-default-border
1356
- * --badge-default-hover-bg
1357
- * --badge-bg
1358
- * --badge-fg
1359
- * --badge-border
1360
- */
1361
- /* Intent boundary — inherited intents do not tint this component.
1362
- Local intent classes still win because :where() has zero specificity. */
1363
- :where(.badge) {
1364
- --intent: initial;
1365
- --intent-fg: initial;
1366
- --ui-bg: initial;
1367
- --ui-fg: initial;
1368
- --ui-border: initial;
1369
- --ui-hover-bg: initial;
1370
- }
1371
-
1372
- .badge {
1373
- --badge-size: var(--density-compact-size);
1374
- --badge-pad-x: 0.55em;
1375
- --badge-font-size: 0.8125rem;
1376
- --badge-dot-size: 0.5rem;
1377
- --badge-default-bg: var(--surface-subtle);
1378
- --badge-default-fg: var(--intent, var(--text));
1379
- --badge-default-border: var(--border);
1380
- --badge-default-hover-bg: var(--badge-bg);
1381
- --badge-bg: var(--ui-bg, var(--badge-default-bg));
1382
- --badge-fg: var(--ui-fg, var(--badge-default-fg));
1383
- --badge-border: var(--ui-border, var(--badge-default-border));
1384
- --badge-radius: var(--radius-full);
1385
-
1386
- display: inline-flex;
1387
- align-items: center;
1388
- justify-content: center;
1389
- gap: 0.35em;
1390
- inline-size: fit-content;
1391
- max-inline-size: 100%;
1392
- min-block-size: var(--badge-size);
1393
- padding-inline: var(--badge-pad-x);
1394
- border: var(--border-width) solid var(--badge-border);
1395
- border-radius: var(--badge-radius);
1396
- background: var(--badge-bg);
1397
- color: var(--badge-fg);
1398
- font-size: var(--badge-font-size);
1399
- font-weight: var(--font-weight-strong);
1400
- line-height: 1;
1401
- text-decoration: none;
1402
- white-space: nowrap;
1403
- transition:
1404
- background-color var(--duration),
1405
- border-color var(--duration),
1406
- color var(--duration),
1407
- box-shadow var(--duration);
1408
- }
1409
-
1410
- /* Soft by default. Local defaults remain behind --ui-* in the resolution
1411
- chain, so explicit shared treatments compose without selector exclusions. */
1412
- @supports (color: color-mix(in oklch, red, white)) {
1413
- .badge {
1414
- /* @sync soft-recipe */
1415
- --badge-default-bg: color-mix(
1416
- in oklch,
1417
- var(--surface) var(--soft-bg-mix),
1418
- var(--intent, var(--neutral))
1419
- );
1420
- --badge-default-border: color-mix(
1421
- in oklch,
1422
- var(--surface) var(--soft-border-mix),
1423
- var(--intent, var(--neutral))
1424
- );
1425
- /* @sync soft-recipe */
1426
- }
1427
- }
1428
-
1429
- .badge:empty {
1430
- --badge-size: var(--badge-dot-size);
1431
- --badge-bg: var(--intent, var(--neutral));
1432
- --badge-fg: var(--intent-fg, var(--neutral-fg));
1433
- --badge-border: transparent;
1434
- padding: 0;
1435
- border: 0;
1436
- inline-size: var(--badge-size);
1437
- block-size: var(--badge-size);
1438
- }
1439
-
1440
- .badge > button {
1441
- display: inline-grid;
1442
- place-items: center;
1443
- flex: 0 0 auto;
1444
- min-inline-size: 1.5rem;
1445
- min-block-size: 1.5rem;
1446
- margin-block: -0.25rem;
1447
- margin-inline-start: -0.15em;
1448
- margin-inline-end: -0.45em;
1449
- padding: 0.25rem;
1450
- border: 0;
1451
- border-radius: var(--radius-full);
1452
- background: transparent;
1453
- color: inherit;
1454
- font: inherit;
1455
- line-height: 1;
1456
- cursor: pointer;
1457
- }
1458
-
1459
- .badge > button:not(:disabled):hover {
1460
- background: var(--hover-overlay);
1461
- }
1462
-
1463
- /* Hover recipes — sets --ui-hover-bg consumed by the hover rule below.
1464
- Declared outside :hover like button.css for the same reason. */
1465
- :is(a.badge, button.badge).outline {
1466
- --ui-hover-bg: transparent;
1467
- }
1468
-
1469
- @supports (color: color-mix(in oklch, red, white)) {
1470
- :is(a.badge, button.badge):is(.solid, :empty) {
1471
- --ui-hover-bg: color-mix(in oklch, var(--intent, var(--neutral)) 88%, black);
1472
- }
1473
-
1474
- :is(a.badge, button.badge):where(:not(.solid, .outline, :empty)) {
1475
- --badge-default-hover-bg: color-mix(
1476
- in oklch,
1477
- var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
1478
- var(--intent, var(--neutral))
1479
- );
1480
- }
1481
-
1482
- :is(a.badge, button.badge).outline {
1483
- --ui-hover-bg: color-mix(in oklch, currentColor 10%, transparent);
1484
- }
1485
- }
1486
-
1487
- :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"])):hover {
1488
- background: var(--ui-hover-bg, var(--badge-default-hover-bg));
1489
- }
1490
-
1491
- :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"], .outline)):hover {
1492
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
1493
- }
1494
-
1495
- button.badge {
1496
- cursor: pointer;
1497
- }
1498
-
1499
- button.badge:disabled,
1500
- .badge[aria-disabled="true"] {
1501
- opacity: var(--disabled-opacity);
1502
- cursor: not-allowed;
1503
- }
1504
-
1505
- a.badge[aria-disabled="true"] {
1506
- pointer-events: none;
1507
- }
1508
-
1509
- @media (forced-colors: active) {
1510
- .badge:empty {
1511
- background: ButtonText;
1512
- }
1513
-
1514
- :where(a.badge, button.badge):hover:not(:where(:disabled, [aria-disabled="true"])) {
1515
- outline: var(--border-width) solid ButtonText;
1516
- outline-offset: 2px;
1517
- }
1518
-
1519
- .badge > button:not(:disabled):hover {
1520
- outline: var(--border-width) solid currentColor;
1521
- outline-offset: 1px;
1522
- }
1523
-
1524
- button.badge:disabled,
1525
- .badge[aria-disabled="true"] {
1526
- border-color: GrayText;
1527
- background: Canvas;
1528
- color: GrayText;
1529
- }
1530
- }
1531
-
1532
- /*
1533
- * Alerts for inline feedback and status messages.
1534
- *
1535
- * Keeps alert defaults local while consuming the shared --ui-* variant
1536
- * plumbing for emphasis.
1537
- *
1538
- * --alert-border-inline-start-color/-width are unset extension points, mirroring
1539
- * --btn-border-block-end-* (button.css), for themes that give alerts a
1540
- * callout-style colored flag on the leading edge instead of a uniform border.
1541
- *
1542
- * Public hooks:
1543
- * --alert-pad-block
1544
- * --alert-pad-inline
1545
- * --alert-font-size
1546
- * --alert-radius
1547
- * --alert-icon-size
1548
- * --alert-dismiss-size
1549
- * --alert-border-inline-start-color
1550
- * --alert-border-inline-start-width
1551
- *
1552
- * Internal:
1553
- * --alert-default-bg
1554
- * --alert-default-border
1555
- * --alert-bg
1556
- * --alert-fg
1557
- * --alert-border
1558
- * --alert-dismiss-icon-size
1559
- */
1560
- /* Intent boundary — inherited intents do not tint this component.
1561
- Local intent classes still win because :where() has zero specificity. */
1562
- :where(.alert) {
1563
- --intent: initial;
1564
- --intent-fg: initial;
1565
- --ui-bg: initial;
1566
- --ui-fg: initial;
1567
- --ui-border: initial;
1568
- }
1569
-
1570
- .alert {
1571
- --alert-pad-block: var(--space-30);
1572
- --alert-pad-inline: var(--space-40);
1573
- --alert-font-size: var(--font-size-md);
1574
- --alert-icon-size: 1.25rem;
1575
- --alert-default-bg: var(--surface-subtle);
1576
- --alert-default-border: var(--border);
1577
- --alert-bg: var(--ui-bg, var(--alert-default-bg));
1578
- --alert-fg: var(--ui-fg, var(--intent, var(--text)));
1579
- --alert-border: var(--ui-border, var(--alert-default-border));
1580
-
1581
- padding-block: var(--alert-pad-block);
1582
- padding-inline: var(--alert-pad-inline);
1583
- background: var(--alert-bg);
1584
- color: var(--alert-fg);
1585
- border: var(--border-width) solid var(--alert-border);
1586
- border-inline-start-color: var(--alert-border-inline-start-color, var(--alert-border));
1587
- border-inline-start-width: var(--alert-border-inline-start-width, var(--border-width));
1588
- border-radius: var(--alert-radius, var(--radius-lg));
1589
- font-size: var(--alert-font-size);
1590
- }
1591
-
1592
- .alert > * {
1593
- min-inline-size: 0;
1594
- }
1595
-
1596
- .alert > :first-child {
1597
- margin-block-start: 0;
1598
- }
1599
-
1600
- .alert > :last-child {
1601
- margin-block-end: 0;
1602
- }
1603
-
1604
- /* Alerts are soft by default. The recipe writes local defaults rather than
1605
- --ui-* so an explicit shared treatment always wins without exclusions. */
1606
- @supports (color: color-mix(in oklch, red, white)) {
1607
- .alert {
1608
- /* @sync soft-recipe */
1609
- --alert-default-bg: color-mix(
1610
- in oklch,
1611
- var(--surface) var(--soft-bg-mix),
1612
- var(--intent, var(--neutral))
1613
- );
1614
- --alert-default-border: color-mix(
1615
- in oklch,
1616
- var(--surface) var(--soft-border-mix),
1617
- var(--intent, var(--neutral))
1618
- );
1619
- /* @sync soft-recipe */
1620
- }
1621
- }
1622
-
1623
- /* Decorative leading icon — use .alert-icon on the icon element. */
1624
- .alert > .alert-icon {
1625
- display: inline-block;
1626
- flex: none;
1627
- inline-size: var(--alert-icon-size);
1628
- block-size: var(--alert-icon-size);
1629
- font-size: var(--alert-icon-size);
1630
- line-height: 1;
1631
- vertical-align: -0.125em;
1632
- }
1633
-
1634
- /* Dismiss — compact close button for inline alerts. Transparent at rest with
1635
- a reduced opacity glyph so it stays secondary inside an already-colored
1636
- alert; hover reinforces it via the shared overlay. Uses the shared
1637
- --icon-close mask, so no icon font or text glyph is required in the markup.
1638
- Standard alerts place it as a direct trailing child; admonitions place it
1639
- inside .alert-title (see below). */
1640
- .alert-dismiss {
1641
- --alert-dismiss-size: 1.75rem;
1642
- --alert-dismiss-icon-size: 0.875rem;
1643
-
1644
- display: inline-grid;
1645
- place-items: center;
1646
- inline-size: var(--alert-dismiss-size);
1647
- block-size: var(--alert-dismiss-size);
1648
- flex: none;
1649
- padding: 0;
1650
- border: 0;
1651
- border-radius: var(--radius);
1652
- background: transparent;
1653
- color: currentColor;
1654
- opacity: 0.65;
1655
- cursor: pointer;
1656
- }
1657
-
1658
- .alert-dismiss::before {
1659
- content: "";
1660
- inline-size: var(--alert-dismiss-icon-size);
1661
- block-size: var(--alert-dismiss-icon-size);
1662
- background: currentColor;
1663
- -webkit-mask: var(--icon-close) center / contain no-repeat;
1664
- mask: var(--icon-close) center / contain no-repeat;
1665
- }
1666
-
1667
- @media (hover: hover) {
1668
- .alert-dismiss:hover {
1669
- background: var(--hover-overlay);
1670
- opacity: 1;
1671
- }
1672
- }
1673
-
1674
- /* Grid anatomy — activated by a leading icon, a trailing dismiss button, or
1675
- both. Icon and dismiss are pinned to the outer columns; anything else (the
1676
- message content) flows into the remaining text column. */
1677
- @supports selector(.alert:has(> *)) {
1678
- .alert:has(> .alert-icon, > .alert-dismiss) {
1679
- display: grid;
1680
- grid-template-columns: minmax(0, 1fr);
1681
- align-items: var(--items-align, start);
1682
- column-gap: var(--space-30);
1683
- }
1684
-
1685
- .alert:has(> .alert-icon) {
1686
- grid-template-columns: auto minmax(0, 1fr);
1687
- }
1688
-
1689
- .alert:has(> .alert-icon) > .alert-icon {
1690
- grid-column: 1;
1691
- grid-row: 1 / -1;
1692
- display: inline-grid;
1693
- place-items: center;
1694
- margin-block-start: 0.125em;
1695
- }
1696
-
1697
- .alert:has(> .alert-icon) > :not(.alert-icon, .alert-dismiss) {
1698
- grid-column: 2;
1699
- }
1700
-
1701
- .alert:has(> .alert-dismiss) {
1702
- grid-template-columns: minmax(0, 1fr) auto;
1703
- }
1704
-
1705
- .alert:has(> .alert-icon):has(> .alert-dismiss) {
1706
- grid-template-columns: auto minmax(0, 1fr) auto;
1707
- }
1708
-
1709
- .alert:has(> .alert-dismiss) > .alert-dismiss {
1710
- grid-column: -2 / -1;
1711
- grid-row: 1;
1712
- }
1713
- }
1714
-
1715
- /* Links */
1716
- .alert :where(a:not(.btn)) {
1717
- color: inherit;
1718
- font-weight: var(--font-weight-strong);
1719
- }
1720
-
1721
- /* Forced colors: no override needed. The always-on border gets a forced
1722
- readable color, and text on a Highlight surface would trigger Chromium's
1723
- Canvas backplate (unreadable) — so .solid intentionally falls back to the
1724
- default look. */
1725
-
1726
- /* Callout — neutral surface with a thick accent border on the leading edge.
1727
- The thickness follows the shared --alert-border-inline-start-width hook. */
1728
- .alert.callout {
1729
- --alert-bg: var(--surface-subtle);
1730
- --alert-fg: var(--text);
1731
- border: 0;
1732
- border-inline-start: var(--alert-border-inline-start-width, 4px) solid
1733
- var(--alert-border-inline-start-color, var(--intent, var(--neutral)));
1734
- }
1735
-
1736
- /* Admonition — structured static box with a tinted title bar and a body
1737
- that rests on the page surface. Not collapsible — use .accordion inside
1738
- the body if the content needs to be expandable. */
1739
- .alert.admonition {
1740
- padding: 0;
1741
- display: block;
1742
- overflow: clip;
1743
- border-color: var(--intent, var(--border));
1744
- }
1745
-
1746
- .alert.admonition > .alert-title {
1747
- display: flex;
1748
- align-items: center;
1749
- gap: var(--space-20);
1750
- padding-block: var(--alert-pad-block);
1751
- padding-inline: var(--alert-pad-inline);
1752
- border-start-start-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
1753
- border-start-end-radius: calc(var(--alert-radius, var(--radius-lg)) - var(--border-width));
1754
- }
1755
-
1756
- /* Dismiss inside an admonition title bar rides the inline end. */
1757
- .alert.admonition > .alert-title > .alert-dismiss {
1758
- margin-inline-start: auto;
1759
- }
1760
-
1761
- @supports (color: color-mix(in oklch, red, white)) {
1762
- .alert.admonition > .alert-title {
1763
- background: color-mix(
1764
- in oklch,
1765
- var(--surface) var(--soft-bg-mix),
1766
- var(--intent, var(--neutral))
1767
- );
1768
- }
1769
- }
1770
-
1771
- @supports not (color: color-mix(in oklch, red, white)) {
1772
- .alert.admonition > .alert-title {
1773
- background: var(--intent, var(--neutral));
1774
- color: var(--intent-fg, var(--neutral-fg));
1775
- }
1776
- }
1777
-
1778
- .alert.admonition > .alert-body {
1779
- padding-block: var(--alert-pad-block);
1780
- padding-inline: var(--alert-pad-inline);
1781
- background: var(--surface);
1782
- color: var(--text);
1783
- }
1784
-
1785
- .alert.admonition > .alert-body > :first-child {
1786
- margin-block-start: 0;
1787
- }
1788
-
1789
- .alert.admonition > .alert-body > :last-child {
1790
- margin-block-end: 0;
1791
- }
1792
-
1793
- /*
1794
- * Actions — a horizontal action-button cluster.
1795
- *
1796
- * Composes with .btn (or any inline element) and feeds layout via the shared
1797
- * --gap token.
1798
- *
1799
- * Internal:
1800
- * --gap (local value for the shared gap token)
1801
- */
1802
- .actions {
1803
- --gap: var(--space-20);
1804
-
1805
- display: flex;
1806
- flex-wrap: wrap;
1807
- align-items: center;
1808
- gap: var(--gap);
1809
- margin: 0;
1810
- padding: 0;
1811
- list-style: none;
1812
- }
1813
-
1814
- .actions > li {
1815
- margin: 0;
1816
- }
1817
-
1818
-
1819
- /* Surfaces */
1820
- /*
1821
- * Cards provide a neutral raised surface for grouped content.
1822
- *
1823
- * Surface, border, radius, padding, density, and elevation stay local here.
1824
- * Direct header/footer children are structural slots. Page layout remains the
1825
- * job of markup and layout primitives outside the card.
1826
- *
1827
- * Public hooks:
1828
- * --card-pad
1829
- * --card-radius
1830
- * --card-max-inline-size
1831
- *
1832
- * Child contract:
1833
- * Direct .bleed children escape --card-pad (negative inset, wider media);
1834
- * edge children inherit the card corners. .bleed must be a direct child.
1835
- *
1836
- * Internal:
1837
- * --card-bg
1838
- * --card-fg
1839
- * --heading
1840
- * --busy-overlay-bg
1841
- */
1842
- /* Intent boundary — inherited intents do not tint this component.
1843
- Local intent classes still win because :where() has zero specificity. */
1844
- :where(.card) {
1845
- --ui-bg: initial;
1846
- --ui-fg: initial;
1847
- --ui-border: initial;
1848
- }
1849
-
1850
- .card {
1851
- --card-pad: var(--space-40);
1852
- --card-radius: var(--radius-lg);
1853
- --card-max-inline-size: none;
1854
- --card-bg: var(--ui-bg, var(--surface-raised));
1855
- --card-fg: var(--ui-fg, var(--text));
1856
-
1857
- /* Keep contextual descendants tied to the surface this card actually owns. */
1858
- --heading: var(--card-fg);
1859
- --busy-overlay-bg: var(--card-bg);
1860
-
1861
- max-inline-size: var(--card-max-inline-size);
1862
- padding: var(--card-pad);
1863
- background: var(--card-bg);
1864
- color: var(--card-fg);
1865
- border: var(--border-width) solid var(--ui-border, var(--border));
1866
- border-radius: var(--card-radius);
1867
- }
1868
-
1869
- .card > * {
1870
- min-inline-size: 0;
1871
- }
1872
-
1873
- .card > :where(header, footer) {
1874
- display: flex;
1875
- flex-wrap: wrap;
1876
- align-items: var(--items-align, center);
1877
- justify-content: space-between;
1878
- gap: var(--space-30);
1879
- min-inline-size: 0;
1880
- }
1881
-
1882
- .card > :where(header, footer) > * {
1883
- min-inline-size: 0;
1884
- margin-block: 0;
1885
- }
1886
-
1887
- .card > header {
1888
- align-items: var(--items-align, flex-start);
1889
- }
1890
-
1891
- .card > :first-child {
1892
- margin-block-start: 0;
1893
- }
1894
-
1895
- .card > :last-child {
1896
- margin-block-end: 0;
1897
- }
1898
-
1899
- .card.raised {
1900
- box-shadow: var(--shadow);
1901
- }
1902
- .card.subtle {
1903
- background: var(--surface-subtle);
1904
- }
1905
- .card.compact {
1906
- --card-pad: var(--space-20);
1907
- }
1908
-
1909
- /* Full-bleed children */
1910
- .card > .bleed {
1911
- margin-inline: calc(var(--card-pad) * -1);
1912
- max-inline-size: none;
1913
- }
1914
-
1915
- .card > .bleed:first-child {
1916
- margin-block-start: calc(var(--card-pad) * -1);
1917
- border-start-start-radius: inherit;
1918
- border-start-end-radius: inherit;
1919
- }
1920
-
1921
- .card > .bleed:last-child {
1922
- margin-block-end: calc(var(--card-pad) * -1);
1923
- border-end-start-radius: inherit;
1924
- border-end-end-radius: inherit;
1925
- }
1926
-
1927
- /* Media bleed */
1928
- .card > :where(img, picture, video, canvas, svg).bleed {
1929
- display: block;
1930
- inline-size: calc(100% + var(--card-pad) * 2);
1931
- max-inline-size: none;
1932
- }
1933
-
1934
- .card > picture.bleed {
1935
- overflow: hidden;
1936
- }
1937
-
1938
- .card > picture.bleed > img {
1939
- display: block;
1940
- inline-size: 100%;
1941
- block-size: auto;
1942
- }
1943
-
1944
- /* Padded structural bleed */
1945
- .card > :where(header, footer, figure).bleed {
1946
- padding: var(--card-pad);
1947
- }
1948
-
1949
- /*
1950
- * Surface — shared modal scrim and backdrop for flyout and drawer.
1951
- *
1952
- * Drawn by .drawer::backdrop (native dialog top-layer) and .surface-backdrop
1953
- * (custom fixed div for flyout sheets on coarse pointers). The native
1954
- * .modal::backdrop uses the same fill and animates opacity from 0 to 1
1955
- * (modal.css), so the scrim strength stays consistent across the dialog
1956
- * family.
1957
- *
1958
- * .surface-backdrop, .is-sheet, and .is-open are runtime markers set by
1959
- * surface.js. Authors opt in via [data-flyout-mobile="sheet|auto|none|anchored"]
1960
- * — the JS resolves "auto" into .is-sheet at run time.
1961
- */
1962
- /* Custom backdrop for overlays that do not use the native dialog top-layer.
1963
- * Its z-index sits just below the flyout it accompanies. */
1964
- .surface-backdrop {
1965
- position: fixed;
1966
- inset: 0;
1967
- z-index: calc(var(--z-menu) - 1);
1968
- background: var(--backdrop-fill);
1969
- opacity: 1;
1970
- transition: opacity var(--duration) var(--ease);
1971
- }
1972
-
1973
- .surface-backdrop[hidden] {
1974
- display: none;
1975
- opacity: 0;
1976
- }
1977
-
1978
- @starting-style {
1979
- .surface-backdrop:not([hidden]) {
1980
- opacity: 0;
1981
- }
1982
- }
1983
-
1984
- @supports (transition-behavior: allow-discrete) {
1985
- .surface-backdrop {
1986
- transition:
1987
- opacity var(--duration) var(--ease),
1988
- display var(--duration) allow-discrete;
1989
- }
1990
- }
1991
-
1992
- /*
1993
- * Modal shell styling for native dialog elements.
1994
- *
1995
- * Spacing, radius, transitions, and focus treatment come from shared tokens.
1996
- * Opening behavior and intent-specific decisions stay outside this component.
1997
- *
1998
- * Public hooks:
1999
- * --modal-size
2000
- * --dialog-viewport-gap
2001
- * --dialog-icon-size
2002
- * --dialog-icon-glyph-size
2003
- *
2004
- * Internal:
2005
- * --dialog-close-icon-size
2006
- * --dialog-viewport-gap-2x
2007
- */
2008
- dialog.modal {
2009
- --modal-size: 32rem;
2010
- --dialog-viewport-gap: var(--space-40);
2011
- /* calc() multiplication is not in the supported browser range, so the
2012
- doubled gap is spelled out with an additive calc. */
2013
- --dialog-viewport-gap-2x: calc(var(--dialog-viewport-gap) + var(--dialog-viewport-gap));
2014
-
2015
- /* Keep the surface anchored to the viewport in the top layer. This also
2016
- provides the containing block for the absolutely positioned close button. */
2017
- position: fixed;
2018
-
2019
- max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size));
2020
- max-block-size: calc(var(--viewport-block) - var(--dialog-viewport-gap-2x));
2021
- border: var(--border-width) solid var(--border);
2022
- border-radius: var(--radius-lg);
2023
- background: var(--surface-raised);
2024
- color: var(--text);
2025
- box-shadow: var(--shadow);
2026
- padding: 0;
2027
- overflow: visible;
2028
- opacity: 0;
2029
- /*
2030
- * Keep the open dialog root untransformed. Fixed overlays mounted inside a
2031
- * modal dialog use viewport coordinates; transform/translate/scale on this
2032
- * element would make them position against the dialog instead. The closed
2033
- * state can still carry a small transform for enter/exit motion.
2034
- */
2035
- transform: translateY(0.35rem) scale(0.98);
2036
- transition:
2037
- opacity var(--duration),
2038
- transform var(--duration);
2039
- }
2040
-
2041
- dialog.modal > form,
2042
- dialog.modal > .stack {
2043
- display: grid;
2044
- gap: var(--space-40);
2045
- max-block-size: calc(var(--viewport-block) - var(--dialog-viewport-gap-2x));
2046
- overflow: auto;
2047
- overscroll-behavior: contain;
2048
- padding: var(--space-40);
2049
- }
2050
-
2051
- /* Keep the dialog off the viewport edges on small screens. */
2052
- @media (max-width: 30rem) {
2053
- dialog.modal {
2054
- --dialog-viewport-gap: var(--space-30);
2055
- }
2056
- }
2057
-
2058
- dialog.modal.scrollable > form,
2059
- dialog.modal.scrollable > .stack {
2060
- grid-template-rows: auto minmax(0, 1fr) auto;
2061
- overflow: hidden;
2062
- }
2063
-
2064
- dialog.modal.scrollable > form > :not(header, footer),
2065
- dialog.modal.scrollable > .stack > :not(header, footer) {
2066
- min-block-size: 0;
2067
- overflow: auto;
2068
- overscroll-behavior: contain;
2069
- }
2070
-
2071
- dialog.modal header > :first-child,
2072
- dialog.modal footer > :first-child {
2073
- margin-block-start: 0;
2074
- }
2075
-
2076
- dialog.modal header > :last-child,
2077
- dialog.modal footer > :last-child {
2078
- margin-block-end: 0;
2079
- }
2080
-
2081
- /* The close button is anchored to the dialog at top inline-end, out of the
2082
- content flow; headers reserve inline room so titles never run under it. */
2083
- dialog.modal > form > header,
2084
- dialog.modal > .stack > header {
2085
- padding-inline-end: calc(var(--control-size) + var(--space-30));
2086
- }
2087
-
2088
- dialog.modal footer {
2089
- display: flex;
2090
- flex-wrap: wrap;
2091
- justify-content: flex-end;
2092
- gap: var(--gap);
2093
- }
2094
-
2095
- /* Confirmation layout — a focused message followed by a visually distinct
2096
- action band. The media primitive owns icon/content alignment; this variant
2097
- only removes the standard form inset so both regions can own their padding. */
2098
- dialog.modal.dialog-confirmation > form {
2099
- gap: 0;
2100
- padding: 0;
2101
- }
2102
-
2103
- dialog.modal.dialog-confirmation > form > .media {
2104
- gap: var(--space-40);
2105
- padding: var(--space-50);
2106
- }
2107
-
2108
- dialog.modal.dialog-confirmation > form > footer {
2109
- padding: var(--space-40) var(--space-50);
2110
- border-end-start-radius: calc(var(--radius-lg) - var(--border-width));
2111
- border-end-end-radius: calc(var(--radius-lg) - var(--border-width));
2112
- background: var(--surface-subtle);
2113
- }
2114
-
2115
- /* Intent boundary — inherited intents do not tint the icon well. A local
2116
- intent plus .soft opts into the shared soft-variant recipe. */
2117
- :where(.dialog-icon) {
2118
- --intent: initial;
2119
- --intent-fg: initial;
2120
- --ui-bg: initial;
2121
- --ui-fg: initial;
2122
- }
2123
-
2124
- .dialog-icon {
2125
- --dialog-icon-size: 3rem;
2126
- --dialog-icon-glyph-size: 1.5rem;
2127
-
2128
- display: inline-grid;
2129
- place-items: center;
2130
- inline-size: var(--dialog-icon-size);
2131
- block-size: var(--dialog-icon-size);
2132
- flex: none;
2133
- border-radius: var(--radius-full);
2134
- background: var(--ui-bg, var(--surface-subtle));
2135
- color: var(--ui-fg, var(--intent, var(--text-muted)));
2136
- }
2137
-
2138
- .dialog-icon > * {
2139
- display: inline-grid;
2140
- place-items: center;
2141
- inline-size: var(--dialog-icon-glyph-size);
2142
- block-size: var(--dialog-icon-glyph-size);
2143
- font-size: var(--dialog-icon-glyph-size);
2144
- line-height: 1;
2145
- }
2146
-
2147
- @media (max-width: 30rem) {
2148
- dialog.modal.dialog-confirmation > form > .media,
2149
- dialog.modal.dialog-confirmation > form > footer {
2150
- padding: var(--space-40);
2151
- }
2152
- }
2153
-
2154
- dialog.modal[open] {
2155
- opacity: 1;
2156
- transform: none;
2157
- }
2158
-
2159
- @starting-style {
2160
- dialog.modal[open] {
2161
- opacity: 0;
2162
- transform: translateY(0.35rem) scale(0.98);
2163
- }
2164
- }
2165
-
2166
- dialog.modal::backdrop {
2167
- background: var(--backdrop-fill);
2168
- opacity: 0;
2169
- transition: opacity var(--duration);
2170
- }
2171
-
2172
- @supports (transition-behavior: allow-discrete) {
2173
- dialog.modal {
2174
- transition:
2175
- opacity var(--duration),
2176
- transform var(--duration),
2177
- overlay var(--duration) allow-discrete,
2178
- display var(--duration) allow-discrete;
2179
- }
2180
-
2181
- dialog.modal::backdrop {
2182
- transition:
2183
- opacity var(--duration),
2184
- overlay var(--duration) allow-discrete,
2185
- display var(--duration) allow-discrete;
2186
- }
2187
- }
2188
-
2189
- dialog.modal[open]::backdrop {
2190
- opacity: 1;
2191
- }
2192
-
2193
- html.has-modal-open {
2194
- overflow: hidden;
2195
- }
2196
-
2197
- html.has-modal-open.had-scrollbar {
2198
- scrollbar-gutter: stable;
2199
- }
2200
-
2201
- @starting-style {
2202
- dialog.modal[open]::backdrop {
2203
- opacity: 0;
2204
- }
2205
- }
2206
-
2207
- @media (prefers-reduced-motion: no-preference) {
2208
- dialog.modal.is-static {
2209
- animation: actual-dialog-static var(--duration-slow) var(--ease);
2210
- }
2211
-
2212
- ::view-transition-group(actual-dialog) {
2213
- animation-duration: var(--duration);
2214
- animation-timing-function: var(--ease);
2215
- }
2216
- }
2217
-
2218
- @keyframes actual-dialog-static {
2219
- 0%,
2220
- 100% {
2221
- transform: none;
2222
- }
2223
-
2224
- 35% {
2225
- transform: translateX(-0.35rem);
2226
- }
2227
-
2228
- 70% {
2229
- transform: translateX(0.35rem);
2230
- }
2231
- }
2232
-
2233
- @media (forced-colors: active) {
2234
- dialog.modal {
2235
- border-color: ButtonText;
2236
- background: Canvas;
2237
- color: CanvasText;
2238
- box-shadow: none;
2239
- }
2240
-
2241
- dialog.modal::backdrop {
2242
- background: Canvas;
2243
- }
2244
-
2245
- .dialog-icon {
2246
- border: var(--border-width) solid CanvasText;
2247
- }
2248
- }
2249
-
2250
- /* Close controls. A close button is an action attached to a surface, so it
2251
- never participates in the content flow. The modal and the drawer share the
2252
- same simple placement — absolute at the panel's top inline-end, inside the
2253
- surface — so both close buttons look and behave identically. The glyph comes
2254
- from --icon-close, masked to currentColor, so no icon font or text glyph is
2255
- required in the markup. */
2256
- .dialog-close,
2257
- .drawer-close {
2258
- --dialog-close-icon-size: 1rem;
2259
-
2260
- display: inline-grid;
2261
- place-items: center;
2262
- padding: 0;
2263
- border: 0;
2264
- border-radius: var(--radius);
2265
- color: var(--text-muted);
2266
- cursor: pointer;
2267
-
2268
- /* Out of flow, anchored to the positioned dialog surface, not the header or
2269
- any scroll container inside it. */
2270
- position: absolute;
2271
- z-index: 1;
2272
- inset-block-start: var(--space-20);
2273
- inset-inline-end: var(--space-20);
2274
-
2275
- inline-size: var(--control-size);
2276
- block-size: var(--control-size);
2277
-
2278
- background: var(--surface-subtle);
2279
- }
2280
-
2281
- .dialog-close::before,
2282
- .drawer-close::before {
2283
- content: "";
2284
- inline-size: var(--dialog-close-icon-size);
2285
- block-size: var(--dialog-close-icon-size);
2286
- background: currentColor;
2287
- -webkit-mask: var(--icon-close) center / contain no-repeat;
2288
- mask: var(--icon-close) center / contain no-repeat;
2289
- }
2290
-
2291
- @media (hover: hover) {
2292
- .dialog-close:hover,
2293
- .drawer-close:hover {
2294
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
2295
- color: var(--text);
2296
- }
2297
- }
2298
-
2299
- .dialog-close:active,
2300
- .drawer-close:active {
2301
- box-shadow: inset 0 0 0 999px var(--hover-overlay);
2302
- }
2303
-
2304
- .dialog-close:focus-visible,
2305
- .drawer-close:focus-visible {
2306
- outline: 2px solid transparent;
2307
- box-shadow: var(--focus-ring-shadow);
2308
- }
2309
-
2310
- /*
2311
- * Drawer — modal side-sheet built on the native dialog element.
2312
- *
2313
- * Top-layer rendering, ::backdrop, Escape, and modal focus come from the
2314
- * native dialog. CSS owns progressive enter/exit motion where browsers support
2315
- * discrete display/overlay transitions. The drawer is a layout shell only —
2316
- * navigation content inside uses .nav-link and .accordion, which carry their
2317
- * own link/summary styling.
2318
- *
2319
- * Public hooks:
2320
- * --drawer-size
2321
- * --drawer-pad
2322
- *
2323
- * Internal:
2324
- * --drawer-closed-translate (side/RTL positioning state)
2325
- */
2326
- dialog.drawer {
2327
- --drawer-size: 20rem;
2328
- --drawer-pad: var(--space-40);
2329
- --drawer-closed-translate: -100%;
2330
-
2331
- /* Keep the surface anchored to the viewport in the top layer. This also
2332
- provides the containing block for the absolutely positioned close button.
2333
- A percentage height resolves against the fixed containing viewport, while
2334
- dynamic viewport units can be stale in embedded previews. */
2335
- position: fixed;
2336
- inset-block-start: 0;
2337
-
2338
- inline-size: min(calc(var(--viewport-inline) * 0.9), var(--drawer-size));
2339
- block-size: 100%;
2340
- max-inline-size: none;
2341
- max-block-size: 100%;
2342
- margin: 0;
2343
- padding: var(--drawer-pad);
2344
- border: 0;
2345
- border-inline-end: var(--border-width) solid var(--border);
2346
- border-radius: 0;
2347
- background: var(--surface-raised);
2348
- color: var(--text);
2349
- box-shadow: var(--shadow);
2350
- opacity: 0;
2351
- transform: translateX(var(--drawer-closed-translate));
2352
- transition:
2353
- opacity var(--duration-slow),
2354
- transform var(--duration-slow);
2355
- }
2356
-
2357
- dialog.drawer[open] {
2358
- display: grid;
2359
- grid-template-rows: auto minmax(0, 1fr) auto;
2360
- gap: var(--space-40);
2361
- opacity: 1;
2362
- transform: translateX(0);
2363
- }
2364
-
2365
- /* Backdrop color sourced from components/surface.css so it matches .modal
2366
- and .surface-backdrop across the dialog family. */
2367
- dialog.drawer::backdrop {
2368
- background: var(--backdrop-fill);
2369
- opacity: 0;
2370
- transition: opacity var(--duration-slow);
2371
- }
2372
-
2373
- dialog.drawer[data-side="end"] {
2374
- --drawer-closed-translate: 100%;
2375
-
2376
- margin-inline-start: auto;
2377
- margin-inline-end: 0;
2378
- border-inline-end: 0;
2379
- border-inline-start: var(--border-width) solid var(--border);
2380
- }
2381
-
2382
- dialog.drawer[open]::backdrop {
2383
- opacity: 1;
2384
- }
2385
-
2386
- @starting-style {
2387
- dialog.drawer[open] {
2388
- opacity: 0;
2389
- transform: translateX(var(--drawer-closed-translate));
2390
- }
2391
-
2392
- dialog.drawer[open]::backdrop {
2393
- opacity: 0;
2394
- }
2395
- }
2396
-
2397
- /* Scrollable navigation region inside the drawer. */
2398
- dialog.drawer nav {
2399
- min-block-size: 0;
2400
- overflow-y: auto;
2401
- }
2402
-
2403
- @supports (transition-behavior: allow-discrete) {
2404
- dialog.drawer {
2405
- transition:
2406
- opacity var(--duration-slow),
2407
- transform var(--duration-slow),
2408
- overlay var(--duration-slow) allow-discrete,
2409
- display var(--duration-slow) allow-discrete;
2410
- }
2411
-
2412
- dialog.drawer::backdrop {
2413
- transition:
2414
- opacity var(--duration-slow),
2415
- overlay var(--duration-slow) allow-discrete,
2416
- display var(--duration-slow) allow-discrete;
2417
- }
2418
- }
2419
-
2420
- /* RTL: swap closed transform directions so the drawer slides from the correct
2421
- inline side. */
2422
- [dir="rtl"] dialog.drawer {
2423
- --drawer-closed-translate: 100%;
2424
- }
2425
-
2426
- [dir="rtl"] dialog.drawer[data-side="end"] {
2427
- --drawer-closed-translate: -100%;
2428
- }
2429
-
2430
- @media (forced-colors: active) {
2431
- dialog.drawer::backdrop {
2432
- background: Canvas;
2433
- }
2434
- }
2435
-
2436
- @media (prefers-reduced-motion: no-preference) {
2437
- dialog.drawer.is-static {
2438
- animation: actual-dialog-static var(--duration-slow) var(--ease);
2439
- }
2440
- }
2441
-
2442
- /* The close button shares the modal's interior placement (modal.css): absolute
2443
- at the panel's top inline-end, out of the content flow. The header reserves
2444
- inline room so the title never runs under it. */
2445
- dialog.drawer > header,
2446
- dialog.drawer > form > header {
2447
- padding-inline-end: calc(var(--control-size) + var(--space-30));
2448
- }
2449
-
2450
-
2451
- /* Overlays */
2452
- /*
2453
- * Flyout — floating panel for actions and links.
2454
- *
2455
- * JavaScript owns positioning (position: fixed + left/top), state
2456
- * (hidden + .is-open), focus, and keyboard behavior. CSS owns the surfaced
2457
- * panel appearance, item styles, and animation.
2458
- *
2459
- * Runtime markers (managed by surface.js): .is-open marks an open flyout,
2460
- * .is-sheet marks "auto" resolved to bottom-sheet presentation on coarse
2461
- * pointers. Authors opt into mobile behavior with
2462
- * [data-flyout-mobile="sheet|auto|none|anchored"].
2463
- * --surface-anchor-width is set by surface.js while an open flyout is positioned.
2464
- *
2465
- * Public hooks:
2466
- * --flyout-inline-size
2467
- * --flyout-max-inline-size
2468
- *
2469
- * Internal:
2470
- * --menu-item-size (derived from --control-size for menu rows)
2471
- * --available-height (measured by floating.js)
2472
- * --surface-anchor-width (measured by surface.js)
2473
- */
2474
-
2475
- .flyout-trigger {
2476
- position: relative;
2477
- display: inline-block;
2478
- }
2479
-
2480
- /* Stretch: for a flyout trigger that must span its container — e.g. the
2481
- last row of a full-width sidebar nav list. Stretches both the
2482
- positioning wrapper and its trigger control. Named for the effect
2483
- (stretch to fill), not the display value, to avoid reading like a
2484
- generic display-block utility. */
2485
- .flyout-trigger.stretch {
2486
- display: block;
2487
- inline-size: 100%;
2488
- }
2489
-
2490
- .flyout-trigger.stretch > [aria-haspopup="menu"] {
2491
- inline-size: 100%;
2492
- }
2493
-
2494
- .flyout {
2495
- --menu-item-size: var(--control-size);
2496
- z-index: var(--z-menu);
2497
- /* No-JS fallback: below the trigger inside the relative .flyout-trigger.
2498
- surface.js switches an open flyout to position: fixed with measured
2499
- coordinates; this only covers a flyout revealed without the runtime. */
2500
- position: absolute;
2501
- inset-block-start: calc(100% + var(--space-10));
2502
- inset-inline-start: 0;
2503
- display: grid;
2504
- gap: var(--space-10);
2505
- inline-size: var(--flyout-inline-size, max-content);
2506
- min-inline-size: min(
2507
- var(--surface-anchor-width, 0px),
2508
- min(var(--flyout-max-inline-size, 20rem), calc(var(--viewport-inline) - var(--space-60)))
2509
- );
2510
- max-inline-size: min(
2511
- var(--flyout-max-inline-size, 20rem),
2512
- calc(var(--viewport-inline) - var(--space-60))
2513
- );
2514
- max-block-size: min(
2515
- 24rem,
2516
- var(--available-height, calc(var(--viewport-block) - var(--space-60)))
2517
- );
2518
- overflow: auto;
2519
- margin: 0;
2520
- padding: var(--space-10);
2521
- border: var(--border-width) solid var(--border);
2522
- border-radius: var(--radius);
2523
- background: var(--surface-raised);
2524
- box-shadow: var(--shadow-popout);
2525
- list-style: none;
2526
- opacity: 1;
2527
- scale: 1;
2528
- transition:
2529
- opacity var(--duration-fast) var(--ease),
2530
- scale var(--duration-fast) var(--ease),
2531
- translate var(--duration-fast) var(--ease);
2532
- }
2533
-
2534
- .flyout.is-open:not(.is-sheet) {
2535
- inset: auto;
2536
- }
2537
-
2538
- @starting-style {
2539
- .flyout.is-open {
2540
- opacity: 0;
2541
- scale: 0.98;
2542
- }
2543
- }
2544
-
2545
- /* Bottom-sheet presentation. surface.js toggles .is-sheet when the flyout is
2546
- opened on coarse pointer / narrow viewports. */
2547
- .flyout.is-sheet {
2548
- --menu-item-size: var(--control-size-lg);
2549
- position: fixed;
2550
- inset-inline: 0;
2551
- inset-block-start: auto;
2552
- inset-block-end: 0;
2553
- inline-size: auto;
2554
- min-inline-size: 0;
2555
- max-inline-size: none;
2556
- max-block-size: min(calc(var(--viewport-block) * 0.75), 32rem);
2557
- overflow: auto;
2558
- grid-template-columns: 1fr;
2559
- padding: max(var(--space-30), env(safe-area-inset-top)) var(--space-30)
2560
- max(var(--space-30), env(safe-area-inset-bottom));
2561
- border-end-start-radius: 0;
2562
- border-end-end-radius: 0;
2563
- }
2564
-
2565
- .flyout[hidden] {
2566
- display: none;
2567
- opacity: 0;
2568
- scale: 0.98;
2569
- }
2570
-
2571
- .flyout.is-sheet[hidden] {
2572
- scale: 1;
2573
- translate: 0 var(--space-30);
2574
- }
2575
-
2576
- @starting-style {
2577
- .flyout.is-sheet.is-open {
2578
- scale: 1;
2579
- translate: 0 var(--space-30);
2580
- }
2581
- }
2582
-
2583
- @supports (transition-behavior: allow-discrete) {
2584
- .flyout {
2585
- transition:
2586
- opacity var(--duration-fast) var(--ease),
2587
- scale var(--duration-fast) var(--ease),
2588
- translate var(--duration-fast) var(--ease),
2589
- display var(--duration-fast) allow-discrete;
2590
- }
2591
- }
2592
-
2593
- .flyout > li {
2594
- margin: 0;
2595
- }
2596
-
2597
- .flyout > :where(section, footer) {
2598
- min-inline-size: 0;
2599
- padding: var(--space-20);
2600
- }
2601
-
2602
- .flyout > section > :where(h1, h2, h3, h4, h5, h6) {
2603
- margin: 0;
2604
- color: var(--text-muted);
2605
- font-size: var(--font-size-xs);
2606
- font-weight: var(--font-weight-strong);
2607
- }
2608
-
2609
- .flyout > section > ul {
2610
- display: grid;
2611
- gap: var(--space-10);
2612
- margin: var(--space-10) 0 0;
2613
- padding: 0;
2614
- list-style: none;
2615
- }
2616
-
2617
- .flyout > section > ul > li {
2618
- margin: 0;
2619
- }
2620
-
2621
- /* Vertical dividers — .menu-separator (menu.css) is the canonical way to
2622
- separate menu items. Flyout panels that are not menus use auto grid gap
2623
- declared on .flyout itself. */
2624
-
2625
- /*
2626
- * Menu — action list primitive consumed by flyout and context-menu.
2627
- *
2628
- * Strict anatomy: .menu > li > .menu-item. Separators and section labels
2629
- * sit directly under .menu as siblings of <li>.
2630
- *
2631
- * Public hooks:
2632
- * --menu-item-icon-size
2633
- */
2634
-
2635
- .menu {
2636
- display: grid;
2637
- gap: var(--space-10);
2638
- margin: 0;
2639
- padding: var(--space-10);
2640
- list-style: none;
2641
- }
2642
-
2643
- .menu > li {
2644
- margin: 0;
2645
- }
2646
-
2647
- .menu-item {
2648
- --menu-item-icon-size: 1.25em;
2649
-
2650
- display: flex;
2651
- align-items: center;
2652
- gap: var(--space-20);
2653
- inline-size: 100%;
2654
- min-block-size: var(--menu-item-size, var(--control-size));
2655
- padding-inline: var(--space-20);
2656
- border: 0;
2657
- border-radius: var(--radius-sm);
2658
- background: transparent;
2659
- color: var(--text);
2660
- font: inherit;
2661
- text-align: start;
2662
- text-decoration: none;
2663
- white-space: nowrap;
2664
- cursor: pointer;
2665
- }
2666
-
2667
- .menu-item-icon {
2668
- display: grid;
2669
- flex: 0 0 var(--menu-item-icon-size);
2670
- inline-size: var(--menu-item-icon-size);
2671
- place-items: center;
2672
- }
2673
-
2674
- .menu-item-text {
2675
- flex: 1;
2676
- min-inline-size: 0;
2677
- overflow: hidden;
2678
- text-overflow: ellipsis;
2679
- }
2680
-
2681
- .menu-item-end {
2682
- display: inline-flex;
2683
- align-items: center;
2684
- gap: var(--space-10);
2685
- margin-inline-start: auto;
2686
- color: var(--text-muted);
2687
- }
2688
-
2689
- /* Checkable menu items are one command with one accessible state. Their
2690
- indicator is generated so authors do not add a second interactive input. */
2691
- .menu-item:is([role="menuitemcheckbox"], [role="menuitemradio"])::before {
2692
- display: grid;
2693
- flex: 0 0 var(--menu-item-icon-size);
2694
- inline-size: var(--menu-item-icon-size);
2695
- place-items: center;
2696
- }
2697
-
2698
- .menu-item[role="menuitemcheckbox"]::before {
2699
- content: "";
2700
- }
2701
-
2702
- .menu-item[role="menuitemcheckbox"][aria-checked="true"]::before {
2703
- content: "✓";
2704
- }
2705
-
2706
- .menu-item[role="menuitemcheckbox"][aria-checked="mixed"]::before {
2707
- content: "−";
2708
- }
2709
-
2710
- .menu-item[role="menuitemradio"]::before {
2711
- content: "○";
2712
- }
2713
-
2714
- .menu-item[role="menuitemradio"][aria-checked="true"]::before {
2715
- content: "●";
2716
- }
2717
-
2718
- .menu-item:focus-visible {
2719
- background: var(--surface-subtle);
2720
- }
2721
-
2722
- .menu-item:hover:not(:where(:disabled, [aria-disabled="true"])) {
2723
- background: var(--surface-subtle);
2724
- }
2725
-
2726
- .menu-item:is(:disabled, [aria-disabled="true"]) {
2727
- cursor: not-allowed;
2728
- opacity: var(--disabled-opacity);
2729
- }
2730
-
2731
- .menu-label {
2732
- padding: var(--space-20);
2733
- color: var(--text-muted);
2734
- font-size: var(--font-size-xs);
2735
- font-weight: var(--font-weight-strong);
2736
- }
2737
-
2738
- .menu-separator {
2739
- margin: 0;
2740
- border: 0;
2741
- border-block-start: var(--border-width) solid var(--border);
2742
- }
2743
-
2744
- @media (forced-colors: active) {
2745
- .menu-item:focus-visible {
2746
- outline: var(--border-width) solid ButtonText;
2747
- outline-offset: -1px;
2748
- }
2749
- }
2750
-
2751
- /*
2752
- * Tooltip — supplemental label overlaid near a trigger.
2753
- *
2754
- * Position, placement, and arrow position are managed by JavaScript
2755
- * via position: fixed, left, top, data-placement, and --arrow-x.
2756
- * Visibility is managed with hidden; CSS owns entry and exit animation.
2757
- *
2758
- * Public hooks:
2759
- * --tooltip-bg
2760
- * --tooltip-fg
2761
- * --tooltip-arrow-size
2762
- *
2763
- * Internal:
2764
- * --arrow-x (arrow position, written by floating.js)
2765
- * --arrow-y (arrow position, written by floating.js)
2766
- */
2767
- .tooltip {
2768
- --tooltip-bg: var(--surface-solid);
2769
- --tooltip-fg: var(--surface);
2770
- --tooltip-arrow-size: 5px;
2771
-
2772
- position: fixed;
2773
- z-index: var(--z-tooltip);
2774
- max-inline-size: min(24rem, calc(var(--viewport-inline) - 2rem));
2775
- padding: 0.25em 0.6em;
2776
- border-radius: var(--radius-sm);
2777
- background: var(--tooltip-bg);
2778
- color: var(--tooltip-fg);
2779
- font-size: var(--font-size-xs);
2780
- line-height: var(--line-height-tight);
2781
- overflow-wrap: anywhere;
2782
- pointer-events: auto;
2783
- user-select: none;
2784
- opacity: 1;
2785
- scale: 1;
2786
- transition:
2787
- opacity var(--duration-fast) var(--ease),
2788
- scale var(--duration-fast) var(--ease);
2789
- }
2790
-
2791
- .tooltip[hidden] {
2792
- display: none;
2793
- opacity: 0;
2794
- scale: 0.96;
2795
- }
2796
-
2797
- @starting-style {
2798
- .tooltip:not([hidden]) {
2799
- opacity: 0;
2800
- scale: 0.96;
2801
- }
2802
- }
2803
-
2804
- @supports (transition-behavior: allow-discrete) {
2805
- .tooltip {
2806
- transition:
2807
- opacity var(--duration-fast) var(--ease),
2808
- scale var(--duration-fast) var(--ease),
2809
- display var(--duration-fast) allow-discrete;
2810
- }
2811
- }
2812
-
2813
- /* ── Arrow — clipped triangle ──────────────────────── */
2814
-
2815
- .tooltip::before {
2816
- content: "";
2817
- position: absolute;
2818
- inline-size: calc(var(--tooltip-arrow-size) * 2);
2819
- block-size: var(--tooltip-arrow-size);
2820
- background: var(--tooltip-bg);
2821
- }
2822
-
2823
- .tooltip[data-placement^="top"]::before {
2824
- bottom: calc(-0.98 * var(--tooltip-arrow-size));
2825
- left: var(--arrow-x, 50%);
2826
- translate: -50% 0;
2827
- clip-path: polygon(50% 100%, 0 0, 100% 0);
2828
- }
2829
-
2830
- .tooltip[data-placement^="bottom"]::before {
2831
- top: calc(-0.98 * var(--tooltip-arrow-size));
2832
- left: var(--arrow-x, 50%);
2833
- translate: -50% 0;
2834
- clip-path: polygon(50% 0, 0 100%, 100% 100%);
2835
- }
2836
-
2837
- .tooltip[data-placement^="left"]::before {
2838
- right: calc(-0.98 * var(--tooltip-arrow-size));
2839
- top: var(--arrow-y, 50%);
2840
- inline-size: var(--tooltip-arrow-size);
2841
- block-size: calc(var(--tooltip-arrow-size) * 2);
2842
- translate: 0 -50%;
2843
- clip-path: polygon(100% 50%, 0 0, 0 100%);
2844
- }
2845
-
2846
- .tooltip[data-placement^="right"]::before {
2847
- left: calc(-0.98 * var(--tooltip-arrow-size));
2848
- top: var(--arrow-y, 50%);
2849
- inline-size: var(--tooltip-arrow-size);
2850
- block-size: calc(var(--tooltip-arrow-size) * 2);
2851
- translate: 0 -50%;
2852
- clip-path: polygon(0 50%, 100% 0, 100% 100%);
2853
- }
2854
-
2855
- @media (forced-colors: active) {
2856
- .tooltip {
2857
- --tooltip-bg: Canvas;
2858
- --tooltip-fg: CanvasText;
2859
- }
2860
-
2861
- .tooltip::before {
2862
- display: none;
2863
- }
2864
- }
2865
-
2866
-
2867
- /* Disclosure */
2868
- /*
2869
- * Accordion disclosures built on native details and summary.
2870
- *
2871
- * Keeps the clickable summary affordance aligned and usable without JavaScript
2872
- * or demo styles. Decorative palettes and custom markers belong outside the
2873
- * base component.
2874
- *
2875
- * Public hooks:
2876
- * --accordion-radius
2877
- * --accordion-marker-color
2878
- */
2879
- .accordion {
2880
- --accordion-radius: var(--radius);
2881
- --accordion-marker-color: var(--text-muted);
2882
-
2883
- border: var(--border-width) solid var(--border);
2884
- border-radius: var(--accordion-radius);
2885
- background: var(--surface-raised);
2886
- }
2887
-
2888
- .accordion > details + details {
2889
- border-block-start: var(--border-width) solid var(--border);
2890
- }
2891
-
2892
- .accordion summary {
2893
- display: flex;
2894
- align-items: center;
2895
- gap: var(--space-30);
2896
- cursor: pointer;
2897
- list-style: none;
2898
- padding: var(--space-40);
2899
- color: var(--text);
2900
- font-weight: var(--font-weight-bold);
2901
- }
2902
-
2903
- .accordion summary:hover {
2904
- color: var(--primary);
2905
- }
2906
-
2907
- .accordion summary::-webkit-details-marker {
2908
- display: none;
2909
- }
2910
-
2911
- .accordion summary::after {
2912
- content: "";
2913
- flex: 0 0 auto;
2914
- margin-inline-start: auto;
2915
- inline-size: 1rem;
2916
- block-size: 1rem;
2917
- background-color: var(--accordion-marker-color);
2918
- -webkit-mask: var(--icon-chevron) center / contain no-repeat;
2919
- mask: var(--icon-chevron) center / contain no-repeat;
2920
- transition: transform var(--duration);
2921
- }
2922
-
2923
- .accordion details[open] summary::after {
2924
- transform: rotate(180deg);
2925
- }
2926
-
2927
- .accordion details > summary + * {
2928
- padding-block: 0 var(--space-40);
2929
- padding-inline: var(--space-40);
2930
- color: var(--text-muted);
2931
- margin-block: 0;
2932
- }
2933
-
2934
- .accordion details[open] > summary + * {
2935
- padding-block-start: var(--space-10);
2936
- }
2937
-
2938
- .accordion details > summary + * > :first-child {
2939
- margin-block-start: 0;
2940
- }
2941
-
2942
- .accordion details > summary + ul,
2943
- .accordion details > summary + ol {
2944
- margin: 0;
2945
- padding-inline-start: 2rem;
2946
- }
2947
-
2948
- @media (forced-colors: active) {
2949
- .accordion {
2950
- --accordion-marker-color: CanvasText;
2951
- }
2952
- }
2953
-
2954
-
2955
- /* Feedback */
2956
- /*
2957
- * Spinner — compact loading feedback.
2958
- *
2959
- * Uses currentColor so it adapts to buttons, alerts, badges, and text. The
2960
- * [aria-busy] overlay composition lives in components/busy.css.
2961
- *
2962
- * Public hooks:
2963
- * --spinner-size
2964
- * --spinner-width
2965
- * --spinner-track
2966
- * --spinner-value
2967
- */
2968
- /* Intent boundary — inherited intents do not tint this component.
2969
- Local intent classes still win because :where() has zero specificity. */
2970
- :where(.spinner) {
2971
- --intent: initial;
2972
- }
2973
-
2974
- .spinner {
2975
- --spinner-size: 1em;
2976
- --spinner-width: 0.125em;
2977
- --spinner-track: currentColor;
2978
- --spinner-value: transparent;
2979
-
2980
- color: var(--intent, currentColor);
2981
- display: inline-block;
2982
- flex-shrink: 0;
2983
- inline-size: var(--spinner-size);
2984
- block-size: var(--spinner-size);
2985
- border: var(--spinner-width) solid var(--spinner-track);
2986
- border-inline-end-color: var(--spinner-value);
2987
- border-radius: var(--radius-full);
2988
- opacity: 0.85;
2989
- animation: actual-spin var(--duration-spin) linear infinite;
2990
- vertical-align: text-bottom;
2991
- }
2992
-
2993
- @supports (color: color-mix(in oklch, red, white)) {
2994
- .spinner {
2995
- --spinner-track: color-mix(in oklch, currentColor, transparent 70%);
2996
- --spinner-value: currentColor;
2997
- opacity: 1;
2998
- }
2999
- }
3000
-
3001
- .spinner.sm {
3002
- --spinner-size: 0.75em;
3003
- }
3004
-
3005
- .spinner.lg {
3006
- --spinner-size: 2rem;
3007
- --spinner-width: 3px;
3008
- }
3009
-
3010
- @keyframes actual-spin {
3011
- to {
3012
- transform: rotate(360deg);
3013
- }
3014
- }
3015
-
3016
- /*
3017
- * Skeleton placeholders for loading states.
3018
- *
3019
- * Shows approximate loading structure without implying real content hierarchy.
3020
- * Geometry, animation, and color stay theme-aware.
3021
- *
3022
- * Public hooks:
3023
- * --skeleton-size
3024
- * --skeleton-width
3025
- * --skeleton-radius
3026
- * --skeleton-track
3027
- * --skeleton-highlight
3028
- */
3029
- .skeleton {
3030
- --skeleton-size: 1rem;
3031
- --skeleton-width: 100%;
3032
- --skeleton-radius: var(--radius);
3033
- --skeleton-track: var(--surface-subtle);
3034
- --skeleton-highlight: var(--surface-raised);
3035
-
3036
- display: block;
3037
- inline-size: var(--skeleton-width);
3038
- max-inline-size: 100%;
3039
- block-size: var(--skeleton-size);
3040
- border-radius: var(--skeleton-radius);
3041
- background:
3042
- linear-gradient(90deg, var(--skeleton-track), var(--skeleton-highlight), var(--skeleton-track))
3043
- 0 0 / 200% 100%;
3044
- }
3045
-
3046
- @media (prefers-reduced-motion: no-preference) {
3047
- .skeleton {
3048
- animation: actual-skeleton var(--duration-shimmer) ease-in-out infinite;
3049
- }
3050
- }
3051
-
3052
- .skeleton[data-shape="text"] {
3053
- --skeleton-size: 0.85rem;
3054
- --skeleton-width: 32rem;
3055
- }
3056
-
3057
- .skeleton[data-shape="title"] {
3058
- --skeleton-size: 1.4rem;
3059
- --skeleton-width: 18rem;
3060
- }
3061
-
3062
- .skeleton[data-shape="avatar"] {
3063
- --skeleton-size: var(--avatar-size, 2.25rem);
3064
- --skeleton-width: var(--skeleton-size);
3065
- --skeleton-radius: var(--radius-full);
3066
-
3067
- flex: 0 0 auto;
3068
- }
3069
-
3070
- .skeleton[data-shape="box"] {
3071
- --skeleton-size: 8rem;
3072
- }
3073
-
3074
- @media (forced-colors: active) {
3075
- .skeleton {
3076
- border: var(--border-width) solid ButtonText;
3077
- background: GrayText;
3078
- }
3079
- }
3080
-
3081
- @keyframes actual-skeleton {
3082
- from {
3083
- background-position: 200% 0;
3084
- }
3085
-
3086
- to {
3087
- background-position: -200% 0;
3088
- }
3089
- }
3090
-
3091
- /*
3092
- * Busy overlay — non-button loading state.
3093
- *
3094
- * A non-button element with aria-busy="true" and a direct last-child .spinner
3095
- * shows that spinner as a centered overlay over a faded surface. Buttons keep
3096
- * their spinners inline (e.g. .btn[aria-busy] > .spinner stays inline-flex).
3097
- *
3098
- * Public hooks:
3099
- * --busy-overlay-bg
3100
- *
3101
- * Internal:
3102
- * --spinner-size (overlay copy of the shared spinner hook)
3103
- */
3104
- [aria-busy] > .spinner:last-child {
3105
- display: none;
3106
- }
3107
-
3108
- [aria-busy="true"] > .spinner:last-child {
3109
- display: inline-block;
3110
- }
3111
-
3112
- @supports selector([aria-busy="true"]:has(> .spinner:last-child)) {
3113
- [aria-busy="true"]:not(.btn):has(> .spinner:last-child) {
3114
- position: relative;
3115
- }
3116
-
3117
- [aria-busy="true"]:not(.btn):has(> .spinner:last-child)::before {
3118
- content: "";
3119
- position: absolute;
3120
- inset: 0;
3121
- z-index: 1;
3122
- border-radius: inherit;
3123
- background: var(--busy-overlay-bg, var(--surface));
3124
- }
3125
-
3126
- [aria-busy="true"]:not(.btn) > .spinner:last-child {
3127
- --spinner-size: 2rem;
3128
- position: absolute;
3129
- inset: 0;
3130
- z-index: 2;
3131
- margin: auto;
3132
- display: block;
3133
- }
3134
- }
3135
-
3136
- @supports (selector([aria-busy="true"]:has(> .spinner:last-child))) and
3137
- (color: color-mix(in oklch, red, white)) {
3138
- [aria-busy="true"]:not(.btn):has(> .spinner:last-child)::before {
3139
- background: color-mix(in oklch, var(--busy-overlay-bg, var(--surface)), transparent 20%);
3140
- }
3141
- }
3142
-
3143
- /*
3144
- * Progress — native progress bar with theme-aware track and value.
3145
- *
3146
- * Consumes the shared --intent/--primary for the value bar and --bar-height
3147
- * for thickness. Indeterminate state animates a shimmer using --duration-shimmer.
3148
- *
3149
- * Public hooks:
3150
- * --progress-track
3151
- * --progress-value
3152
- */
3153
- /* Intent boundary — inherited intents do not tint this component.
3154
- Local intent classes still win because :where() has zero specificity. */
3155
- :where(.progress) {
3156
- --intent: initial;
3157
- }
3158
-
3159
- .progress {
3160
- --progress-track: var(--surface-subtle);
3161
- --progress-value: var(--intent, var(--primary));
3162
-
3163
- appearance: none;
3164
- -webkit-appearance: none;
3165
- display: block;
3166
- inline-size: 100%;
3167
- block-size: var(--bar-height);
3168
- overflow: hidden;
3169
- border: 0;
3170
- border-radius: var(--radius-full);
3171
- background: var(--progress-track);
3172
- }
3173
-
3174
- .progress::-webkit-progress-bar {
3175
- block-size: var(--bar-height);
3176
- border-radius: var(--radius-full);
3177
- background: var(--progress-track);
3178
- }
3179
-
3180
- .progress::-webkit-progress-value {
3181
- block-size: var(--bar-height);
3182
- border-radius: var(--radius-full);
3183
- background: var(--progress-value);
3184
- }
3185
-
3186
- .progress::-moz-progress-bar {
3187
- block-size: var(--bar-height);
3188
- border-radius: var(--radius-full);
3189
- background: var(--progress-value);
3190
- }
3191
-
3192
- /* Indeterminate progress */
3193
- .progress:indeterminate {
3194
- background:
3195
- linear-gradient(to right, var(--progress-value) 30%, var(--progress-track) 30%) top left / 150%
3196
- 150% no-repeat,
3197
- var(--progress-track);
3198
- }
3199
-
3200
- .progress:indeterminate::-webkit-progress-bar {
3201
- background: transparent;
3202
- }
3203
-
3204
- .progress:indeterminate::-webkit-progress-value {
3205
- background: transparent;
3206
- }
3207
-
3208
- .progress:indeterminate::-moz-progress-bar {
3209
- background: transparent;
3210
- }
3211
-
3212
- @media (prefers-reduced-motion: no-preference) {
3213
- .progress:indeterminate {
3214
- animation: actual-progress-indeterminate var(--duration-shimmer) ease-in-out infinite;
3215
- }
3216
-
3217
- :where([dir="rtl"] .progress:indeterminate) {
3218
- animation-direction: reverse;
3219
- }
3220
- }
3221
-
3222
- @keyframes actual-progress-indeterminate {
3223
- 0% {
3224
- background-position: 200% 0;
3225
- }
3226
-
3227
- 20% {
3228
- background-position: 100% 0;
3229
- }
3230
-
3231
- 80% {
3232
- background-position: -100% 0;
3233
- }
3234
-
3235
- 100% {
3236
- background-position: -200% 0;
3237
- }
3238
- }
3239
-
3240
- @media (forced-colors: active) {
3241
- .progress::-webkit-progress-value,
3242
- .progress:indeterminate::-webkit-progress-value {
3243
- background: Highlight;
3244
- }
3245
-
3246
- .progress::-moz-progress-bar,
3247
- .progress:indeterminate::-moz-progress-bar {
3248
- background: Highlight;
3249
- }
3250
- }
3251
-
3252
- /*
3253
- * Status bar — singleton floating status area.
3254
- *
3255
- * One element in the HTML, empty by default, hidden when :empty but kept in the
3256
- * accessibility tree so it stays a reliable live region. JavaScript only sets
3257
- * text content; the visual shows and hides with the content.
3258
- *
3259
- * Consumes intent like badge or alert. No need for status-bar-specific modifier
3260
- * classes — .danger / .success / .warning / .neutral from intents.css drive
3261
- * the coloring via --intent / --intent-fg. status.js applies these as plain
3262
- * classes on the target; it does not know their names or that they exist.
3263
- *
3264
- * CSS API: .status-bar
3265
- * JS target contract: [data-status][role="status"]
3266
- * Normally combined, but they describe separate concerns.
3267
- *
3268
- * Internal:
3269
- * --status-bg
3270
- * --status-fg
3271
- */
3272
-
3273
- /* Intent boundary — inherited intents do not tint this component.
3274
- Local intent classes still win because :where() has zero specificity. */
3275
- :where(.status-bar) {
3276
- --intent: initial;
3277
- --intent-fg: initial;
3278
- }
3279
-
3280
- .status-bar {
3281
- --status-bg: var(--intent, var(--text));
3282
- --status-fg: var(--intent-fg, var(--surface));
3283
-
3284
- position: fixed;
3285
- inset-block-end: var(--space-40);
3286
- inset-inline: var(--space-40);
3287
- z-index: var(--z-status);
3288
-
3289
- display: inline-flex;
3290
- align-items: center;
3291
- justify-content: center;
3292
- gap: var(--space-20);
3293
-
3294
- inline-size: fit-content;
3295
- max-inline-size: min(calc(var(--viewport-inline) - var(--space-60)), 32rem);
3296
- margin-inline: auto;
3297
- overflow-wrap: anywhere;
3298
-
3299
- padding-block: var(--space-20);
3300
- padding-inline: var(--space-30);
3301
-
3302
- border-radius: var(--radius-full);
3303
- background: var(--status-bg);
3304
- color: var(--status-fg);
3305
- box-shadow: var(--shadow-popout);
3306
-
3307
- opacity: 1;
3308
- scale: 1;
3309
- translate: 0 0;
3310
- pointer-events: none;
3311
-
3312
- transition:
3313
- opacity var(--duration-slow) var(--ease),
3314
- scale var(--duration-slow) var(--ease),
3315
- translate var(--duration-slow) var(--ease);
3316
- }
3317
-
3318
- .status-bar:empty {
3319
- opacity: 0;
3320
- scale: 0.96;
3321
- translate: 0 var(--space-20);
3322
- pointer-events: none;
3323
- }
3324
-
3325
-
3326
- /* Display */
3327
- /*
3328
- * Avatars for initials, images, links, and buttons.
3329
- *
3330
- * The visual box stays the same across semantic elements. Grouping and status
3331
- * dots (an empty .badge child) compose around the avatar without changing its
3332
- * meaning.
3333
- *
3334
- * Public hooks:
3335
- * --avatar-size
3336
- * --avatar-radius
3337
- * --avatar-font-size
3338
- * --avatar-stack-size
3339
- * --avatar-stack-font-size
3340
- * --avatar-stack-overlap
3341
- * --avatar-stack-ring
3342
- *
3343
- * Internal:
3344
- * --avatar-bg
3345
- * --avatar-fg
3346
- * --avatar-status-translate-x
3347
- */
3348
- /* Intent boundary — inherited intents do not tint this component.
3349
- Local intent classes still win because :where() has zero specificity. */
3350
- :where(.avatar) {
3351
- --intent: initial;
3352
- --intent-fg: initial;
3353
- }
3354
-
3355
- .avatar {
3356
- --avatar-size: var(--avatar-stack-size, 2.25rem);
3357
- --avatar-radius: var(--radius-full);
3358
- --avatar-font-size: var(--avatar-stack-font-size, var(--font-size-sm));
3359
- --avatar-bg: var(--intent, var(--surface-subtle));
3360
- --avatar-fg: var(--intent-fg, var(--tone-fg, var(--text-muted)));
3361
-
3362
- position: relative;
3363
- display: inline-grid;
3364
- place-items: center;
3365
- inline-size: var(--avatar-size);
3366
- block-size: var(--avatar-size);
3367
- overflow: visible;
3368
- padding: 0;
3369
- border: 0;
3370
- border-radius: var(--avatar-radius);
3371
- background: var(--avatar-bg);
3372
- color: var(--avatar-fg);
3373
- font: inherit;
3374
- font-size: var(--avatar-font-size);
3375
- font-weight: var(--font-weight-strong);
3376
- line-height: 1;
3377
- text-decoration: none;
3378
- vertical-align: middle;
3379
- user-select: none;
3380
- transition:
3381
- box-shadow var(--duration),
3382
- opacity var(--duration),
3383
- transform var(--duration);
3384
- }
3385
-
3386
- /* Forced colors wipe the background and the stack ring (box-shadow), so the
3387
- shape needs a real border to stay visible. */
3388
- @media (forced-colors: active) {
3389
- .avatar {
3390
- border: var(--border-width) solid CanvasText;
3391
- }
3392
-
3393
- :where(a.avatar, button.avatar)::after {
3394
- display: none;
3395
- }
3396
- }
3397
-
3398
- .avatar > :first-child:not(:where(img, picture, .badge)) {
3399
- display: inline-grid;
3400
- place-items: center;
3401
- inline-size: 100%;
3402
- block-size: 100%;
3403
- overflow: hidden;
3404
- border-radius: inherit;
3405
- }
3406
-
3407
- .avatar > img,
3408
- .avatar > picture {
3409
- display: block;
3410
- inline-size: 100%;
3411
- block-size: 100%;
3412
- overflow: hidden;
3413
- border-radius: inherit;
3414
- }
3415
-
3416
- .avatar > img,
3417
- .avatar > picture > img {
3418
- inline-size: 100%;
3419
- block-size: 100%;
3420
- object-fit: cover;
3421
- border-radius: inherit;
3422
- }
3423
-
3424
- .avatar > picture > img {
3425
- display: block;
3426
- }
3427
-
3428
- button.avatar {
3429
- appearance: none;
3430
- cursor: pointer;
3431
- }
3432
-
3433
- :where(a.avatar, button.avatar)::after {
3434
- content: "";
3435
- position: absolute;
3436
- inset: 0;
3437
- border-radius: inherit;
3438
- background: var(--hover-overlay);
3439
- opacity: 0;
3440
- pointer-events: none;
3441
- transition: opacity var(--duration);
3442
- }
3443
-
3444
- :where(a.avatar, button.avatar):not(:where(:disabled, [aria-disabled="true"])):hover::after {
3445
- opacity: 1;
3446
- }
3447
-
3448
- :where(a.avatar, button.avatar):not(:where(:disabled, [aria-disabled="true"])):active {
3449
- transform: translateY(1px);
3450
- }
3451
-
3452
- button.avatar:disabled,
3453
- a.avatar[aria-disabled="true"] {
3454
- cursor: not-allowed;
3455
- opacity: var(--disabled-opacity);
3456
- }
3457
-
3458
- a.avatar[aria-disabled="true"] {
3459
- pointer-events: none;
3460
- }
3461
-
3462
- .avatar.sm {
3463
- --avatar-stack-size: 1.75rem;
3464
- --avatar-stack-font-size: var(--font-size-xs);
3465
- }
3466
-
3467
- .avatar.lg {
3468
- --avatar-stack-size: 3rem;
3469
- --avatar-stack-font-size: var(--font-size-lg);
3470
- }
3471
-
3472
- /* Status dot: an empty .badge positioned at the avatar's bottom-end corner.
3473
- The dot conveys no text — always pair it with an aria-label on the badge. */
3474
- .avatar > .badge:empty {
3475
- --avatar-status-translate-x: 50%;
3476
-
3477
- position: absolute;
3478
- inset-block-end: var(--indicator-offset);
3479
- inset-inline-end: var(--indicator-offset);
3480
- z-index: 1;
3481
- box-shadow: 0 0 0 2px var(--indicator-ring);
3482
- transform: translate(var(--avatar-status-translate-x), 50%);
3483
- }
3484
-
3485
- .avatar:dir(rtl) > .badge:empty {
3486
- --avatar-status-translate-x: -50%;
3487
- }
3488
-
3489
- /* Avatar stack — explicit class for overlapping avatar groups.
3490
- Does not rely on role="group" auto-detection. */
3491
- .avatar-stack {
3492
- --avatar-stack-overlap: calc(var(--avatar-stack-size, 2.25rem) / -4);
3493
- --avatar-stack-ring: 2px;
3494
-
3495
- display: inline-flex;
3496
- align-items: center;
3497
- }
3498
-
3499
- .avatar-stack.sm {
3500
- --avatar-stack-size: 1.75rem;
3501
- --avatar-stack-font-size: var(--font-size-xs);
3502
- }
3503
-
3504
- .avatar-stack.lg {
3505
- --avatar-stack-size: 3rem;
3506
- --avatar-stack-font-size: var(--font-size-lg);
3507
- }
3508
-
3509
- @supports selector(:has(> .avatar)) {
3510
- .avatar-stack:has(> .avatar.sm) {
3511
- --avatar-stack-size: 1.75rem;
3512
- --avatar-stack-font-size: var(--font-size-xs);
3513
- }
3514
-
3515
- .avatar-stack:has(> .avatar.lg) {
3516
- --avatar-stack-size: 3rem;
3517
- --avatar-stack-font-size: var(--font-size-lg);
3518
- }
3519
- }
3520
-
3521
- .avatar-stack > .avatar {
3522
- box-shadow: 0 0 0 var(--avatar-stack-ring) var(--surface);
3523
- }
3524
-
3525
- .avatar-stack > .avatar + .avatar {
3526
- margin-inline-start: var(--avatar-stack-overlap);
3527
- }
3528
-
3529
- .avatar-stack > .avatar:is(:hover, :focus-visible) {
3530
- z-index: 1;
3531
- }
3532
-
3533
- /*
3534
- * Key — keyboard keys and shortcut tokens for UI chrome.
3535
- *
3536
- * Use .key outside prose. Plain kbd inside .prose gets the same visual
3537
- * treatment from prose.css so authored content stays convenient.
3538
- *
3539
- * Visual twin of .prose kbd (prose.css) — keep the two recipes in sync.
3540
- */
3541
- .key {
3542
- display: inline-flex;
3543
- align-items: center;
3544
- justify-content: center;
3545
- min-inline-size: 1.75em;
3546
- min-block-size: 1.6em;
3547
- padding-inline: 0.45em;
3548
- border: var(--border-width) solid var(--border);
3549
- border-block-end-width: calc(var(--border-width) * 2);
3550
- border-radius: var(--radius-sm);
3551
- background: var(--surface-raised);
3552
- color: var(--text);
3553
- font-family: var(--font-mono);
3554
- font-size: 0.875em;
3555
- font-weight: var(--font-weight-medium);
3556
- line-height: 1;
3557
- white-space: nowrap;
3558
- vertical-align: 0.08em;
3559
- }
3560
-
3561
- /*
3562
- * Overline — editorial small-cap label for section openers.
3563
- *
3564
- * Base form is unstyled chrome that takes its surrounding color. Add .pill for
3565
- * the bordered rounded shape, and combine with an intent class (.primary,
3566
- * .success, etc.) to tint the text, border, and background.
3567
- *
3568
- * Public hooks:
3569
- * --overline-radius
3570
- */
3571
- /* Intent boundary — inherited intents do not tint this component.
3572
- Local intent classes still win because :where() has zero specificity. */
3573
- :where(.overline) {
3574
- --intent: initial;
3575
- }
3576
-
3577
- .overline {
3578
- color: var(--text-muted);
3579
- font-size: var(--font-size-sm);
3580
- font-weight: var(--font-weight-medium);
3581
- letter-spacing: 0.04em;
3582
- text-transform: uppercase;
3583
- }
3584
-
3585
- .overline.pill {
3586
- --overline-radius: var(--radius-full);
3587
-
3588
- display: inline-flex;
3589
- align-items: center;
3590
- gap: 0.5em;
3591
- padding-inline: 0.6em;
3592
- padding-block: 0.25em;
3593
- inline-size: fit-content;
3594
- max-inline-size: 100%;
3595
- color: var(--intent, var(--text-muted));
3596
- border: var(--border-width) solid var(--border);
3597
- border-radius: var(--overline-radius);
3598
- background: var(--surface);
3599
- }
3600
-
3601
- /* Tinted pill surface where color-mix is available; the flat fallback above
3602
- keeps a readable bordered pill everywhere else. */
3603
- @supports (color: color-mix(in oklch, red, white)) {
3604
- .overline.pill {
3605
- border-color: color-mix(in oklch, var(--intent, var(--border)) 35%, var(--border));
3606
- background: color-mix(in oklch, var(--intent, transparent) 6%, var(--surface));
3607
- }
3608
- }
3609
-
3610
-
3611
- /* Navigation */
3612
- /*
3613
- * Navbar — top-level navigation bar.
3614
- *
3615
- * .navbar is a horizontal bar; .navbar-brand is the brand link; .navbar-nav is
3616
- * the inline link list using .nav-link items. .nav-list is the vertical nav
3617
- * list shared with .drawer, fed by the shared --gap token.
3618
- *
3619
- * Internal:
3620
- * --gap (local value for the shared gap token)
3621
- */
3622
- /* Intent boundary — inherited intents do not tint this component.
3623
- Local intent classes still win because :where() has zero specificity. */
3624
- :where(.navbar) {
3625
- --ui-bg: initial;
3626
- --ui-fg: initial;
3627
- --ui-border: initial;
3628
- }
3629
-
3630
- .navbar {
3631
- display: flex;
3632
- flex-wrap: wrap;
3633
- align-items: center;
3634
- justify-content: space-between;
3635
- gap: var(--gap);
3636
- padding-block: var(--space-30);
3637
- padding-inline: var(--space-40);
3638
- border: var(--border-width) solid var(--ui-border, var(--border));
3639
- border-radius: var(--radius-lg);
3640
- background: var(--ui-bg, var(--surface-raised));
3641
- color: var(--ui-fg, var(--text));
3642
- }
3643
-
3644
- .navbar-brand {
3645
- display: inline-flex;
3646
- align-items: center;
3647
- gap: var(--space-20);
3648
- color: var(--ui-fg, var(--text));
3649
- font-weight: var(--font-weight-bold);
3650
- text-decoration: none;
3651
- }
3652
-
3653
- .navbar-brand:focus-visible {
3654
- border-radius: var(--radius-sm);
3655
- }
3656
-
3657
- .navbar-nav {
3658
- display: flex;
3659
- flex-wrap: wrap;
3660
- align-items: center;
3661
- gap: var(--space-10);
3662
- margin: 0;
3663
- padding: 0;
3664
- list-style: none;
3665
- }
3666
-
3667
- .nav-link {
3668
- display: inline-flex;
3669
- align-items: center;
3670
- min-block-size: var(--control-size-sm);
3671
- padding-inline: var(--space-20);
3672
- border-radius: var(--radius);
3673
- color: var(--ui-fg, var(--text-muted));
3674
- font-weight: var(--font-weight-medium);
3675
- text-decoration: none;
3676
- }
3677
-
3678
- .nav-link:where([aria-current]:not([aria-current="false"])) {
3679
- background: var(--surface-subtle);
3680
- color: var(--text);
3681
- }
3682
-
3683
- .nav-link:hover {
3684
- background: var(--surface-subtle);
3685
- color: var(--text);
3686
- }
3687
-
3688
- /* Nav list — a vertical navigation list. Used by .drawer for stacked links. */
3689
- .nav-list {
3690
- display: grid;
3691
- --gap: var(--space-20);
3692
- gap: var(--gap);
3693
- margin: 0;
3694
- padding: 0;
3695
- list-style: none;
3696
- }
3697
-
3698
- .nav-list > li {
3699
- margin: 0;
3700
- }
3701
-
3702
- @media (forced-colors: active) {
3703
- .nav-link:where([aria-current]:not([aria-current="false"])) {
3704
- outline: var(--border-width) solid Highlight;
3705
- background: Highlight;
3706
- color: HighlightText;
3707
- forced-color-adjust: none;
3708
- }
3709
-
3710
- .nav-link:hover:not(:where([aria-current]:not([aria-current="false"]))) {
3711
- outline: var(--border-width) solid ButtonText;
3712
- outline-offset: 2px;
3713
- background: Canvas;
3714
- color: CanvasText;
3715
- }
3716
- }
3717
-
3718
- /*
3719
- * Breadcrumb — current-location trail from the site root.
3720
- *
3721
- * Inline list separated by "/". Mark the current page with `aria-current="page"`
3722
- * either on the item or on the current link.
3723
- */
3724
- .breadcrumb {
3725
- display: flex;
3726
- flex-wrap: wrap;
3727
- align-items: center;
3728
- gap: var(--space-10);
3729
- margin: 0;
3730
- padding: 0;
3731
- color: var(--text-subtle);
3732
- font-size: var(--font-size-sm);
3733
- list-style: none;
3734
- }
3735
-
3736
- .breadcrumb li {
3737
- display: inline-flex;
3738
- align-items: center;
3739
- gap: var(--space-10);
3740
- }
3741
-
3742
- .breadcrumb li + li::before {
3743
- content: "/";
3744
- color: var(--text-subtle);
3745
- }
3746
-
3747
- .breadcrumb a {
3748
- color: var(--text-muted);
3749
- text-decoration: none;
3750
- }
3751
-
3752
- .breadcrumb a:not([aria-current]):hover,
3753
- .breadcrumb a[aria-current="false"]:hover {
3754
- color: var(--text);
3755
- text-decoration: underline;
3756
- }
3757
-
3758
- .breadcrumb a:focus-visible {
3759
- border-radius: var(--radius-sm);
3760
- }
3761
-
3762
- .breadcrumb :where(a, span):where([aria-current]:not([aria-current="false"])) {
3763
- color: var(--text);
3764
- font-weight: var(--font-weight-medium);
3765
- }
3766
-
3767
- /*
3768
- * Pagination — page navigation for paginated lists.
3769
- *
3770
- * Render .btn elements inside a .pagination list. The active page is marked
3771
- * with aria-current="page"; .btn owns the active state styling.
3772
- *
3773
- * Truncated ranges are plain text (`.muted`, `aria-hidden="true"`), not .btn
3774
- * — they are decorative, not actionable. They still get the same box sizing
3775
- * as .btn below so the row stays visually aligned.
3776
- */
3777
- .pagination {
3778
- display: flex;
3779
- flex-wrap: wrap;
3780
- align-items: center;
3781
- gap: var(--space-10);
3782
- margin: 0;
3783
- padding: 0;
3784
- list-style: none;
3785
- }
3786
-
3787
- .pagination .btn {
3788
- min-inline-size: var(--control-size);
3789
- }
3790
-
3791
- .pagination > li > [aria-hidden="true"] {
3792
- display: inline-flex;
3793
- align-items: center;
3794
- justify-content: center;
3795
- min-inline-size: var(--control-size);
3796
- }
3797
-
3798
- /*
3799
- * Tabs — in-place panel switcher with real tab semantics.
3800
- *
3801
- * JavaScript owns roving tabindex, aria-selected, panel visibility,
3802
- * and arrow-key navigation. CSS owns the tab strip appearance and
3803
- * active indicator.
3804
- *
3805
- * Public hooks:
3806
- * --tab-gap
3807
- */
3808
- /* Intent boundary — inherited intents do not tint this component.
3809
- Local intent classes still win because :where() has zero specificity. */
3810
- :where(.tab) {
3811
- --intent: initial;
3812
- }
3813
-
3814
- .tabs {
3815
- display: flex;
3816
- flex-wrap: wrap;
3817
- gap: var(--space-10);
3818
- margin: 0;
3819
- padding: 0;
3820
- border-block-end: var(--border-width) solid var(--border);
3821
- list-style: none;
3822
- }
3823
-
3824
- .tabs[aria-orientation="vertical"] {
3825
- display: grid;
3826
- align-content: start;
3827
- justify-items: stretch;
3828
- border-block-end: 0;
3829
- border-inline-end: var(--border-width) solid var(--border);
3830
- }
3831
-
3832
- .tab {
3833
- display: inline-flex;
3834
- align-items: center;
3835
- gap: var(--tab-gap, 0.375em);
3836
- min-block-size: var(--control-size);
3837
- padding-inline: var(--space-30);
3838
- border: 0;
3839
- border-block-end: 2px solid transparent;
3840
- border-radius: 0;
3841
- background: transparent;
3842
- color: var(--text-muted);
3843
- font: inherit;
3844
- font-weight: var(--font-weight-strong);
3845
- text-decoration: none;
3846
- cursor: pointer;
3847
- }
3848
-
3849
- .tabs[aria-orientation="vertical"] .tab {
3850
- justify-content: flex-start;
3851
- border-block-end: 0;
3852
- border-inline-end: 2px solid transparent;
3853
- padding-inline-end: var(--space-40);
3854
- }
3855
-
3856
- .tab:is(:disabled, [aria-disabled="true"]) {
3857
- cursor: not-allowed;
3858
- opacity: var(--disabled-opacity);
3859
- }
3860
-
3861
- .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3862
- color: var(--text);
3863
- }
3864
-
3865
- .tab:focus-visible {
3866
- outline: var(--focus-outline);
3867
- outline-color: var(--focus);
3868
- outline-offset: -2px;
3869
- border-radius: var(--radius-sm) var(--radius-sm) 0 0;
3870
- }
3871
-
3872
- .tab[aria-selected="true"] {
3873
- border-block-end-color: var(--intent, var(--state-selected));
3874
- color: var(--intent, var(--state-selected));
3875
- }
3876
-
3877
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
3878
- border-block-end-color: transparent;
3879
- border-inline-end-color: var(--intent, var(--state-selected));
3880
- }
3881
-
3882
- @media (forced-colors: active) {
3883
- .tab:hover:not(:where(:disabled, [aria-disabled="true"])) {
3884
- text-decoration: underline;
3885
- text-underline-offset: 0.18em;
3886
- }
3887
-
3888
- .tab[aria-selected="true"] {
3889
- border-block-end-color: Highlight;
3890
- color: Highlight;
3891
- }
3892
-
3893
- .tabs[aria-orientation="vertical"] .tab[aria-selected="true"] {
3894
- border-block-end-color: transparent;
3895
- border-inline-end-color: Highlight;
3896
- }
3897
- }
3898
-
3899
- /*
3900
- * Scrollspy — behavior hook for navigation that marks the active section
3901
- * while the page scrolls.
3902
- *
3903
- * Does not add visual styles by itself. scrollspy.js watches
3904
- * <nav class="scrollspy">
3905
- * containing <a href="#section"> links and toggles aria-current="location" on
3906
- * the link whose target is in view. Pair with .nav-list and .nav-link, or style
3907
- * [aria-current] yourself.
3908
- *
3909
- * Progressive enhancement: on browsers that support :target-current,
3910
- * this file enables the native target group and marks the active link.
3911
- *
3912
- * Falls back gracefully: links still work when :target-current is unavailable
3913
- * and when the JavaScript enhancer is not loaded at all.
3914
- */
3915
-
3916
- @supports selector(.scrollspy a:target-current) {
3917
- .scrollspy {
3918
- scroll-target-group: auto;
3919
- }
3920
-
3921
- .scrollspy a:target-current {
3922
- color: var(--text);
3923
- font-weight: var(--font-weight-medium);
3924
- }
3925
- }
3926
-
3927
-
3928
- /* Data */
3929
- /*
3930
- * Table — structured tabular data.
3931
- *
3932
- * .table-wrap provides the scroll container and outer chrome; .table owns row
3933
- * and cell rules. Tokens are sourced from the shared scale.
3934
- *
3935
- * Public hooks:
3936
- * --table-min
3937
- * --table-cell-pad
3938
- */
3939
- .table-wrap {
3940
- inline-size: 100%;
3941
- max-inline-size: 100%;
3942
- min-inline-size: 0;
3943
- overflow-x: auto;
3944
- border: var(--border-width) solid var(--border);
3945
- border-radius: var(--radius-lg);
3946
- background: var(--surface-raised);
3947
- }
3948
-
3949
- .table {
3950
- --table-min: 100%;
3951
- --table-cell-pad: var(--space-30);
3952
-
3953
- inline-size: max(var(--table-min), 100%);
3954
- border-collapse: collapse;
3955
- background: transparent;
3956
- color: var(--text);
3957
- font-size: var(--font-size-md);
3958
- }
3959
-
3960
- .table caption {
3961
- padding: var(--table-cell-pad);
3962
- color: var(--text-muted);
3963
- text-align: var(--text-align, start);
3964
- }
3965
-
3966
- .table th,
3967
- .table td {
3968
- padding: var(--table-cell-pad);
3969
- border-block-end: var(--border-width) solid var(--border);
3970
- text-align: var(--text-align, start);
3971
- vertical-align: baseline;
3972
- }
3973
-
3974
- .table thead th {
3975
- vertical-align: bottom;
3976
- }
3977
-
3978
- .table th {
3979
- color: var(--text-muted);
3980
- font-weight: var(--font-weight-strong);
3981
- }
3982
-
3983
- .table thead {
3984
- background: var(--surface-subtle);
3985
- }
3986
-
3987
- .table tbody tr:hover {
3988
- background-color: var(--hover-overlay);
3989
- }
3990
-
3991
- .table > tr:last-child > *,
3992
- .table > :last-child > tr:last-child > * {
3993
- border-block-end: 0;
3994
- }
3995
-
3996
- /*
3997
- * Meter — native gauge for known-range values.
3998
- *
3999
- * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
4000
- * to the shared success / warning / danger intent colors.
4001
- */
4002
-
4003
- .meter {
4004
- appearance: none;
4005
- -webkit-appearance: none;
4006
- display: block;
4007
- inline-size: 100%;
4008
- block-size: var(--bar-height);
4009
- overflow: hidden;
4010
- border: 0;
4011
- border-radius: var(--radius-full);
4012
- background: var(--surface-subtle);
4013
- }
4014
-
4015
- .meter::-webkit-meter-bar {
4016
- -webkit-appearance: none;
4017
- block-size: var(--bar-height);
4018
- border: 0;
4019
- border-radius: var(--radius-full);
4020
- background: var(--surface-subtle);
4021
- }
4022
-
4023
- .meter::-webkit-meter-optimum-value {
4024
- block-size: var(--bar-height);
4025
- border-radius: var(--radius-full);
4026
- background: var(--success);
4027
- }
4028
-
4029
- .meter::-webkit-meter-suboptimum-value {
4030
- block-size: var(--bar-height);
4031
- border-radius: var(--radius-full);
4032
- background: var(--warning);
4033
- }
4034
-
4035
- .meter::-webkit-meter-even-less-good-value {
4036
- block-size: var(--bar-height);
4037
- border-radius: var(--radius-full);
4038
- background: var(--danger);
4039
- }
4040
-
4041
- .meter::-moz-meter-bar {
4042
- block-size: var(--bar-height);
4043
- border-radius: var(--radius-full);
4044
- }
4045
-
4046
- @media (forced-colors: active) {
4047
- .meter::-webkit-meter-optimum-value,
4048
- .meter::-webkit-meter-suboptimum-value,
4049
- .meter::-webkit-meter-even-less-good-value,
4050
- .meter::-moz-meter-bar {
4051
- background: Highlight;
4052
- }
4053
- }
4054
-
4055
- /* Forms — import order matters: switch.css follows choice.css so its rules
4056
- land after the shared choice recipe. validation.css is part of the default
4057
- forms bundle, but custom builds may omit it when validation styling is
4058
- app-owned. */
4059
- /*
4060
- * Form layout and supporting text.
4061
- *
4062
- * Individual controls keep their own files; .choice label layout lives in
4063
- * choice.css; .form-actions lives in form-actions.css.
4064
- *
4065
- * Public API: .field, .field-label, .field-help, .field-error, .field-group,
4066
- * .required-mark.
4067
- *
4068
- * Public hooks:
4069
- * --fieldset-legend-padding-inline
4070
- */
4071
- .field {
4072
- display: grid;
4073
- gap: var(--space-10);
4074
- color: var(--text);
4075
- }
4076
-
4077
- .field-group {
4078
- min-inline-size: 0;
4079
- margin: 0;
4080
- padding: var(--space-30);
4081
- border: var(--border-width) solid var(--border);
4082
- border-radius: var(--radius);
4083
- }
4084
-
4085
- .field-group + .field-group {
4086
- margin-block-start: var(--space-40);
4087
- }
4088
-
4089
- .field-label,
4090
- .field-group > legend {
4091
- color: var(--text);
4092
- font-weight: var(--font-weight-strong);
4093
- }
4094
-
4095
- .field-group > legend {
4096
- padding-block: 0;
4097
- padding-inline: var(--fieldset-legend-padding-inline, var(--space-10));
4098
- font-size: var(--font-size-xs);
4099
- }
4100
-
4101
- .field-help {
4102
- color: var(--text-muted);
4103
- font-size: var(--font-size-sm);
4104
- }
4105
-
4106
- .field-error {
4107
- display: none;
4108
- color: var(--danger);
4109
- font-size: var(--font-size-sm);
4110
- }
4111
-
4112
- .required-mark {
4113
- color: var(--danger);
4114
- }
4115
-
4116
- .field.danger .field-help {
4117
- color: var(--danger);
4118
- }
4119
-
4120
- /*
4121
- * Text controls — input, textarea, select.
4122
- *
4123
- * Neutral geometry and interaction states shared across all text-like
4124
- * controls. select shares this base recipe; its chevron/appearance layer
4125
- * lives in select.css. checkbox, radio, range, switch, and file have their
4126
- * own files.
4127
- *
4128
- * --control-* are the per-state control tokens, reserved for control geometry.
4129
- * State selectors only flip tokens; non-text controls do not consume them, so
4130
- * matching those selectors is a no-op. The .field wrapper uses --field-*
4131
- * (form.css).
4132
- *
4133
- * --control-border-block-end-color/-width are unset extension points, mirroring
4134
- * --btn-border-block-end-* (button.css), for themes that give text controls an
4135
- * underline-style lower edge instead of a uniform border.
4136
- *
4137
- * Public hooks:
4138
- * --control-border
4139
- * --control-bg
4140
- * --control-fg
4141
- * --control-cursor
4142
- * --control-border-block-end-color
4143
- * --control-border-block-end-width
4144
- */
4145
- .input,
4146
- .textarea,
4147
- .select {
4148
- --control-border: var(--border);
4149
- --control-bg: var(--surface);
4150
- --control-fg: var(--text);
4151
- --control-cursor: auto;
4152
-
4153
- appearance: none;
4154
- inline-size: 100%;
4155
- min-block-size: var(--control-size);
4156
- padding-inline: var(--space-30);
4157
- border: var(--border-width) solid var(--form-invalid-border, var(--control-border));
4158
- border-block-end-color: var(
4159
- --form-invalid-border,
4160
- var(--control-border-block-end-color, var(--control-border))
4161
- );
4162
- border-block-end-width: var(--control-border-block-end-width, var(--border-width));
4163
- border-radius: var(--radius);
4164
- background: var(--control-bg);
4165
- color: var(--control-fg);
4166
- font-size: var(--control-font-size);
4167
- cursor: var(--control-cursor);
4168
- outline: none;
4169
- transition:
4170
- border-color var(--duration),
4171
- box-shadow var(--duration),
4172
- background-color var(--duration),
4173
- color var(--duration);
4174
- }
4175
-
4176
- .textarea {
4177
- min-block-size: 6rem;
4178
- padding-block: var(--space-20);
4179
- resize: vertical;
4180
- }
4181
-
4182
- .input:hover:not(:disabled),
4183
- .textarea:hover:not(:disabled) {
4184
- --control-border: var(--text-subtle);
4185
- }
4186
-
4187
- .select:hover:not(:disabled) {
4188
- --control-bg: var(--surface-subtle);
4189
- }
4190
-
4191
- .input:focus-visible,
4192
- .textarea:focus-visible,
4193
- .select:focus-visible {
4194
- --control-border: var(--focus);
4195
- outline: 2px solid transparent;
4196
- box-shadow: var(--focus-ring-shadow);
4197
- }
4198
-
4199
- .input[readonly],
4200
- .textarea[readonly] {
4201
- --control-bg: var(--surface-subtle);
4202
- --control-border: var(--border);
4203
- }
4204
-
4205
- .input:disabled,
4206
- .textarea:disabled,
4207
- .select:disabled {
4208
- --control-bg: var(--surface-subtle);
4209
- --control-fg: var(--state-disabled);
4210
- --control-border: var(--border);
4211
- --control-cursor: not-allowed;
4212
- }
4213
-
4214
- .input::placeholder,
4215
- .textarea::placeholder {
4216
- color: var(--text-muted);
4217
- }
4218
-
4219
- /*
4220
- * Input Icon — decorative icon inside a relative wrapper, leading or trailing.
4221
- *
4222
- * Icon-library agnostic: detects position by DOM order. First child that is
4223
- * not .input = leading icon; icon after .input = trailing. The icon keeps a
4224
- * fixed size; density contexts shrink the control geometry around it, not
4225
- * the pictogram.
4226
- *
4227
- * Public hooks:
4228
- * --input-icon-size
4229
- */
4230
- .input-icon {
4231
- --input-icon-size: 1.25rem;
4232
-
4233
- display: block;
4234
- position: relative;
4235
- }
4236
-
4237
- /* Icon — shared by leading and trailing */
4238
- .input-icon > :where(:not(.input)) {
4239
- position: absolute;
4240
- inset-block-start: 50%;
4241
- transform: translateY(-50%);
4242
- font-size: var(--input-icon-size);
4243
- color: var(--text-muted);
4244
- pointer-events: none;
4245
- }
4246
-
4247
- /* Leading: icon is the first child */
4248
- .input-icon > :first-child:where(:not(.input)) {
4249
- inset-inline-start: var(--space-30);
4250
- }
4251
-
4252
- /* Trailing: input is the first child, icon follows */
4253
- .input-icon > .input:first-child ~ :where(:not(.input)) {
4254
- inset-inline-end: var(--space-30);
4255
- }
4256
-
4257
- /* font-size only sizes text-metric icons (webfont <i>, <span>). An inline
4258
- <svg> with just a viewBox ignores it, so give it explicit dimensions. */
4259
- .input-icon > svg:where(:not(.input)) {
4260
- inline-size: var(--input-icon-size);
4261
- block-size: var(--input-icon-size);
4262
- }
4263
-
4264
- /* Leading: input gets start padding to clear the icon */
4265
- .input-icon > :first-child:where(:not(.input)) ~ .input {
4266
- padding-inline-start: calc(var(--space-30) * 2 + var(--input-icon-size));
4267
- }
4268
-
4269
- /* Trailing: input is first child and has a sibling → end padding */
4270
- .input-icon > .input:first-child:where(:not(:last-child)) {
4271
- padding-inline-end: calc(var(--space-30) * 2 + var(--input-icon-size));
4272
- }
4273
-
4274
- /*
4275
- * Checkbox and radio controls.
4276
- *
4277
- * Native inputs keep their behavior while sharing custom geometry, checked,
4278
- * focus, and disabled states across semantic forms and classed aliases. This
4279
- * file also owns the .choice label layout (input + label text) so the whole
4280
- * concept lives in one file.
4281
- *
4282
- * Switches (input[role="switch"]) are excluded so switch.css owns their
4283
- * geometry and states. switch.css is imported after this file.
4284
- *
4285
- * Public hooks:
4286
- * --choice-control-offset
4287
- * --choice-border
4288
- * --choice-bg
4289
- * --choice-mark
4290
- */
4291
- /* Intent boundary — inherited intents do not tint this component.
4292
- Local intent classes still win because :where() has zero specificity. */
4293
- :where(.choice, .check, .radio) {
4294
- --intent: initial;
4295
- --intent-fg: initial;
4296
- }
4297
-
4298
- /* A classed .choice is the component surface. Let its direct input consume a
4299
- local intent without reopening inherited page-level intents. */
4300
- :where(.choice) > :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
4301
- --intent: inherit;
4302
- --intent-fg: inherit;
4303
- }
4304
-
4305
- .choice {
4306
- --choice-control-offset: 0.25em;
4307
-
4308
- display: inline-flex;
4309
- align-items: flex-start;
4310
- gap: var(--space-20);
4311
- color: var(--text);
4312
- font-size: var(--control-font-size);
4313
- cursor: pointer;
4314
- }
4315
-
4316
- /* Optical alignment: nudge the control down so the checkbox/radio sits
4317
- visually centered with the first line of label text, not flush with the
4318
- top of the line-box. Default 0.25em (~4px at 16px) places the control just
4319
- below the cap-height, matching what Bootstrap, Material, and most polished
4320
- form libraries ship. Multi-line labels still keep the control at the top
4321
- thanks to align-items: flex-start. Override per-instance via
4322
- --choice-control-offset for themes with a different line-height or control
4323
- size. */
4324
- .choice > :is(input[type="checkbox"]:not([role="switch"]), input[type="radio"]) {
4325
- flex: 0 0 auto;
4326
- margin-block-start: var(--choice-control-offset);
4327
- }
4328
-
4329
- .choice > span > .field-help {
4330
- display: block;
4331
- }
4332
-
4333
- /* Progressive enhancement: dim the whole row when one of its controls is
4334
- disabled. Without :has() the native disabled control keeps its own state
4335
- styling — the row-level dim is lost but the field stays clearly disabled. */
4336
- .choice:has(> :disabled) {
4337
- color: var(--state-disabled);
4338
- cursor: not-allowed;
4339
- }
4340
-
4341
- /* Control geometry. --choice-* are the per-state tokens. */
4342
- .check,
4343
- .radio {
4344
- --choice-border: var(--border);
4345
- --choice-bg: var(--surface);
4346
- --choice-mark: var(--primary-fg);
4347
-
4348
- position: relative;
4349
- vertical-align: middle;
4350
- display: inline-grid;
4351
- place-content: center;
4352
- inline-size: 1.125em;
4353
- block-size: 1.125em;
4354
- margin: 0;
4355
- border: var(--border-width) solid var(--form-invalid-border, var(--choice-border));
4356
- background: var(--choice-bg);
4357
- color: var(--choice-mark);
4358
- appearance: none;
4359
- cursor: pointer;
4360
- }
4361
-
4362
- .check {
4363
- border-radius: var(--radius-sm);
4364
- }
4365
-
4366
- .radio {
4367
- border-radius: var(--radius-full);
4368
- }
4369
-
4370
- .check::before,
4371
- .radio::before {
4372
- content: "";
4373
- position: absolute;
4374
- inset: 0;
4375
- margin: auto;
4376
- transform: scale(0);
4377
- transition: transform var(--duration);
4378
- }
4379
-
4380
- .check::before {
4381
- inline-size: 0.58em;
4382
- block-size: 0.34em;
4383
- border-inline-start: 2px solid currentColor;
4384
- border-block-end: 2px solid currentColor;
4385
- transform: translateY(-0.06em) rotate(-45deg) scale(0);
4386
- }
4387
-
4388
- .radio::before {
4389
- inline-size: 0.5em;
4390
- block-size: 0.5em;
4391
- border-radius: var(--radius-full);
4392
- background: currentColor;
4393
- }
4394
-
4395
- .check:is(:checked, :indeterminate),
4396
- .radio:checked {
4397
- --choice-border: var(--intent, var(--primary));
4398
- --choice-bg: var(--intent, var(--primary));
4399
- --choice-mark: var(--intent-fg, var(--primary-fg));
4400
- }
4401
-
4402
- .check:checked::before {
4403
- transform: translateY(-0.06em) rotate(-45deg) scale(1);
4404
- }
4405
-
4406
- .check:indeterminate::before {
4407
- border-inline-start: 0;
4408
- inline-size: 0.6em;
4409
- block-size: 0;
4410
- border-block-end: 2px solid currentColor;
4411
- transform: scale(1);
4412
- }
4413
-
4414
- .radio:checked::before {
4415
- transform: scale(1);
4416
- }
4417
-
4418
- .check:hover:not(:checked, :indeterminate, :disabled),
4419
- .radio:hover:not(:checked, :indeterminate, :disabled) {
4420
- --choice-border: var(--text-subtle);
4421
- }
4422
-
4423
- .check:focus-visible,
4424
- .radio:focus-visible {
4425
- --choice-border: var(--focus);
4426
- outline: 2px solid transparent;
4427
- box-shadow: var(--focus-ring-shadow);
4428
- }
4429
-
4430
- .check:disabled,
4431
- .radio:disabled {
4432
- --choice-bg: var(--surface-subtle);
4433
- --choice-border: var(--border);
4434
- --choice-mark: var(--state-disabled);
4435
- cursor: not-allowed;
4436
- opacity: 1;
4437
- }
4438
-
4439
- @media (forced-colors: active) {
4440
- .check,
4441
- .radio {
4442
- --choice-border: ButtonText;
4443
- --choice-bg: Canvas;
4444
- --choice-mark: ButtonText;
4445
- }
4446
-
4447
- .check:checked,
4448
- .radio:checked,
4449
- .check:indeterminate {
4450
- --choice-border: Highlight;
4451
- --choice-bg: Highlight;
4452
- --choice-mark: HighlightText;
4453
- forced-color-adjust: none;
4454
- }
4455
-
4456
- .check:disabled,
4457
- .radio:disabled {
4458
- --choice-border: GrayText;
4459
- --choice-bg: Canvas;
4460
- --choice-mark: GrayText;
4461
- }
4462
- }
4463
-
4464
- /*
4465
- * Choice cards — card-style radio and checkbox labels.
4466
- *
4467
- * Each .choice-card is a <label> wrapping a native radio or checkbox. When
4468
- * :has() is supported the input is visually hidden and the card mirrors its
4469
- * state: checked border and tint, checkmark badge (round for radio, rounded
4470
- * square for checkbox), focus ring, disabled dimming. Without :has() the
4471
- * native input stays visible so selection state never disappears.
4472
- *
4473
- * The badge overhangs the top-right corner by 0.375em; ancestors with
4474
- * overflow: hidden will clip it. Pair with grid or flex utilities for group
4475
- * layout.
4476
- *
4477
- * Public hooks:
4478
- * --choice-card-pad
4479
- * --choice-card-radius
4480
- * --choice-card-border
4481
- * --choice-card-bg
4482
- * --choice-card-check-size
4483
- */
4484
- :where(.choice-card) {
4485
- --intent: initial;
4486
- --intent-fg: initial;
4487
- }
4488
-
4489
- .choice-card {
4490
- --choice-card-pad: var(--density-space, var(--space-40));
4491
- --choice-card-radius: var(--radius);
4492
- --choice-card-border: var(--border);
4493
- --choice-card-bg: var(--surface);
4494
- --choice-card-check-size: 1.5em;
4495
-
4496
- position: relative;
4497
- display: block;
4498
- padding: var(--choice-card-pad);
4499
- border: var(--border-width) solid var(--form-invalid-border, var(--choice-card-border));
4500
- border-radius: var(--choice-card-radius);
4501
- background: var(--choice-card-bg);
4502
- color: var(--text);
4503
- cursor: pointer;
4504
- transition:
4505
- border-color var(--duration),
4506
- background-color var(--duration),
4507
- box-shadow var(--duration);
4508
- }
4509
-
4510
- /* The native input is visually hidden but stays focusable and participates in
4511
- form submission and keyboard navigation (sr-only pattern). Guarded: without
4512
- :has() the card cannot mirror the input's state, so the visible native
4513
- control is the fallback affordance. */
4514
- @supports selector(:has(*)) {
4515
- .choice-card > :is(input[type="radio"], input[type="checkbox"]) {
4516
- position: absolute;
4517
- inline-size: 1px;
4518
- block-size: 1px;
4519
- padding: 0;
4520
- margin: -1px;
4521
- overflow: hidden;
4522
- clip: rect(0, 0, 0, 0);
4523
- white-space: nowrap;
4524
- border: 0;
4525
- }
4526
- }
4527
-
4528
- /* Focus ring on the label follows the hidden input's focus. */
4529
- .choice-card:has(:focus-visible) {
4530
- outline: 2px solid transparent;
4531
- box-shadow: var(--focus-ring-shadow);
4532
- }
4533
-
4534
- .choice-card:has(:disabled) {
4535
- opacity: var(--disabled-opacity);
4536
- cursor: not-allowed;
4537
- }
4538
-
4539
- .choice-card:hover:not(:has(:disabled)) {
4540
- --choice-card-border: var(--text-subtle);
4541
- }
4542
-
4543
- /* Checked border — shared by radio and checkbox */
4544
- .choice-card:has(:checked) {
4545
- --choice-card-border: var(--intent, var(--primary));
4546
- }
4547
-
4548
- /* Subtle background tint on checked — modern browsers only */
4549
- @supports (color: color-mix(in oklch, red, white)) {
4550
- .choice-card:has(:checked) {
4551
- --choice-card-bg: color-mix(in oklch, var(--surface) 96%, var(--intent, var(--primary)));
4552
- }
4553
- }
4554
-
4555
- /* Checkmark badge — always rendered at scale(0) and scaled in on check, so
4556
- the entrance can transition (a pseudo created on :checked could not). */
4557
- .choice-card::before,
4558
- .choice-card::after {
4559
- content: "";
4560
- position: absolute;
4561
- inset-inline-end: -0.375em;
4562
- inset-block-start: -0.375em;
4563
- inline-size: var(--choice-card-check-size);
4564
- block-size: var(--choice-card-check-size);
4565
- transform: scale(0);
4566
- transition: transform var(--duration);
4567
- }
4568
-
4569
- /* Badge disc — circle for radio, rounded square for checkbox */
4570
- .choice-card::before {
4571
- background: var(--intent, var(--primary));
4572
- box-shadow: 0 0 0 2px var(--surface);
4573
- z-index: 1;
4574
- }
4575
-
4576
- .choice-card:has(input[type="radio"])::before {
4577
- border-radius: var(--radius-full);
4578
- }
4579
-
4580
- .choice-card:has(input[type="checkbox"])::before {
4581
- border-radius: var(--radius-sm);
4582
- }
4583
-
4584
- /* Check mark */
4585
- .choice-card::after {
4586
- background: var(--intent-fg, var(--primary-fg));
4587
- -webkit-mask: var(--icon-check) center / 60% no-repeat;
4588
- mask: var(--icon-check) center / 60% no-repeat;
4589
- z-index: 2;
4590
- }
4591
-
4592
- .choice-card:has(:checked)::before,
4593
- .choice-card:has(:checked)::after {
4594
- transform: scale(1);
4595
- }
4596
-
4597
- @media (forced-colors: active) {
4598
- .choice-card {
4599
- --choice-card-border: ButtonText;
4600
- --choice-card-bg: Canvas;
4601
- }
4602
-
4603
- .choice-card:has(:checked) {
4604
- --choice-card-border: Highlight;
4605
- /* Pin the background: with forced-color-adjust: none the color-mix tint
4606
- above would actually render, uncontrolled in high-contrast themes. */
4607
- --choice-card-bg: Canvas;
4608
- forced-color-adjust: none;
4609
- }
4610
-
4611
- .choice-card:has(:checked)::before {
4612
- background: Highlight;
4613
- box-shadow: 0 0 0 2px Canvas;
4614
- }
4615
-
4616
- .choice-card:has(:checked)::after {
4617
- background: HighlightText;
4618
- }
4619
- }
4620
-
4621
- /*
4622
- * Select — chevron and appearance layer on top of the shared text-control
4623
- * recipe.
4624
- *
4625
- * Base geometry and interaction states come from control.css (select shares
4626
- * the --control-* tokens). This file only removes the native arrow and draws
4627
- * the CSS chevron, so the picker (appearance: base-select) enhancement in
4628
- * custom-select.css can be opted out independently. Add .native to a specific
4629
- * select to opt out of the enhancement.
4630
- */
4631
- .select:not([multiple], [size]:not([size="1"])) {
4632
- appearance: none;
4633
- padding-inline-end: calc(var(--space-30) + var(--space-60));
4634
- background-image: var(--icon-chevron);
4635
- background-position: center right var(--space-30);
4636
- background-size: 1rem auto;
4637
- background-repeat: no-repeat;
4638
- }
4639
-
4640
- /* The chevron sits on the inline-end side; flip it in RTL so it follows
4641
- the padding-inline-end space reserved for it. */
4642
- .select:not([multiple], [size]:not([size="1"])):dir(rtl) {
4643
- background-position: center left var(--space-30);
4644
- }
4645
-
4646
- /* Both forced-colors and prefers-contrast hand the arrow back to the
4647
- browser instead of guessing a higher-contrast recolor of our own icon —
4648
- simpler than maintaining contrast variants, and native rendering already
4649
- matches whatever contrast mode the user picked. */
4650
- @media (forced-colors: active), (prefers-contrast: more) {
4651
- .select:not([multiple], [size]:not([size="1"])) {
4652
- appearance: auto;
4653
- background-image: none;
4654
- padding-inline-end: var(--space-30);
4655
- }
4656
- }
4657
-
4658
- /*
4659
- * Customizable select — appearance: base-select picker enhancement.
4660
- *
4661
- * Progressive enhancement on top of the native .select baseline in select.css.
4662
- * Wrapped in @supports and gated on fine pointer devices so it never blocks a
4663
- * layout. Add .native to a specific select to opt out.
4664
- *
4665
- * No layout or interaction contract may depend on appearance: base-select.
4666
- * Arbitrary menu and popover behavior belongs elsewhere.
4667
- *
4668
- * Internal:
4669
- * --control-border
4670
- * --control-bg
4671
- */
4672
- @media (forced-colors: none) and (prefers-contrast: no-preference) and (hover: hover) and (
4673
- pointer: fine
4674
- ) {
4675
- @supports (appearance: base-select) {
4676
- .select:not([multiple], .native, [size]:not([size="1"])) {
4677
- appearance: base-select;
4678
- align-items: center;
4679
-
4680
- &::picker(select) {
4681
- appearance: base-select;
4682
- max-height: min(24rem, calc(var(--viewport-block) * 0.7));
4683
- margin-block: var(--space-20);
4684
- padding: 0;
4685
- border: var(--border-width) solid var(--border);
4686
- border-radius: var(--radius);
4687
- background-color: var(--surface-raised);
4688
- color: inherit;
4689
- box-shadow: var(--shadow-popout);
4690
- }
4691
-
4692
- &:open {
4693
- --control-border: var(--border);
4694
- --control-bg: var(--surface-raised);
4695
- outline: none;
4696
- box-shadow: var(--focus-ring-shadow);
4697
- }
4698
-
4699
- &::picker-icon {
4700
- display: none;
4701
- }
4702
-
4703
- & option {
4704
- position: relative;
4705
- padding-block: var(--space-20);
4706
- padding-inline: var(--space-30);
4707
- white-space: normal;
4708
- transition-property: color, background-color;
4709
- transition-duration: var(--duration);
4710
-
4711
- &:not(:disabled):hover {
4712
- background-color: var(--surface-subtle);
4713
- color: var(--text);
4714
- cursor: pointer;
4715
- }
4716
-
4717
- &:not(:disabled):focus-visible {
4718
- background-color: var(--primary);
4719
- color: var(--primary-fg);
4720
- outline: none;
4721
- }
4722
-
4723
- &:active {
4724
- background-color: var(--surface-subtle);
4725
- color: var(--text);
4726
- }
4727
-
4728
- &::checkmark {
4729
- content: "";
4730
- display: block;
4731
- inline-size: 1rem;
4732
- block-size: 1rem;
4733
- background-color: currentColor;
4734
- -webkit-mask: var(--icon-check) center / contain no-repeat;
4735
- mask: var(--icon-check) center / contain no-repeat;
4736
- }
4737
- }
4738
-
4739
- & optgroup {
4740
- padding-block-start: 0.5em;
4741
-
4742
- & option:first-child {
4743
- margin-block-start: 0.5em;
4744
- }
4745
- }
4746
- }
4747
- }
4748
- }
4749
-
4750
- /*
4751
- * Native-only control polish: the file input and color picker mostly defeat
4752
- * custom styling, so the framework stays out of the way and just keeps them
4753
- * readable and theme-aware.
4754
- *
4755
- * Range has its own custom-styled file (range.css) so the focus ring can
4756
- * target the thumb pseudo-element rather than the rectangular CSS box.
4757
- */
4758
- .file {
4759
- color: var(--text-muted);
4760
- }
4761
-
4762
- .file::file-selector-button {
4763
- margin-inline-end: var(--space-30);
4764
- padding-block: var(--space-10);
4765
- padding-inline: var(--space-20);
4766
- border: var(--border-width) solid var(--border);
4767
- border-radius: var(--radius-sm);
4768
- background: var(--surface-subtle);
4769
- color: var(--text);
4770
- font: inherit;
4771
- font-size: var(--font-size-sm);
4772
- font-weight: var(--font-weight-medium);
4773
- cursor: pointer;
4774
- transition:
4775
- background-color var(--duration),
4776
- border-color var(--duration),
4777
- color var(--duration);
4778
- }
4779
-
4780
- .file:disabled::file-selector-button {
4781
- cursor: not-allowed;
4782
- }
4783
-
4784
- .file:not(:disabled)::file-selector-button:hover {
4785
- background: var(--border);
4786
- }
4787
-
4788
- .file:focus-visible {
4789
- outline: 2px solid transparent;
4790
- box-shadow: var(--focus-ring-shadow);
4791
- }
4792
-
4793
- /* Native color picker — keep the browser's dialog and selected-color UI. */
4794
- .color {
4795
- inline-size: var(--control-size);
4796
- block-size: var(--control-size);
4797
- padding: var(--space-10);
4798
- border: var(--border-width) solid var(--border);
4799
- border-radius: var(--radius);
4800
- background: var(--surface);
4801
- cursor: pointer;
4802
- }
4803
-
4804
- .color::-webkit-color-swatch-wrapper {
4805
- padding: 0;
4806
- }
4807
-
4808
- .color::-webkit-color-swatch {
4809
- border: 0;
4810
- }
4811
-
4812
- .color::-moz-color-swatch {
4813
- border: 0;
4814
- }
4815
-
4816
- .color:disabled {
4817
- cursor: not-allowed;
4818
- opacity: var(--disabled-opacity);
4819
- }
4820
-
4821
- .color:focus-visible {
4822
- outline: 2px solid transparent;
4823
- box-shadow: var(--focus-ring-shadow);
4824
- }
4825
-
4826
- @media (forced-colors: active) {
4827
- .color {
4828
- border-color: ButtonText;
4829
- background: ButtonFace;
4830
- forced-color-adjust: none;
4831
- }
4832
- }
4833
-
4834
- /*
4835
- * Range slider — custom-styled so the focus ring always targets the
4836
- * user-manipulated surface (the thumb) rather than the CSS box.
4837
- *
4838
- * appearance: none gives us full control over track and thumb; without it,
4839
- * pseudo-element rules for ::-webkit-slider-thumb and ::-moz-range-thumb
4840
- * are not reliably applied across browsers.
4841
- *
4842
- * Public hooks:
4843
- * --range-thumb-size
4844
- * --range-track-height
4845
- * --range-thumb-bg
4846
- * --range-track-bg
4847
- */
4848
-
4849
- /* Intent boundary — inherited intents do not tint this component.
4850
- Local intent classes still win because :where() has zero specificity. */
4851
- :where(.range) {
4852
- --intent: initial;
4853
- }
4854
-
4855
- .range {
4856
- --range-thumb-size: 1.25rem;
4857
- --range-track-height: 0.25rem;
4858
- --range-thumb-bg: var(--intent, var(--primary));
4859
- --range-track-bg: var(--border);
4860
-
4861
- appearance: none;
4862
- inline-size: 100%;
4863
- block-size: var(--range-thumb-size);
4864
- background: transparent;
4865
- cursor: pointer;
4866
- }
4867
-
4868
- .range::-webkit-slider-runnable-track {
4869
- block-size: var(--range-track-height);
4870
- border-radius: var(--radius-full);
4871
- background: var(--range-track-bg);
4872
- }
4873
-
4874
- .range::-moz-range-track {
4875
- block-size: var(--range-track-height);
4876
- border-radius: var(--radius-full);
4877
- background: var(--range-track-bg);
4878
- }
4879
-
4880
- .range::-webkit-slider-thumb {
4881
- appearance: none;
4882
- inline-size: var(--range-thumb-size);
4883
- block-size: var(--range-thumb-size);
4884
- border-radius: var(--radius-full);
4885
- border: 0;
4886
- background: var(--range-thumb-bg);
4887
- margin-block-start: calc((var(--range-track-height) - var(--range-thumb-size)) / 2);
4888
- cursor: inherit;
4889
- transition: box-shadow var(--duration);
4890
- }
4891
-
4892
- .range::-moz-range-thumb {
4893
- inline-size: var(--range-thumb-size);
4894
- block-size: var(--range-thumb-size);
4895
- border-radius: var(--radius-full);
4896
- border: 0;
4897
- background: var(--range-thumb-bg);
4898
- cursor: inherit;
4899
- transition: box-shadow var(--duration);
4900
- }
4901
-
4902
- .range:focus-visible {
4903
- outline: 2px solid transparent;
4904
- }
4905
-
4906
- /*
4907
- * The 1px surface-coloured separator keeps the ring visually distinct from
4908
- * the small, coloured thumb — unlike a full-size input where the same gap
4909
- * would read as an unwanted void.
4910
- */
4911
- .range:focus-visible::-webkit-slider-thumb {
4912
- box-shadow:
4913
- 0 0 0 1px var(--surface),
4914
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
4915
- }
4916
-
4917
- .range:focus-visible::-moz-range-thumb {
4918
- box-shadow:
4919
- 0 0 0 1px var(--surface),
4920
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
4921
- }
4922
-
4923
- .range:disabled {
4924
- --range-thumb-bg: var(--state-disabled);
4925
- cursor: not-allowed;
4926
- opacity: var(--disabled-opacity);
4927
- }
4928
-
4929
- /* Forced colors: map the custom thumb/track to system colors. The focus
4930
- indicator stays visible through the transparent outline on :focus-visible. */
4931
- @media (forced-colors: active) {
4932
- .range {
4933
- --range-thumb-bg: ButtonText;
4934
- --range-track-bg: Canvas;
4935
- }
4936
- }
4937
-
4938
- /*
4939
- * Switch controls for checkbox inputs with switch semantics.
4940
- *
4941
- * The switch remains an input control, not a decorative toggle. Checked, hover,
4942
- * focus-visible, and disabled states stay clear.
4943
- *
4944
- * Public hooks:
4945
- * --switch-bg
4946
- * --switch-border
4947
- * --switch-knob
4948
- * --switch-width
4949
- * --switch-block-size
4950
- * --switch-knob-margin
4951
- * --switch-knob-size
4952
- * --switch-travel
4953
- */
4954
- /* Intent boundary — inherited intents do not tint this component.
4955
- Local intent classes still win because :where() has zero specificity. */
4956
- :where(.switch) {
4957
- --intent: initial;
4958
- --intent-fg: initial;
4959
- }
4960
-
4961
- .switch {
4962
- --switch-bg: var(--border);
4963
- --switch-border: var(--border);
4964
- --switch-knob: var(--surface-raised);
4965
- --switch-width: calc(var(--control-size) * 1.16);
4966
- --switch-block-size: calc(var(--control-size) * 0.63);
4967
- --switch-knob-margin: calc(var(--switch-block-size) * 0.12);
4968
- --switch-knob-size: calc(var(--switch-block-size) - 2 * var(--switch-knob-margin));
4969
- --switch-travel: calc(
4970
- var(--switch-width) -
4971
- var(--switch-knob-size) -
4972
- 2 *
4973
- var(--border-width) -
4974
- 2 *
4975
- var(--switch-knob-margin)
4976
- );
4977
-
4978
- position: relative;
4979
- display: inline-grid;
4980
- align-items: center;
4981
- justify-content: start;
4982
- inline-size: var(--switch-width);
4983
- block-size: var(--switch-block-size);
4984
- margin: 0;
4985
- border: var(--border-width) solid var(--form-invalid-border, var(--switch-border));
4986
- border-radius: var(--radius-full);
4987
- background: var(--switch-bg);
4988
- appearance: none;
4989
- cursor: pointer;
4990
- }
4991
-
4992
- .switch::before {
4993
- content: "";
4994
- inline-size: var(--switch-knob-size);
4995
- block-size: var(--switch-knob-size);
4996
- margin-inline: var(--switch-knob-margin);
4997
- border-radius: var(--radius-full);
4998
- background: var(--switch-knob);
4999
- box-shadow: var(--shadow);
5000
- transition:
5001
- transform var(--duration),
5002
- background-color var(--duration),
5003
- box-shadow var(--duration);
5004
- }
5005
-
5006
- .switch:hover:not(:disabled) {
5007
- --switch-border: var(--intent, var(--primary));
5008
- }
5009
-
5010
- .switch:checked {
5011
- --switch-bg: var(--intent, var(--primary));
5012
- --switch-border: var(--intent, var(--primary));
5013
- --switch-knob: var(--intent-fg, var(--primary-fg));
5014
- }
5015
-
5016
- .switch:checked::before {
5017
- transform: translateX(var(--switch-travel));
5018
- }
5019
-
5020
- /* The knob travels toward inline-end; in RTL that is leftward, so the
5021
- translateX sign flips. translateX is physical, not logical. */
5022
- .switch:checked:dir(rtl)::before {
5023
- transform: translateX(calc(var(--switch-travel) * -1));
5024
- }
5025
-
5026
- .switch:focus-visible {
5027
- --switch-border: var(--focus);
5028
- outline: 2px solid transparent;
5029
- box-shadow: var(--focus-ring-shadow);
5030
- }
5031
-
5032
- .switch:disabled {
5033
- --switch-bg: var(--surface-subtle);
5034
- --switch-border: var(--border);
5035
- --switch-knob: var(--state-disabled);
5036
- opacity: 1;
5037
- cursor: not-allowed;
5038
- }
5039
-
5040
- @media (forced-colors: active) {
5041
- /* --switch-knob must be set on the element, not on ::before: a custom
5042
- property declared on the pseudo would shadow the :checked override. */
5043
- .switch {
5044
- --switch-border: ButtonText;
5045
- --switch-knob: ButtonText;
5046
- }
5047
-
5048
- .switch::before {
5049
- box-shadow: none;
5050
- }
5051
-
5052
- .switch:checked {
5053
- --switch-bg: Highlight;
5054
- --switch-border: Highlight;
5055
- --switch-knob: HighlightText;
5056
- forced-color-adjust: none;
5057
- }
5058
-
5059
- .switch:disabled {
5060
- --switch-bg: Canvas;
5061
- --switch-border: GrayText;
5062
- --switch-knob: GrayText;
5063
- forced-color-adjust: auto;
5064
- }
5065
- }
5066
-
5067
- /*
5068
- * Form validation states.
5069
- *
5070
- * Imported by default through forms/index.css. Custom builds may omit this
5071
- * file when an app wants to own validation styling completely.
5072
- *
5073
- * State coverage (which states each component ships) is audited in
5074
- * tests/validation.test.js and summarized in docs/pages/forms/validation.md —
5075
- * not kept as a source comment that can drift from the implementation.
5076
- *
5077
- * Public hooks:
5078
- * --form-invalid-border
5079
- *
5080
- * Internal:
5081
- * --choice-bg
5082
- * --switch-bg
5083
- */
5084
-
5085
- .field.danger {
5086
- --form-invalid-border: var(--danger);
5087
- }
5088
-
5089
- .field.danger > .field-error {
5090
- display: block;
5091
- }
5092
-
5093
- @supports selector(:has(*)) {
5094
- .field:has([aria-invalid="true"]) > .field-error,
5095
- .needs-validation.was-validated .field:has(:invalid) > .field-error {
5096
- display: block;
5097
- }
5098
- }
5099
-
5100
- :where(.input, .textarea, .select, .check, .radio, .switch)[aria-invalid="true"],
5101
- .needs-validation.was-validated :is(.input, .textarea, .select, .check, .radio, .switch):invalid {
5102
- --form-invalid-border: var(--danger);
5103
- }
5104
-
5105
- /* Override checked/indeterminate fill so both border and fill signal danger
5106
- instead of a red border around a primary-colored control. All three
5107
- invalid routes must converge: aria-invalid, was-validated :invalid, and
5108
- :user-invalid. */
5109
- .check:is(:checked, :indeterminate)[aria-invalid="true"],
5110
- .needs-validation.was-validated .check:is(:checked, :indeterminate):invalid,
5111
- .radio:checked[aria-invalid="true"],
5112
- .needs-validation.was-validated .radio:checked:invalid {
5113
- --choice-bg: var(--danger);
5114
- }
5115
-
5116
- .switch:checked[aria-invalid="true"],
5117
- .needs-validation.was-validated .switch:checked:invalid {
5118
- --switch-bg: var(--danger);
5119
- }
5120
-
5121
- @supports selector(:user-invalid) {
5122
- .needs-validation .check:is(:checked, :indeterminate):user-invalid,
5123
- .needs-validation .radio:checked:user-invalid {
5124
- --choice-bg: var(--danger);
5125
- }
5126
-
5127
- .needs-validation .switch:checked:user-invalid {
5128
- --switch-bg: var(--danger);
5129
- }
5130
- }
5131
-
5132
- /* Choice cards hide their native input, so invalid state must surface on the
5133
- card itself. Sets the same --form-invalid-border hook the card's border
5134
- consumes. */
5135
- @supports selector(:has(*)) {
5136
- .choice-card:has([aria-invalid="true"]),
5137
- .needs-validation.was-validated .choice-card:has(:invalid) {
5138
- --form-invalid-border: var(--danger);
5139
- }
5140
- }
5141
-
5142
- @supports selector(:user-invalid) {
5143
- .needs-validation :is(.input, .textarea, .select, .check, .radio, .switch):user-invalid {
5144
- --form-invalid-border: var(--danger);
5145
- }
5146
- }
5147
-
5148
- @supports selector(:has(:user-invalid)) {
5149
- .needs-validation .field:has(:user-invalid) > .field-error {
5150
- display: block;
5151
- }
5152
-
5153
- .needs-validation .choice-card:has(:user-invalid) {
5154
- --form-invalid-border: var(--danger);
5155
- }
5156
- }
5157
-
5158
- /*
5159
- * Form actions — bottom form bar with sticky variant.
5160
- *
5161
- * A flex cluster of buttons/actions that closes a form. The sticky variant
5162
- * docks to the viewport bottom with a translucent blur where supported; at
5163
- * rest it stays visually neutral so it does not double as a separator.
5164
- *
5165
- * Public hooks:
5166
- * --form-actions-align
5167
- * --form-actions-justify
5168
- * --form-actions-margin-block-start
5169
- *
5170
- * Internal:
5171
- * --form-actions-sticky-block-offset (owned by .form-actions.sticky)
5172
- * --form-actions-sticky-inline-offset (owned by .form-actions.sticky)
5173
- * --form-actions-sticky-padding (owned by .form-actions.sticky)
5174
- */
5175
- .form-actions {
5176
- display: flex;
5177
- flex-wrap: wrap;
5178
- gap: var(--gap);
5179
- align-items: var(--form-actions-align, center);
5180
- justify-content: var(--form-actions-justify, flex-start);
5181
- margin-block-start: var(--form-actions-margin-block-start, var(--space-50));
5182
- }
5183
-
5184
- /* Opt-in sticky variant. When the bar is not stuck (at rest) the translucent
5185
- surface and backdrop-blur are visually neutral. Content scrolling behind
5186
- the bar reveals the blur as a natural separator instead of relying on a
5187
- shadow. */
5188
- .form-actions.sticky {
5189
- position: sticky;
5190
- inset-block-end: var(--form-actions-sticky-block-offset, 0px);
5191
- z-index: var(--z-sticky);
5192
- isolation: isolate;
5193
-
5194
- margin-inline: calc(var(--form-actions-sticky-inline-offset, 0px) * -1);
5195
- padding: var(--form-actions-sticky-padding, var(--space-30));
5196
-
5197
- border-block-start: var(--border-width) solid var(--border);
5198
- background: var(--surface);
5199
- -webkit-backdrop-filter: blur(1rem);
5200
- backdrop-filter: blur(1rem);
5201
- }
5202
-
5203
- /* The translucent surface needs @supports, not a double declaration: the
5204
- value contains var(), so a browser without color-mix() cannot reject it at
5205
- parse time — it wins the cascade, then fails at computed-value time and
5206
- the background disappears instead of falling back to the opaque line. */
5207
- @supports (background: color-mix(in oklch, red 50%, transparent)) {
5208
- .form-actions.sticky {
5209
- background: color-mix(in oklch, var(--surface) 88%, transparent);
5210
- }
5211
- }
5212
-
5213
- /*
5214
- * Small, single-purpose utilities for common escape hatches.
5215
- *
5216
- * These support components and layout primitives without becoming a
5217
- * utility-first API.
5218
- */
5219
- .sr-only {
5220
- position: absolute;
5221
- inline-size: 1px;
5222
- block-size: 1px;
5223
- padding: 0;
5224
- margin: -1px;
5225
- overflow: hidden;
5226
- clip: rect(0, 0, 0, 0);
5227
- white-space: nowrap;
5228
- border: 0;
5229
- }
5230
-
5231
- .overflow-auto {
5232
- overflow: auto;
5233
- }
5234
-
5235
- .fit {
5236
- inline-size: fit-content;
5237
- max-inline-size: 100%;
5238
- }
5239
-
5240
- .grow {
5241
- flex: 1 1 auto;
5242
- min-inline-size: 0;
5243
- }
5244
-
5245
- .text-nowrap {
5246
- white-space: nowrap;
5247
- }
5248
-
5249
- .truncate {
5250
- min-inline-size: 0;
5251
- overflow: hidden;
5252
- text-overflow: ellipsis;
5253
- white-space: nowrap;
5254
- }
5255
-
5256
- .items-start {
5257
- --items-align: start;
5258
- align-items: start;
5259
- }
5260
-
5261
- .items-center {
5262
- --items-align: center;
5263
- align-items: center;
5264
- }
5265
-
5266
- .items-end {
5267
- --items-align: end;
5268
- align-items: end;
5269
- }
5270
-
5271
- .muted {
5272
- color: var(--text-muted);
5273
- }
5274
-
5275
- /* Circle: forces a fully round shape without mutating global radius tokens.
5276
- Works on any component because utilities load after components. .join is
5277
- the one exception: it writes corner longhands on its children, so
5278
- .join.circle keeps its own --join-radius hook (join.css). */
5279
- .circle {
5280
- border-radius: var(--radius-full);
5281
- }
5282
-
5283
- /* Text alignment — uses logical properties so it follows writing direction. */
5284
- .text-start {
5285
- --text-align: start;
5286
- text-align: start;
5287
- }
5288
-
5289
- .text-center {
5290
- --text-align: center;
5291
- text-align: center;
5292
- }
5293
-
5294
- .text-end {
5295
- --text-align: end;
5296
- text-align: end;
5297
- }
5298
-
5299
- /* Text wrapping utilities. Unsupported values are ignored by the browser. */
5300
- .text-balance {
5301
- text-wrap: balance;
5302
- }
5303
-
5304
- .text-pretty {
5305
- text-wrap: pretty;
5306
- }
5307
-
5308
- /* Generic list chrome reset. Use for any list (tags, footer columns, related
5309
- items). For navigation lists, prefer .nav-list for semantic intent.
5310
- components/{actions,navbar}.css apply their own resets where needed. */
5311
- .list-reset {
5312
- margin: 0;
5313
- padding: 0;
5314
- list-style: none;
5315
- }
5316
-
5317
- /* Separator dot for inline metadata rows (byline, timestamp, read time,
5318
- breadcrumb-like facts). Pair with a flex/cluster container; flex: none
5319
- keeps a flex parent from shrinking it to nothing. */
5320
- .dot {
5321
- flex: none;
5322
- inline-size: 0.25rem;
5323
- block-size: 0.25rem;
5324
- border-radius: var(--radius-full);
5325
- background: var(--text-subtle);
5326
- }
5327
-
5328
- /* Link variants for chrome that should not look like prose links. Use in
5329
- navigation, tags, footer, tables of contents, and similar surfaces. */
5330
- .link-plain {
5331
- color: inherit;
5332
- text-decoration: none;
5333
- }
5334
-
5335
- .link-muted {
5336
- color: var(--text-muted);
5337
- text-decoration: none;
5338
- }
5339
-
5340
- .link-muted:hover {
5341
- color: var(--text);
5342
- }
5343
-
5344
- /* Spacing step helpers — semantic scale (sm / default / lg).
5345
- Names map to logical properties for writing-direction safety. The default
5346
- step uses --space-40, which matches the framework's built-in padding on
5347
- cards, dialogs, and form-actions. */
5348
-
5349
- /* Gap */
5350
- .gap-none {
5351
- gap: 0;
5352
- }
5353
-
5354
- /* Padding — block (vertical) */
5355
- .py {
5356
- padding-block: var(--density-space);
5357
- }
5358
-
5359
- /* Padding — inline (horizontal) */
5360
- .px {
5361
- padding-inline: var(--density-space);
5362
- }
5363
-
5364
- /* Margin — block-start */
5365
- .mbs {
5366
- margin-block-start: var(--density-space);
5367
- }
5368
-
5369
- /* Margin — block-end */
5370
- .mbe {
5371
- margin-block-end: var(--density-space);
5372
- }
5373
-
5374
- /* Joined controls are a component, but must load after the controls it groups. */
5375
- /*
5376
- * Join — visually groups adjacent controls into a single unit.
5377
- *
5378
- * This is a cross-cutting control primitive, not a forms-only component:
5379
- * it applies equally to inputs, selects, buttons, and button-like links.
5380
- *
5381
- * Cascade note: .join writes border-radius longhands on its direct children.
5382
- * Keep this file loaded after control/component styles so those corners win at
5383
- * equal specificity.
5384
- *
5385
- * Public hooks:
5386
- * --join-radius
5387
- *
5388
- * Internal:
5389
- * --join-ss
5390
- * --join-se
5391
- * --join-es
5392
- * --join-ee
5393
- * --control-border
5394
- * --control-bg
5395
- * --control-fg
5396
- */
5397
- .join {
5398
- --join-radius: var(--radius);
5399
- --join-ss: 0;
5400
- --join-se: 0;
5401
- --join-es: 0;
5402
- --join-ee: 0;
5403
-
5404
- align-items: stretch;
5405
- display: inline-flex;
5406
- }
5407
-
5408
- /* The .circle utility cannot round a join: corners are written as longhands
5409
- on the children below, so the group radius must flow through the token. */
5410
- .join.circle {
5411
- --join-radius: var(--radius-full);
5412
- }
5413
-
5414
- .join-addon {
5415
- --control-border: var(--border);
5416
- --control-bg: var(--surface-subtle);
5417
- --control-fg: var(--text-muted);
5418
-
5419
- align-items: center;
5420
- display: inline-flex;
5421
- justify-content: center;
5422
- min-block-size: var(--control-size);
5423
- padding-inline: var(--space-30);
5424
- border: var(--border-width) solid var(--control-border);
5425
- border-radius: var(--radius);
5426
- background: var(--control-bg);
5427
- color: var(--control-fg);
5428
- font-size: var(--control-font-size);
5429
- line-height: 1;
5430
- white-space: nowrap;
5431
- }
5432
-
5433
- .join > * {
5434
- border-start-start-radius: var(--join-ss);
5435
- border-start-end-radius: var(--join-se);
5436
- border-end-end-radius: var(--join-ee);
5437
- border-end-start-radius: var(--join-es);
5438
- }
5439
-
5440
- .join > :first-child {
5441
- --join-ss: var(--join-radius);
5442
- --join-es: var(--join-radius);
5443
- }
5444
-
5445
- .join > :last-child {
5446
- --join-se: var(--join-radius);
5447
- --join-ee: var(--join-radius);
5448
- }
5449
-
5450
- .join > :only-child {
5451
- --join-ss: var(--join-radius);
5452
- --join-se: var(--join-radius);
5453
- --join-es: var(--join-radius);
5454
- --join-ee: var(--join-radius);
5455
- }
5456
-
5457
- .join > * + * {
5458
- margin-inline-start: calc(var(--border-width) * -1);
5459
- }
5460
-
5461
- .join > :is(:hover, :focus, :focus-within) {
5462
- z-index: 1;
5463
- }
5464
-
5465
- /* Keep an active focus ring above an adjacent hovered control. This matters
5466
- on platforms where clicking a button does not move focus from the input. */
5467
- .join > :is(:focus, :focus-within) {
5468
- z-index: 2;
5469
- }
5470
-
5471
- /* An input's border must read as the "primary" edge against an attached
5472
- action (e.g. a password-reveal button) at all times, not only while
5473
- hovered or focused — otherwise the shared edge flips ownership as the
5474
- user's pointer moves, which reads as a rendering glitch. */
5475
- .join > :is(.input, .textarea, .select) {
5476
- z-index: 3;
5477
- }
5478
-
5479
- /* An input/select with an attached action reads as one composite field. Move
5480
- its outer focus treatment to the join; button-only groups keep per-button
5481
- focus, and unsupported :has() browsers retain the control's normal ring. */
5482
- .join:has(> :is(.input, .textarea, .select):focus-visible) {
5483
- border-radius: var(--join-radius);
5484
- outline: 2px solid transparent;
5485
- box-shadow: var(--focus-ring-shadow);
5486
- }
5487
-
5488
- .join:has(> :is(.input, .textarea, .select)) > :is(.input, .textarea, .select):focus-visible {
5489
- outline: none;
5490
- box-shadow: none;
5491
- }
5492
-
5493
- /* Focus baseline — a visible outline is the guaranteed focus affordance. The
5494
- :focus outline covers legacy browsers; modern browsers hide it for
5495
- pointer/mouse focus and interactive components layer their ring on top.
5496
-
5497
- Invariant: an interactive component must never rely on box-shadow alone
5498
- for its keyboard focus indicator. Components that remove the native
5499
- outline on :focus-visible replace it with a transparent outline — invisible
5500
- normally, forced to the system focus color under forced-colors. */
5501
- :focus {
5502
- outline: var(--focus-outline);
5503
- outline-offset: var(--focus-outline-offset);
5504
- }
5505
-
5506
- @supports selector(:focus-visible) {
5507
- :focus:not(:focus-visible) {
5508
- outline: none;
5509
- }
5510
-
5511
- /* Pointer focus must still signal in high-contrast mode, where the shared
5512
- box-shadow rings are disabled (--focus-ring-shadow: none). */
5513
- @media (forced-colors: active) {
5514
- :focus:not(:focus-visible) {
5515
- outline: var(--focus-outline);
5516
- }
5517
- }
5518
- }
5519
-
5520
- @media print {
5521
- :root,
5522
- body {
5523
- /* Save some ink in dark mode */
5524
- color-scheme: light;
5525
- background: #fff;
5526
- color: #000;
5527
- }
5528
-
5529
- .flyout,
5530
- .tooltip,
5531
- .surface-backdrop,
5532
- .status-bar,
5533
- .spinner,
5534
- dialog.modal[open],
5535
- dialog.drawer[open] {
5536
- display: none !important;
5537
- }
5538
-
5539
- button.btn,
5540
- input[type="button"].btn,
5541
- input[type="submit"].btn,
5542
- input[type="reset"].btn {
5543
- display: none !important;
5544
- }
5545
-
5546
- a.btn[href^="http"]::after {
5547
- content: " (" attr(href) ")";
5548
- font-size: 0.875em;
5549
- }
5550
-
5551
- .card,
5552
- .alert,
5553
- .badge {
5554
- border: var(--border-width) solid CanvasText;
5555
- break-inside: avoid;
5556
- }
5557
-
5558
- h1,
5559
- h2,
5560
- h3,
5561
- h4,
5562
- h5,
5563
- h6 {
5564
- break-after: avoid;
5565
- }
5566
- }
5567
-
5568
-
5569
- /*
5570
- * NOTE: The 'optional/' directory contains layout and utility extensions
5571
- * (such as optional grids, split, visual effects, and extra utility classes
5572
- * like .gap-sm, .flex-nowrap, .shadow-sm, etc.). These are intentionally not
5573
- * bundled here. Two entry points ship them:
5574
- *
5575
- * actual-css/css/optional/index optional layer only
5576
- * actual-css/css/actual.full core + optional
5577
- *
5578
- * Import one of these paths in your project when you need them.
5579
- */