morethanui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +310 -0
- package/LICENSE +24 -0
- package/README.md +119 -0
- package/css/base.css +200 -0
- package/css/components.css +1496 -0
- package/css/layout.css +380 -0
- package/css/tokens.css +318 -0
- package/dist/mtui.css +2394 -0
- package/dist/mtui.js +1719 -0
- package/dist/mtui.min.css +1 -0
- package/dist/mtui.min.js +1 -0
- package/fonts/OFL.txt +93 -0
- package/fonts/noto-sans-arabic.woff2 +0 -0
- package/fonts/noto-sans-latin-ext.woff2 +0 -0
- package/fonts/noto-sans-latin.woff2 +0 -0
- package/fonts/noto-sans-mono-latin.woff2 +0 -0
- package/js/accordion.js +64 -0
- package/js/feedback.js +171 -0
- package/js/menu.js +72 -0
- package/js/pull-refresh.js +122 -0
- package/js/telegram.js +175 -0
- package/js/x-colorswatches.js +144 -0
- package/js/x-contextmenu.js +190 -0
- package/js/x-datepicker.js +321 -0
- package/js/x-select.js +139 -0
- package/js/x-tabs.js +48 -0
- package/js/x-theme.js +154 -0
- package/js/x-toast.js +119 -0
- package/llms.txt +409 -0
- package/package.json +48 -0
|
@@ -0,0 +1,1496 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
MTUI (MoreThanUI) — components.css
|
|
3
|
+
Tier: Basic (zero JS) for every component in this file.
|
|
4
|
+
Semantic tokens only. Zero borders, zero drop shadows — separation is
|
|
5
|
+
tonal; the only borders live in the forced-colors block at the end.
|
|
6
|
+
Interaction language (set by .btn, copied by everything):
|
|
7
|
+
hover = background tone shift and/or translateY(-2px) (cards -3px,
|
|
8
|
+
rows nudge 3px inline-start)
|
|
9
|
+
press = scale(.96–.98)
|
|
10
|
+
focus = --ring-focus (global, base.css)
|
|
11
|
+
fade = never (reserved for deletion + popover exit)
|
|
12
|
+
Icons: real Lucide paths only — inline SVG in markup, or the CSS masks
|
|
13
|
+
below for pseudo-element icons (they tint via background-color). Never
|
|
14
|
+
border-drawn glyphs.
|
|
15
|
+
============================================================================ */
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--icon-chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
|
19
|
+
--icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
|
|
20
|
+
--icon-loader: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-6.219-8.56'/%3E%3C/svg%3E");
|
|
21
|
+
|
|
22
|
+
/* Shipped icon set (real Lucide paths). Placed via .icon[data-icon] below,
|
|
23
|
+
so apps/AI never hand-author path data. viewBox/stroke match Lucide. */
|
|
24
|
+
--icon-home: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'/%3E%3Cpath d='M9 22V12h6v10'/%3E%3C/svg%3E");
|
|
25
|
+
--icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");
|
|
26
|
+
--icon-settings: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
|
|
27
|
+
--icon-user: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
|
|
28
|
+
--icon-bell: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9'/%3E%3Cpath d='M10.3 21a1.94 1.94 0 0 0 3.4 0'/%3E%3C/svg%3E");
|
|
29
|
+
--icon-mail: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='20' height='16' x='2' y='4' rx='2'/%3E%3Cpath d='m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7'/%3E%3C/svg%3E");
|
|
30
|
+
--icon-calendar: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v4'/%3E%3Cpath d='M16 2v4'/%3E%3Crect width='18' height='18' x='3' y='4' rx='2'/%3E%3Cpath d='M3 10h18'/%3E%3C/svg%3E");
|
|
31
|
+
--icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='M12 5v14'/%3E%3C/svg%3E");
|
|
32
|
+
--icon-x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E");
|
|
33
|
+
--icon-chevron-right: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E");
|
|
34
|
+
--icon-chevron-left: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 18-6-6 6-6'/%3E%3C/svg%3E");
|
|
35
|
+
--icon-chevron-up: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");
|
|
36
|
+
--icon-more-vertical: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='12' cy='5' r='1'/%3E%3Ccircle cx='12' cy='19' r='1'/%3E%3C/svg%3E");
|
|
37
|
+
--icon-trash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'/%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
|
|
38
|
+
--icon-heart: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z'/%3E%3C/svg%3E");
|
|
39
|
+
--icon-star: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26'/%3E%3C/svg%3E");
|
|
40
|
+
--icon-share: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'/%3E%3Ccircle cx='6' cy='12' r='3'/%3E%3Ccircle cx='18' cy='19' r='3'/%3E%3Cline x1='8.59' x2='15.42' y1='13.51' y2='17.49'/%3E%3Cline x1='15.41' x2='8.59' y1='6.51' y2='10.49'/%3E%3C/svg%3E");
|
|
41
|
+
--icon-pencil: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 20h9'/%3E%3Cpath d='M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z'/%3E%3C/svg%3E");
|
|
42
|
+
--icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
|
|
43
|
+
--icon-alert-triangle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3'/%3E%3Cpath d='M12 9v4'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E");
|
|
44
|
+
--icon-circle-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m9 12 2 2 4-4'/%3E%3C/svg%3E");
|
|
45
|
+
--icon-circle-x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m15 9-6 6'/%3E%3Cpath d='m9 9 6 6'/%3E%3C/svg%3E");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ================================ icons ====================================
|
|
49
|
+
<span class="icon" data-icon="home"></span> — monochrome (currentColor
|
|
50
|
+
mask), scales with font-size. Decorative by default; for a standalone
|
|
51
|
+
meaning add role="img" + aria-label. For an icon outside the set, inline a
|
|
52
|
+
Lucide SVG (stroke-width 2, currentColor). */
|
|
53
|
+
.icon {
|
|
54
|
+
display: inline-block;
|
|
55
|
+
inline-size: 1.25em;
|
|
56
|
+
block-size: 1.25em;
|
|
57
|
+
vertical-align: -0.2em;
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
background: currentColor;
|
|
60
|
+
-webkit-mask: var(--i) center / contain no-repeat;
|
|
61
|
+
mask: var(--i) center / contain no-repeat;
|
|
62
|
+
}
|
|
63
|
+
.icon[data-icon="home"] { --i: var(--icon-home); }
|
|
64
|
+
.icon[data-icon="search"] { --i: var(--icon-search); }
|
|
65
|
+
.icon[data-icon="settings"] { --i: var(--icon-settings); }
|
|
66
|
+
.icon[data-icon="user"] { --i: var(--icon-user); }
|
|
67
|
+
.icon[data-icon="bell"] { --i: var(--icon-bell); }
|
|
68
|
+
.icon[data-icon="mail"] { --i: var(--icon-mail); }
|
|
69
|
+
.icon[data-icon="calendar"] { --i: var(--icon-calendar); }
|
|
70
|
+
.icon[data-icon="plus"] { --i: var(--icon-plus); }
|
|
71
|
+
.icon[data-icon="x"] { --i: var(--icon-x); }
|
|
72
|
+
.icon[data-icon="check"] { --i: var(--icon-check); }
|
|
73
|
+
.icon[data-icon="chevron-up"] { --i: var(--icon-chevron-up); }
|
|
74
|
+
.icon[data-icon="chevron-down"] { --i: var(--icon-chevron-down); }
|
|
75
|
+
.icon[data-icon="chevron-left"] { --i: var(--icon-chevron-left); }
|
|
76
|
+
.icon[data-icon="chevron-right"] { --i: var(--icon-chevron-right); }
|
|
77
|
+
.icon[data-icon="more-vertical"] { --i: var(--icon-more-vertical); }
|
|
78
|
+
.icon[data-icon="trash"] { --i: var(--icon-trash); }
|
|
79
|
+
.icon[data-icon="heart"] { --i: var(--icon-heart); }
|
|
80
|
+
.icon[data-icon="star"] { --i: var(--icon-star); }
|
|
81
|
+
.icon[data-icon="share"] { --i: var(--icon-share); }
|
|
82
|
+
.icon[data-icon="pencil"] { --i: var(--icon-pencil); }
|
|
83
|
+
.icon[data-icon="info"] { --i: var(--icon-info); }
|
|
84
|
+
.icon[data-icon="alert-triangle"] { --i: var(--icon-alert-triangle); }
|
|
85
|
+
.icon[data-icon="circle-check"] { --i: var(--icon-circle-check); }
|
|
86
|
+
.icon[data-icon="circle-x"] { --i: var(--icon-circle-x); }
|
|
87
|
+
/* chevron-right is the directional one; flip in RTL like .chevron */
|
|
88
|
+
[dir="rtl"] .icon[data-icon="chevron-right"],
|
|
89
|
+
[dir="rtl"] .icon[data-icon="chevron-left"] { transform: scaleX(-1); }
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
/* ================================ button ===================================
|
|
93
|
+
<button class="btn" data-variant="primary">Save changes</button>
|
|
94
|
+
data-variant: primary | accent | secondary (default) | ghost | danger
|
|
95
|
+
(accent uses the secondary brand color --accent-2, which
|
|
96
|
+
mirrors --accent until a custom theme sets it apart)
|
|
97
|
+
data-size: icon (square, icon-only — give it an aria-label)
|
|
98
|
+
data-loading: swaps the label for an in-place spinner, keeps size. */
|
|
99
|
+
.btn {
|
|
100
|
+
position: relative;
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
gap: var(--sp-8);
|
|
105
|
+
min-block-size: var(--ctl-h);
|
|
106
|
+
min-inline-size: var(--tap-min);
|
|
107
|
+
padding-inline: var(--sp-20);
|
|
108
|
+
border: 0;
|
|
109
|
+
border-radius: var(--r-ctl);
|
|
110
|
+
background: var(--surface-2);
|
|
111
|
+
color: var(--text);
|
|
112
|
+
font-size: var(--fs-control);
|
|
113
|
+
font-weight: var(--fw-control);
|
|
114
|
+
line-height: var(--lh-control);
|
|
115
|
+
text-decoration: none;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
-webkit-tap-highlight-color: transparent;
|
|
118
|
+
transition:
|
|
119
|
+
background-color var(--t-micro) var(--ease),
|
|
120
|
+
color var(--t-micro) var(--ease),
|
|
121
|
+
transform var(--t-micro) var(--ease);
|
|
122
|
+
}
|
|
123
|
+
/* Invisible hit-slop, not a visual effect: hover moves the button -2px, so
|
|
124
|
+
a cursor resting near the original bottom edge would otherwise end up
|
|
125
|
+
outside the shifted box, firing mouseleave, which un-hovers it, which
|
|
126
|
+
shifts it back under the cursor, re-triggering hover — flicker. This
|
|
127
|
+
pseudo rides along with the same transform (it's part of .btn's own
|
|
128
|
+
box) so the effective hoverable region still reaches ~2px past the
|
|
129
|
+
shift in every direction, comfortably covering that boundary case. */
|
|
130
|
+
.btn::before {
|
|
131
|
+
content: "";
|
|
132
|
+
position: absolute;
|
|
133
|
+
inset: calc(-1 * var(--sp-4));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.btn:hover { background: var(--surface-3); transform: translateY(-2px); }
|
|
137
|
+
.btn:active { transform: scale(.97); }
|
|
138
|
+
|
|
139
|
+
.btn[data-variant="primary"] { background: var(--accent); color: var(--on-accent); }
|
|
140
|
+
.btn[data-variant="primary"]:hover { background: var(--accent-hover); }
|
|
141
|
+
|
|
142
|
+
.btn[data-variant="accent"] { background: var(--accent-2); color: var(--on-accent-2); }
|
|
143
|
+
.btn[data-variant="accent"]:hover { background: var(--accent-2-hover); }
|
|
144
|
+
|
|
145
|
+
.btn[data-variant="ghost"] { background: transparent; }
|
|
146
|
+
.btn[data-variant="ghost"]:hover { background: var(--surface-2); }
|
|
147
|
+
|
|
148
|
+
/* No --danger-hover token exists — danger hover is movement only. */
|
|
149
|
+
.btn[data-variant="danger"] { background: var(--danger); color: var(--on-accent); }
|
|
150
|
+
|
|
151
|
+
.btn[data-size="icon"] { inline-size: var(--ctl-h); padding-inline: 0; }
|
|
152
|
+
|
|
153
|
+
.btn:disabled {
|
|
154
|
+
color: var(--text-faint); /* sanctioned: §2.1 lists disabled text here */
|
|
155
|
+
cursor: default;
|
|
156
|
+
}
|
|
157
|
+
.btn:disabled:hover { background: var(--surface-2); transform: none; }
|
|
158
|
+
.btn[data-variant="primary"]:disabled,
|
|
159
|
+
.btn[data-variant="accent"]:disabled,
|
|
160
|
+
.btn[data-variant="danger"]:disabled { background: var(--surface-2); }
|
|
161
|
+
|
|
162
|
+
/* Loading: label goes invisible (size preserved), the Lucide loader icon
|
|
163
|
+
centers on top and rotates. Rotation reuses --t-skeleton, the
|
|
164
|
+
loading-loop duration token. */
|
|
165
|
+
.btn[data-loading] { pointer-events: none; color: transparent; }
|
|
166
|
+
.btn[data-loading]::after {
|
|
167
|
+
content: "";
|
|
168
|
+
position: absolute;
|
|
169
|
+
inset: 0;
|
|
170
|
+
margin: auto;
|
|
171
|
+
inline-size: var(--sp-20);
|
|
172
|
+
block-size: var(--sp-20);
|
|
173
|
+
background: var(--text);
|
|
174
|
+
-webkit-mask: var(--icon-loader) center / contain no-repeat;
|
|
175
|
+
mask: var(--icon-loader) center / contain no-repeat;
|
|
176
|
+
animation: mtui-spin var(--t-skeleton) linear infinite;
|
|
177
|
+
}
|
|
178
|
+
.btn[data-variant="primary"][data-loading]::after,
|
|
179
|
+
.btn[data-variant="danger"][data-loading]::after {
|
|
180
|
+
background: var(--on-accent);
|
|
181
|
+
}
|
|
182
|
+
.btn[data-variant="accent"][data-loading]::after { background: var(--on-accent-2); }
|
|
183
|
+
|
|
184
|
+
@keyframes mtui-spin { to { transform: rotate(360deg); } }
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
/* ================================= card ====================================
|
|
188
|
+
<div class="card">…</div>
|
|
189
|
+
Interactive: <a class="card" href>…</a> / <button class="card">. */
|
|
190
|
+
.card {
|
|
191
|
+
background: var(--surface);
|
|
192
|
+
border-radius: var(--r-card);
|
|
193
|
+
padding: var(--pad);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
a.card,
|
|
197
|
+
button.card {
|
|
198
|
+
position: relative;
|
|
199
|
+
display: block;
|
|
200
|
+
inline-size: 100%;
|
|
201
|
+
border: 0;
|
|
202
|
+
color: inherit;
|
|
203
|
+
font: inherit;
|
|
204
|
+
text-align: start;
|
|
205
|
+
text-decoration: none;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
-webkit-tap-highlight-color: transparent;
|
|
208
|
+
transition: transform var(--t-lift) var(--ease);
|
|
209
|
+
}
|
|
210
|
+
/* Same hit-slop reasoning as .btn::before — the -3px lift needs a bigger
|
|
211
|
+
buffer than the button's -2px. */
|
|
212
|
+
a.card::before,
|
|
213
|
+
button.card::before {
|
|
214
|
+
content: "";
|
|
215
|
+
position: absolute;
|
|
216
|
+
inset: calc(-1 * var(--sp-8));
|
|
217
|
+
}
|
|
218
|
+
a.card:hover,
|
|
219
|
+
button.card:hover { transform: translateY(-3px); }
|
|
220
|
+
a.card:active,
|
|
221
|
+
button.card:active { transform: scale(.98); }
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
/* ================================= field ===================================
|
|
225
|
+
<label class="field">
|
|
226
|
+
<span class="t-label">Email</span>
|
|
227
|
+
<input type="email" placeholder="you@example.com">
|
|
228
|
+
<span class="field-hint">We never share it.</span>
|
|
229
|
+
<span class="field-error">Enter a valid email.</span>
|
|
230
|
+
</label>
|
|
231
|
+
Selects wrap in a chevron holder: <span class="select"><select>…</select></span>
|
|
232
|
+
Error state: data-invalid on the .field (hint hides, error shows). */
|
|
233
|
+
.field {
|
|
234
|
+
display: flex;
|
|
235
|
+
flex-direction: column;
|
|
236
|
+
gap: var(--sp-8);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.field input,
|
|
240
|
+
.field select,
|
|
241
|
+
.field textarea {
|
|
242
|
+
min-block-size: var(--field-h);
|
|
243
|
+
padding-inline: var(--sp-16);
|
|
244
|
+
border: 0;
|
|
245
|
+
border-radius: var(--r-input);
|
|
246
|
+
background: var(--surface-2);
|
|
247
|
+
color: var(--text);
|
|
248
|
+
font-size: var(--fs-body); /* 16px floor stops iOS zoom-on-focus */
|
|
249
|
+
line-height: var(--lh-body);
|
|
250
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
251
|
+
}
|
|
252
|
+
.field input:hover,
|
|
253
|
+
.field select:hover,
|
|
254
|
+
.field textarea:hover { background: var(--surface-3); }
|
|
255
|
+
|
|
256
|
+
.field textarea {
|
|
257
|
+
min-block-size: calc(2 * var(--field-h));
|
|
258
|
+
padding-block: var(--sp-12);
|
|
259
|
+
resize: vertical;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.field ::placeholder { color: var(--text-faint); opacity: 1; }
|
|
263
|
+
|
|
264
|
+
.select { position: relative; display: block; }
|
|
265
|
+
.select select {
|
|
266
|
+
appearance: none;
|
|
267
|
+
inline-size: 100%;
|
|
268
|
+
padding-inline-end: var(--sp-40);
|
|
269
|
+
}
|
|
270
|
+
.select::after {
|
|
271
|
+
content: "";
|
|
272
|
+
position: absolute;
|
|
273
|
+
inset-block: 0;
|
|
274
|
+
inset-inline-end: var(--sp-16);
|
|
275
|
+
margin-block: auto;
|
|
276
|
+
inline-size: var(--sp-16);
|
|
277
|
+
block-size: var(--sp-16);
|
|
278
|
+
background: var(--text-muted);
|
|
279
|
+
-webkit-mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
280
|
+
mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.field-hint,
|
|
285
|
+
.field-error {
|
|
286
|
+
font-size: var(--fs-secondary);
|
|
287
|
+
line-height: var(--lh-secondary);
|
|
288
|
+
}
|
|
289
|
+
.field-hint { color: var(--text-muted); }
|
|
290
|
+
.field-error { color: var(--danger-text); display: none; }
|
|
291
|
+
|
|
292
|
+
.field[data-invalid] input,
|
|
293
|
+
.field[data-invalid] select,
|
|
294
|
+
.field[data-invalid] textarea { background: var(--danger-tint); }
|
|
295
|
+
.field[data-invalid] .field-error { display: block; }
|
|
296
|
+
.field[data-invalid] .field-hint { display: none; }
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/* ================================ toggles ==================================
|
|
300
|
+
<input type="checkbox" class="switch"> (pair inside a row label)
|
|
301
|
+
<input type="checkbox" class="checkbox">
|
|
302
|
+
<input type="radio" class="radio" name="…">
|
|
303
|
+
Canonical row: <label class="row" data-align="between">
|
|
304
|
+
<span class="t-row">Dark mode</span>
|
|
305
|
+
<input type="checkbox" class="switch"></label>
|
|
306
|
+
The label is the ≥44px tap target; knob motion is --t-knob --spring. */
|
|
307
|
+
.switch,
|
|
308
|
+
.checkbox,
|
|
309
|
+
.radio {
|
|
310
|
+
appearance: none;
|
|
311
|
+
flex-shrink: 0;
|
|
312
|
+
margin: 0;
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
-webkit-tap-highlight-color: transparent;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.switch {
|
|
318
|
+
inline-size: var(--sp-48);
|
|
319
|
+
block-size: var(--sp-24);
|
|
320
|
+
border-radius: var(--r-ctl);
|
|
321
|
+
background: var(--surface-3);
|
|
322
|
+
position: relative;
|
|
323
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
324
|
+
}
|
|
325
|
+
.switch::before {
|
|
326
|
+
content: "";
|
|
327
|
+
position: absolute;
|
|
328
|
+
inset-block-start: 2px; /* 1px-scale internal detail (sanctioned) */
|
|
329
|
+
inset-inline-start: 2px;
|
|
330
|
+
inline-size: var(--sp-20);
|
|
331
|
+
block-size: var(--sp-20);
|
|
332
|
+
border-radius: 50%;
|
|
333
|
+
background: var(--on-accent);
|
|
334
|
+
transition: translate var(--t-knob) var(--spring);
|
|
335
|
+
}
|
|
336
|
+
.switch:checked { background: var(--accent); }
|
|
337
|
+
.switch:checked::before { translate: var(--sp-24) 0; }
|
|
338
|
+
[dir="rtl"] .switch:checked::before { translate: calc(-1 * var(--sp-24)) 0; }
|
|
339
|
+
|
|
340
|
+
.checkbox,
|
|
341
|
+
.radio {
|
|
342
|
+
inline-size: var(--sp-24);
|
|
343
|
+
block-size: var(--sp-24);
|
|
344
|
+
background: var(--surface-2);
|
|
345
|
+
display: inline-flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
justify-content: center;
|
|
348
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
349
|
+
}
|
|
350
|
+
.checkbox:hover, .radio:hover { background: var(--surface-3); }
|
|
351
|
+
.checkbox:checked, .radio:checked { background: var(--accent); }
|
|
352
|
+
|
|
353
|
+
/* Radius clamps so the round preset doesn't turn checkboxes into radios. */
|
|
354
|
+
.checkbox { border-radius: min(var(--r-chip), 33%); }
|
|
355
|
+
.radio { border-radius: 50%; }
|
|
356
|
+
|
|
357
|
+
.checkbox::before {
|
|
358
|
+
content: "";
|
|
359
|
+
inline-size: var(--sp-16);
|
|
360
|
+
block-size: var(--sp-16);
|
|
361
|
+
background: var(--on-accent);
|
|
362
|
+
-webkit-mask: var(--icon-check) center / contain no-repeat;
|
|
363
|
+
mask: var(--icon-check) center / contain no-repeat;
|
|
364
|
+
transform: scale(0);
|
|
365
|
+
transition: transform var(--t-knob) var(--spring);
|
|
366
|
+
}
|
|
367
|
+
.checkbox:checked::before { transform: scale(1); }
|
|
368
|
+
|
|
369
|
+
.radio::before {
|
|
370
|
+
content: "";
|
|
371
|
+
inline-size: var(--sp-8);
|
|
372
|
+
block-size: var(--sp-8);
|
|
373
|
+
border-radius: 50%;
|
|
374
|
+
background: var(--on-accent);
|
|
375
|
+
transform: scale(0);
|
|
376
|
+
transition: transform var(--t-knob) var(--spring);
|
|
377
|
+
}
|
|
378
|
+
.radio:checked::before { transform: scale(1); }
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
/* ============================ badge and chip ===============================
|
|
382
|
+
<span class="badge" data-tint="mint">New</span>
|
|
383
|
+
<span class="badge" data-status="danger">3 errors</span>
|
|
384
|
+
<button class="chip" aria-pressed="true">Filter</button>
|
|
385
|
+
<span class="chip">Tag <button class="chip-x" aria-label="Remove"><span class="icon" data-icon="x"></span></button></span> */
|
|
386
|
+
.badge {
|
|
387
|
+
display: inline-flex;
|
|
388
|
+
align-items: center;
|
|
389
|
+
gap: var(--sp-4);
|
|
390
|
+
min-block-size: var(--sp-20);
|
|
391
|
+
padding-inline: var(--sp-8);
|
|
392
|
+
border-radius: var(--r-chip);
|
|
393
|
+
background: var(--surface-2);
|
|
394
|
+
color: var(--text-muted);
|
|
395
|
+
font-size: var(--fs-label);
|
|
396
|
+
font-weight: var(--fw-label);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.chip {
|
|
400
|
+
display: inline-flex;
|
|
401
|
+
align-items: center;
|
|
402
|
+
gap: var(--sp-4);
|
|
403
|
+
min-block-size: var(--sp-32);
|
|
404
|
+
padding-inline: var(--sp-12);
|
|
405
|
+
border: 0;
|
|
406
|
+
border-radius: var(--r-chip);
|
|
407
|
+
background: var(--surface-2);
|
|
408
|
+
color: var(--text);
|
|
409
|
+
font-size: var(--fs-label);
|
|
410
|
+
font-weight: var(--fw-label);
|
|
411
|
+
-webkit-tap-highlight-color: transparent;
|
|
412
|
+
}
|
|
413
|
+
button.chip {
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
transition:
|
|
416
|
+
background-color var(--t-micro) var(--ease),
|
|
417
|
+
color var(--t-micro) var(--ease),
|
|
418
|
+
transform var(--t-micro) var(--ease);
|
|
419
|
+
}
|
|
420
|
+
button.chip:hover { background: var(--surface-3); }
|
|
421
|
+
button.chip:active { transform: scale(.96); }
|
|
422
|
+
.chip[aria-pressed="true"],
|
|
423
|
+
.chip[data-selected] { background: var(--accent-tint); color: var(--accent-text); }
|
|
424
|
+
|
|
425
|
+
.chip-x {
|
|
426
|
+
display: inline-flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
justify-content: center;
|
|
429
|
+
border: 0;
|
|
430
|
+
padding: 0;
|
|
431
|
+
background: none;
|
|
432
|
+
color: inherit;
|
|
433
|
+
font: inherit;
|
|
434
|
+
cursor: pointer;
|
|
435
|
+
position: relative;
|
|
436
|
+
}
|
|
437
|
+
.chip-x::after { content: ""; position: absolute; inset: calc(-1 * var(--sp-12)); } /* tap area */
|
|
438
|
+
|
|
439
|
+
/* Shared tint/status color pairs (badge, chip, avatar, alert, empty). */
|
|
440
|
+
[data-tint="rose"] { background: var(--tint-rose-bg); color: var(--tint-rose-text); }
|
|
441
|
+
[data-tint="sky"] { background: var(--tint-sky-bg); color: var(--tint-sky-text); }
|
|
442
|
+
[data-tint="mint"] { background: var(--tint-mint-bg); color: var(--tint-mint-text); }
|
|
443
|
+
[data-tint="butter"] { background: var(--tint-butter-bg); color: var(--tint-butter-text); }
|
|
444
|
+
[data-tint="lavender"] { background: var(--tint-lavender-bg); color: var(--tint-lavender-text); }
|
|
445
|
+
[data-status="success"] { background: var(--success-tint); color: var(--success-text); }
|
|
446
|
+
[data-status="warning"] { background: var(--warning-tint); color: var(--warning-text); }
|
|
447
|
+
[data-status="danger"] { background: var(--danger-tint); color: var(--danger-text); }
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
/* ================================ avatar ===================================
|
|
451
|
+
<span class="avatar" data-tint="sky">AK</span>
|
|
452
|
+
<span class="avatar"><img src="…" alt=""></span>
|
|
453
|
+
<span class="avatar-stack"> …avatars… <span class="avatar">+3</span></span>
|
|
454
|
+
data-size: sm | md (default) | lg */
|
|
455
|
+
.avatar {
|
|
456
|
+
display: inline-flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
justify-content: center;
|
|
459
|
+
inline-size: var(--sp-40);
|
|
460
|
+
block-size: var(--sp-40);
|
|
461
|
+
border-radius: 50%;
|
|
462
|
+
overflow: hidden;
|
|
463
|
+
flex-shrink: 0;
|
|
464
|
+
background: var(--surface-2);
|
|
465
|
+
color: var(--text-muted);
|
|
466
|
+
font-size: var(--fs-label);
|
|
467
|
+
font-weight: var(--fw-label);
|
|
468
|
+
}
|
|
469
|
+
.avatar[data-size="sm"] { inline-size: var(--sp-32); block-size: var(--sp-32); }
|
|
470
|
+
.avatar[data-size="lg"] { inline-size: var(--sp-48); block-size: var(--sp-48); }
|
|
471
|
+
.avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }
|
|
472
|
+
|
|
473
|
+
.avatar-stack { display: inline-flex; }
|
|
474
|
+
.avatar-stack .avatar { box-shadow: var(--ring-avatar); }
|
|
475
|
+
.avatar-stack .avatar + .avatar { margin-inline-start: calc(-1 * var(--sp-8)); }
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
/* =============================== list item =================================
|
|
479
|
+
(`.row` belongs to the layout primitive, so the list row is `.item`.)
|
|
480
|
+
<a class="item" href>
|
|
481
|
+
<span class="avatar" data-tint="mint">M</span>
|
|
482
|
+
<span class="item-text">
|
|
483
|
+
<span class="item-title">Maya Chen</span>
|
|
484
|
+
<span class="item-sub">Sounds good — tomorrow works.</span>
|
|
485
|
+
</span>
|
|
486
|
+
<svg class="chevron" …></svg>
|
|
487
|
+
</a>
|
|
488
|
+
Lists separate by tone/gap only — never dividers. */
|
|
489
|
+
.item {
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
gap: var(--gap);
|
|
493
|
+
min-block-size: var(--row-h);
|
|
494
|
+
padding-block: var(--sp-8);
|
|
495
|
+
padding-inline: var(--pad);
|
|
496
|
+
color: inherit;
|
|
497
|
+
text-decoration: none;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
a.item,
|
|
501
|
+
button.item {
|
|
502
|
+
border: 0;
|
|
503
|
+
border-radius: var(--r-input);
|
|
504
|
+
background: none;
|
|
505
|
+
font: inherit;
|
|
506
|
+
text-align: start;
|
|
507
|
+
inline-size: 100%;
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
-webkit-tap-highlight-color: transparent;
|
|
510
|
+
transition:
|
|
511
|
+
background-color var(--t-micro) var(--ease),
|
|
512
|
+
transform var(--t-micro) var(--ease);
|
|
513
|
+
}
|
|
514
|
+
a.item:hover,
|
|
515
|
+
button.item:hover {
|
|
516
|
+
/* Soft wash: half-strength well tone, works on surface and bg alike.
|
|
517
|
+
Engines without color-mix fall back to the solid well. */
|
|
518
|
+
background: var(--surface-2);
|
|
519
|
+
background: color-mix(in srgb, var(--surface-2) 50%, transparent);
|
|
520
|
+
}
|
|
521
|
+
a.item:active,
|
|
522
|
+
button.item:active { transform: scale(.98); }
|
|
523
|
+
|
|
524
|
+
.item-text {
|
|
525
|
+
flex: 1;
|
|
526
|
+
min-inline-size: 0;
|
|
527
|
+
display: flex;
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
gap: 2px; /* 1px-scale detail: keeps title/sub optically paired */
|
|
530
|
+
}
|
|
531
|
+
.item-title {
|
|
532
|
+
font-size: var(--fs-control);
|
|
533
|
+
font-weight: var(--fw-control);
|
|
534
|
+
line-height: var(--lh-control);
|
|
535
|
+
}
|
|
536
|
+
.item-sub {
|
|
537
|
+
color: var(--text-muted);
|
|
538
|
+
font-size: var(--fs-secondary);
|
|
539
|
+
line-height: var(--lh-secondary);
|
|
540
|
+
}
|
|
541
|
+
.item-title,
|
|
542
|
+
.item-sub {
|
|
543
|
+
overflow: hidden;
|
|
544
|
+
text-overflow: ellipsis;
|
|
545
|
+
white-space: nowrap;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* Trailing region: because .item-text is flex:1, anything after it sits at
|
|
549
|
+
the trailing edge (a .chevron, a badge, a timestamp). .item-meta stacks a
|
|
550
|
+
couple of those (e.g. time over an unread count). */
|
|
551
|
+
.item-meta {
|
|
552
|
+
flex-shrink: 0;
|
|
553
|
+
display: flex;
|
|
554
|
+
flex-direction: column;
|
|
555
|
+
align-items: flex-end;
|
|
556
|
+
gap: var(--sp-4);
|
|
557
|
+
color: var(--text-muted);
|
|
558
|
+
font-size: var(--fs-label);
|
|
559
|
+
text-align: end;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/* Directional chevrons flip with direction (use on any inline icon). */
|
|
563
|
+
[dir="rtl"] .chevron { transform: scaleX(-1); }
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
/* ============================== segmented ==================================
|
|
567
|
+
<fieldset class="segmented">
|
|
568
|
+
<label><input type="radio" name="view" checked><span>Day</span></label>
|
|
569
|
+
<label><input type="radio" name="view"><span>Week</span></label>
|
|
570
|
+
</fieldset>
|
|
571
|
+
Selected = surface pill on the well (tonal). The sliding pill animation
|
|
572
|
+
is the phase-5 x-tabs enhancement; Basic swaps instantly. */
|
|
573
|
+
.segmented {
|
|
574
|
+
position: relative;
|
|
575
|
+
display: flex;
|
|
576
|
+
border: 0;
|
|
577
|
+
margin: 0;
|
|
578
|
+
padding: var(--sp-4);
|
|
579
|
+
border-radius: var(--r-ctl);
|
|
580
|
+
background: var(--surface-2);
|
|
581
|
+
}
|
|
582
|
+
.segmented label { flex: 1; display: block; position: relative; }
|
|
583
|
+
.segmented input {
|
|
584
|
+
appearance: none;
|
|
585
|
+
position: absolute;
|
|
586
|
+
inset: 0;
|
|
587
|
+
margin: 0;
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
border-radius: var(--r-ctl);
|
|
590
|
+
}
|
|
591
|
+
.segmented span {
|
|
592
|
+
position: relative;
|
|
593
|
+
pointer-events: none; /* the stretched input owns the whole hit area */
|
|
594
|
+
display: flex;
|
|
595
|
+
align-items: center;
|
|
596
|
+
justify-content: center;
|
|
597
|
+
min-block-size: calc(var(--ctl-h) - 2 * var(--sp-4));
|
|
598
|
+
padding-inline: var(--sp-12);
|
|
599
|
+
border-radius: var(--r-ctl);
|
|
600
|
+
color: var(--text-muted);
|
|
601
|
+
font-size: var(--fs-control);
|
|
602
|
+
font-weight: var(--fw-control);
|
|
603
|
+
transition: background-color var(--t-micro) var(--ease), color var(--t-micro) var(--ease);
|
|
604
|
+
}
|
|
605
|
+
.segmented input:checked + span { background: var(--surface); color: var(--text); }
|
|
606
|
+
.segmented input:focus-visible + span { box-shadow: var(--ring-focus); }
|
|
607
|
+
|
|
608
|
+
/* Sliding pill (350ms --spring, per §2.12) — pure CSS via :has(); engines
|
|
609
|
+
without :has() keep the instant per-segment fallback above. Supports up
|
|
610
|
+
to 5 segments. */
|
|
611
|
+
@supports selector(:has(*)) {
|
|
612
|
+
.segmented input:checked + span { background: transparent; }
|
|
613
|
+
.segmented::before {
|
|
614
|
+
content: "";
|
|
615
|
+
position: absolute;
|
|
616
|
+
inset-block: var(--sp-4);
|
|
617
|
+
inset-inline-start: var(--sp-4);
|
|
618
|
+
inline-size: calc((100% - 2 * var(--sp-4)) / var(--seg-count, 1));
|
|
619
|
+
border-radius: var(--r-ctl);
|
|
620
|
+
background: var(--surface);
|
|
621
|
+
translate: calc(var(--seg-index, 0) * 100%) 0;
|
|
622
|
+
transition: translate var(--t-tab) var(--spring);
|
|
623
|
+
}
|
|
624
|
+
[dir="rtl"] .segmented::before { translate: calc(var(--seg-index, 0) * -100%) 0; }
|
|
625
|
+
|
|
626
|
+
.segmented:has(> label:nth-child(2):last-child) { --seg-count: 2; }
|
|
627
|
+
.segmented:has(> label:nth-child(3):last-child) { --seg-count: 3; }
|
|
628
|
+
.segmented:has(> label:nth-child(4):last-child) { --seg-count: 4; }
|
|
629
|
+
.segmented:has(> label:nth-child(5):last-child) { --seg-count: 5; }
|
|
630
|
+
.segmented:has(> label:nth-child(1) input:checked) { --seg-index: 0; }
|
|
631
|
+
.segmented:has(> label:nth-child(2) input:checked) { --seg-index: 1; }
|
|
632
|
+
.segmented:has(> label:nth-child(3) input:checked) { --seg-index: 2; }
|
|
633
|
+
.segmented:has(> label:nth-child(4) input:checked) { --seg-index: 3; }
|
|
634
|
+
.segmented:has(> label:nth-child(5) input:checked) { --seg-index: 4; }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
/* ============================== accordion ==================================
|
|
639
|
+
<details class="accordion">
|
|
640
|
+
<summary>Shipping details</summary>
|
|
641
|
+
<div class="accordion-body">…</div>
|
|
642
|
+
</details>
|
|
643
|
+
Tier: Basic (native, instant open/close) + lightly Enhanced via
|
|
644
|
+
js/accordion.js (smooth height + chevron animation both ways, WAAPI).
|
|
645
|
+
data-expanded is JS-only and decouples chevron timing from the native
|
|
646
|
+
[open] attribute — js/accordion.js intentionally keeps [open] true until
|
|
647
|
+
the CLOSE animation finishes so the body stays rendered while it
|
|
648
|
+
collapses; without that decoupling the chevron would stay rotated for
|
|
649
|
+
the whole close animation and only snap back at the very end. */
|
|
650
|
+
.accordion {
|
|
651
|
+
background: var(--surface);
|
|
652
|
+
border-radius: var(--r-card);
|
|
653
|
+
overflow: hidden;
|
|
654
|
+
}
|
|
655
|
+
.accordion summary {
|
|
656
|
+
display: flex;
|
|
657
|
+
align-items: center;
|
|
658
|
+
justify-content: space-between;
|
|
659
|
+
gap: var(--gap);
|
|
660
|
+
min-block-size: var(--ctl-h);
|
|
661
|
+
padding-block: var(--sp-8);
|
|
662
|
+
padding-inline: var(--pad);
|
|
663
|
+
font-size: var(--fs-control);
|
|
664
|
+
font-weight: var(--fw-control);
|
|
665
|
+
border-radius: var(--r-card);
|
|
666
|
+
cursor: pointer;
|
|
667
|
+
list-style: none;
|
|
668
|
+
-webkit-tap-highlight-color: transparent;
|
|
669
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
670
|
+
}
|
|
671
|
+
.accordion summary::-webkit-details-marker { display: none; }
|
|
672
|
+
.accordion summary:hover { background: var(--surface-2); }
|
|
673
|
+
.accordion summary::after {
|
|
674
|
+
content: "";
|
|
675
|
+
flex-shrink: 0;
|
|
676
|
+
inline-size: var(--sp-16);
|
|
677
|
+
block-size: var(--sp-16);
|
|
678
|
+
background: var(--text-muted);
|
|
679
|
+
-webkit-mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
680
|
+
mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
681
|
+
transition: transform var(--t-knob) var(--spring);
|
|
682
|
+
}
|
|
683
|
+
/* No-JS fallback: native [open] drives the chevron directly. */
|
|
684
|
+
.accordion:not([data-expanded])[open] summary::after { transform: rotate(180deg); }
|
|
685
|
+
/* JS-managed: data-expanded solely drives it once js/accordion.js is
|
|
686
|
+
handling clicks, independent of [open]'s lingering-true close window. */
|
|
687
|
+
.accordion[data-expanded="true"] summary::after { transform: rotate(180deg); }
|
|
688
|
+
.accordion-body { padding-block: var(--sp-4) var(--pad); padding-inline: var(--pad); }
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
/* =============================== code block ================================
|
|
692
|
+
<pre class="code">…</pre> · inline: <code class="code-inline">…</code>
|
|
693
|
+
The only mono in MTUI. Stays LTR inside RTL documents. */
|
|
694
|
+
.code {
|
|
695
|
+
background: var(--surface-2);
|
|
696
|
+
border-radius: var(--r-input);
|
|
697
|
+
padding: var(--pad);
|
|
698
|
+
overflow-x: auto;
|
|
699
|
+
font-family: var(--font-mono);
|
|
700
|
+
font-size: var(--fs-code);
|
|
701
|
+
line-height: var(--lh-code);
|
|
702
|
+
direction: ltr;
|
|
703
|
+
unicode-bidi: isolate;
|
|
704
|
+
text-align: start;
|
|
705
|
+
}
|
|
706
|
+
.code-inline {
|
|
707
|
+
background: var(--surface-2);
|
|
708
|
+
border-radius: min(var(--r-chip), 33%);
|
|
709
|
+
padding-inline: var(--sp-4);
|
|
710
|
+
font-family: var(--font-mono);
|
|
711
|
+
font-size: var(--fs-code);
|
|
712
|
+
direction: ltr;
|
|
713
|
+
unicode-bidi: isolate;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
/* ================================ skeleton =================================
|
|
718
|
+
<span class="skeleton" data-shape="title|text|circle|row"></span>
|
|
719
|
+
Sheen = a soft bar translating across (--t-skeleton loop) — never an
|
|
720
|
+
opacity pulse. Compose shapes to mirror the final layout. */
|
|
721
|
+
.skeleton {
|
|
722
|
+
display: block;
|
|
723
|
+
position: relative;
|
|
724
|
+
overflow: hidden;
|
|
725
|
+
background: var(--surface-2);
|
|
726
|
+
border-radius: var(--r-input);
|
|
727
|
+
min-block-size: var(--sp-16);
|
|
728
|
+
}
|
|
729
|
+
.skeleton[data-shape="title"] { min-block-size: var(--sp-24); max-inline-size: 60%; }
|
|
730
|
+
.skeleton[data-shape="text"] { border-radius: var(--r-chip); }
|
|
731
|
+
.skeleton[data-shape="circle"] {
|
|
732
|
+
inline-size: var(--sp-40);
|
|
733
|
+
block-size: var(--sp-40);
|
|
734
|
+
min-block-size: var(--sp-40);
|
|
735
|
+
border-radius: 50%;
|
|
736
|
+
flex-shrink: 0;
|
|
737
|
+
}
|
|
738
|
+
.skeleton[data-shape="row"] { min-block-size: var(--row-h); }
|
|
739
|
+
|
|
740
|
+
.skeleton::after {
|
|
741
|
+
content: "";
|
|
742
|
+
position: absolute;
|
|
743
|
+
inset-block: 0;
|
|
744
|
+
inset-inline-start: 0;
|
|
745
|
+
inline-size: 65%;
|
|
746
|
+
background: linear-gradient(
|
|
747
|
+
90deg,
|
|
748
|
+
transparent,
|
|
749
|
+
color-mix(in srgb, var(--surface-3) 70%, transparent),
|
|
750
|
+
transparent
|
|
751
|
+
);
|
|
752
|
+
/* linear, not --ease: --ease is an interaction curve (snappy start, soft
|
|
753
|
+
stop) — on an infinite loop that reads as a fast dart each cycle. A
|
|
754
|
+
wide, softly-tinted band gliding at constant speed is what reads as
|
|
755
|
+
calm/ambient instead of mechanical. */
|
|
756
|
+
animation: mtui-sheen var(--t-skeleton) linear infinite;
|
|
757
|
+
}
|
|
758
|
+
[dir="rtl"] .skeleton::after {
|
|
759
|
+
background: linear-gradient(
|
|
760
|
+
-90deg,
|
|
761
|
+
transparent,
|
|
762
|
+
color-mix(in srgb, var(--surface-3) 70%, transparent),
|
|
763
|
+
transparent
|
|
764
|
+
);
|
|
765
|
+
animation-name: mtui-sheen-rtl;
|
|
766
|
+
}
|
|
767
|
+
@keyframes mtui-sheen {
|
|
768
|
+
from { transform: translateX(-100%); }
|
|
769
|
+
to { transform: translateX(215%); }
|
|
770
|
+
}
|
|
771
|
+
@keyframes mtui-sheen-rtl {
|
|
772
|
+
from { transform: translateX(100%); }
|
|
773
|
+
to { transform: translateX(-215%); }
|
|
774
|
+
}
|
|
775
|
+
/* Reduced motion: sheen stops translating but stays visible — a frozen
|
|
776
|
+
skeleton reads as broken (decision recorded in ROADMAP step 1). */
|
|
777
|
+
@media (prefers-reduced-motion: reduce) {
|
|
778
|
+
.skeleton::after {
|
|
779
|
+
animation: none;
|
|
780
|
+
inset-inline: 25%;
|
|
781
|
+
inline-size: 50%;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
/* ============================== empty state ================================
|
|
787
|
+
<div class="empty">
|
|
788
|
+
<span class="empty-icon"><svg …></svg></span>
|
|
789
|
+
<span class="t-card">No messages yet</span>
|
|
790
|
+
<p class="t-secondary">Start a conversation and it shows up here.</p>
|
|
791
|
+
<button class="btn" data-variant="primary">New message</button>
|
|
792
|
+
</div>
|
|
793
|
+
Error flavor: data-status="danger" on .empty-icon + a Retry button. */
|
|
794
|
+
.empty {
|
|
795
|
+
display: flex;
|
|
796
|
+
flex-direction: column;
|
|
797
|
+
align-items: center;
|
|
798
|
+
gap: var(--sp-12);
|
|
799
|
+
text-align: center;
|
|
800
|
+
padding-block: var(--sp-32);
|
|
801
|
+
padding-inline: var(--pad);
|
|
802
|
+
max-inline-size: 24rem;
|
|
803
|
+
margin-inline: auto;
|
|
804
|
+
}
|
|
805
|
+
.empty-icon {
|
|
806
|
+
display: flex;
|
|
807
|
+
align-items: center;
|
|
808
|
+
justify-content: center;
|
|
809
|
+
inline-size: var(--sp-48);
|
|
810
|
+
block-size: var(--sp-48);
|
|
811
|
+
border-radius: 50%;
|
|
812
|
+
background: var(--accent-tint);
|
|
813
|
+
color: var(--accent-text);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
/* ================================= alert ===================================
|
|
818
|
+
<div class="alert" data-status="success">
|
|
819
|
+
<svg …></svg>
|
|
820
|
+
<div><span class="alert-title">Saved</span>
|
|
821
|
+
<p>Your changes are live.</p></div>
|
|
822
|
+
</div>
|
|
823
|
+
One plain sentence. Never a layout region. */
|
|
824
|
+
.alert {
|
|
825
|
+
display: flex;
|
|
826
|
+
gap: var(--sp-12);
|
|
827
|
+
padding: var(--sp-16);
|
|
828
|
+
border-radius: var(--r-card);
|
|
829
|
+
background: var(--surface-2);
|
|
830
|
+
color: var(--text);
|
|
831
|
+
font-size: var(--fs-secondary);
|
|
832
|
+
line-height: var(--lh-secondary);
|
|
833
|
+
}
|
|
834
|
+
.alert > svg { flex-shrink: 0; }
|
|
835
|
+
.alert-title {
|
|
836
|
+
display: block;
|
|
837
|
+
font-size: var(--fs-control);
|
|
838
|
+
font-weight: var(--fw-control);
|
|
839
|
+
line-height: var(--lh-control);
|
|
840
|
+
}
|
|
841
|
+
.alert p { margin: 0; }
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
/* ================================ dialog ===================================
|
|
845
|
+
Native <dialog> on the scrim. Open with .showModal(); a
|
|
846
|
+
<form method="dialog"> closes it without JS. 400ms spring in, scrim
|
|
847
|
+
fades in over its own 300ms (§2.12 — scrim color transition is
|
|
848
|
+
sanctioned; content never fades).
|
|
849
|
+
<dialog class="dialog" id="confirm">
|
|
850
|
+
<form method="dialog" class="stack" data-gap="12">
|
|
851
|
+
<span class="t-card">Delete this note?</span>
|
|
852
|
+
<p class="t-secondary">This can't be undone.</p>
|
|
853
|
+
<div class="row" data-gap="8" data-align="between">
|
|
854
|
+
<button class="btn" value="cancel">Cancel</button>
|
|
855
|
+
<button class="btn" data-variant="danger" value="confirm">Delete</button>
|
|
856
|
+
</div>
|
|
857
|
+
</form>
|
|
858
|
+
</dialog> */
|
|
859
|
+
/* Transition-based (not @keyframes) so CLOSING animates too: closing a
|
|
860
|
+
native <dialog> just flips [open] off and lets the UA drop it from the
|
|
861
|
+
top layer — a one-shot entrance @keyframes animation never runs in
|
|
862
|
+
reverse for that. `display`/`overlay` transitions with allow-discrete
|
|
863
|
+
keep the dialog rendered and interpolating right up until the moment it
|
|
864
|
+
actually leaves the top layer, so close mirrors open instead of
|
|
865
|
+
flashing away. Degrades gracefully (no animation, no flash of missing
|
|
866
|
+
content) on engines without @starting-style/allow-discrete support. */
|
|
867
|
+
.dialog {
|
|
868
|
+
margin: auto; /* restore UA centering killed by the base reset */
|
|
869
|
+
border: 0;
|
|
870
|
+
padding: var(--pad);
|
|
871
|
+
background: var(--surface);
|
|
872
|
+
color: var(--text);
|
|
873
|
+
border-radius: var(--r-card);
|
|
874
|
+
inline-size: min(24rem, calc(100vw - 2 * var(--pad)));
|
|
875
|
+
opacity: 1;
|
|
876
|
+
transform: translateY(0) scale(1);
|
|
877
|
+
transition:
|
|
878
|
+
opacity var(--t-dialog) var(--ease),
|
|
879
|
+
transform var(--t-dialog) var(--spring),
|
|
880
|
+
overlay var(--t-dialog) allow-discrete,
|
|
881
|
+
display var(--t-dialog) allow-discrete;
|
|
882
|
+
}
|
|
883
|
+
.dialog:not([open]) {
|
|
884
|
+
opacity: 0;
|
|
885
|
+
transform: translateY(var(--sp-16)) scale(.96);
|
|
886
|
+
}
|
|
887
|
+
@starting-style {
|
|
888
|
+
.dialog[open] {
|
|
889
|
+
opacity: 0;
|
|
890
|
+
transform: translateY(var(--sp-16)) scale(.96);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.dialog::backdrop {
|
|
895
|
+
background: transparent;
|
|
896
|
+
transition:
|
|
897
|
+
background-color var(--t-popover) var(--ease),
|
|
898
|
+
overlay var(--t-popover) allow-discrete,
|
|
899
|
+
display var(--t-popover) allow-discrete;
|
|
900
|
+
}
|
|
901
|
+
.dialog[open]::backdrop { background: var(--scrim); }
|
|
902
|
+
@starting-style {
|
|
903
|
+
.dialog[open]::backdrop { background: transparent; }
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
/* ================================= menu ====================================
|
|
908
|
+
Popover-attribute menu on the 6px bg ring (never a shadow). Enter =
|
|
909
|
+
300ms spring transform + 220ms opacity (the sanctioned popover fade).
|
|
910
|
+
js/menu.js anchors it to its trigger; without JS it opens centered —
|
|
911
|
+
still fully usable (light-dismiss and Esc are platform behavior).
|
|
912
|
+
<button class="btn" popovertarget="m1">Options</button>
|
|
913
|
+
<div class="menu" id="m1" popover>
|
|
914
|
+
<button class="menu-item">Rename</button>
|
|
915
|
+
<button class="menu-item" data-danger>Delete</button>
|
|
916
|
+
</div> */
|
|
917
|
+
/* One shared foundation for every dropdown/popover this library ships —
|
|
918
|
+
select, datepicker, colorswatches, context menu, the plain menu demo —
|
|
919
|
+
so they read as one system instead of each inventing its own look.
|
|
920
|
+
The 1px --surface-3 border is the sanctioned overlay exception to the
|
|
921
|
+
zero-borders rule (DESIGN.md §2.9, v1.3): floating panels open over an
|
|
922
|
+
unpredictable backdrop, where the --ring-popover bg-ring reads as a stray
|
|
923
|
+
line — a subtle tonal border reads as intentional against any backdrop.
|
|
924
|
+
The exception is overlays ONLY; on-page components stay borderless. */
|
|
925
|
+
.menu {
|
|
926
|
+
border: 1px solid var(--surface-3);
|
|
927
|
+
padding: var(--sp-8);
|
|
928
|
+
background: var(--surface);
|
|
929
|
+
color: var(--text);
|
|
930
|
+
border-radius: var(--r-card);
|
|
931
|
+
}
|
|
932
|
+
.menu:popover-open {
|
|
933
|
+
animation:
|
|
934
|
+
mtui-menu-pop var(--t-popover) var(--spring),
|
|
935
|
+
mtui-menu-fade var(--t-micro) var(--ease);
|
|
936
|
+
}
|
|
937
|
+
@keyframes mtui-menu-pop {
|
|
938
|
+
from { transform: translateY(calc(-1 * var(--sp-4))) scale(.98); }
|
|
939
|
+
}
|
|
940
|
+
@keyframes mtui-menu-fade {
|
|
941
|
+
from { opacity: 0; }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.menu-item {
|
|
945
|
+
display: flex;
|
|
946
|
+
align-items: center;
|
|
947
|
+
gap: var(--sp-12);
|
|
948
|
+
inline-size: 100%;
|
|
949
|
+
min-block-size: var(--tap-min);
|
|
950
|
+
padding-inline: var(--sp-12);
|
|
951
|
+
border: 0;
|
|
952
|
+
border-radius: var(--r-input);
|
|
953
|
+
background: none;
|
|
954
|
+
color: inherit;
|
|
955
|
+
font-size: var(--fs-control);
|
|
956
|
+
font-weight: var(--fw-control);
|
|
957
|
+
text-align: start;
|
|
958
|
+
cursor: pointer;
|
|
959
|
+
-webkit-tap-highlight-color: transparent;
|
|
960
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
961
|
+
}
|
|
962
|
+
.menu-item:hover {
|
|
963
|
+
background: var(--surface-2);
|
|
964
|
+
background: color-mix(in srgb, var(--surface-2) 50%, transparent);
|
|
965
|
+
}
|
|
966
|
+
.menu-item:active { transform: scale(.98); }
|
|
967
|
+
.menu-item[data-danger] { color: var(--danger-text); }
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
/* ================================= tabs ====================================
|
|
971
|
+
<x-tabs> (js/x-tabs.js, tier: Enhanced over Basic segmented) shows only
|
|
972
|
+
the panel matching the checked radio. Without JS all panels render —
|
|
973
|
+
functional Basic fallback.
|
|
974
|
+
<x-tabs>
|
|
975
|
+
<fieldset class="segmented">
|
|
976
|
+
<label><input type="radio" name="t" value="general" checked><span>General</span></label>
|
|
977
|
+
<label><input type="radio" name="t" value="privacy"><span>Privacy</span></label>
|
|
978
|
+
</fieldset>
|
|
979
|
+
<div data-panel="general">…</div>
|
|
980
|
+
<div data-panel="privacy">…</div>
|
|
981
|
+
</x-tabs> */
|
|
982
|
+
x-tabs {
|
|
983
|
+
display: flex;
|
|
984
|
+
flex-direction: column;
|
|
985
|
+
gap: var(--gap);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
/* ================================= toast ===================================
|
|
990
|
+
<x-toast> mount (js/x-toast.js, JS-only tier) + mtui.toast('Saved').
|
|
991
|
+
Inverse pill dropping from top center (500ms spring), stack of 3,
|
|
992
|
+
leaves by sliding up. Never carries the only copy of critical info. */
|
|
993
|
+
x-toast {
|
|
994
|
+
position: fixed;
|
|
995
|
+
inset-block-start: 0;
|
|
996
|
+
inset-inline: 0;
|
|
997
|
+
z-index: 10; /* the only fixed layer MTUI creates — above sticky
|
|
998
|
+
headers (z1) and any modest app chrome */
|
|
999
|
+
display: flex;
|
|
1000
|
+
flex-direction: column;
|
|
1001
|
+
align-items: center;
|
|
1002
|
+
gap: var(--sp-8);
|
|
1003
|
+
padding-block-start: calc(var(--sp-16) + env(safe-area-inset-top, 0px));
|
|
1004
|
+
pointer-events: none;
|
|
1005
|
+
}
|
|
1006
|
+
.toast {
|
|
1007
|
+
pointer-events: auto;
|
|
1008
|
+
display: flex;
|
|
1009
|
+
align-items: center;
|
|
1010
|
+
gap: var(--sp-12);
|
|
1011
|
+
min-block-size: var(--ctl-h);
|
|
1012
|
+
padding-inline: var(--sp-20);
|
|
1013
|
+
max-inline-size: calc(100vw - 2 * var(--pad));
|
|
1014
|
+
background: var(--inverse);
|
|
1015
|
+
color: var(--on-inverse);
|
|
1016
|
+
border-radius: var(--r-ctl);
|
|
1017
|
+
font-size: var(--fs-control);
|
|
1018
|
+
font-weight: var(--fw-control);
|
|
1019
|
+
animation: mtui-toast-in var(--t-toast) var(--spring);
|
|
1020
|
+
}
|
|
1021
|
+
/* Same duration/easing as the entrance (DESIGN.md §2.12 lists toast as a
|
|
1022
|
+
single "500ms spring" row — using the snappier --t-knob/--ease here,
|
|
1023
|
+
over the SAME long travel distance, made the exit ~40% faster than the
|
|
1024
|
+
entrance covering identical ground: it read as flung rather than calm. */
|
|
1025
|
+
.toast[data-leaving] {
|
|
1026
|
+
animation: mtui-toast-out var(--t-toast) var(--spring) forwards;
|
|
1027
|
+
}
|
|
1028
|
+
/* Travel distance is an ABSOLUTE clearance, not the toast's own %height —
|
|
1029
|
+
a %-based translate only carries toast #2/#3 a short relative distance,
|
|
1030
|
+
so they visibly drop through/behind whatever already sits above them
|
|
1031
|
+
instead of falling from above the whole stack. -6×--sp-48 comfortably
|
|
1032
|
+
clears a full 3-toast stack (including a 2-line + action toast) plus
|
|
1033
|
+
top safe-area padding, regardless of which slot a toast lands in. */
|
|
1034
|
+
@keyframes mtui-toast-in {
|
|
1035
|
+
from { transform: translateY(calc(-6 * var(--sp-48))) scale(.92); }
|
|
1036
|
+
}
|
|
1037
|
+
@keyframes mtui-toast-out {
|
|
1038
|
+
to { transform: translateY(calc(-6 * var(--sp-48))) scale(.92); }
|
|
1039
|
+
}
|
|
1040
|
+
.toast-action {
|
|
1041
|
+
border: 0;
|
|
1042
|
+
background: none;
|
|
1043
|
+
color: inherit;
|
|
1044
|
+
font: inherit;
|
|
1045
|
+
font-weight: var(--fw-card);
|
|
1046
|
+
padding-inline: var(--sp-4);
|
|
1047
|
+
min-block-size: var(--tap-min);
|
|
1048
|
+
cursor: pointer;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
/* ==================== interaction: no text selection =======================
|
|
1053
|
+
Anything clicked or toggled never selects its text — a double-click that
|
|
1054
|
+
highlights a label reads as broken. */
|
|
1055
|
+
.btn, .chip, .chip-x, a.item, button.item,
|
|
1056
|
+
.segmented, .accordion summary, .avatar, .avatar-stack,
|
|
1057
|
+
.badge, .field .t-label, .empty-icon,
|
|
1058
|
+
.menu-item, .toast {
|
|
1059
|
+
-webkit-user-select: none;
|
|
1060
|
+
user-select: none;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/* Rows wrapping a toggle. Kept as separate rules: an engine without :has()
|
|
1064
|
+
drops the whole selector list it appears in, and must not lose the block
|
|
1065
|
+
above. */
|
|
1066
|
+
label:has(.switch),
|
|
1067
|
+
label:has(.checkbox),
|
|
1068
|
+
label:has(.radio) {
|
|
1069
|
+
-webkit-user-select: none;
|
|
1070
|
+
user-select: none;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
/* ================================ x-select ==================================
|
|
1075
|
+
Tier: Enhanced over Basic .select (native <select> — see js/x-select.js).
|
|
1076
|
+
The native select is hidden (kept as value holder + no-JS fallback);
|
|
1077
|
+
this trigger + panel take over as the visible face. */
|
|
1078
|
+
.select-trigger {
|
|
1079
|
+
position: relative;
|
|
1080
|
+
display: flex;
|
|
1081
|
+
align-items: center;
|
|
1082
|
+
inline-size: 100%;
|
|
1083
|
+
min-block-size: var(--field-h);
|
|
1084
|
+
padding-inline: var(--sp-16);
|
|
1085
|
+
padding-inline-end: var(--sp-40);
|
|
1086
|
+
border: 0;
|
|
1087
|
+
border-radius: var(--r-input);
|
|
1088
|
+
background: var(--surface-2);
|
|
1089
|
+
color: var(--text);
|
|
1090
|
+
font-size: var(--fs-body);
|
|
1091
|
+
line-height: var(--lh-body);
|
|
1092
|
+
text-align: start;
|
|
1093
|
+
cursor: pointer;
|
|
1094
|
+
-webkit-tap-highlight-color: transparent;
|
|
1095
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
1096
|
+
}
|
|
1097
|
+
.select-trigger:hover { background: var(--surface-3); }
|
|
1098
|
+
.select-trigger::after {
|
|
1099
|
+
content: "";
|
|
1100
|
+
position: absolute;
|
|
1101
|
+
inset-block: 0;
|
|
1102
|
+
inset-inline-end: var(--sp-16);
|
|
1103
|
+
margin-block: auto;
|
|
1104
|
+
inline-size: var(--sp-16);
|
|
1105
|
+
block-size: var(--sp-16);
|
|
1106
|
+
background: var(--text-muted);
|
|
1107
|
+
-webkit-mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
1108
|
+
mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
1109
|
+
transition: transform var(--t-micro) var(--ease);
|
|
1110
|
+
}
|
|
1111
|
+
.select-trigger[aria-expanded="true"]::after { transform: rotate(180deg); }
|
|
1112
|
+
.select-trigger:disabled { color: var(--text-faint); cursor: default; }
|
|
1113
|
+
|
|
1114
|
+
.select-panel {
|
|
1115
|
+
max-block-size: 18rem;
|
|
1116
|
+
overflow-y: auto;
|
|
1117
|
+
}
|
|
1118
|
+
.select-option[aria-selected="true"] {
|
|
1119
|
+
background: var(--accent-tint);
|
|
1120
|
+
color: var(--accent-text);
|
|
1121
|
+
}
|
|
1122
|
+
.select-option:disabled { color: var(--text-faint); cursor: default; }
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
/* ============================== x-datepicker =================================
|
|
1126
|
+
Tier: Enhanced over Basic native <input type="date"> (js/x-datepicker.js).
|
|
1127
|
+
Single date only — no ranges, no times (kept out of v1 scope). */
|
|
1128
|
+
.datepicker-panel {
|
|
1129
|
+
padding: var(--sp-8);
|
|
1130
|
+
inline-size: 20rem;
|
|
1131
|
+
}
|
|
1132
|
+
.datepicker-header {
|
|
1133
|
+
display: flex;
|
|
1134
|
+
align-items: center;
|
|
1135
|
+
gap: var(--sp-8);
|
|
1136
|
+
padding: var(--sp-8);
|
|
1137
|
+
}
|
|
1138
|
+
.datepicker-header-label {
|
|
1139
|
+
flex: 1;
|
|
1140
|
+
border: 0;
|
|
1141
|
+
background: none;
|
|
1142
|
+
border-radius: var(--r-input);
|
|
1143
|
+
padding-block: var(--sp-4);
|
|
1144
|
+
text-align: center;
|
|
1145
|
+
font: inherit;
|
|
1146
|
+
font-size: var(--fs-control);
|
|
1147
|
+
font-weight: var(--fw-control);
|
|
1148
|
+
color: inherit;
|
|
1149
|
+
cursor: pointer;
|
|
1150
|
+
-webkit-tap-highlight-color: transparent;
|
|
1151
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
1152
|
+
}
|
|
1153
|
+
.datepicker-header-label:hover { background: var(--surface-2); }
|
|
1154
|
+
.datepicker-nav {
|
|
1155
|
+
display: flex;
|
|
1156
|
+
align-items: center;
|
|
1157
|
+
justify-content: center;
|
|
1158
|
+
inline-size: var(--tap-min);
|
|
1159
|
+
block-size: var(--tap-min);
|
|
1160
|
+
border: 0;
|
|
1161
|
+
border-radius: 50%;
|
|
1162
|
+
background: none;
|
|
1163
|
+
color: var(--text-muted);
|
|
1164
|
+
cursor: pointer;
|
|
1165
|
+
-webkit-tap-highlight-color: transparent;
|
|
1166
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
1167
|
+
}
|
|
1168
|
+
.datepicker-nav:hover { background: var(--surface-2); }
|
|
1169
|
+
.datepicker-nav::after {
|
|
1170
|
+
content: "";
|
|
1171
|
+
inline-size: var(--sp-16);
|
|
1172
|
+
block-size: var(--sp-16);
|
|
1173
|
+
background: currentColor;
|
|
1174
|
+
-webkit-mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
1175
|
+
mask: var(--icon-chevron-down) center / contain no-repeat;
|
|
1176
|
+
transform: rotate(90deg);
|
|
1177
|
+
}
|
|
1178
|
+
.datepicker-nav[data-dir="next"]::after { transform: rotate(-90deg); }
|
|
1179
|
+
[dir="rtl"] .datepicker-nav::after { transform: rotate(-90deg); }
|
|
1180
|
+
[dir="rtl"] .datepicker-nav[data-dir="next"]::after { transform: rotate(90deg); }
|
|
1181
|
+
|
|
1182
|
+
/* :not([hidden]) — an unconditional `display` here would override the
|
|
1183
|
+
[hidden] attribute's UA display:none at equal specificity, same trap as
|
|
1184
|
+
the swatches-panel popover fix above. */
|
|
1185
|
+
.datepicker-weekdays:not([hidden]),
|
|
1186
|
+
.datepicker-grid[data-view="days"] {
|
|
1187
|
+
display: grid;
|
|
1188
|
+
grid-template-columns: repeat(7, 1fr);
|
|
1189
|
+
gap: 2px; /* 1px-scale internal detail (sanctioned) — tight day grid */
|
|
1190
|
+
}
|
|
1191
|
+
.datepicker-grid[data-view="months"] {
|
|
1192
|
+
display: grid;
|
|
1193
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1194
|
+
gap: var(--sp-8);
|
|
1195
|
+
padding-block: var(--sp-8);
|
|
1196
|
+
}
|
|
1197
|
+
.datepicker-grid[data-view="years"] {
|
|
1198
|
+
display: flex;
|
|
1199
|
+
flex-direction: column;
|
|
1200
|
+
gap: 2px;
|
|
1201
|
+
max-block-size: 14rem;
|
|
1202
|
+
overflow-y: auto;
|
|
1203
|
+
scrollbar-width: thin;
|
|
1204
|
+
}
|
|
1205
|
+
.datepicker-weekdays > span {
|
|
1206
|
+
display: flex;
|
|
1207
|
+
align-items: center;
|
|
1208
|
+
justify-content: center;
|
|
1209
|
+
block-size: var(--sp-32);
|
|
1210
|
+
color: var(--text-muted);
|
|
1211
|
+
font-size: var(--fs-label);
|
|
1212
|
+
font-weight: var(--fw-label);
|
|
1213
|
+
}
|
|
1214
|
+
.datepicker-day {
|
|
1215
|
+
display: flex;
|
|
1216
|
+
align-items: center;
|
|
1217
|
+
justify-content: center;
|
|
1218
|
+
aspect-ratio: 1;
|
|
1219
|
+
border: 0;
|
|
1220
|
+
border-radius: 50%;
|
|
1221
|
+
background: none;
|
|
1222
|
+
color: var(--text);
|
|
1223
|
+
font-size: var(--fs-secondary);
|
|
1224
|
+
cursor: pointer;
|
|
1225
|
+
-webkit-tap-highlight-color: transparent;
|
|
1226
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
1227
|
+
}
|
|
1228
|
+
.datepicker-day:hover { background: var(--surface-2); }
|
|
1229
|
+
.datepicker-day[data-today] { color: var(--accent-text); font-weight: var(--fw-control); }
|
|
1230
|
+
.datepicker-day[aria-selected="true"] { background: var(--accent); color: var(--on-accent); }
|
|
1231
|
+
.datepicker-day:disabled {
|
|
1232
|
+
color: var(--text-faint);
|
|
1233
|
+
cursor: default;
|
|
1234
|
+
pointer-events: none;
|
|
1235
|
+
}
|
|
1236
|
+
.datepicker-day[data-outside] { color: var(--text-faint); }
|
|
1237
|
+
|
|
1238
|
+
.datepicker-month {
|
|
1239
|
+
aspect-ratio: auto;
|
|
1240
|
+
min-block-size: var(--tap-min);
|
|
1241
|
+
border-radius: var(--r-input);
|
|
1242
|
+
font-size: var(--fs-control);
|
|
1243
|
+
font-weight: var(--fw-control);
|
|
1244
|
+
}
|
|
1245
|
+
.datepicker-year {
|
|
1246
|
+
aspect-ratio: auto;
|
|
1247
|
+
inline-size: 100%;
|
|
1248
|
+
min-block-size: var(--tap-min);
|
|
1249
|
+
border-radius: var(--r-input);
|
|
1250
|
+
font-size: var(--fs-control);
|
|
1251
|
+
font-weight: var(--fw-control);
|
|
1252
|
+
flex-shrink: 0;
|
|
1253
|
+
}
|
|
1254
|
+
.datepicker-footer {
|
|
1255
|
+
display: flex;
|
|
1256
|
+
justify-content: center;
|
|
1257
|
+
padding-block-start: var(--sp-8);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
/* ============================ x-colorswatches ================================
|
|
1262
|
+
Tier: Enhanced over Basic native <input type="color"> (js/x-colorswatches.js).
|
|
1263
|
+
Curated swatches only — never a free color wheel. */
|
|
1264
|
+
.swatches-trigger {
|
|
1265
|
+
position: relative;
|
|
1266
|
+
display: flex;
|
|
1267
|
+
align-items: center;
|
|
1268
|
+
gap: var(--sp-12);
|
|
1269
|
+
inline-size: 100%;
|
|
1270
|
+
min-block-size: var(--field-h);
|
|
1271
|
+
padding-inline: var(--sp-16);
|
|
1272
|
+
border: 0;
|
|
1273
|
+
border-radius: var(--r-input);
|
|
1274
|
+
background: var(--surface-2);
|
|
1275
|
+
color: var(--text);
|
|
1276
|
+
font-size: var(--fs-body);
|
|
1277
|
+
cursor: pointer;
|
|
1278
|
+
-webkit-tap-highlight-color: transparent;
|
|
1279
|
+
transition: background-color var(--t-micro) var(--ease);
|
|
1280
|
+
}
|
|
1281
|
+
.swatches-trigger:hover { background: var(--surface-3); }
|
|
1282
|
+
.swatches-trigger-swatch {
|
|
1283
|
+
inline-size: var(--sp-24);
|
|
1284
|
+
block-size: var(--sp-24);
|
|
1285
|
+
border-radius: 50%;
|
|
1286
|
+
flex-shrink: 0;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.swatches-panel {
|
|
1290
|
+
/* display is scoped to :popover-open — an unconditional `display: grid`
|
|
1291
|
+
here would override the UA's default "hidden until shown" popover
|
|
1292
|
+
rule (author styles win over the UA stylesheet at equal specificity),
|
|
1293
|
+
leaving the panel permanently visible even when closed. */
|
|
1294
|
+
padding: var(--sp-12);
|
|
1295
|
+
}
|
|
1296
|
+
.swatches-panel:popover-open {
|
|
1297
|
+
display: grid;
|
|
1298
|
+
grid-template-columns: repeat(5, 1fr);
|
|
1299
|
+
gap: var(--sp-8);
|
|
1300
|
+
}
|
|
1301
|
+
.swatch {
|
|
1302
|
+
position: relative;
|
|
1303
|
+
inline-size: var(--sp-32);
|
|
1304
|
+
block-size: var(--sp-32);
|
|
1305
|
+
border: 0;
|
|
1306
|
+
border-radius: 50%;
|
|
1307
|
+
cursor: pointer;
|
|
1308
|
+
-webkit-tap-highlight-color: transparent;
|
|
1309
|
+
transition: transform var(--t-micro) var(--ease);
|
|
1310
|
+
}
|
|
1311
|
+
.swatch:hover { transform: scale(1.08); }
|
|
1312
|
+
.swatch[aria-checked="true"] {
|
|
1313
|
+
box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px currentColor;
|
|
1314
|
+
}
|
|
1315
|
+
.swatch::after {
|
|
1316
|
+
content: "";
|
|
1317
|
+
position: absolute;
|
|
1318
|
+
inset: 0;
|
|
1319
|
+
margin: auto;
|
|
1320
|
+
inline-size: 60%;
|
|
1321
|
+
block-size: 60%;
|
|
1322
|
+
background: var(--swatch-check-color, var(--on-accent));
|
|
1323
|
+
-webkit-mask: var(--icon-check) center / contain no-repeat;
|
|
1324
|
+
mask: var(--icon-check) center / contain no-repeat;
|
|
1325
|
+
opacity: 0;
|
|
1326
|
+
transition: opacity var(--t-micro) var(--ease);
|
|
1327
|
+
}
|
|
1328
|
+
.swatch[aria-checked="true"]::after { opacity: 1; }
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
/* ============================= x-contextmenu ==================================
|
|
1332
|
+
Tier: Enhanced. Wraps any region; opens an MTUI menu (reuses .menu) on
|
|
1333
|
+
right-click or long-press (js/x-contextmenu.js). No-JS: the browser's
|
|
1334
|
+
native context menu appears — content itself is unaffected. */
|
|
1335
|
+
x-contextmenu { display: contents; }
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
/* =========================== pull-to-refresh ================================
|
|
1339
|
+
Tier: Enhanced over .screen-list (js/pull-refresh.js). No-JS: no
|
|
1340
|
+
affordance, the list scrolls normally. */
|
|
1341
|
+
x-pull-refresh {
|
|
1342
|
+
display: block;
|
|
1343
|
+
block-size: 100%;
|
|
1344
|
+
overflow: hidden;
|
|
1345
|
+
}
|
|
1346
|
+
x-pull-refresh > .screen-list,
|
|
1347
|
+
x-pull-refresh > [data-pull-scroll] {
|
|
1348
|
+
block-size: 100%;
|
|
1349
|
+
overflow-y: auto;
|
|
1350
|
+
/* it's a drag target (mouse or touch) — dragging shouldn't select the
|
|
1351
|
+
row text underneath. */
|
|
1352
|
+
-webkit-user-select: none;
|
|
1353
|
+
user-select: none;
|
|
1354
|
+
}
|
|
1355
|
+
.pull-indicator {
|
|
1356
|
+
position: absolute;
|
|
1357
|
+
inset-inline: 0;
|
|
1358
|
+
inset-block-start: 0;
|
|
1359
|
+
display: flex;
|
|
1360
|
+
align-items: center;
|
|
1361
|
+
justify-content: center;
|
|
1362
|
+
block-size: var(--sp-48);
|
|
1363
|
+
margin-block-start: calc(-1 * var(--sp-48));
|
|
1364
|
+
color: var(--text-muted);
|
|
1365
|
+
pointer-events: none;
|
|
1366
|
+
}
|
|
1367
|
+
.pull-indicator::after {
|
|
1368
|
+
content: "";
|
|
1369
|
+
inline-size: var(--sp-20);
|
|
1370
|
+
block-size: var(--sp-20);
|
|
1371
|
+
background: currentColor;
|
|
1372
|
+
-webkit-mask: var(--icon-loader) center / contain no-repeat;
|
|
1373
|
+
mask: var(--icon-loader) center / contain no-repeat;
|
|
1374
|
+
}
|
|
1375
|
+
.pull-indicator[data-spinning]::after {
|
|
1376
|
+
animation: mtui-spin var(--t-skeleton) linear infinite;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
/* ============================== x-theme =====================================
|
|
1381
|
+
Tier: JS-only (js/x-theme.js). An in-app appearance controller — the
|
|
1382
|
+
Telegram "Appearance" panel, MTUI-native: flip theme, tap an accent, set
|
|
1383
|
+
corners + density. Writes only to <html> knobs (root-only rule) and
|
|
1384
|
+
persists to localStorage. Built from canonical .segmented groups plus a
|
|
1385
|
+
row of accent dots. No-JS: empty (the app keeps its current/persisted
|
|
1386
|
+
theme; an in-app switcher inherently needs JS). */
|
|
1387
|
+
x-theme { display: block; }
|
|
1388
|
+
.theme-group + .theme-group { margin-block-start: var(--gap); }
|
|
1389
|
+
.theme-group > .t-label {
|
|
1390
|
+
display: block;
|
|
1391
|
+
margin-block-end: var(--sp-8);
|
|
1392
|
+
color: var(--text-muted);
|
|
1393
|
+
}
|
|
1394
|
+
.theme-accents {
|
|
1395
|
+
display: flex;
|
|
1396
|
+
gap: var(--sp-8);
|
|
1397
|
+
flex-wrap: wrap;
|
|
1398
|
+
}
|
|
1399
|
+
.theme-accent {
|
|
1400
|
+
position: relative;
|
|
1401
|
+
display: grid;
|
|
1402
|
+
place-items: center;
|
|
1403
|
+
inline-size: var(--tap-min);
|
|
1404
|
+
block-size: var(--tap-min);
|
|
1405
|
+
cursor: pointer;
|
|
1406
|
+
-webkit-tap-highlight-color: transparent;
|
|
1407
|
+
}
|
|
1408
|
+
.theme-accent input {
|
|
1409
|
+
appearance: none;
|
|
1410
|
+
position: absolute;
|
|
1411
|
+
inset: 0;
|
|
1412
|
+
margin: 0;
|
|
1413
|
+
cursor: pointer;
|
|
1414
|
+
border-radius: 50%;
|
|
1415
|
+
}
|
|
1416
|
+
.theme-accent-dot {
|
|
1417
|
+
position: relative;
|
|
1418
|
+
inline-size: var(--sp-32);
|
|
1419
|
+
block-size: var(--sp-32);
|
|
1420
|
+
border-radius: 50%;
|
|
1421
|
+
background: var(--dot);
|
|
1422
|
+
pointer-events: none;
|
|
1423
|
+
transition: transform var(--t-micro) var(--ease);
|
|
1424
|
+
}
|
|
1425
|
+
.theme-accent:hover .theme-accent-dot { transform: translateY(-2px); }
|
|
1426
|
+
.theme-accent input:active + .theme-accent-dot { transform: scale(.96); }
|
|
1427
|
+
.theme-accent input:focus-visible + .theme-accent-dot { box-shadow: var(--ring-focus); }
|
|
1428
|
+
/* Selected = white check inside the dot (all accents pass white contrast). */
|
|
1429
|
+
.theme-accent input:checked + .theme-accent-dot::after {
|
|
1430
|
+
content: "";
|
|
1431
|
+
position: absolute;
|
|
1432
|
+
inset: 22%;
|
|
1433
|
+
background: var(--on-accent);
|
|
1434
|
+
-webkit-mask: var(--icon-check) center / contain no-repeat;
|
|
1435
|
+
mask: var(--icon-check) center / contain no-repeat;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/* Shared: none of the Enhanced trigger/option surfaces select their text.
|
|
1439
|
+
General rule: anything that's a click-toggle, drag, or long-press
|
|
1440
|
+
target gets user-select:none — dragging/pressing shouldn't leave a
|
|
1441
|
+
text selection behind, which reads as broken. */
|
|
1442
|
+
.select-trigger, .select-option, .datepicker-day, .datepicker-nav,
|
|
1443
|
+
.datepicker-header-label, .swatches-trigger, .swatch, x-contextmenu,
|
|
1444
|
+
.theme-accent {
|
|
1445
|
+
-webkit-user-select: none;
|
|
1446
|
+
user-select: none;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
/* ============================ forced colors ================================
|
|
1451
|
+
Tonal separation disappears — every component draws real borders. */
|
|
1452
|
+
@media (forced-colors: active) {
|
|
1453
|
+
.btn, .chip, .badge, .avatar, .segmented, .alert, .card,
|
|
1454
|
+
.field input, .field select, .field textarea,
|
|
1455
|
+
.switch, .checkbox, .radio, .accordion, .code, .code-inline,
|
|
1456
|
+
.skeleton, .empty-icon {
|
|
1457
|
+
border: 1px solid ButtonBorder;
|
|
1458
|
+
}
|
|
1459
|
+
/* Rows separate by tone only — give them a real divider in forced colors. */
|
|
1460
|
+
.item { border-block-end: 1px solid CanvasText; }
|
|
1461
|
+
.switch::before, .radio:checked::before { background: ButtonText; }
|
|
1462
|
+
.switch:checked, .checkbox:checked, .radio:checked {
|
|
1463
|
+
background: SelectedItem;
|
|
1464
|
+
}
|
|
1465
|
+
.segmented input:checked + span { border: 1px solid ButtonBorder; }
|
|
1466
|
+
.segmented::before { display: none; }
|
|
1467
|
+
.btn[data-loading]::after,
|
|
1468
|
+
.select::after,
|
|
1469
|
+
.accordion summary::after,
|
|
1470
|
+
.checkbox::before {
|
|
1471
|
+
forced-color-adjust: none;
|
|
1472
|
+
background: ButtonText;
|
|
1473
|
+
}
|
|
1474
|
+
.checkbox:checked::before { background: SelectedItemText; }
|
|
1475
|
+
.skeleton::after { display: none; }
|
|
1476
|
+
.dialog, .menu, .menu-item, .toast { border: 1px solid CanvasText; }
|
|
1477
|
+
|
|
1478
|
+
.select-trigger, .swatches-trigger, .datepicker-panel, .datepicker-day,
|
|
1479
|
+
.swatch {
|
|
1480
|
+
border: 1px solid ButtonBorder;
|
|
1481
|
+
}
|
|
1482
|
+
.select-trigger::after, .datepicker-nav::after, .pull-indicator::after {
|
|
1483
|
+
forced-color-adjust: none;
|
|
1484
|
+
background: ButtonText;
|
|
1485
|
+
}
|
|
1486
|
+
.select-option[aria-selected="true"],
|
|
1487
|
+
.datepicker-day[aria-selected="true"] { background: SelectedItem; }
|
|
1488
|
+
.swatch[aria-checked="true"] { box-shadow: none; outline: 2px solid ButtonText; }
|
|
1489
|
+
.swatch::after { forced-color-adjust: none; background: ButtonText; }
|
|
1490
|
+
|
|
1491
|
+
/* x-theme: dots lose their tonal fill — draw a border, mark the checked
|
|
1492
|
+
one with an outline + keep the check glyph visible. */
|
|
1493
|
+
.theme-accent-dot { forced-color-adjust: none; border: 1px solid ButtonBorder; }
|
|
1494
|
+
.theme-accent input:checked + .theme-accent-dot { outline: 2px solid ButtonText; }
|
|
1495
|
+
.theme-accent input:checked + .theme-accent-dot::after { background: ButtonText; }
|
|
1496
|
+
}
|