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