1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/* Field
The labeled field wrapper — a <label> that wraps its own caption text
together with the control, so the association is native and needs no
`for`/`id` pair. The caption is mono, uppercase, tracked — the same
catalog-label convention as Badge and type-label.
A <label> is reused for three different shapes across this system (this
wrapper, Checkbox/Radio's row, Switch's row), so it cannot be styled bare
without one shape leaking into another. Field is the one of the three with
no distinguishing attribute of its own, so it gets the sanctioned class;
Checkbox/Radio tell themselves apart structurally instead (see below). */
.field {
display: flex;
flex-direction: column;
gap: var(--gl-space-2xs);
min-width: 0;
font-family: var(--gl-font-mono);
font-size: var(--gl-step--2);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gl-color-text-secondary);
}
/* Hint and error read the native fine-print element. `role="alert"` is a
real live-region role for validation text, so it doubles as the selector
that turns a hint red. */
.field > small {
font-family: var(--gl-font-sans);
font-size: var(--gl-step--2);
text-transform: none;
letter-spacing: normal;
font-weight: 400;
color: var(--gl-color-text-tertiary);
}
.field > small[role="alert"] { color: var(--gl-color-danger); }
/* Input / Select — bordered, matched to each other exactly. Both tags ARE
the component, so they are styled bare; nothing else on this site uses a
text input or a select. Inline padding is the xs rung less the border;
block padding is a full xs rung, so a control stands a touch taller than
a default Button. */
input:not([type="checkbox"]):not([type="radio"]),
select {
width: 100%;
padding: var(--gl-space-xs) calc(var(--gl-space-xs) - 1px);
line-height: 1.15;
background: var(--gl-color-surface-card);
border: 1px solid var(--gl-color-border-default);
border-radius: var(--gl-radius-sm);
color: var(--gl-color-text-primary);
font-family: var(--gl-font-sans);
font-size: var(--gl-step--1);
text-transform: none;
letter-spacing: normal;
font-weight: 400;
outline: none;
transition: border-color var(--gl-dur-fast) var(--gl-ease),
box-shadow var(--gl-dur-fast) var(--gl-ease);
}
input::placeholder { color: var(--gl-color-text-tertiary); }
input:not([type="checkbox"]):not([type="radio"]):focus,
select:focus {
border-color: var(--gl-color-accent);
box-shadow: 0 0 0 2px var(--gl-color-accent-soft);
}
input:not([type="checkbox"]):not([type="radio"]):disabled,
select:disabled {
opacity: 0.5;
cursor: default;
}
select {
appearance: none;
padding-inline-end: var(--gl-space-l);
background-image: var(--gl-icon-chevron-down);
background-repeat: no-repeat;
background-position: right var(--gl-space-xs) center;
}
input[aria-invalid="true"],
select[aria-invalid="true"] {
border-color: var(--gl-color-danger);
background-color: var(--gl-color-danger-bg);
}
/* Checkbox / Radio — the real native controls, tinted with accent-color
instead of a hand-drawn box. This is not just a rename: it deletes the
custom SVG-checkmark markup entirely and lets the browser draw its own
(platform-appropriate) check and dot. `role="switch"` is excluded here so
Switch — a checkbox playing a different role — can style itself below
without this rule fighting it. A <label> wrapping a checkbox/radio is told
apart from Field's <label> structurally: it :has() one as a direct child,
no class required. */
input[type="checkbox"]:not([role="switch"]),
input[type="radio"] {
width: 1.125rem;
height: 1.125rem;
margin: 0;
accent-color: var(--gl-color-accent);
cursor: pointer;
}
input[type="checkbox"]:not([role="switch"]):focus-visible,
input[type="radio"]:focus-visible {
outline: 2px solid var(--gl-color-accent);
outline-offset: 2px;
}
label:has(> input[type="checkbox"]:not([role="switch"])),
label:has(> input[type="radio"]) {
display: inline-flex;
align-items: center;
gap: var(--gl-space-2xs);
cursor: pointer;
font-family: var(--gl-font-sans);
font-size: var(--gl-step--1);
font-weight: 400;
text-transform: none;
letter-spacing: normal;
color: var(--gl-color-text-primary);
}
label:has(> input:disabled) { cursor: default; opacity: 0.5; }
/* Switch — for on/off settings only, never a substitute for Checkbox.
`input[type="checkbox"][role="switch"]` is real ARIA (the correct role for
a toggle), not an invented hook, but the visual track+thumb needs a second
element for the thumb's independent position and transition — more than
`appearance: none` plus pseudo-elements on the input itself can cleanly
carry — so `.switch` is a deliberate, documented fallback class rather
than a first choice. */
.switch {
display: inline-flex;
align-items: center;
gap: var(--gl-space-2xs);
cursor: pointer;
font-family: var(--gl-font-sans);
font-size: var(--gl-step--1);
color: var(--gl-color-text-primary);
}
.switch input[role="switch"] { position: absolute; opacity: 0; width: 2.25rem; height: 1.25rem; margin: 0; }
.switch span {
position: relative;
flex: none;
width: 2.25rem;
height: 1.25rem;
border-radius: var(--gl-radius-full);
background: var(--gl-color-border-default);
transition: background-color var(--gl-dur-base) var(--gl-ease);
}
.switch span::after {
content: "";
position: absolute;
inset-block-start: 2px;
inset-inline-start: 2px;
width: 1rem;
height: 1rem;
border-radius: var(--gl-radius-full);
background: var(--gl-color-accent-fg);
box-shadow: var(--gl-shadow-sm);
transition: translate var(--gl-dur-base) var(--gl-ease);
}
.switch input[role="switch"]:checked + span { background: var(--gl-color-accent); }
.switch input[role="switch"]:checked + span::after { translate: 1rem 0; }
.switch input[role="switch"]:focus-visible + span { outline: 2px solid var(--gl-color-accent); outline-offset: 2px; }
.switch:has(input:disabled) { cursor: default; opacity: 0.5; }
/* Rows of fields — used by the settings-form pattern. `<form>`/`<fieldset>`
are the elements that already mean this, so they are styled bare. A row
grouping two fields side by side has no element of its own, so it keeps
one plain class. */
form {
display: flex;
flex-direction: column;
gap: var(--gl-space-s);
max-width: 28rem;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: var(--gl-space-xs);
}
/* Vertically stacked fields — the other half of form layout alongside
.form-row's side-by-side pair. */
.field-stack { display: flex; flex-direction: column; gap: var(--gl-space-s); }
fieldset {
display: flex;
flex-direction: column;
gap: var(--gl-space-2xs);
margin: 0;
padding: 0;
border: 0;
}
fieldset > legend {
padding: 0 0 var(--gl-space-2xs);
font-family: var(--gl-font-mono);
font-size: var(--gl-step--2);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gl-color-text-secondary);
}