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
/* Button
   No border, no ornament — hierarchy is ink density, not an outline. A
   STAMP (primary, dark, danger) is a solid block of colour; a QUIET action
   (default, ghost) carries no fill at rest and picks up a flat tint of the
   stock on hover, the same gesture as a CardRow. Variants: default, primary,
   dark, ghost, danger. Sizes: sm, md (default), lg — padding and font step
   only, weight and radius stay put.

   The default used to pad itself with --gl-space-xs/-s, which are the rungs
   a layout reaches for between two controls, not inside one: on a wide
   viewport they grew the default button taller than the large one. Every
   size now sits one rung further down the same scale — see the block at
   the bottom.

   The contract is the element itself: a bare <button> is styled directly, no
   class required. An <a> that acts like a button (navigates rather than
   changes something — see the Button page's "Links that act like buttons")
   opts in with a single `.btn` class, since a bare `a` selector would catch
   every ordinary prose link on the site. `:not(.icon-button)` keeps this
   block from leaking onto IconButton, which shares the <button> tag but is
   an entirely different shape. */

button:not(.icon-button),
a.btn {
  --_bg: transparent;
  --_fg: var(--gl-color-text-primary);
  --_pad-y: var(--gl-space-2xs);
  --_pad-x: var(--gl-space-xs);
  --_font: var(--gl-step--1);
  /* The flex gap already separates the icon from the label. */
  --gl-icon-start: var(--gl-icon);
  --gl-icon-style-spacing: 0;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gl-space-2xs);
  padding: var(--_pad-y) var(--_pad-x);
  border: none;
  border-radius: var(--gl-radius-md);
  background: var(--_bg);
  color: var(--_fg);
  font-family: var(--gl-font-mono);
  font-size: var(--_font);
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1.15;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--gl-dur-fast) var(--gl-ease),
              color var(--gl-dur-fast) var(--gl-ease),
              filter var(--gl-dur-fast) var(--gl-ease);
}

/* Quiet variants (default, ghost) carry no fill at rest; hovering picks up
   a flat tint of the stock instead of darkening a border that isn't there.
   Tabs are <button>s too but own their hover (tabs.css): this rule's
   specificity would otherwise paint the selected segment the colour of its
   own track until the pointer left it. */
:is(button:not(.icon-button), a.btn):not([role="tab"]):not([data-variant="primary"]):not([data-variant="dark"]):not([data-variant="danger"]):hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--gl-color-surface-fill);
  color: var(--gl-color-text-primary);
}

/* Stamp variants (primary, dark, danger) are already ink — hovering just
   darkens the fill in place. */
:is(button:not(.icon-button), a.btn)[data-variant="primary"]:hover:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="dark"]:hover:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="danger"]:hover:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.96); }
:is(button:not(.icon-button), a.btn)[data-variant="primary"]:active:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="dark"]:active:not(:disabled):not([aria-disabled="true"]),
:is(button:not(.icon-button), a.btn)[data-variant="danger"]:active:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.92); }

button:not(.icon-button):disabled,
:is(button:not(.icon-button), a.btn)[aria-disabled="true"] {
  opacity: 0.5;
  cursor: default;
}

:is(button:not(.icon-button), a.btn)[data-variant="primary"] {
  --_bg: var(--gl-color-accent);
  --_fg: var(--gl-color-accent-fg);
}

/* Reserved for the single most-committing action in a view — a final Save,
   never a whole row of them. */
:is(button:not(.icon-button), a.btn)[data-variant="dark"] {
  --_bg: var(--gl-color-text-primary);
  --_fg: var(--gl-color-surface-page);
}

/* Same quiet block as default, one step further back: text-secondary at
   rest instead of text-primary. */
:is(button:not(.icon-button), a.btn)[data-variant="ghost"] {
  --_fg: var(--gl-color-text-secondary);
}

/* The source hardcodes this label white rather than deriving it — danger
   is not "on a filled accent surface", so --gl-color-accent-fg is reused
   here only because its resolved value is the same literal white, not
   because the two roles are the same thing. */
:is(button:not(.icon-button), a.btn)[data-variant="danger"] {
  --_bg: var(--gl-color-danger);
  --_fg: var(--gl-color-accent-fg);
}

/* Each size is one step of the space scale wide and, at the ends, one step
   of the type scale. Medium and large share a rung vertically and part on
   type size, which is what keeps them in order at every viewport width: the
   scale is fluid, so two sizes that differed only by which fluid rung they
   padded with could — and did — cross over on a wide screen. */
:is(button:not(.icon-button), a.btn)[data-size="sm"] { --_pad-y: var(--gl-space-3xs); --_pad-x: var(--gl-space-2xs); --_font: 0.8125rem; }
:is(button:not(.icon-button), a.btn)[data-size="lg"] { --_pad-x: var(--gl-space-s); --_font: var(--gl-step-0); }

:is(button:not(.icon-button), a.btn)[data-block] { width: 100%; }

/* Button group — [role="group"] is already the correct ARIA role for a
   toolbar of related buttons, so it doubles as the styling hook for free.
   A hairline divider stands in for the border the buttons themselves no
   longer carry — without one, two adjacent quiet buttons would read as a
   single wide button. */
[role="group"] {
  display: inline-flex;
  isolation: isolate;
}
[role="group"] > button { border-radius: 0; }
[role="group"] > button:first-child { border-start-start-radius: var(--gl-radius-md); border-end-start-radius: var(--gl-radius-md); }
[role="group"] > button:last-child { border-start-end-radius: var(--gl-radius-md); border-end-end-radius: var(--gl-radius-md); }
[role="group"] > button + button { border-inline-start: 1px solid var(--gl-color-border-hairline); }
[role="group"] > button:hover,
[role="group"] > button:focus-visible { z-index: 1; }

/* IconButton — a square icon-only button. Always needs an aria-label; it
   becomes both the accessible name and the native tooltip. There is no
   attribute that distinguishes "a button that is only an icon" from a plain
   Button, so this is the one sanctioned class in this file.

   The box is not a number of its own: it is written the way Button's height
   arrives — two rungs of padding around one 1.15 line of the label type it
   would carry if it had a label. So the two are the same height at every
   viewport width, and stay that way when the scale is retuned, instead of a
   fixed square drifting away from a fluid button. */
.icon-button {
  --_pad: var(--gl-space-2xs);
  --_line: var(--gl-step--1);
  --_size: calc(2 * var(--_pad) + 1.15 * var(--_line));
  --_bg: transparent;
  --_fg: var(--gl-color-text-secondary);
  /* No spacing, so stray whitespace in the markup can't push the icon
     off-center. */
  --gl-icon-start: var(--gl-icon);
  --gl-icon-style-spacing: 0;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--_size);
  height: var(--_size);
  padding: 0;
  flex: none;
  border: none;
  border-radius: var(--gl-radius-md);
  background: var(--_bg);
  color: var(--_fg);
  font-size: var(--_line);
  cursor: pointer;
  transition: background-color var(--gl-dur-fast) var(--gl-ease),
              color var(--gl-dur-fast) var(--gl-ease);
}
.icon-button:hover:not(:disabled) { background: var(--gl-color-surface-fill); }
.icon-button:disabled { opacity: 0.5; cursor: default; }

/* Same two overrides Button's own sm and lg make, so the pairs stay level. */
.icon-button[data-size="sm"] { --_pad: var(--gl-space-3xs); --_line: 0.8125rem; }
.icon-button[data-size="lg"] { --_line: var(--gl-step-0); }

/* No border on either variant — "outline" keeps its name for API
   compatibility, but reads as a raised fill instead of a bordered box. */
.icon-button[data-variant="outline"] { --_bg: var(--gl-color-surface-fill); --_fg: var(--gl-color-text-primary); }
.icon-button[data-variant="accent"]  { --_bg: var(--gl-color-accent-soft); --_fg: var(--gl-color-accent-ink); }