@axiapps/axi-design 1.6.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.
@@ -0,0 +1,301 @@
1
+ /* axi design language - primitives.
2
+ The pieces that recur in every property. Two rules govern the whole file:
3
+ a raised thing is outlined in --axi-ink-line and carries a hard offset
4
+ block, never a blur; and a filled chip asserts a value while an outlined
5
+ one annotates. */
6
+
7
+ /* ---------- panel ---------- */
8
+ /* The surface every larger component is built on. Kept as its own class so a
9
+ consumer can raise an arbitrary block into the language without waiting for
10
+ us to ship a component for it. */
11
+ .axi-panel {
12
+ background: var(--axi-surface);
13
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
14
+ border-radius: var(--axi-radius);
15
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
16
+ /* A panel owns its padding so every consumer doesn't have to invent one;
17
+ override per instance with --axi-panel-pad where a tighter or looser
18
+ fit is genuinely called for. */
19
+ padding: var(--axi-panel-pad, 26px);
20
+ }
21
+
22
+ /* ---------- button ---------- */
23
+ .axi-btn {
24
+ display: inline-flex; align-items: center; gap: 8px;
25
+ padding: 12px 20px;
26
+ border: var(--axi-border-control) solid var(--axi-ink-line);
27
+ border-radius: 8px;
28
+ background: var(--axi-ground);
29
+ color: var(--axi-text-dim);
30
+ font: var(--axi-t-label);
31
+ letter-spacing: var(--axi-ls-label);
32
+ text-transform: uppercase;
33
+ text-decoration: none;
34
+ cursor: pointer;
35
+ transition: transform .1s, box-shadow .1s;
36
+ }
37
+ /* Hover lifts. Nothing in this language fades or glows: the block deepens and
38
+ the element moves against it, which reads instantly even in peripheral
39
+ vision and costs no colour. */
40
+ .axi-btn:hover {
41
+ color: var(--axi-text);
42
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
43
+ transform: translate(-2px, -2px);
44
+ }
45
+ .axi-btn--primary {
46
+ background: var(--axi-accent);
47
+ color: var(--axi-accent-ink);
48
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
49
+ }
50
+ /* The one button that rests with a block already under it, so the generic
51
+ hover's translate alone would move element and block together and leave the
52
+ lower-right edge where it started - the "grows rather than lifts" failure
53
+ rule 4 warns about. It lifts the way a panel does: the block deepens too. */
54
+ .axi-btn--primary:hover {
55
+ color: var(--axi-accent-ink);
56
+ box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
57
+ }
58
+ .axi-btn--ghost { background: transparent; }
59
+ .axi-btn--dashed { background: transparent; border-style: dashed; }
60
+
61
+ /* ---------- pill ---------- */
62
+ /* A filter toggle. Its pressed state fills with whatever colour the consumer
63
+ has put on it, so the control reads as the thing it filters to rather than
64
+ as a generic "selected". Consumers set that colour with --axi-pill-fill,
65
+ on the pill or on any ancestor - it is read through a fallback and never
66
+ declared on the component, which is what keeps both placements working. */
67
+ .axi-pill {
68
+ display: inline-flex; align-items: center; gap: 6px;
69
+ padding: 10px 9px;
70
+ border: var(--axi-border-control) solid var(--axi-ink-line);
71
+ border-radius: 8px;
72
+ background: var(--axi-ground);
73
+ color: var(--axi-text-dim);
74
+ font: var(--axi-t-label);
75
+ letter-spacing: var(--axi-ls-label);
76
+ text-transform: uppercase;
77
+ cursor: pointer;
78
+ transition: transform .1s, box-shadow .1s;
79
+ }
80
+ .axi-pill:hover {
81
+ color: var(--axi-text);
82
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
83
+ transform: translate(-2px, -2px);
84
+ }
85
+ .axi-pill[aria-pressed="true"] {
86
+ background: var(--axi-pill-fill, var(--axi-accent));
87
+ color: var(--axi-accent-ink);
88
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
89
+ }
90
+ /* Spelled out rather than left to source order: `.axi-pill:hover` and
91
+ `.axi-pill[aria-pressed="true"]` have identical specificity, so without this
92
+ rule a pressed pill under the cursor kept its pressed block while still
93
+ taking the hover's translate - a slide, not a lift, decided by which rule
94
+ happened to come last in the file. A pressed pill keeps its fill and lifts. */
95
+ .axi-pill[aria-pressed="true"]:hover {
96
+ background: var(--axi-pill-fill, var(--axi-accent));
97
+ color: var(--axi-accent-ink);
98
+ box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
99
+ transform: translate(-2px, -2px);
100
+ }
101
+
102
+ /* ---------- switch ---------- */
103
+ /* On/off for a setting, where a pill's "pressed" is not enough: a pill says
104
+ which of several filters you chose, a switch says what state one thing is
105
+ in when nothing else is on screen to compare it against. Rule 5 is what
106
+ makes it legible - the filled track asserts the setting's status, and the
107
+ status is the only thing that changes between the two states.
108
+
109
+ It is a slot with something sitting in it, so it takes no block: the block
110
+ belongs to things you press. The track carries a full ink edge at the
111
+ control weight and the raised surface, because an off switch inside a panel
112
+ is a surface on a surface - without the edge the track vanishes and all you
113
+ see is a slug floating in the card.
114
+
115
+ The slug is ink in both states. That is deliberate: on and off differ in
116
+ what colour is in the slot, never in how bright the part that moves is. */
117
+ .axi-switch {
118
+ position: relative;
119
+ flex: none;
120
+ display: inline-block;
121
+ width: var(--axi-switch-w, 46px);
122
+ height: var(--axi-switch-h, 26px);
123
+ padding: 0;
124
+ border: var(--axi-border-control) solid var(--axi-ink-line);
125
+ border-radius: var(--axi-radius-sm);
126
+ background: var(--axi-surface-raised);
127
+ cursor: pointer;
128
+ }
129
+ /* Read through a fallback and never declared on the component, so a consumer
130
+ can set the on-colour on the switch or on any ancestor - the same contract
131
+ as --axi-pill-fill. A switch that turns something dangerous on can fill in
132
+ --axi-danger and say so. */
133
+ .axi-switch[aria-checked="true"] { background: var(--axi-switch-fill, var(--axi-accent)); }
134
+ /* Pinned to the track's insets rather than given a height and centred, so the
135
+ slug stays correct if the track is resized. Only its travel is a transform,
136
+ which is the one thing rule 11 lets move. */
137
+ .axi-switch__knob {
138
+ position: absolute;
139
+ top: 2px; bottom: 2px; left: 2px;
140
+ width: var(--axi-switch-knob, 16px);
141
+ border-radius: 2px;
142
+ background: var(--axi-ink-line);
143
+ transition: transform .1s;
144
+ }
145
+ /* The travel is the track's inner width less the slug and its two insets.
146
+ Spelled as a calc over the same tokens the track is built from so it stays
147
+ right at any size, and - the part that matters - at any border weight: a
148
+ hard-coded travel measured against one weight puts the slug through the
149
+ right-hand edge the moment rule 3's numbers change. */
150
+ .axi-switch[aria-checked="true"] .axi-switch__knob {
151
+ transform: translateX(calc(
152
+ var(--axi-switch-w, 46px) - 2 * var(--axi-border-control)
153
+ - var(--axi-switch-knob, 16px) - 4px));
154
+ }
155
+
156
+ /* ---------- chip ---------- */
157
+ /* Filled asserts, outlined annotates. The unmodified chip is a neutral fact;
158
+ the status modifiers assert a value; --meta is the only outlined variant and
159
+ is drawn in the reserved cool ink, so a reader can tell commentary from
160
+ data without reading either. */
161
+ .axi-chip {
162
+ display: inline-flex; align-items: center; gap: 6px;
163
+ padding: 5px 9px;
164
+ border: var(--axi-border-control) solid var(--axi-ink-line);
165
+ border-radius: var(--axi-radius-sm);
166
+ background: var(--axi-surface-raised);
167
+ color: var(--axi-text-dim);
168
+ font: var(--axi-t-micro);
169
+ letter-spacing: var(--axi-ls-micro);
170
+ text-transform: uppercase;
171
+ }
172
+ .axi-chip--ok { background: var(--axi-ok); color: var(--axi-ink-line); }
173
+ .axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-line); }
174
+ .axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-line); }
175
+ .axi-chip--accent { background: var(--axi-accent); color: var(--axi-accent-ink); }
176
+ .axi-chip--meta {
177
+ background: transparent;
178
+ border-color: var(--axi-meta);
179
+ color: var(--axi-meta);
180
+ }
181
+
182
+ /* ---------- count badge ---------- */
183
+ .axi-badge-count {
184
+ display: inline-block;
185
+ padding: 1px 7px;
186
+ border-radius: 99px;
187
+ background: var(--axi-accent);
188
+ color: var(--axi-accent-ink);
189
+ font: var(--axi-t-micro);
190
+ }
191
+
192
+ /* ---------- diamond ---------- */
193
+ /* The family motif: a rotated outlined square. Used as a bullet, a status dot
194
+ and, scaled up behind a glyph, the brand sigil. */
195
+ .axi-diamond {
196
+ display: inline-block;
197
+ width: 9px; height: 9px; flex: none;
198
+ transform: rotate(45deg);
199
+ background: var(--axi-meta);
200
+ border: var(--axi-border-control) solid var(--axi-ink-line);
201
+ }
202
+ .axi-diamond--accent { background: var(--axi-accent); }
203
+ .axi-diamond--ok { background: var(--axi-ok); }
204
+ .axi-diamond--warn { background: var(--axi-warn); }
205
+ .axi-diamond--danger { background: var(--axi-danger); }
206
+ /* A legend key, where the diamond stands for a series rather than a state.
207
+ Opt-in rather than a default on .axi-diamond: --axi-series is read from
208
+ whatever ancestor sets it, and a chart panel that sets one would otherwise
209
+ silently recolour every bullet inside it. */
210
+ .axi-diamond--series { background: var(--axi-series, var(--axi-accent)); }
211
+
212
+ /* ---------- input ---------- */
213
+ .axi-input {
214
+ width: 100%;
215
+ padding: 11px 12px;
216
+ background: var(--axi-ground);
217
+ border: var(--axi-border-control) solid var(--axi-ink-line);
218
+ border-radius: 8px;
219
+ color: var(--axi-text);
220
+ font: var(--axi-t-label);
221
+ font-size: 14px;
222
+ }
223
+ .axi-input::placeholder { color: var(--axi-text-faint); font-weight: 500; }
224
+
225
+ /* A search input with the magnifier drawn inside it. The glyph is the
226
+ consumer's; this only reserves the room and positions it. */
227
+ .axi-search { position: relative; }
228
+ .axi-search .axi-input { padding-left: 38px; }
229
+ .axi-search__icon {
230
+ position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
231
+ color: var(--axi-accent); font-weight: 900; pointer-events: none;
232
+ }
233
+
234
+ /* ---------- select ---------- */
235
+ /* The closed box is ours everywhere: strip the native control and draw the
236
+ caret, so a select sits alongside the other controls as just another
237
+ outlined chip instead of announcing the OS. */
238
+ .axi-select {
239
+ appearance: none;
240
+ padding: 10px 30px 10px 9px;
241
+ border: var(--axi-border-control) solid var(--axi-ink-line);
242
+ border-radius: 8px;
243
+ background-color: var(--axi-ground);
244
+ color: var(--axi-text-dim);
245
+ font: var(--axi-t-label);
246
+ font-size: 12.5px;
247
+ letter-spacing: var(--axi-ls-label);
248
+ text-transform: uppercase;
249
+ cursor: pointer;
250
+ /* Two triangles meeting: a caret with no image and no icon font. */
251
+ background-image:
252
+ linear-gradient(45deg, transparent 50%, var(--axi-accent) 50%),
253
+ linear-gradient(135deg, var(--axi-accent) 50%, transparent 50%);
254
+ background-position: calc(100% - 16px) calc(50% + 2px), calc(100% - 11px) calc(50% + 2px);
255
+ background-size: 5px 5px, 5px 5px;
256
+ background-repeat: no-repeat;
257
+ transition: transform .1s, box-shadow .1s;
258
+ }
259
+ .axi-select:hover {
260
+ color: var(--axi-text);
261
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
262
+ transform: translate(-2px, -2px);
263
+ }
264
+
265
+ /* The popup stays OS chrome until a browser lets us style it. Where one does
266
+ (Chromium's base-select), the list is drawn with the same outline and offset
267
+ block as our own popovers, so both dropdown kinds read as one family; where
268
+ it does not, the closed box above is still ours and the list is native. */
269
+ @supports (appearance: base-select) {
270
+ /* base-select draws its own ::picker-icon, so the hand-drawn caret above
271
+ would be a second arrow. */
272
+ .axi-select, .axi-select::picker(select) { appearance: base-select; }
273
+ .axi-select { background-image: none; padding-right: 9px; }
274
+ .axi-select::picker-icon { color: var(--axi-accent); transition: none; }
275
+ .axi-select::picker(select) {
276
+ margin-top: 9px; padding: 6px;
277
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
278
+ border-radius: 9px;
279
+ background: var(--axi-surface-raised);
280
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
281
+ }
282
+ .axi-select option {
283
+ display: flex; align-items: center; gap: 9px;
284
+ padding: 8px 9px;
285
+ border-radius: var(--axi-radius-sm);
286
+ background: transparent;
287
+ color: var(--axi-text-dim);
288
+ font: var(--axi-t-label);
289
+ font-size: 12.5px;
290
+ letter-spacing: var(--axi-ls-label);
291
+ text-transform: uppercase;
292
+ }
293
+ .axi-select option:hover, .axi-select option:focus {
294
+ background: var(--axi-ground); color: var(--axi-text);
295
+ }
296
+ /* The page-wide focus ring sits 2px outside its element; inside a picker
297
+ that is 2px into the neighbouring row, so pull it back in. */
298
+ .axi-select option:focus-visible { outline-offset: -3px; }
299
+ .axi-select option:checked { color: var(--axi-text); }
300
+ .axi-select option::checkmark { content: "\2713"; color: var(--axi-accent); font-weight: 900; }
301
+ }
package/src/prose.css ADDED
@@ -0,0 +1,93 @@
1
+ /* axi design language - prose.
2
+ A single class wrapping rendered markdown, so a content pipeline can emit
3
+ plain HTML with no classes at all and still land inside the language.
4
+
5
+ This layer has no ancestor in the source site, which was all cards and
6
+ controls. The restraint it needs is different: long-form reading wants
7
+ generous leading and quiet rules, and the offset blocks that make a card
8
+ feel physical would make a paragraph feel shouted. So nothing in here is
9
+ blocked except the code block and the table, which are genuinely objects
10
+ dropped into the text rather than part of its flow. */
11
+ .axi-prose { color: var(--axi-text-dim); font: var(--axi-t-body); }
12
+
13
+ .axi-prose > :first-child { margin-top: 0; }
14
+ .axi-prose > :last-child { margin-bottom: 0; }
15
+
16
+ .axi-prose h1 { margin: 0 0 18px; font: var(--axi-t-h1); letter-spacing: var(--axi-ls-h1); color: var(--axi-text); }
17
+ .axi-prose h2 {
18
+ margin: 38px 0 14px; padding-top: 22px;
19
+ border-top: var(--axi-border-control) solid var(--axi-rule);
20
+ font: var(--axi-t-h2); letter-spacing: var(--axi-ls-h2); color: var(--axi-text);
21
+ }
22
+ .axi-prose h3 { margin: 28px 0 10px; font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3); color: var(--axi-text); }
23
+ .axi-prose h4 {
24
+ margin: 22px 0 8px;
25
+ font: var(--axi-t-eyebrow); letter-spacing: var(--axi-ls-eyebrow);
26
+ text-transform: uppercase; color: var(--axi-text-faint);
27
+ }
28
+
29
+ .axi-prose p { margin: 0 0 16px; }
30
+ .axi-prose strong { color: var(--axi-text); font-weight: 800; }
31
+ /* Prose is the one place a link should announce itself: a reader scanning an
32
+ article is looking for them, and inheriting the body ink would hide them. */
33
+ .axi-prose a { color: var(--axi-accent); font-weight: 600; text-underline-offset: 2px; }
34
+ .axi-prose a:hover { color: var(--axi-text); }
35
+
36
+ .axi-prose ul, .axi-prose ol { margin: 0 0 16px; padding-left: 22px; }
37
+ .axi-prose li { margin: 0 0 7px; }
38
+ /* The family motif as a bullet. Drawn with ::marker's replacement rather than
39
+ a background image so it inherits the list's own indentation. */
40
+ .axi-prose ul { list-style: none; padding-left: 20px; }
41
+ .axi-prose ul > li { position: relative; }
42
+ .axi-prose ul > li::before {
43
+ content: ""; position: absolute; left: -20px; top: .55em;
44
+ width: 7px; height: 7px; transform: rotate(45deg);
45
+ background: var(--axi-accent);
46
+ border: var(--axi-border-hairline) solid var(--axi-ink-line);
47
+ }
48
+
49
+ .axi-prose code {
50
+ font-family: var(--axi-mono); font-size: .88em;
51
+ background: var(--axi-ground); color: var(--axi-text);
52
+ border: var(--axi-border-hairline) solid var(--axi-ink-line);
53
+ border-radius: 4px; padding: 1px 5px;
54
+ }
55
+ .axi-prose pre {
56
+ margin: 0 0 18px; padding: 14px 16px; overflow-x: auto;
57
+ background: var(--axi-ground);
58
+ border: var(--axi-border-control) solid var(--axi-ink-line);
59
+ border-radius: var(--axi-radius-sm);
60
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
61
+ }
62
+ .axi-prose pre code { background: none; border: 0; padding: 0; font-size: 12.5px; line-height: 1.6; }
63
+
64
+ .axi-prose blockquote {
65
+ margin: 0 0 18px;
66
+ padding: 11px 13px;
67
+ border-left: var(--axi-border-panel) solid var(--axi-accent);
68
+ background: var(--axi-ground);
69
+ border-radius: 0 var(--axi-radius-sm) var(--axi-radius-sm) 0;
70
+ }
71
+ .axi-prose blockquote p { margin: 0; font-style: italic; }
72
+
73
+ .axi-prose table {
74
+ width: 100%; margin: 0 0 18px;
75
+ border-collapse: collapse;
76
+ border: var(--axi-border-control) solid var(--axi-ink-line);
77
+ border-radius: var(--axi-radius-sm);
78
+ overflow: hidden;
79
+ font-size: 13.5px;
80
+ }
81
+ .axi-prose th {
82
+ text-align: left; padding: 10px 12px;
83
+ background: var(--axi-surface-raised); color: var(--axi-text);
84
+ font: var(--axi-t-micro); letter-spacing: var(--axi-ls-micro); text-transform: uppercase;
85
+ }
86
+ .axi-prose td { padding: 10px 12px; border-top: var(--axi-border-hairline) solid var(--axi-rule); }
87
+
88
+ .axi-prose hr {
89
+ margin: 30px 0; height: 0;
90
+ border: 0; border-top: var(--axi-border-control) solid var(--axi-rule);
91
+ }
92
+
93
+ .axi-prose img { max-width: 100%; height: auto; border-radius: var(--axi-radius-sm); }
package/src/shells.css ADDED
@@ -0,0 +1,303 @@
1
+ /* axi design language - shells.
2
+ Assembled components. Each is built from the primitives rather than
3
+ redefining them, so a change to .axi-panel moves the notice, the toolbar,
4
+ the popover and the card together. */
5
+
6
+ /* The small uppercase heading that labels a panel's contents. */
7
+ .axi-eyebrow {
8
+ margin: 0 0 14px;
9
+ font: var(--axi-t-eyebrow);
10
+ letter-spacing: var(--axi-ls-eyebrow);
11
+ text-transform: uppercase;
12
+ color: var(--axi-text-faint);
13
+ }
14
+
15
+ /* ---------- masthead ---------- */
16
+ .axi-mast {
17
+ position: sticky; top: 0; z-index: 40;
18
+ background: var(--axi-ground);
19
+ border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
20
+ }
21
+ .axi-mast__in {
22
+ max-width: var(--axi-page); margin-inline: auto;
23
+ padding-inline: var(--axi-gutter);
24
+ display: flex; align-items: center; gap: 22px; min-height: 66px; flex-wrap: wrap;
25
+ }
26
+ .axi-brand { display: flex; align-items: center; gap: 12px; text-decoration: none; }
27
+ /* The sigil is the diamond motif scaled up with a glyph on top. The diamond is
28
+ a rotated pseudo-element behind the text rather than a rotated box, so the
29
+ glyph stays upright. isolation keeps the negative z-index inside the sigil
30
+ instead of dropping it behind the masthead. */
31
+ .axi-sigil {
32
+ position: relative; isolation: isolate;
33
+ width: 34px; height: 34px; flex: none;
34
+ display: grid; place-items: center;
35
+ color: var(--axi-accent-ink); font-weight: 900; font-size: 13px;
36
+ }
37
+ .axi-sigil::before {
38
+ content: ""; position: absolute; inset: 0; z-index: -1;
39
+ transform: rotate(45deg);
40
+ border-radius: 5px;
41
+ background: var(--axi-accent);
42
+ border: var(--axi-border-control) solid var(--axi-ink-line);
43
+ }
44
+ .axi-brand__name { font: 800 17px/1.15 var(--axi-sans); letter-spacing: -.02em; }
45
+ .axi-brand__name small {
46
+ display: block;
47
+ font: 700 10.5px/1.4 var(--axi-sans);
48
+ color: var(--axi-text-faint);
49
+ letter-spacing: .09em; text-transform: uppercase;
50
+ }
51
+ .axi-tabs { display: flex; gap: 8px; margin-left: auto; }
52
+ .axi-tabs a {
53
+ padding: 8px 16px;
54
+ font: 800 13.5px/1 var(--axi-sans);
55
+ color: var(--axi-text-dim); text-decoration: none;
56
+ border: var(--axi-border-control) solid transparent;
57
+ border-radius: 8px;
58
+ }
59
+ .axi-tabs a:hover { color: var(--axi-text); border-color: var(--axi-rule); }
60
+ /* The current tab is filled and blocked - the same treatment a pressed pill
61
+ gets, because it is the same idea: this one is on. */
62
+ .axi-tabs a[aria-current="page"] {
63
+ background: var(--axi-accent);
64
+ color: var(--axi-accent-ink);
65
+ border-color: var(--axi-ink-line);
66
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
67
+ }
68
+
69
+ /* ---------- notice ---------- */
70
+ .axi-notice {
71
+ display: flex; gap: 14px; padding: 15px 18px;
72
+ background: var(--axi-surface);
73
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
74
+ border-radius: var(--axi-radius);
75
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
76
+ }
77
+ .axi-notice__icon {
78
+ flex: none; width: 26px; height: 26px;
79
+ display: grid; place-items: center;
80
+ border-radius: var(--axi-radius-sm);
81
+ background: var(--axi-accent); color: var(--axi-accent-ink);
82
+ font-size: 14px; font-weight: 900;
83
+ border: var(--axi-border-control) solid var(--axi-ink-line);
84
+ }
85
+ .axi-notice p { margin: 0; font: var(--axi-t-small); color: var(--axi-text-dim); }
86
+ .axi-notice b { color: var(--axi-accent); font-weight: 800; }
87
+
88
+ /* ---------- toolbar ---------- */
89
+ .axi-toolbar {
90
+ display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
91
+ padding: 14px;
92
+ background: var(--axi-surface);
93
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
94
+ border-radius: var(--axi-radius);
95
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
96
+ }
97
+
98
+ /* ---------- menu popover ---------- */
99
+ /* A disclosure, not a permanent row of controls. The trigger is an .axi-btn
100
+ with aria-expanded; this styles the panel it opens. */
101
+ .axi-menu { position: relative; }
102
+ .axi-menu__pop {
103
+ /* Above .axi-mast's z-index: 40. Both sit in the root stacking context, so
104
+ a toolbar scrolled under the sticky masthead would otherwise open its
105
+ menu behind it. */
106
+ position: absolute; top: calc(100% + 9px); left: 0; z-index: 41;
107
+ width: var(--axi-menu-width, 310px);
108
+ max-height: 340px; overflow-y: auto; padding: 8px;
109
+ background: var(--axi-surface-raised);
110
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
111
+ border-radius: 9px;
112
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
113
+ }
114
+ .axi-menu__pop[hidden] { display: none; }
115
+ .axi-menu__pop label {
116
+ display: flex; gap: 9px; align-items: flex-start;
117
+ padding: 7px 8px; border-radius: var(--axi-radius-sm);
118
+ font-size: 13px; font-weight: 600; color: var(--axi-text-dim); cursor: pointer;
119
+ }
120
+ .axi-menu__pop label:hover { background: var(--axi-ground); color: var(--axi-text); }
121
+ .axi-menu__pop input { margin: 3px 0 0; accent-color: var(--axi-accent); }
122
+
123
+ /* ---------- card ---------- */
124
+ .axi-card {
125
+ position: relative; overflow: hidden;
126
+ width: 100%; text-align: left;
127
+ display: flex; flex-direction: column; gap: 11px;
128
+ padding: 22px 16px 14px;
129
+ background: var(--axi-surface);
130
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
131
+ border-radius: var(--axi-radius);
132
+ box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
133
+ font: inherit; color: inherit; text-decoration: none; cursor: pointer;
134
+ transition: transform .1s, box-shadow .1s;
135
+ }
136
+ .axi-card:hover {
137
+ transform: translate(-3px, -3px);
138
+ box-shadow: var(--axi-offset-panel-hover) var(--axi-offset-panel-hover) 0 var(--axi-ink-line);
139
+ }
140
+ /* The top strip is drawn from the card itself so a renderer does not have to
141
+ emit an extra element for it. It is opt-in: a card only gets a strip when
142
+ the consumer has a real value to encode in it, because a coloured strip that
143
+ means nothing is decoration impersonating data. */
144
+ .axi-card--strip::before {
145
+ content: ""; position: absolute; top: 0; left: 0; right: 0; height: 9px;
146
+ background: var(--axi-card-strip, var(--axi-accent));
147
+ border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
148
+ }
149
+ .axi-card__head { display: flex; gap: 12px; align-items: flex-start; }
150
+ .axi-card__glyph {
151
+ flex: none; width: 40px; height: 40px;
152
+ display: grid; place-items: center;
153
+ border-radius: 8px;
154
+ border: var(--axi-border-control) solid var(--axi-ink-line);
155
+ background: var(--axi-surface-raised);
156
+ color: var(--axi-text);
157
+ font: 900 15px/1 var(--axi-sans);
158
+ }
159
+ .axi-card__title { flex: 1; min-width: 0; }
160
+ .axi-card__title .axi-card__name {
161
+ display: block;
162
+ font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3);
163
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
164
+ }
165
+ .axi-card__title .axi-card__kind {
166
+ display: block; margin-top: 3px;
167
+ font: 700 11px/1.3 var(--axi-sans);
168
+ color: var(--axi-text-faint);
169
+ letter-spacing: .08em; text-transform: uppercase;
170
+ }
171
+ .axi-card p { margin: 0; font: var(--axi-t-small); color: var(--axi-text-dim); }
172
+ /* Pushed to the bottom so that in an equalised grid row every card's meta
173
+ strip lands on the same line and the rules read as continuous across the
174
+ grid. The cost is empty space on cards with less content. */
175
+ .axi-card__meta {
176
+ display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
177
+ margin-top: auto; padding-top: 12px;
178
+ border-top: var(--axi-border-control) solid var(--axi-rule);
179
+ font: 700 12px/1 var(--axi-sans);
180
+ color: var(--axi-text-faint);
181
+ }
182
+ .axi-card__go { margin-left: auto; color: var(--axi-accent); font-weight: 900; text-transform: uppercase; letter-spacing: .05em; }
183
+ .axi-card:hover .axi-card__go { color: var(--axi-text); }
184
+
185
+ /* ---------- drawer ---------- */
186
+ .axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); border: 0; }
187
+ .axi-drawer {
188
+ position: fixed; top: 0; right: 0; bottom: 0; z-index: 51;
189
+ width: min(var(--axi-drawer-width, 560px), 100vw);
190
+ display: flex; flex-direction: column;
191
+ background: var(--axi-surface);
192
+ border-left: var(--axi-border-panel) solid var(--axi-ink-line);
193
+ }
194
+ .axi-drawer[hidden], .axi-scrim[hidden] { display: none; }
195
+ .axi-drawer__head {
196
+ position: relative;
197
+ padding: 20px 24px 18px;
198
+ border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
199
+ }
200
+ .axi-drawer__head h2 {
201
+ margin: 2px 0 0; padding-right: 44px;
202
+ font: var(--axi-t-h2); letter-spacing: var(--axi-ls-h2);
203
+ }
204
+ .axi-drawer__close {
205
+ position: absolute; top: 16px; right: 18px;
206
+ width: 32px; height: 32px;
207
+ display: grid; place-items: center;
208
+ border-radius: 8px;
209
+ background: var(--axi-ground);
210
+ border: var(--axi-border-control) solid var(--axi-ink-line);
211
+ color: var(--axi-text-dim);
212
+ font-size: 15px; font-weight: 900; cursor: pointer;
213
+ transition: transform .1s, box-shadow .1s;
214
+ }
215
+ .axi-drawer__close:hover {
216
+ background: var(--axi-accent); color: var(--axi-accent-ink);
217
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
218
+ transform: translate(-2px, -2px);
219
+ }
220
+ .axi-drawer__body { overflow-y: auto; padding: 18px 24px 30px; }
221
+
222
+ /* ---------- pull-quote ---------- */
223
+ /* A cited quotation. The accent rule down the left is the only place in the
224
+ language where colour marks a block rather than filling a chip - a quote is
225
+ someone else's words and wants to look borrowed. */
226
+ .axi-quote {
227
+ margin: 0;
228
+ padding: 11px 13px;
229
+ border-left: var(--axi-border-panel) solid var(--axi-accent);
230
+ background: var(--axi-ground);
231
+ border-radius: 0 var(--axi-radius-sm) var(--axi-radius-sm) 0;
232
+ }
233
+ .axi-quote p { margin: 0; font-size: 13px; font-style: italic; color: var(--axi-text-dim); line-height: 1.5; }
234
+ .axi-quote cite {
235
+ display: block; margin-top: 7px;
236
+ font-style: normal; font-size: 11.5px; font-weight: 700;
237
+ color: var(--axi-accent);
238
+ }
239
+
240
+ /* ---------- window ---------- */
241
+ /* Chrome for a frameless desktop app, where the app is not a page inside a
242
+ browser but the outermost thing on the screen. Rule 3 says a raised
243
+ element is outlined; a window is the most raised element there is, so it
244
+ takes the panel outline. It carries no block, because a block is an
245
+ element's shadow on the surface behind it and there is nothing behind a
246
+ window that this language is entitled to draw on.
247
+ The titlebar is filled with the ink line rather than a surface: it is the
248
+ outline widened into a strip, which is why the app's content reads as
249
+ sitting inside the outline instead of under a second toolbar. */
250
+ .axi-window {
251
+ height: 100vh;
252
+ display: flex; flex-direction: column; overflow: hidden;
253
+ background: var(--axi-ground);
254
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
255
+ border-radius: var(--axi-radius);
256
+ }
257
+ .axi-titlebar {
258
+ flex: none;
259
+ display: flex; align-items: center; gap: 12px;
260
+ height: 38px; padding-left: 14px;
261
+ background: var(--axi-ink-line);
262
+ border-bottom: var(--axi-border-control) solid var(--axi-ink-line);
263
+ font: var(--axi-t-micro);
264
+ letter-spacing: var(--axi-ls-micro);
265
+ text-transform: uppercase;
266
+ color: var(--axi-text-faint);
267
+ /* The strip is the drag handle; the buttons opt back out in Electron. */
268
+ -webkit-app-region: drag;
269
+ }
270
+ .axi-titlebar__btns { margin-left: auto; display: flex; height: 100%; -webkit-app-region: no-drag; }
271
+ .axi-titlebar__btns button {
272
+ width: 44px;
273
+ border: 0;
274
+ background: transparent;
275
+ color: var(--axi-text-faint);
276
+ font: var(--axi-t-label);
277
+ cursor: pointer;
278
+ }
279
+ /* No lift here. A window button is chrome, not content: it has nothing to
280
+ lift off, and 44px of movement in the corner of every screen is exactly
281
+ the scattered motion rule 4 rations. It answers on the neutral ramp. */
282
+ .axi-titlebar__btns button:hover { background: var(--axi-surface); color: var(--axi-text); }
283
+ /* Close is the one destructive control in the chrome, and the one place a
284
+ status ink belongs in a titlebar. */
285
+ .axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-line); }
286
+ /* The scrolling region under the chrome. */
287
+ .axi-window__body { flex: 1; overflow-y: auto; }
288
+
289
+ @media (max-width: 640px) {
290
+ .axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
291
+ .axi-tabs { margin-left: 0; width: 100%; overflow-x: auto; }
292
+ /* The offset blocks eat horizontal room a phone does not have, so the panel
293
+ step drops to the control step here rather than to an invented number.
294
+ The card's hover has to come down with it - left alone it kept deepening
295
+ to the panel hover block, so a card on a hover-emulating touch device
296
+ jumped further than it does on the desktop it was tuned for. */
297
+ .axi-notice, .axi-toolbar, .axi-card {
298
+ box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
299
+ }
300
+ .axi-card:hover {
301
+ box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
302
+ }
303
+ }